diff --git a/server/api/index.js b/server/api/index.js index 3b3947a..c483dec 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -13,7 +13,7 @@ export async function getTimetable(req, res) { return; } const requestedClass = req.query.class.toLowerCase(); - const timetable = await prisma.timetable.findFirst({ + const timetables = await prisma.timetable.findMany({ where: { class: requestedClass, }, @@ -22,19 +22,9 @@ export async function getTimetable(req, res) { }, }); const times = await prisma.time.findMany(); - if (!timetable) { - res.status(404).send({ - success: false, - error: "no_timetable", - message: "No timetable was found for this class", - }); - return; - } res.send({ - trusted: timetable.trusted, - source: timetable.source, - data: timetable.data, - times: times, + timetables, + times, }); } diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 36ec17d..26e3a9e 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -9,6 +9,7 @@ datasource db { model Timetable { id Int @id @unique @default(autoincrement()) + title String @default("Default") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt class String diff --git a/src/components/settings/timetable-card.vue b/src/components/settings/timetable-card.vue new file mode 100644 index 0000000..7fca0b2 --- /dev/null +++ b/src/components/settings/timetable-card.vue @@ -0,0 +1,73 @@ + + + + + + + + + + {{ timetable.title }} + {{ $t("settings.source") }}: {{ timetable.source }} + + + + + + + + + + + + diff --git a/src/components/timetable-list.vue b/src/components/timetable-list.vue index 05f0a7f..19ebf64 100644 --- a/src/components/timetable-list.vue +++ b/src/components/timetable-list.vue @@ -1,5 +1,5 @@ diff --git a/src/router/index.js b/src/router/index.js index 47b65ae..0f14795 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -8,6 +8,7 @@ import SettingsView from "@/views/SettingsView.vue"; import LoginView from "@/views/LoginView.vue"; import TokenView from "@/views/TokenView.vue"; import FilteringSettings from "@/views/settings/FilteringSettings.vue"; +import TimetableSettings from "@/views/settings/TimetableSettings.vue"; import TimetableGroupSettings from "@/views/settings/TimetableGroupSettings.vue"; import AppearanceSettings from "@/views/settings/AppearanceSettings.vue"; import AboutPage from "@/views/settings/AboutPage.vue"; @@ -53,6 +54,11 @@ const router = createRouter({ name: "title.settings.filtering", component: FilteringSettings, }, + { + path: "timetable", + name: "title.settings.timetable", + component: TimetableSettings, + }, { path: "groups", name: "title.settings.groups", diff --git a/src/store.js b/src/store.js index f12ba82..93901ed 100644 --- a/src/store.js +++ b/src/store.js @@ -10,6 +10,7 @@ export const loadingFailed = ref(false); /* Preferences */ export const classFilter = ref(localStorage.getItem("classFilter") || "none"); +export const timetableId = ref(localStorage.getItem("timetableId") || "none"); export const timetableGroups = ref( JSON.parse(localStorage.getItem("timetableGroups") || "[]") ); @@ -20,6 +21,9 @@ watch(classFilter, (newValue) => { localStorage.setItem("classFilter", newValue); fetchData(getNextAndPrevDay(selectedDate.value), false); }); +watch(timetableId, (newValue) => { + localStorage.setItem("timetableId", newValue); +}); watch( timetableGroups, (newValue) => { @@ -47,7 +51,14 @@ export const changeDay = ref(0); export const changeDate = ref(new Date()); /* Data store */ -export const timetable = ref({ trusted: true }); +export const timetable = computed(() => { + const selectedTimetable = timetables.value.find( + (e) => e.id == timetableId.value + ); + return selectedTimetable || { trusted: true, source: "", data: [] }; +}); +export const timetables = ref([]); +export const times = ref([]); export const substitutions = ref({}); export const history = ref({}); export const classList = ref([]); @@ -92,7 +103,7 @@ export async function fetchData(days, partial) { if (!partial) { await fetchClassList(); loadingProgress.value = step++ / steps; - await fetchTimetable(); + await fetchTimetables(); loadingProgress.value = step++ / steps; } for (const day of days) { @@ -117,15 +128,18 @@ export async function fetchClassList() { classList.value = classListData; } -export async function fetchTimetable() { +export async function fetchTimetables() { const timetableResponse = await fetch( `${baseUrl}/timetable?class=${classFilter.value}` ); const timetableData = await timetableResponse.json(); if (timetableData.error) { console.warn("API Error: " + timetableData.error); - timetable.value = { trusted: true, source: "", data: [] }; - } else timetable.value = timetableData; + timetables.value = []; + } else { + timetables.value = timetableData.timetables; + times.value = timetableData.times; + } } export async function fetchSubstitutions(day) { diff --git a/src/strings.js b/src/strings.js index 0e58975..b005d2f 100644 --- a/src/strings.js +++ b/src/strings.js @@ -9,6 +9,7 @@ export const strings = { settings: { main: "Settings", filtering: "Filtering", + timetable: "Manage Timetables", groups: "Timetable Groups", appearance: "Appearance", about: "About", @@ -18,6 +19,7 @@ export const strings = { heading: { filtering: "Filtering", timetableGroups: "Timetable Groups", + remoteTimetables: "Remote Timetables", language: "Language", about: "About", theme: "Theme", @@ -37,6 +39,7 @@ export const strings = { back: "Back", none: "None", version: "Version", + source: "Source", theme: { auto: "Auto", dark: "Dark", @@ -88,6 +91,7 @@ export const strings = { titles: { loading: "Loading data", loadingFailed: "Loading failed", + noTimetable: "No timetable", noEntries: "No Substitutions", noHistory: "No History", }, @@ -96,6 +100,8 @@ export const strings = { loadingTimetable: "The timetable is still being loaded", loadingFailed: "The data could not be loaded. Plase check your internet connection!", + noTimetable: + "No timetable active. You can manage your timetables in the settings!", noEntries: "There are no substitutions for this day yet", noHistory: "No substitutions for this day have changed yet", }, @@ -113,6 +119,7 @@ export const strings = { settings: { main: "Einstellungen", filtering: "Filter", + timetable: "Stundenpläne Verwalten", groups: "Stundenplan-Gruppen", appearance: "Aussehen", about: "Über", @@ -121,6 +128,7 @@ export const strings = { settings: { heading: { filtering: "Filter", + remoteTimetables: "Online Stundenpläne", timetableGroups: "Stundenplan-Gruppen", language: "Sprache", about: "Über diese Anwendung", @@ -141,6 +149,7 @@ export const strings = { back: "Zurück", none: "Keine", version: "Version", + source: "Quelle", theme: { auto: "Automatisch", dark: "Dunkel", @@ -193,6 +202,7 @@ export const strings = { titles: { loading: "Läd noch", loadingFailed: "Fehler", + noTimetable: "Kein Stundenplan", noEntries: "Keine Vertretungen", noHistory: "Noch keine Änderungen", }, @@ -201,6 +211,8 @@ export const strings = { loadingTimetable: "Der Stundenplan wird noch geladen", loadingFailed: "Die Daten konnten nicht geladen werden. Bitte überprüfe deine Internetverbindung!", + noTimetable: + "Kein Stundenplan ausgewählt. Du kannst deine Stundenpläne in den Einstellungen verwalten!", noEntries: "Es gibt noch keine Vertretungen für diesen Tag", noHistory: "An den Vertretungen für diesen Tag wurde noch nichts geändert", diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 1478116..4ba3e0a 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -3,6 +3,7 @@ import ScrollableContainer from "@/components/scrollable-container.vue"; import PageCard from "@/components/settings/page-card.vue"; import { FilterIcon, + CalendarIcon, CopyCheckIcon, PaletteIcon, InfoIcon, @@ -19,6 +20,11 @@ import { :icon="FilterIcon" route="settings/filtering" /> + import TimetableSetup from "@/components/timetable-setup.vue"; -import { classList, classFilter, timetable, loadingFailed } from "@/store"; +import { + classList, + classFilter, + timetable, + loadingFailed, + loading, +} from "@/store"; import DayCarousel from "@/components/day-carousel.vue"; import TimetableList from "@/components/timetable-list.vue"; import InfoCard from "@/components/info-card.vue"; -import { ClockIcon } from "lucide-vue-next"; -import { CloudOffIcon } from "lucide-vue-next"; +import { ClockIcon, CloudOffIcon, CalendarOffIcon } from "lucide-vue-next"; @@ -20,11 +25,18 @@ import { CloudOffIcon } from "lucide-vue-next"; > + +import TimetableCard from "@/components/settings/timetable-card.vue"; +import { timetables, timetableId } from "@/store"; + + + + {{ $t("settings.heading.remoteTimetables") }} + + + + + +