🚸 ♻️ Refactor store and improve UX
- Move view related functions out of the store - Restructure the store.js file - Improve error handling of `/check` - Show red loading spinner if data fetch failed - Prefetch current, next and previous day instead of only current - Refactor some other frontend file
This commit is contained in:
@ -4,7 +4,7 @@ import TitleBar from "./components/titlebar-element.vue";
|
|||||||
import BottomNavbar from "./components/bottom-navbar.vue";
|
import BottomNavbar from "./components/bottom-navbar.vue";
|
||||||
import DateSelector from "./components/date-selector.vue";
|
import DateSelector from "./components/date-selector.vue";
|
||||||
import LoadingElement from "./components/loading-element.vue";
|
import LoadingElement from "./components/loading-element.vue";
|
||||||
import { loading, loadingProgress, theme } from "./store";
|
import { loading, loadingProgress, loadingFailed, theme } from "./store";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
const autoThemes = { true: "dark", false: "light" };
|
const autoThemes = { true: "dark", false: "light" };
|
||||||
@ -31,7 +31,11 @@ const isDataView = computed(() => {
|
|||||||
:class="theme == 'auto' ? `theme-${autoTheme}` : `theme-${theme}`"
|
:class="theme == 'auto' ? `theme-${autoTheme}` : `theme-${theme}`"
|
||||||
>
|
>
|
||||||
<TitleBar />
|
<TitleBar />
|
||||||
<LoadingElement :active="loading" :progress="loadingProgress" />
|
<LoadingElement
|
||||||
|
:active="loading"
|
||||||
|
:progress="loadingProgress"
|
||||||
|
:error="loadingFailed"
|
||||||
|
/>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<main>
|
<main>
|
||||||
<DateSelector v-show="isDataView" />
|
<DateSelector v-show="isDataView" />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { historyOfDate } from "../store";
|
import { history } from "../store";
|
||||||
import { getSubstitutionText } from "../util";
|
import { getSubstitutionText } from "../util";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
@ -10,39 +10,27 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const history = computed(() => {
|
const historyOfDate = computed(() => {
|
||||||
return historyOfDate.value[props.date.setUTCHours(0, 0, 0, 0)];
|
return history.value[props.date.getTime()];
|
||||||
});
|
});
|
||||||
|
|
||||||
function getChar(type) {
|
const chars = {
|
||||||
switch (type) {
|
change: "~",
|
||||||
case "change":
|
addition: "+",
|
||||||
return "~";
|
deletion: "-",
|
||||||
case "addition":
|
};
|
||||||
return "+";
|
|
||||||
case "deletion":
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColor(type) {
|
|
||||||
switch (type) {
|
|
||||||
case "change":
|
|
||||||
return "background-color: var(--substitution-background-change)";
|
|
||||||
case "addition":
|
|
||||||
return "background-color: var(--substitution-background-addition);";
|
|
||||||
case "deletion":
|
|
||||||
return "background-color: var(--substitution-background-deletion);";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="history">
|
<div class="history">
|
||||||
<template v-for="event in history" :key="event">
|
<template v-for="event in historyOfDate" :key="event">
|
||||||
<div class="change" :style="getColor(event.type)">
|
<div
|
||||||
<span class="hour">{{ event.lesson }}{{ getChar(event.type) }}</span>
|
class="change"
|
||||||
|
:style="`background-color: var(--substitution-background-${event.type}`"
|
||||||
|
>
|
||||||
|
<span class="hour">{{ event.lesson }}{{ chars[event.type] }}</span>
|
||||||
<div class="infos">
|
<div class="infos">
|
||||||
|
<!-- If the entry is a change show which values changed -->
|
||||||
<span class="text" v-if="event.type == 'change'">
|
<span class="text" v-if="event.type == 'change'">
|
||||||
<template v-for="(change, key) in event.change" :key="key">
|
<template v-for="(change, key) in event.change" :key="key">
|
||||||
<p>
|
<p>
|
||||||
@ -52,6 +40,7 @@ function getColor(type) {
|
|||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
|
<!-- If the entry is an addition or deletion generate a text -->
|
||||||
<span class="text" v-else
|
<span class="text" v-else
|
||||||
>{{
|
>{{
|
||||||
$t(getSubstitutionText(event.change), {
|
$t(getSubstitutionText(event.change), {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { substitutionsForDate } from "../store";
|
import { substitutions } from "../store";
|
||||||
import { getSubstitutionText, getSubstitutionColor } from "../util";
|
import { getSubstitutionText, getSubstitutionColor } from "../util";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
|
||||||
@ -9,13 +9,13 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const substitutions = computed(() => {
|
const substitutionsForDate = computed(() => {
|
||||||
return substitutionsForDate.value[props.date.setUTCHours(0, 0, 0, 0)];
|
return substitutions.value[props.date.getTime()];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<template v-for="substitution in substitutions" :key="substitution">
|
<template v-for="substitution in substitutionsForDate" :key="substitution">
|
||||||
<div class="substitution" :style="getSubstitutionColor(substitution)">
|
<div class="substitution" :style="getSubstitutionColor(substitution)">
|
||||||
<span class="hour">{{ substitution.lesson }}</span>
|
<span class="hour">{{ substitution.lesson }}</span>
|
||||||
<div class="infos">
|
<div class="infos">
|
||||||
|
@ -17,14 +17,17 @@ const linkedTimetable = computed(() => {
|
|||||||
const newDay = currentDay.map((e, index) => {
|
const newDay = currentDay.map((e, index) => {
|
||||||
const newElement = { ...e };
|
const newElement = { ...e };
|
||||||
// Find a substitution mathing this lesson
|
// Find a substitution mathing this lesson
|
||||||
newElement.substitution = substitutions.value.find((entry) => {
|
if (substitutions.value[props.date.getTime()])
|
||||||
const entryDay = new Date(entry.date).getTime();
|
newElement.substitution = substitutions.value[props.date.getTime()].find(
|
||||||
return (
|
(entry) => {
|
||||||
entry.lesson == index + 1 &&
|
const entryDay = new Date(entry.date).getTime();
|
||||||
entryDay == props.date.getTime() &&
|
return (
|
||||||
(entry.teacher == e.teacher || !entry.teacher || !e.teacher)
|
entry.lesson == index + 1 &&
|
||||||
|
entryDay == props.date.getTime() &&
|
||||||
|
(entry.teacher == e.teacher || !entry.teacher || !e.teacher)
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
|
||||||
return newElement;
|
return newElement;
|
||||||
});
|
});
|
||||||
return newDay;
|
return newDay;
|
||||||
|
223
src/store.js
223
src/store.js
@ -1,11 +1,15 @@
|
|||||||
import { ref, watch, computed } from "vue";
|
import { ref, watch, computed } from "vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import i18n from "./main";
|
import i18n from "./main";
|
||||||
|
import { getNextAndPrevDay } from "./util";
|
||||||
|
|
||||||
|
/* Router */
|
||||||
export const lastRoute = ref();
|
export const lastRoute = ref();
|
||||||
export const loading = ref(false);
|
export const loading = ref(false);
|
||||||
export const loadingProgress = ref(0);
|
export const loadingProgress = ref(0);
|
||||||
|
export const loadingFailed = ref(false);
|
||||||
|
|
||||||
|
/* Preferences */
|
||||||
export const classFilter = ref(localStorage.getItem("classFilter") || "none");
|
export const classFilter = ref(localStorage.getItem("classFilter") || "none");
|
||||||
export const timetableGroups = ref(
|
export const timetableGroups = ref(
|
||||||
JSON.parse(localStorage.getItem("timetableGroups") || "[]")
|
JSON.parse(localStorage.getItem("timetableGroups") || "[]")
|
||||||
@ -29,12 +33,7 @@ watch(theme, (newValue) => {
|
|||||||
localStorage.setItem("theme", newValue);
|
localStorage.setItem("theme", newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set this to a positive or negative integer
|
/* Date selector */
|
||||||
// to change selectedDate by this number
|
|
||||||
export const changeDay = ref(0);
|
|
||||||
// Set this to jump to a specific date
|
|
||||||
export const changeDate = ref(new Date());
|
|
||||||
|
|
||||||
export const selectedDate = ref(new Date(new Date().setUTCHours(0, 0, 0, 0)));
|
export const selectedDate = ref(new Date(new Date().setUTCHours(0, 0, 0, 0)));
|
||||||
export const selectedDay = computed(() => selectedDate.value.getDay() - 1);
|
export const selectedDay = computed(() => selectedDate.value.getDay() - 1);
|
||||||
// Jump to next Monday if it is weekend
|
// Jump to next Monday if it is weekend
|
||||||
@ -43,109 +42,73 @@ if (selectedDay.value == 5)
|
|||||||
if (selectedDay.value == -1)
|
if (selectedDay.value == -1)
|
||||||
selectedDate.value = new Date(selectedDate.value.getTime() + 86400000);
|
selectedDate.value = new Date(selectedDate.value.getTime() + 86400000);
|
||||||
|
|
||||||
// Load new data if date changes
|
// Set this to a positive or negative integer
|
||||||
watch(selectedDate, async () => {
|
// to change selectedDate by this number
|
||||||
loadingProgress.value = 0.1;
|
export const changeDay = ref(0);
|
||||||
loading.value = true;
|
// Set this to jump to a specific date
|
||||||
await fetchSubstitutions();
|
export const changeDate = ref(new Date());
|
||||||
loadingProgress.value = 1 / 2;
|
|
||||||
await fetchHistory();
|
|
||||||
loadingProgress.value = 1;
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/* Data store */
|
||||||
export const timetable = ref({ trusted: true });
|
export const timetable = ref({ trusted: true });
|
||||||
export const substitutions = ref([]);
|
export const substitutions = ref({});
|
||||||
export const history = ref([]);
|
export const history = ref({});
|
||||||
export const classList = ref([]);
|
export const classList = ref({});
|
||||||
|
|
||||||
export const historyOfDate = computed(() => {
|
|
||||||
const dates = {};
|
|
||||||
for (const entry of history.value) {
|
|
||||||
const date = entry.date;
|
|
||||||
if (!dates[date]) dates[date] = [];
|
|
||||||
dates[entry.date].push(entry);
|
|
||||||
}
|
|
||||||
return dates;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const substitutionsForDate = computed(() => {
|
|
||||||
const dates = {};
|
|
||||||
for (const substitution of substitutions.value) {
|
|
||||||
const date = substitution.date;
|
|
||||||
if (!dates[date]) dates[date] = [];
|
|
||||||
dates[substitution.date].push(substitution);
|
|
||||||
}
|
|
||||||
return dates;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const parsedTimetable = computed(() => {
|
|
||||||
if (!timetable.value.data) return [];
|
|
||||||
return timetable.value.data.map((day) => {
|
|
||||||
const newDay = [];
|
|
||||||
for (const lesson of day) {
|
|
||||||
var usedLesson = lesson;
|
|
||||||
// Check for timetable groups
|
|
||||||
if (Array.isArray(lesson)) {
|
|
||||||
var matchingLesson = lesson.find((e) =>
|
|
||||||
timetableGroups.value.includes(e.group)
|
|
||||||
);
|
|
||||||
if (!matchingLesson) {
|
|
||||||
matchingLesson = {
|
|
||||||
subject: lesson.map((e) => e.subject).join(" / "),
|
|
||||||
teacher: i18n.global.t("timetable.configureTimetableGroup"),
|
|
||||||
length: lesson[0].length || 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
usedLesson = matchingLesson;
|
|
||||||
}
|
|
||||||
const lessonLength = usedLesson.length || 1;
|
|
||||||
delete usedLesson.length;
|
|
||||||
for (var i = 0; i < lessonLength; i++) newDay.push(usedLesson);
|
|
||||||
}
|
|
||||||
return newDay;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const possibleTimetableGroups = computed(() => {
|
|
||||||
const foundTimetableGroups = [];
|
|
||||||
if (!timetable.value.data) return [];
|
|
||||||
for (const day of timetable.value.data) {
|
|
||||||
for (const lesson of day) {
|
|
||||||
if (Array.isArray(lesson)) {
|
|
||||||
for (const group of lesson) {
|
|
||||||
if (!foundTimetableGroups.includes(group.group)) {
|
|
||||||
foundTimetableGroups.push(group.group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return foundTimetableGroups;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/* API functions */
|
||||||
|
// Set the `VITE_API_ENDPOINT` env variable when
|
||||||
|
// building the frontend to use an external api server
|
||||||
const baseUrl = import.meta.env.VITE_API_ENDPOINT || "/api";
|
const baseUrl = import.meta.env.VITE_API_ENDPOINT || "/api";
|
||||||
|
|
||||||
export async function fetchData() {
|
export async function fetchData(days, partial) {
|
||||||
|
const steps = 2 * days.length + (partial ? 0 : 2);
|
||||||
|
let step = 1;
|
||||||
|
loadingFailed.value = false;
|
||||||
loadingProgress.value = 0.1;
|
loadingProgress.value = 0.1;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
const checkResponse = await fetch(`${baseUrl}/check`);
|
// Check if the API server is reachable
|
||||||
if (checkResponse.status != 200) router.push("/login");
|
// and the user is authenticated
|
||||||
loadingProgress.value = 1 / 5;
|
try {
|
||||||
|
const checkResponse = await fetch(`${baseUrl}/check`);
|
||||||
|
if (checkResponse.status == 401) {
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
} else if (checkResponse.status != 200) {
|
||||||
|
loadingFailed.value = true;
|
||||||
|
loadingProgress.value = 1;
|
||||||
|
console.log("Other error while fetching data: " + checkResponse.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
loadingFailed.value = true;
|
||||||
|
loadingProgress.value = 1;
|
||||||
|
console.log("Error while fetching data: No internet connection!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingProgress.value = step++ / steps;
|
||||||
|
|
||||||
await fetchClassList();
|
if (!partial) {
|
||||||
loadingProgress.value = 2 / 5;
|
await fetchClassList();
|
||||||
await fetchTimetable();
|
loadingProgress.value = step++ / steps;
|
||||||
loadingProgress.value = 3 / 5;
|
await fetchTimetable();
|
||||||
await fetchSubstitutions();
|
loadingProgress.value = step++ / steps;
|
||||||
loadingProgress.value = 4 / 5;
|
}
|
||||||
await fetchHistory();
|
for (const day of days) {
|
||||||
|
await fetchSubstitutions(day);
|
||||||
|
loadingProgress.value = step++ / steps;
|
||||||
|
await fetchHistory(day);
|
||||||
|
loadingProgress.value = step++ / steps;
|
||||||
|
}
|
||||||
|
|
||||||
loadingProgress.value = 1;
|
loadingProgress.value = 1;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load new data if date changes
|
||||||
|
watch(selectedDate, () =>
|
||||||
|
fetchData(getNextAndPrevDay(selectedDate.value), true)
|
||||||
|
);
|
||||||
|
|
||||||
export async function fetchClassList() {
|
export async function fetchClassList() {
|
||||||
const classListResponse = await fetch(`${baseUrl}/classes`);
|
const classListResponse = await fetch(`${baseUrl}/classes`);
|
||||||
const classListData = await classListResponse.json();
|
const classListData = await classListResponse.json();
|
||||||
@ -163,19 +126,19 @@ export async function fetchTimetable() {
|
|||||||
} else timetable.value = timetableData;
|
} else timetable.value = timetableData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSubstitutions() {
|
export async function fetchSubstitutions(day) {
|
||||||
const requestDate = `?date=${selectedDate.value.getTime()}`;
|
const requestDate = `?date=${day}`;
|
||||||
const substitutionResponse = await fetch(
|
const substitutionResponse = await fetch(
|
||||||
classFilter.value == "none"
|
classFilter.value == "none"
|
||||||
? `${baseUrl}/substitutions${requestDate}`
|
? `${baseUrl}/substitutions${requestDate}`
|
||||||
: `${baseUrl}/substitutions${requestDate}&class=${classFilter.value}`
|
: `${baseUrl}/substitutions${requestDate}&class=${classFilter.value}`
|
||||||
);
|
);
|
||||||
const substitutionData = await substitutionResponse.json();
|
const substitutionData = await substitutionResponse.json();
|
||||||
substitutions.value = substitutionData;
|
substitutions.value[day] = substitutionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchHistory() {
|
export async function fetchHistory(day) {
|
||||||
const requestDate = `?date=${selectedDate.value.getTime()}`;
|
const requestDate = `?date=${day}`;
|
||||||
const historyResponse = await fetch(
|
const historyResponse = await fetch(
|
||||||
classFilter.value == "none"
|
classFilter.value == "none"
|
||||||
? `${baseUrl}/history${requestDate}`
|
? `${baseUrl}/history${requestDate}`
|
||||||
@ -183,7 +146,63 @@ export async function fetchHistory() {
|
|||||||
);
|
);
|
||||||
const historyData = await historyResponse.json();
|
const historyData = await historyResponse.json();
|
||||||
if (historyData.error) console.warn("API Error: " + historyData.error);
|
if (historyData.error) console.warn("API Error: " + historyData.error);
|
||||||
else history.value = historyData;
|
else history.value[day] = historyData;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchData();
|
/* Preprocess the timetable data */
|
||||||
|
export const parsedTimetable = computed(() => {
|
||||||
|
// Check if timetable data exists
|
||||||
|
if (!timetable.value.data) return [];
|
||||||
|
return timetable.value.data.map((day) => {
|
||||||
|
const parsedDay = [];
|
||||||
|
for (const lesson of day) {
|
||||||
|
let usedLesson = lesson;
|
||||||
|
// Check if lesson has multiple options
|
||||||
|
// (timetable groups)
|
||||||
|
if (Array.isArray(lesson)) {
|
||||||
|
let matchingLesson = lesson.find((e) =>
|
||||||
|
timetableGroups.value.includes(e.group)
|
||||||
|
);
|
||||||
|
// If no valid timetable group is configured
|
||||||
|
// add a dummy lesson showing a notice
|
||||||
|
if (!matchingLesson) {
|
||||||
|
matchingLesson = {
|
||||||
|
subject: lesson.map((e) => e.subject).join(" / "),
|
||||||
|
teacher: i18n.global.t("timetable.configureTimetableGroup"),
|
||||||
|
length: lesson[0].length || 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
usedLesson = matchingLesson;
|
||||||
|
}
|
||||||
|
// Duplicate the lesson if its length is > 1 for it
|
||||||
|
// to show up multiple times in the timetable view
|
||||||
|
const lessonLength = usedLesson.length || 1;
|
||||||
|
delete usedLesson.length;
|
||||||
|
for (var i = 0; i < lessonLength; i++) parsedDay.push(usedLesson);
|
||||||
|
}
|
||||||
|
return parsedDay;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const possibleTimetableGroups = computed(() => {
|
||||||
|
const foundTimetableGroups = [];
|
||||||
|
if (!timetable.value.data) return [];
|
||||||
|
// Make a list of all possible timetable groups
|
||||||
|
// found in the current timetable
|
||||||
|
for (const day of timetable.value.data) {
|
||||||
|
for (const lesson of day) {
|
||||||
|
if (Array.isArray(lesson)) {
|
||||||
|
for (const group of lesson) {
|
||||||
|
if (!foundTimetableGroups.includes(group.group)) {
|
||||||
|
foundTimetableGroups.push(group.group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundTimetableGroups;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initially fetch data for the
|
||||||
|
// current, next and previous day
|
||||||
|
fetchData(getNextAndPrevDay(selectedDate.value), false);
|
||||||
|
@ -33,3 +33,11 @@ export function getDateSkippingWeekend(date, onlyNext) {
|
|||||||
);
|
);
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNextAndPrevDay(date) {
|
||||||
|
return [
|
||||||
|
new Date(date.getTime()),
|
||||||
|
getDateSkippingWeekend(new Date(date.getTime() + 86400000)),
|
||||||
|
getDateSkippingWeekend(new Date(date.getTime() - 86400000)),
|
||||||
|
].map((e) => e.getTime());
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user