Skip to content

Commit

Permalink
Decoupled components
Browse files Browse the repository at this point in the history
Decoupled components from Nuxt pages.
This should make it easier to use those components in future applications.
Minor bug fix with query setting of station id.
  • Loading branch information
ncpleslie committed Nov 2, 2023
1 parent 149885e commit 90c2a38
Show file tree
Hide file tree
Showing 14 changed files with 284 additions and 130 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ WMATA train tracking application for monitoring train arrival times at your stat

Check it out at https://wmata-train-tracking.vercel.app/.

Tap/Click the right side of the screen to set your station.
Tap/Click the left side of the screen to set your station.
Tap/Click the right side to see incidents, if there are any.

## About

Expand Down
53 changes: 53 additions & 0 deletions components/HomeView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
import TrainsResponseEntity from "~/models/trains_response.entity";
interface MainViewProps {
trainData?: TrainsResponseEntity;
selectedStationName?: string;
hasIncidents: boolean;
isRefreshing: boolean;
}
defineProps<MainViewProps>();
const emit = defineEmits<{
onLeftTap: [];
onMiddleTap: [];
onRightTap: [];
onSeeIncidents: [];
}>();
</script>

<template>
<AreaAction
:on-left-tap="() => emit('onLeftTap')"
:on-middle-tap="() => emit('onMiddleTap')"
:on-right-tap="() => emit('onRightTap')"
>
<div v-if="trainData" class="flex h-screen flex-col justify-between">
<TrainArrivalBoard class="trains" :trains="trainData.trains" />
<div class="mb-2 flex flex-row items-center gap-4">
<SublineText
class="trains mr-auto w-1/2 truncate text-4xl text-gray-700"
>
{{ selectedStationName }}
</SublineText>
<IncidentNotification
v-if="hasIncidents"
@on-see-incidents="() => emit('onSeeIncidents')"
/>
<LastUpdated
class="trains"
:last-updated="new Date(trainData.lastUpdated)"
/>
</div>
</div>
<LoadingIndicator v-if="isRefreshing" />
</AreaAction>
</template>

<style scoped>
.trains {
line-height: 1.7ch;
}
</style>
87 changes: 87 additions & 0 deletions components/IncidentsView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup lang="ts">
import AppConstants from "~/constants/app.constants";
import IncidentEntity from "~/models/incident.entity";
interface IncidentsViewProps {
incidents: IncidentEntity[];
}
const props = defineProps<IncidentsViewProps>();
const emit = defineEmits<{
onSlideEnd: [];
}>();
/**
* This will split the incident text into multiple slides if it is too long.
*/
const incidentDetails = computed(() => {
return props.incidents.reduce(
(acc: string[], { description, linesAffected }) => {
const affected =
linesAffected.length > 0
? `Affected Lines: ${linesAffected.join(" ")}`
: "";
if (description.length > AppConstants.maxIncidentTextLength) {
// Split the string into multiple slides but only after a word.
const splitStrings = description.match(
new RegExp(
`[\\s\\S]{1,${AppConstants.maxIncidentTextLength}}(?![\\S])`,
"g"
)
);
if (splitStrings) {
splitStrings[splitStrings.length - 1] = `${
splitStrings[splitStrings.length - 1]
} ${affected}`;
return [...acc, ...splitStrings];
}
} else {
return [...acc, `${description} ${affected} `];
}
return acc;
},
[]
);
});
</script>

<template>
<div class="h-screen">
<div class="flex h-screen flex-col justify-between gap-8">
<div class="checkered-background"></div>
<h1 class="text-center text-7xl leading-[1.7ch] text-red-600">
SERVICE ADVISORY
</h1>
<TextCarousel
:slides="incidentDetails"
@slide-end="() => emit('onSlideEnd')"
/>
<div class="checkered-background"></div>
</div>
</div>
</template>

<style scoped>
.checkered-background {
width: 100%;
height: 50px;
background-image: linear-gradient(
45deg,
#fbbf24 25%,
transparent 25%,
transparent 75%,
#fbbf24 75%
),
linear-gradient(
45deg,
#fbbf24 25%,
transparent 25%,
transparent 75%,
#fbbf24 75%
);
background-size: 50px 50px;
background-position: 25px 50px, 50px 25px;
}
</style>
25 changes: 11 additions & 14 deletions components/ScrollableStationList.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<script setup lang="ts">
import { useResizeObserver } from "@vueuse/core";
import StationEntity from "~/models/station.entity";
import { useTrainStore } from "~/stores/train.store";
import AppConstants from "~/constants/app.constants";
interface ScrollableStationListProps {
stations: StationEntity[];
selectedStation: StationEntity;
currentPage: number;
}
const props = defineProps<ScrollableStationListProps>();
const stationStore = useTrainStore();
const { selectedStation, currentPage } = toRefs(stationStore);
const parent = ref<HTMLDivElement | null>(null);
const totalPerPage = ref(0);
Expand All @@ -20,7 +19,7 @@ const currentHeight = ref(0);
const currentWidth = ref(0);
const displayedItems = computed(() => {
const startIndex = currentPage.value * totalPerPage.value;
const startIndex = props.currentPage * totalPerPage.value;
const endIndex = startIndex + totalPerPage.value;
return props.stations.slice(startIndex, endIndex);
});
Expand All @@ -44,7 +43,7 @@ useResizeObserver(parent, () => {
// Prevents losing page when reloading component.
if (!firstObservation.value) {
currentPage.value = 0;
emit("onSetPage", 0);
}
firstObservation.value = false;
Expand All @@ -55,26 +54,26 @@ const totalPages = computed(() =>
);
const nextPage = () => {
currentPage.value = (currentPage.value + 1) % totalPages.value;
emit("onSetPage", (props.currentPage + 1) % totalPages.value);
};
const previousPage = () => {
const newPage = currentPage.value - 1;
currentPage.value = newPage >= 0 ? newPage : totalPages.value - 1;
const newPage = props.currentPage - 1;
emit("onSetPage", newPage >= 0 ? newPage : totalPages.value - 1);
};
const onStationClicked = (station: StationEntity) => {
emit("stationClicked");
stationStore.setSelectedStation(station);
emit("stationClicked", station);
};
const onBackedClicked = () => {
emit("backClicked");
};
const emit = defineEmits<{
(e: "stationClicked"): void;
(e: "backClicked"): void;
stationClicked: [station: StationEntity];
backClicked: [];
onSetPage: [page: number];
}>();
</script>

Expand Down Expand Up @@ -118,5 +117,3 @@ const emit = defineEmits<{
</BaseButton>
</div>
</template>

<style scoped></style>
12 changes: 12 additions & 0 deletions composables/use_train.composable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type RouterOutput = inferRouterOutputs<AppRouter>;
type getIncidentsOutput = RouterOutput["train"]["getIncidents"];
type GetTrainsOutput = RouterOutput["train"]["getTrains"];
type GetStationOutput = RouterOutput["train"]["getStations"];
type GetStationByIdOutput = RouterOutput["train"]["getStationById"];

type ErrorOutput = TRPCClientError<AppRouter>;

Expand Down Expand Up @@ -40,6 +41,17 @@ export function useGetTrains() {
);
}

export function useGetStationById(stationId: string) {
const { $client } = useNuxtApp();
return useAsyncData<GetStationByIdOutput, ErrorOutput>(
() =>
$client.train.getStationById.query({
stationId,
}),
{ immediate: false }
);
}

/**
* Retrieves stations using the Nuxt app client and returns the result as asynchronous data.
*
Expand Down
9 changes: 9 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type client } from "./plugins/client";

declare module "#app" {
interface NuxtApp {
$client: client;
}
}

export {};
69 changes: 1 addition & 68 deletions pages/incidents.vue
Original file line number Diff line number Diff line change
@@ -1,44 +1,9 @@
<script setup lang="ts">
import { useTrainStore } from "~/stores/train.store";
import AppConstants from "~/constants/app.constants";
const trainStore = useTrainStore();
const { incidents } = toRefs(trainStore);
/**
* This will split the incident text into multiple slides if it is too long.
*/
const incidentDetails = computed(() => {
return incidents.value.reduce(
(acc: string[], { description, linesAffected }) => {
const affected =
linesAffected.length > 0
? `Affected Lines: ${linesAffected.join(" ")}`
: "";
if (description.length > AppConstants.maxIncidentTextLength) {
// Split the string into multiple slides but only after a word.
const splitStrings = description.match(
new RegExp(
`[\\s\\S]{1,${AppConstants.maxIncidentTextLength}}(?![\\S])`,
"g"
)
);
if (splitStrings) {
splitStrings[splitStrings.length - 1] = `${
splitStrings[splitStrings.length - 1]
} ${affected}`;
return [...acc, ...splitStrings];
}
} else {
return [...acc, `${description} ${affected} `];
}
return acc;
},
[]
);
});
const onSlideEnd = () => {
trainStore.clearIncidents();
navigateTo("/", { replace: true });
Expand All @@ -52,37 +17,5 @@ onMounted(() => {
</script>

<template>
<div class="h-screen">
<div class="flex h-screen flex-col justify-between gap-8">
<div class="checkered-background"></div>
<h1 class="text-center text-7xl leading-[1.7ch] text-red-600">
SERVICE ADVISORY
</h1>
<TextCarousel :slides="incidentDetails" @slide-end="onSlideEnd" />
<div class="checkered-background"></div>
</div>
</div>
<IncidentsView :incidents="incidents" @on-slide-end="onSlideEnd" />
</template>

<style scoped>
.checkered-background {
width: 100%;
height: 50px;
background-image: linear-gradient(
45deg,
#fbbf24 25%,
transparent 25%,
transparent 75%,
#fbbf24 75%
),
linear-gradient(
45deg,
#fbbf24 25%,
transparent 25%,
transparent 75%,
#fbbf24 75%
);
background-size: 50px 50px;
background-position: 25px 50px, 50px 25px;
}
</style>
Loading

1 comment on commit 90c2a38

@vercel
Copy link

@vercel vercel bot commented on 90c2a38 Nov 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.