diff --git a/package.json b/package.json index 4ecf3e5..6adb7ec 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "imagemin-webpack-plugin": "^2.3.0", "mini-css-extract-plugin": "^0.4.2", "portal-vue": "^2.1.7", + "vue-dragscroll": "^1.10.2", "vue2-leaflet": "^1.0.2", "vuex": "^3.0.1", "vuex-class": "^0.3.1", diff --git a/resources/components/departures/departure.html b/resources/components/departures/departure.html index 50c58d9..7af6a51 100644 --- a/resources/components/departures/departure.html +++ b/resources/components/departures/departure.html @@ -34,16 +34,7 @@ </div> </div> <fold :visible="showTrip"> - <div v-if="trip != null" class="trip" :class="[ `trip--${departure.line.type}` ]"> - <ol class="trip__stops"> - <li v-for="stop in trip.schedule" class="trip-stop"> - <div class="trip-stop__marker"/> - <div class="trip-stop__description"> - <stop :stop="stop.stop"/> - </div> - </li> - </ol> - </div> + <trip :schedule="trip.schedule" v-if="trip" :class="[ `trip--${departure.line.type}` ]"/> <div v-else class="text-center"> <fa icon="spinner-third" pulse></fa> </div> diff --git a/resources/components/trip.html b/resources/components/trip.html new file mode 100644 index 0000000..f8a4c71 --- /dev/null +++ b/resources/components/trip.html @@ -0,0 +1,13 @@ +<div class="trip"> + <ol class="trip__stops" v-dragscroll.x> + <li v-for="stop in schedule" class="trip__stop"> + <div class="trip__marker"/> + <div class="trip__description"> + <stop :stop="stop.stop"/> + </div> + <div class="trip__departure"> + {{ stop.departure.format('HH:mm') }} + </div> + </li> + </ol> +</div> diff --git a/resources/styles/_dragscroll.scss b/resources/styles/_dragscroll.scss new file mode 100644 index 0000000..a3acc93 --- /dev/null +++ b/resources/styles/_dragscroll.scss @@ -0,0 +1,6 @@ +.drag-scroll { + cursor : grab; + &:active { + cursor : grabbing; + } +} diff --git a/resources/styles/_trip.scss b/resources/styles/_trip.scss index 10729ff..505d725 100644 --- a/resources/styles/_trip.scss +++ b/resources/styles/_trip.scss @@ -4,7 +4,8 @@ $description-rotation: 60deg; $description-width: 250px; $trip-stop-marker-size: .9rem; -$trip-stop-line-width: .2rem; +$trip-stop-marker-spacing: .75rem; +$trip-line-width: .2rem; .trip { display: flex; @@ -18,45 +19,49 @@ $trip-stop-line-width: .2rem; display: flex; list-style: none; overflow-x: auto; + + @include no-scrollbars; + @extend .drag-scroll; } -.trip-stop { +.trip__stop { width: 2.5rem; position: relative; flex-shrink: 0; display: flex; align-items: center; justify-content: center; + flex-direction: column; } -.trip-stop:first-child { - .trip-stop__marker::before { +.trip__stop:first-child { + .trip__marker::before { content: none; } } -.trip-stop:last-child { - .trip-stop__marker::after { +.trip__stop:last-child { + .trip__marker::after { content: none; } } -.trip-stop__marker { +.trip__marker { width: $trip-stop-marker-size; height: $trip-stop-marker-size; - border: $dark $trip-stop-line-width solid; + border: $dark $trip-line-width solid; border-radius: 100%; background: white; - margin: .75rem 0; + margin: $trip-stop-marker-spacing 0; &::before, &::after { content: ""; display: block; - height: $trip-stop-line-width; + height: $trip-line-width; background: $dark; width: 50%; position: absolute; - top: 50%; + top: $trip-stop-marker-spacing + ($trip-stop-marker-size) / 2; transform: translateY(-50%); z-index: -1; } @@ -72,7 +77,7 @@ $trip-stop-line-width: .2rem; @each $type, $color in $line-types { .trip--#{$type} { - .trip-stop__marker { + .trip__marker { border-color: $color; &::before, &::after { @@ -82,7 +87,7 @@ $trip-stop-line-width: .2rem; } } -.trip-stop__description { +.trip__description { display: flex; transform: rotate(-$description-rotation) translateX(.75rem); transform-origin: 0 50%; @@ -95,3 +100,10 @@ $trip-stop-line-width: .2rem; width: max-content; } } + + +.trip__departure { + font-size: $small-font-size; + font-weight: bold; + margin-bottom: .5rem; +} diff --git a/resources/styles/main.scss b/resources/styles/main.scss index 956b9b8..9d185cd 100644 --- a/resources/styles/main.scss +++ b/resources/styles/main.scss @@ -42,6 +42,16 @@ $grid-gutter-width: $spacer * 2; } } +@mixin no-scrollbars { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* Internet Explorer 10+ */ + + &::-webkit-scrollbar { /* WebKit */ + width: 0; + height: 0; + } +} + @import "common"; @import "stop"; @import "departure"; @@ -52,6 +62,7 @@ $grid-gutter-width: $spacer * 2; @import "form"; @import "fabourites"; @import "trip"; +@import "dragscroll"; body { min-height: 100vh; diff --git a/resources/ts/app.ts b/resources/ts/app.ts index bb4087c..d845656 100644 --- a/resources/ts/app.ts +++ b/resources/ts/app.ts @@ -14,10 +14,12 @@ window['Popper'] = Popper; import Vue from "vue"; import Vuex from 'vuex'; import PortalVue from 'portal-vue'; +import VueDragscroll from 'vue-dragscroll'; import { Workbox } from "workbox-window"; Vue.use(Vuex); Vue.use(PortalVue); +Vue.use(VueDragscroll); // async dependencies (async function () { diff --git a/resources/ts/components/departures.ts b/resources/ts/components/departures.ts index 6b43d76..7668877 100644 --- a/resources/ts/components/departures.ts +++ b/resources/ts/components/departures.ts @@ -5,6 +5,8 @@ import { namespace } from 'vuex-class'; import store from '../store' import { Trip } from "../model/trip"; import urls from "../urls"; +import { Jsonified } from "../utils"; +import * as moment from "moment"; const { State } = namespace('departures'); @@ -19,9 +21,20 @@ export class DeparturesComponent extends Vue { @Component({ template: require("../../components/departures/departure.html") }) export class DepartureComponent extends Vue { @Prop(Object) departure: Departure; + scheduledTrip: Trip = null; showTrip: boolean = false; - trip: Trip = null; + + processTrip(trip: Jsonified<Trip>): Trip { + return { + ...trip, + schedule: trip.schedule.map(s => ({ + ...s, + arrival: moment.parseZone(s.arrival), + departure: moment.parseZone(s.departure), + })) + }; + }; get timeDiffers() { const departure = this.departure; @@ -42,9 +55,21 @@ export class DepartureComponent extends Vue { const response = await fetch(urls.prepare(urls.trip, { id: this.departure.trip.id })); if (response.ok) { - this.trip = await response.json(); + this.scheduledTrip = this.processTrip(await response.json()); } } + + get trip() { + const trip = this.scheduledTrip; + return trip && { + ...trip, + schedule: trip.schedule.map(stop => ({ + ...stop, + arrival: stop.arrival.clone().add(this.departure.delay, 'seconds'), + departure: stop.departure.clone().add(this.departure.delay, 'seconds'), + })) + }; + } } Vue.component('Departures', DeparturesComponent); diff --git a/resources/ts/components/index.ts b/resources/ts/components/index.ts index c7258d3..cc580e7 100644 --- a/resources/ts/components/index.ts +++ b/resources/ts/components/index.ts @@ -7,3 +7,4 @@ export * from './messages' export * from './map' export * from './app' export * from './favourites' +export * from './trip' diff --git a/resources/ts/components/trip.ts b/resources/ts/components/trip.ts new file mode 100644 index 0000000..3575d9e --- /dev/null +++ b/resources/ts/components/trip.ts @@ -0,0 +1,11 @@ +import Vue from "vue"; +import Component from "vue-class-component"; +import { Prop } from "vue-property-decorator"; +import { ScheduledStop } from "../model/trip"; + +@Component({ template: require("../../components/trip.html") }) +export class TripComponent extends Vue { + @Prop(Array) public schedule: ScheduledStop[]; +} + +Vue.component('Trip', TripComponent); diff --git a/resources/ts/model/stop.ts b/resources/ts/model/stop.ts index 415acd3..ebb3bfb 100644 --- a/resources/ts/model/stop.ts +++ b/resources/ts/model/stop.ts @@ -2,7 +2,10 @@ export interface Stop { id: any; name: string; description?: string; - location?: [ number, number ]; + location?: { + lat: number, + lng: number, + }; onDemand?: boolean; variant?: string; } diff --git a/resources/ts/store/departures.ts b/resources/ts/store/departures.ts index 8878c10..220ac06 100644 --- a/resources/ts/store/departures.ts +++ b/resources/ts/store/departures.ts @@ -1,6 +1,6 @@ import { Module } from "vuex"; import { RootState } from "./root"; -import { Departure, Stop } from "../model"; +import { Departure, Line, Stop } from "../model"; import * as moment from 'moment' import common, { CommonState } from './common' import urls from "../urls"; @@ -43,12 +43,12 @@ export const departures: Module<DeparturesState, RootState> = { } const departures = await response.json() as Jsonified<Departure>[]; - commit('update', departures.map(departure => { - departure.scheduled = moment.parseZone(departure.scheduled); - departure.estimated = departure.estimated && moment.parseZone(departure.estimated); - - return departure as Departure; - })); + commit('update', departures.map((departure): Departure => ({ + ...departure, + line: departure.line as Line, + scheduled: moment.parseZone(departure.scheduled), + estimated: departure.estimated && moment.parseZone(departure.estimated), + }))); } } }; diff --git a/resources/ts/utils.ts b/resources/ts/utils.ts index 010c6d7..0fcbb68 100644 --- a/resources/ts/utils.ts +++ b/resources/ts/utils.ts @@ -1,9 +1,13 @@ -type Simplify<T, K = any> = string | +import { Moment } from "moment"; + +type Simplify<T> = string | T extends string ? string : T extends number ? number : T extends boolean ? boolean : - T extends Array<K> ? Array<K> : - T extends Object ? Object : any; + T extends Moment ? string : + T extends Array<infer K> ? Array<Simplify<K>> : + T extends (infer K)[] ? Simplify<K>[] : + T extends Object ? Jsonified<T> : any; export type Jsonified<T> = { [K in keyof T]: Simplify<T[K]> } export type Optionalify<T> = { [K in keyof T]?: T[K] } diff --git a/src/Provider/ScheduleRepository.php b/src/Provider/ScheduleRepository.php index 554d9e5..951a20f 100644 --- a/src/Provider/ScheduleRepository.php +++ b/src/Provider/ScheduleRepository.php @@ -8,7 +8,7 @@ use Tightenco\Collect\Support\Collection; interface ScheduleRepository { - const DEFAULT_DEPARTURES_COUNT = 8; + const DEFAULT_DEPARTURES_COUNT = 16; public function getDeparturesForStop( Stop $stop, diff --git a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php index ba01778..0e956bd 100644 --- a/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php +++ b/src/Provider/ZtmGdansk/ZtmGdanskDepartureRepository.php @@ -46,7 +46,9 @@ class ZtmGdanskDepartureRepository implements DepartureRepository $first = $real->map(t\getter('scheduled'))->min() ?? $now; $scheduled = $this->getScheduledDepartures($stop, $first); - return $this->pair($scheduled, $real); + return $this->pair($scheduled, $real)->filter(function (Departure $departure) use ($now) { + return $departure->getDeparture() > $now; + }); } private function getRealDepartures(Stop $stop) diff --git a/yarn.lock b/yarn.lock index 5d6839c..50bab86 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6197,6 +6197,11 @@ vue-class-component@^6.2.0: resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-6.3.2.tgz#e6037e84d1df2af3bde4f455e50ca1b9eec02be6" integrity sha512-cH208IoM+jgZyEf/g7mnFyofwPDJTM/QvBNhYMjqGB8fCsRyTf68rH2ISw/G20tJv+5mIThQ3upKwoL4jLTr1A== +vue-dragscroll@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/vue-dragscroll/-/vue-dragscroll-1.10.2.tgz#34ace3c6aa7a39edc157cac5e9ea1d5b31b1119c" + integrity sha512-fGVw8KP3ggbp49csa1Tbj2my0YuNmZ1zxYYge4QWIypGNHVwd9hResy/v6QF5HxY0a+qd2EBteeBpxtJxFMp5A== + vue-property-decorator@^7.0.0: version "7.3.0" resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-7.3.0.tgz#d50d67f0b0f1c814f9f2fba36d6eeccbcc62dbb6"