Skip to content

Commit 3144079

Browse files
committed
add interactive map previews and game win condition
1 parent da8c574 commit 3144079

6 files changed

Lines changed: 240 additions & 33 deletions

File tree

via-ui/src/App.vue

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,45 @@
22
import TopBar from '@/components/TopBar.vue'
33
import Main from '@/components/Main.vue'
44
import SideBar from '@/components/SideBar.vue'
5+
import { useGameManager } from '@/api/game/manager.ts'
6+
import { onMounted, ref, watch } from 'vue'
57
8+
const { gameState, startGame, clearStopPreview } = useGameManager()
9+
10+
const modal = ref<HTMLDialogElement | null>(null)
11+
12+
watch(
13+
() => gameState.value?.currentStop,
14+
(newStop) => {
15+
const targetId = gameState.value?.targetStation?.stop_id
16+
17+
if (newStop && targetId && newStop.stop_id === targetId) {
18+
clearStopPreview()
19+
modal.value?.showModal()
20+
}
21+
},
22+
)
23+
24+
onMounted(() => {
25+
startGame()
26+
})
627
</script>
728

829
<template>
930
<div class="h-screen w-screen grid grid-rows-[auto_1fr] bg-base-300 overflow-hidden">
31+
<dialog ref="modal" class="modal">
32+
<div class="modal-box">
33+
<h3 class="text-lg font-bold">Congratulation!</h3>
34+
<p class="py-4">You successfully arrived to {{ gameState.targetStation?.stop_name }}!</p>
35+
<p class="py-4">You took {{ gameState.trip.length }} different trains.</p>
36+
<div class="modal-action">
37+
<form method="dialog">
38+
<button class="btn btn-outline btn-primary" @click="startGame()">Restart</button>
39+
</form>
40+
</div>
41+
</div>
42+
</dialog>
43+
1044
<TopBar />
1145

1246
<div class="grid grid-cols-1 md:grid-cols-[1fr_350px] overflow-hidden">

via-ui/src/api/game/manager.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface GameState {
2222
departures: Departure[]
2323
chosenTrain: TripDetails | null
2424
currentDistance: number | null
25+
shapePreview: ShapePoint[]
26+
stopPreview: Stop | null
2527
}
2628

2729
const state = ref<GameState>({
@@ -33,6 +35,8 @@ const state = ref<GameState>({
3335
departures: [],
3436
chosenTrain: null,
3537
currentDistance: null,
38+
shapePreview: [],
39+
stopPreview: null,
3640
})
3741

3842
const service = new Service('/via/data')
@@ -76,17 +80,18 @@ export function useGameManager() {
7680
START_DATE,
7781
)
7882
state.value.currentDistance = computeDistanceToTarget(state.value.startStation)
83+
state.value.trip = []
7984
}
8085

8186
async function chooseTrain(departure: Departure) {
8287
state.value.chosenTrain = await service.getTripDetails(
8388
departure.trip_id,
8489
state.value.currentStop?.stop_id,
8590
)
86-
console.log(state.value.chosenTrain)
8791
}
8892

8993
async function chooseStop(stop: TripStop) {
94+
state.value.shapePreview = []
9095
const endStation = (await service.getStopDetails(stop.stop_id))!!
9196
const distanceBefore = computeDistanceToTarget(state.value.currentStop!!)
9297
const distanceAfter = computeDistanceToTarget(endStation)
@@ -116,10 +121,30 @@ export function useGameManager() {
116121
state.value.trip.push(trip)
117122
}
118123

124+
async function showTripPreview(departure: Departure) {
125+
state.value.shapePreview = await service.getTripShape(departure.trip_id, state.value.currentStop!!.stop_id)
126+
}
127+
128+
async function clearTripPreview() {
129+
state.value.shapePreview = []
130+
}
131+
132+
async function showStopPreview(stop: TripStop) {
133+
state.value.stopPreview = (await service.getStopDetails(stop.stop_id))!!
134+
}
135+
136+
async function clearStopPreview() {
137+
state.value.stopPreview = null
138+
}
139+
119140
return {
120141
gameState,
121142
startGame,
122143
chooseTrain,
123144
chooseStop,
145+
showTripPreview,
146+
clearTripPreview,
147+
showStopPreview,
148+
clearStopPreview,
124149
}
125150
}

via-ui/src/components/Departure.vue

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,72 @@
11
<script setup lang="ts">
2+
import { ref, watch, onMounted, onUnmounted } from 'vue'
23
import type { Departure } from '@/api/gtfs/types.ts'
34
import { useGameManager } from '@/api/game/manager.ts'
45
import { getIconFilename } from '@/api/game/icons.ts'
56
6-
const { chooseTrain } = useGameManager()
7+
const { chooseTrain, showTripPreview, clearTripPreview } = useGameManager()
78
89
const props = defineProps<{
910
departure: Departure
1011
}>()
1112
13+
const isOpen = ref(false)
14+
const collapseRef = ref<HTMLElement | null>(null)
15+
1216
function departureSelected() {
1317
chooseTrain(props.departure)
1418
}
19+
20+
function previewRoute() {
21+
showTripPreview(props.departure)
22+
}
23+
24+
function clearPreview() {
25+
clearTripPreview()
26+
}
27+
28+
function handleClickOutside(event: MouseEvent) {
29+
if (isOpen.value && collapseRef.value && !collapseRef.value.contains(event.target as Node)) {
30+
isOpen.value = false
31+
}
32+
}
33+
34+
watch(isOpen, (newVal) => {
35+
if (newVal) {
36+
previewRoute()
37+
} else {
38+
clearPreview()
39+
}
40+
})
41+
42+
onMounted(() => {
43+
window.addEventListener('click', handleClickOutside)
44+
})
45+
46+
onUnmounted(() => {
47+
window.removeEventListener('click', handleClickOutside)
48+
})
1549
</script>
1650

1751
<template>
18-
<div tabindex="0" class="collapse bg-base-100 border border-base-300 mx-2 my-1">
52+
<div ref="collapseRef" class="collapse bg-base-100 border border-base-300 mx-2 my-1">
53+
<input type="radio" name="departures-accordion" :checked="isOpen" @click="isOpen = !isOpen" />
54+
1955
<div class="collapse-title font-semibold flex items-center gap-2">
2056
<img
2157
class="inline-block h-5 w-auto object-contain align-middle"
2258
:src="getIconFilename(departure.route_short_name)"
2359
:alt="departure.route_short_name"
2460
/>
2561

26-
<span>{{ departure.departure_time.slice(0,5) }}</span>
62+
<span>{{ departure.departure_time.slice(0, 5) }}</span>
2763
<span>{{ departure.trip_headsign }}</span>
2864
</div>
2965

3066
<div class="collapse-content text-sm z-1">
31-
<button class="btn btn-primary btn-outline w-full" @click="departureSelected()">Board this train</button>
67+
<button class="btn btn-primary btn-outline w-full" @click="departureSelected()">
68+
Board this train
69+
</button>
3270
</div>
3371
</div>
3472
</template>

via-ui/src/components/SideBar.vue

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,22 @@ const departureChosen = computed(() => {
2020
class="sticky top-0 bg-base-200 z-10 p-4 pr-8 pb-3 flex flex-col gap-2 shrink-0 border-b border-base-100"
2121
>
2222
<div v-if="!departureChosen" class="w-full min-w-0">
23-
<h2 class="text-2xl font-bold">
24-
<p
25-
v-if="gameState?.currentStop"
26-
class="text-xs tracking-wide uppercase opacity-60 font-semibold mb-1 truncate"
23+
<div
24+
v-if="!gameState?.currentStop"
25+
class="flex flex-col items-center justify-center py-4 w-full opacity-50"
26+
>
27+
<span class="loading loading-spinner loading-xl"></span>
28+
<span class="text-xs mt-2 font-semibold tracking-wide uppercase"
29+
>Loading game...</span
2730
>
31+
</div>
32+
33+
<h2 v-else class="text-2xl font-bold">
34+
<p class="text-xs tracking-wide uppercase opacity-60 font-semibold mb-1 truncate">
2835
Current location
2936
</p>
3037

31-
<span v-if="!gameState?.currentStop" class="opacity-50 text-base font-normal">
32-
Loading...
33-
</span>
34-
35-
<span v-else class="block truncate" :title="gameState.currentStop?.stop_name">
38+
<span class="block truncate" :title="gameState.currentStop?.stop_name">
3639
{{ gameState.currentStop?.stop_name }}
3740
</span>
3841
</h2>

via-ui/src/components/Stop.vue

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
11
<script setup lang="ts">
2-
3-
2+
import { ref, watch, onMounted, onUnmounted } from 'vue'
43
import type { TripStop } from '@/api/gtfs/types.ts'
54
import { useGameManager } from '@/api/game/manager.ts'
65
7-
const { chooseStop } = useGameManager()
6+
const { chooseStop, showStopPreview, clearStopPreview } = useGameManager()
87
98
const props = defineProps<{
109
stop: TripStop
1110
}>()
1211
12+
const isOpen = ref(false)
13+
const collapseRef = ref<HTMLElement | null>(null)
14+
1315
function stopSelected() {
1416
chooseStop(props.stop)
1517
}
1618
19+
function showPreview() {
20+
showStopPreview(props.stop)
21+
}
22+
23+
function clearPreview() {
24+
clearStopPreview()
25+
}
26+
27+
function handleClickOutside(event: MouseEvent) {
28+
if (isOpen.value && collapseRef.value && !collapseRef.value.contains(event.target as Node)) {
29+
isOpen.value = false
30+
}
31+
}
32+
33+
watch(isOpen, (newVal) => {
34+
if (newVal) {
35+
showPreview()
36+
} else {
37+
clearPreview()
38+
}
39+
})
40+
41+
onMounted(() => {
42+
window.addEventListener('click', handleClickOutside)
43+
})
44+
45+
onUnmounted(() => {
46+
window.removeEventListener('click', handleClickOutside)
47+
})
1748
</script>
1849

1950
<template>
20-
<div tabindex="0" class="collapse bg-base-100 border border-base-300 mx-2 my-1">
21-
<div class="collapse-title font-semibold"> {{stop.arrival_time.slice(0,5)}} {{stop.stop_name}}</div>
51+
<div ref="collapseRef" class="collapse bg-base-100 border border-base-300 mx-2 my-1">
52+
<input type="radio" name="stops-accordion" :checked="isOpen" @click="isOpen = !isOpen" />
53+
54+
<div class="collapse-title font-semibold">
55+
{{ stop.arrival_time.slice(0, 5) }} {{ stop.stop_name }}
56+
</div>
57+
2258
<div class="collapse-content text-sm z-1">
23-
<button class="btn btn-outline btn-secondary w-full" @click="stopSelected()">Get off here</button>
59+
<button class="btn btn-outline btn-secondary w-full" @click="stopSelected()">
60+
Get off here
61+
</button>
2462
</div>
2563
</div>
2664
</template>

0 commit comments

Comments
 (0)