From 4e8c05026cbbb4fa823940833149f7445346c49b Mon Sep 17 00:00:00 2001
From: Robert Eggl
Date: Tue, 13 Jun 2023 19:56:11 +0200
Subject: [PATCH 01/34] strip whitespaces around username (#281)
---
rogue-thi-app/lib/backend/thi-session-handler.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/rogue-thi-app/lib/backend/thi-session-handler.js b/rogue-thi-app/lib/backend/thi-session-handler.js
index fea20be0..4bc96062 100644
--- a/rogue-thi-app/lib/backend/thi-session-handler.js
+++ b/rogue-thi-app/lib/backend/thi-session-handler.js
@@ -32,6 +32,8 @@ export async function createSession (username, password, stayLoggedIn) {
username = username.toLowerCase()
// strip domain if user entered an email address
username = username.replace(/@thi\.de$/, '')
+ // strip username to remove whitespaces
+ username = username.replace(/\s/g, '')
const { session, isStudent } = await API.login(username, password)
From 4c17093cddea0c9689b8f7a53fb536bbeff17be8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20Simon=20Gro=C3=9F?=
<81598210+FabianSimonGross@users.noreply.github.com>
Date: Mon, 19 Jun 2023 13:00:00 +0200
Subject: [PATCH 02/34] Fixed issue #282 // Fixed issue #275 (#284)
* Fixed issue #282
Now only the parts of the page that actually need the userdata are inside of the ReactPlaceholder.
Therefore the settings and legal parts are visible even when there is no data
* Fixed #275 and fixed typo in documentation
---
.../components/modal/DashboardModal.js | 7 +-
rogue-thi-app/package-lock.json | 3 +-
rogue-thi-app/pages/_app.js | 177 +++++++++--------
rogue-thi-app/pages/index.js | 6 +-
rogue-thi-app/pages/personal.js | 184 +++++++++---------
5 files changed, 198 insertions(+), 179 deletions(-)
diff --git a/rogue-thi-app/components/modal/DashboardModal.js b/rogue-thi-app/components/modal/DashboardModal.js
index 8d84a04f..1f2095f5 100644
--- a/rogue-thi-app/components/modal/DashboardModal.js
+++ b/rogue-thi-app/components/modal/DashboardModal.js
@@ -4,13 +4,12 @@ import Button from 'react-bootstrap/Button'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import ListGroup from 'react-bootstrap/ListGroup'
import Modal from 'react-bootstrap/Modal'
-import { ShowDashboardModal } from '../../pages/_app'
+import { DashboardContext, ShowDashboardModal } from '../../pages/_app'
import styles from '../../styles/Personalize.module.css'
-import { useDashboard } from '../../lib/hooks/dashboard'
/**
* A modal component that allows users to personalize their experience by changing the dashboard layout
- * @returns {JSX.Element} The DashboardModal compontent
+ * @returns {JSX.Element} The DashboardModal component
* @constructor
*/
export default function DashboardModal () {
@@ -21,7 +20,7 @@ export default function DashboardModal () {
hideDashboardEntry,
bringBackDashboardEntry,
resetOrder
- } = useDashboard()
+ } = useContext(DashboardContext)
const [showDashboardModal, setShowDashboardModal] = useContext(ShowDashboardModal)
const themeModalBody = useRef()
diff --git a/rogue-thi-app/package-lock.json b/rogue-thi-app/package-lock.json
index 8228af6a..14e7c039 100644
--- a/rogue-thi-app/package-lock.json
+++ b/rogue-thi-app/package-lock.json
@@ -22,7 +22,6 @@
"ical-generator": "^3.6.1",
"idb": "^7.0.0",
"leaflet": "^1.7.1",
- "moment": "^2.29.4",
"next": "^12.1.0",
"node-fetch": "^3.2.3",
"pdf-parse": "^1.1.1",
@@ -3666,6 +3665,8 @@
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
+ "optional": true,
+ "peer": true,
"engines": {
"node": "*"
}
diff --git a/rogue-thi-app/pages/_app.js b/rogue-thi-app/pages/_app.js
index 57dea34f..1ef34ff2 100644
--- a/rogue-thi-app/pages/_app.js
+++ b/rogue-thi-app/pages/_app.js
@@ -12,12 +12,14 @@ import { config } from '@fortawesome/fontawesome-svg-core'
import themes from '../data/themes.json'
import '../styles/globals.css'
+import { useDashboard } from '../lib/hooks/dashboard'
export const ThemeContext = createContext('default')
export const FoodFilterContext = createContext(false)
export const ShowDashboardModal = createContext(false)
export const ShowPersonalDataModal = createContext(false)
export const ShowThemeModal = createContext(false)
+export const DashboardContext = createContext({})
config.autoAddCss = false
@@ -28,6 +30,14 @@ function MyApp ({ Component, pageProps }) {
const [showDashboardModal, setShowDashboardModal] = useState(false)
const [showPersonalDataModal, setShowPersonalDataModal] = useState(false)
const foodFilter = useFoodFilter()
+ const {
+ shownDashboardEntries,
+ hiddenDashboardEntries,
+ moveDashboardEntry,
+ hideDashboardEntry,
+ bringBackDashboardEntry,
+ resetOrder
+ } = useDashboard()
useEffect(() => {
// migrate legacy cookie theme setting to localStorage
@@ -57,85 +67,94 @@ function MyApp ({ Component, pageProps }) {
-
-
-
-
-
-
-
-
-
-
-
- {/* generated using npx pwa-asset-generator (run via Dockerfile) */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {computedTheme === 'hacker' && (
-
-
-
- )}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* generated using npx pwa-asset-generator (run via Dockerfile) */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {computedTheme === 'hacker' && (
+
+
+
+ )}
+
+
+
diff --git a/rogue-thi-app/pages/index.js b/rogue-thi-app/pages/index.js
index 3dafc4a7..8fd22676 100644
--- a/rogue-thi-app/pages/index.js
+++ b/rogue-thi-app/pages/index.js
@@ -5,12 +5,10 @@ import AppContainer from '../components/page/AppContainer'
import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
-import { useDashboard } from '../lib/hooks/dashboard'
-
import styles from '../styles/Home.module.css'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPen } from '@fortawesome/free-solid-svg-icons'
-import { ShowDashboardModal } from './_app'
+import { ShowDashboardModal, DashboardContext } from './_app'
import DashboardModal from '../components/modal/DashboardModal'
/**
@@ -23,7 +21,7 @@ export default function Home () {
const {
shownDashboardEntries,
hideDashboardEntry
- } = useDashboard()
+ } = useContext(DashboardContext)
return (
diff --git a/rogue-thi-app/pages/personal.js b/rogue-thi-app/pages/personal.js
index 4c55cc78..fb818804 100644
--- a/rogue-thi-app/pages/personal.js
+++ b/rogue-thi-app/pages/personal.js
@@ -70,11 +70,11 @@ export default function Personal () {
{label}:{' '}
{value
? (
- <>
+ <>
{value}
- >
+ >
)
: null}
@@ -122,7 +122,7 @@ export default function Personal () {
-
+
{userKind === USER_STUDENT &&
@@ -140,8 +140,8 @@ export default function Personal () {
{userdata && (
<>
-
-
+
+
>
)}
@@ -166,119 +166,121 @@ export default function Personal () {
}
-
+
+
-
+
- {themes.filter(item => item.style.includes(theme[0])).map(item => (
- setShowThemeModal(true)} key={item.style}>
-
+
+
+ {themes.filter(item => item.style.includes(theme[0])).map(item => (
+ setShowThemeModal(true)} key={item.style}>
+
{item.name}{' '}
-
- Theme
-
- ))}
+
+ Theme
+
+ ))}
- setShowDashboardModal(true)}>
-
+
setShowDashboardModal(true)}>
+
-
- Dashboard
-
+
+ Dashboard
+
- setShowFoodFilterModal(true)}>
-
+
setShowFoodFilterModal(true)}>
+
-
- Essenspräferenzen
-
+
+ Essenspräferenzen
+
-
+
-
+
-
+
- window.open('https://www3.primuss.de/cgi-bin/login/index.pl?FH=fhin', '_blank')}>
-
- Primuss
-
+ window.open('https://www3.primuss.de/cgi-bin/login/index.pl?FH=fhin', '_blank')}>
+
+ Primuss
+
- window.open('https://moodle.thi.de/moodle', '_blank')}>
-
- Moodle
-
+ window.open('https://moodle.thi.de/moodle', '_blank')}>
+
+ Moodle
+
- window.open('https://outlook.thi.de/', '_blank')}>
+ window.open('https://outlook.thi.de/', '_blank')}>
+
+ E-Mail
+
+
+ {userKind === USER_EMPLOYEE &&
+ window.open('https://mythi.de', '_blank')}>
- E-Mail
+ MyTHI
+ }
+
- {userKind === USER_EMPLOYEE &&
- window.open('https://mythi.de', '_blank')}>
-
- MyTHI
-
- }
-
-
-
-
-
-
- {showDebug && (
- router.push('/debug')}>
-
- API Spielwiese
-
- )}
+
- window.open(PRIVACY_URL, '_blank')}>
-
- Datenschutzerklärung
-
+
- router.push('/imprint')}>
-
- Impressum
+ {showDebug && (
+ router.push('/debug')}>
+
+ API Spielwiese
+ )}
+
+ window.open(PRIVACY_URL, '_blank')}>
+
+ Datenschutzerklärung
+
+
+ router.push('/imprint')}>
+
+ Impressum
+
+
+
+
+
+
+
+ {userKind === USER_GUEST && (
+ forgetSession(router)}>
+ {'Login '}
+
+
+ )}
+ {userKind !== USER_GUEST && (
+ forgetSession(router)}>
+ {'Logout '}
+
+
+ )}
+
+
+
+
+
-
-
-
-
-
- {userKind === USER_GUEST && (
- forgetSession(router)}>
- {'Login '}
-
-
- )}
- {userKind !== USER_GUEST && (
- forgetSession(router)}>
- {'Logout '}
-
-
- )}
-
-
-
-
-
-
-
)
From 8ec57119f5ae4f2860fc163853aefcec7d1211ee Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Fri, 23 Jun 2023 16:10:20 +0200
Subject: [PATCH 03/34] =?UTF-8?q?=F0=9F=8C=90=20Add=20i18n=20and=20interna?=
=?UTF-8?q?tionalize=20app?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/.env | 3 +-
rogue-thi-app/components/RoomMap.js | 79 ++++--
rogue-thi-app/components/allCards.js | 19 +-
rogue-thi-app/components/cards/BaseCard.js | 13 +-
.../components/cards/CalendarCard.js | 8 +-
.../components/cards/ElectionPrompt.js | 10 +-
rogue-thi-app/components/cards/EventsCard.js | 7 +-
rogue-thi-app/components/cards/FoodCard.js | 41 +--
.../components/cards/InstallPrompt.js | 60 +++--
.../components/cards/MobilityCard.js | 22 +-
rogue-thi-app/components/cards/RoomCard.js | 6 +-
.../components/cards/SurveyPrompt.js | 5 +-
.../components/cards/TimetableCard.js | 17 +-
.../components/modal/DashboardModal.js | 25 +-
.../components/modal/FilterFoodModal.js | 44 +--
.../components/modal/LanguageModal.js | 58 ++++
.../components/modal/PersonalDataModal.js | 41 +--
rogue-thi-app/components/modal/ThemeModal.js | 42 ++-
rogue-thi-app/components/page/AppNavbar.js | 9 +-
rogue-thi-app/data/allergens.json | 187 ++++++++++---
rogue-thi-app/data/languages.json | 4 +
rogue-thi-app/data/mensa-flags.json | 81 ++++--
rogue-thi-app/data/themes.json | 16 +-
rogue-thi-app/lib/backend-utils/food-utils.js | 3 +-
.../lib/backend-utils/mobility-utils.js | 51 ++--
.../lib/backend-utils/translation-utils.js | 58 ++++
rogue-thi-app/lib/date-utils.js | 41 ++-
rogue-thi-app/lib/hooks/dashboard.js | 2 +-
rogue-thi-app/lib/locale-utils.js | 8 +
rogue-thi-app/next-i18next.config.js | 8 +
rogue-thi-app/next.config.js | 7 +-
rogue-thi-app/package-lock.json | 129 +++++++++
rogue-thi-app/package.json | 4 +
rogue-thi-app/pages/_app.js | 205 +++++++-------
rogue-thi-app/pages/api/canisius.js | 13 +-
rogue-thi-app/pages/api/mensa.js | 21 +-
rogue-thi-app/pages/api/reimanns.js | 5 +-
rogue-thi-app/pages/calendar.js | 73 +++--
rogue-thi-app/pages/events.js | 19 +-
rogue-thi-app/pages/food.js | 132 +++++----
rogue-thi-app/pages/grades.js | 63 +++--
rogue-thi-app/pages/imprint.js | 31 ++-
rogue-thi-app/pages/index.js | 23 +-
rogue-thi-app/pages/lecturers.js | 63 +++--
rogue-thi-app/pages/library.js | 67 +++--
rogue-thi-app/pages/login.js | 74 +++--
rogue-thi-app/pages/mobility.js | 61 +++--
rogue-thi-app/pages/personal.js | 134 +++++----
rogue-thi-app/pages/rooms/index.js | 18 +-
rogue-thi-app/pages/rooms/list.js | 16 +-
rogue-thi-app/pages/rooms/search.js | 43 ++-
rogue-thi-app/pages/rooms/suggestions.js | 83 ++++--
rogue-thi-app/pages/timetable.js | 134 +++++----
.../public/locales/de/api-translations.json | 104 +++++++
.../public/locales/de/become-hackerman.json | 1 +
rogue-thi-app/public/locales/de/calendar.json | 33 +++
rogue-thi-app/public/locales/de/common.json | 71 +++++
.../public/locales/de/dashboard.json | 85 ++++++
rogue-thi-app/public/locales/de/debug.json | 1 +
rogue-thi-app/public/locales/de/events.json | 11 +
rogue-thi-app/public/locales/de/food.json | 76 ++++++
rogue-thi-app/public/locales/de/grades.json | 31 +++
rogue-thi-app/public/locales/de/imprint.json | 18 ++
rogue-thi-app/public/locales/de/index.json | 1 +
.../public/locales/de/lecturers.json | 33 +++
rogue-thi-app/public/locales/de/library.json | 38 +++
rogue-thi-app/public/locales/de/login.json | 32 +++
rogue-thi-app/public/locales/de/mobility.json | 45 ++++
rogue-thi-app/public/locales/de/personal.json | 51 ++++
rogue-thi-app/public/locales/de/rooms.json | 68 +++++
.../public/locales/de/timetable.json | 77 ++++++
.../public/locales/en/api-translations.json | 104 +++++++
.../public/locales/en/become-hackerman.json | 1 +
rogue-thi-app/public/locales/en/calendar.json | 33 +++
rogue-thi-app/public/locales/en/common.json | 71 +++++
.../public/locales/en/dashboard.json | 85 ++++++
rogue-thi-app/public/locales/en/debug.json | 1 +
rogue-thi-app/public/locales/en/events.json | 11 +
rogue-thi-app/public/locales/en/food.json | 75 ++++++
rogue-thi-app/public/locales/en/grades.json | 31 +++
rogue-thi-app/public/locales/en/imprint.json | 18 ++
rogue-thi-app/public/locales/en/index.json | 1 +
.../public/locales/en/lecturers.json | 33 +++
rogue-thi-app/public/locales/en/library.json | 38 +++
rogue-thi-app/public/locales/en/login.json | 32 +++
rogue-thi-app/public/locales/en/mobility.json | 46 ++++
rogue-thi-app/public/locales/en/personal.json | 51 ++++
rogue-thi-app/public/locales/en/rooms.json | 68 +++++
.../public/locales/en/timetable.json | 77 ++++++
rogue-thi-app/styles/Mensa.module.css | 5 +
rogue-thi-app/styles/Personal.module.css | 2 +-
thi-translator/.gitignore | 1 +
thi-translator/README.md | 37 +++
thi-translator/requirements.txt | 3 +
thi-translator/thi-translator.py | 255 ++++++++++++++++++
95 files changed, 3511 insertions(+), 730 deletions(-)
create mode 100644 rogue-thi-app/components/modal/LanguageModal.js
create mode 100644 rogue-thi-app/data/languages.json
create mode 100644 rogue-thi-app/lib/backend-utils/translation-utils.js
create mode 100644 rogue-thi-app/lib/locale-utils.js
create mode 100644 rogue-thi-app/next-i18next.config.js
create mode 100644 rogue-thi-app/public/locales/de/api-translations.json
create mode 100644 rogue-thi-app/public/locales/de/become-hackerman.json
create mode 100644 rogue-thi-app/public/locales/de/calendar.json
create mode 100644 rogue-thi-app/public/locales/de/common.json
create mode 100644 rogue-thi-app/public/locales/de/dashboard.json
create mode 100644 rogue-thi-app/public/locales/de/debug.json
create mode 100644 rogue-thi-app/public/locales/de/events.json
create mode 100644 rogue-thi-app/public/locales/de/food.json
create mode 100644 rogue-thi-app/public/locales/de/grades.json
create mode 100644 rogue-thi-app/public/locales/de/imprint.json
create mode 100644 rogue-thi-app/public/locales/de/index.json
create mode 100644 rogue-thi-app/public/locales/de/lecturers.json
create mode 100644 rogue-thi-app/public/locales/de/library.json
create mode 100644 rogue-thi-app/public/locales/de/login.json
create mode 100644 rogue-thi-app/public/locales/de/mobility.json
create mode 100644 rogue-thi-app/public/locales/de/personal.json
create mode 100644 rogue-thi-app/public/locales/de/rooms.json
create mode 100644 rogue-thi-app/public/locales/de/timetable.json
create mode 100644 rogue-thi-app/public/locales/en/api-translations.json
create mode 100644 rogue-thi-app/public/locales/en/become-hackerman.json
create mode 100644 rogue-thi-app/public/locales/en/calendar.json
create mode 100644 rogue-thi-app/public/locales/en/common.json
create mode 100644 rogue-thi-app/public/locales/en/dashboard.json
create mode 100644 rogue-thi-app/public/locales/en/debug.json
create mode 100644 rogue-thi-app/public/locales/en/events.json
create mode 100644 rogue-thi-app/public/locales/en/food.json
create mode 100644 rogue-thi-app/public/locales/en/grades.json
create mode 100644 rogue-thi-app/public/locales/en/imprint.json
create mode 100644 rogue-thi-app/public/locales/en/index.json
create mode 100644 rogue-thi-app/public/locales/en/lecturers.json
create mode 100644 rogue-thi-app/public/locales/en/library.json
create mode 100644 rogue-thi-app/public/locales/en/login.json
create mode 100644 rogue-thi-app/public/locales/en/mobility.json
create mode 100644 rogue-thi-app/public/locales/en/personal.json
create mode 100644 rogue-thi-app/public/locales/en/rooms.json
create mode 100644 rogue-thi-app/public/locales/en/timetable.json
create mode 100644 thi-translator/.gitignore
create mode 100644 thi-translator/README.md
create mode 100644 thi-translator/requirements.txt
create mode 100644 thi-translator/thi-translator.py
diff --git a/rogue-thi-app/.env b/rogue-thi-app/.env
index 18263682..ccfceb8e 100644
--- a/rogue-thi-app/.env
+++ b/rogue-thi-app/.env
@@ -4,4 +4,5 @@ NEXT_PUBLIC_PRIVACY_URL=https://assets.neuland.app/datenschutzerklaerung-app.pdf
NEXT_PUBLIC_GIT_URL=https://github.com/neuland-ingolstadt/THI-App
NEXT_PUBLIC_FEEDBACK_URL=mailto:app-feedback@informatik.sexy
NEXT_PUBLIC_WEBSITE_URL=https://neuland-ingolstadt.de
-NEXT_PUBLIC_CTF_URL=https://neuland-ingolstadt.de/ctf-training/
\ No newline at end of file
+NEXT_PUBLIC_CTF_URL=https://neuland-ingolstadt.de/ctf-training/
+NEXT_PUBLIC_DEEPL_ENDPOINT=https://api-free.deepl.com/v2/translate
\ No newline at end of file
diff --git a/rogue-thi-app/components/RoomMap.js b/rogue-thi-app/components/RoomMap.js
index b2f6c9a2..61c831a8 100644
--- a/rogue-thi-app/components/RoomMap.js
+++ b/rogue-thi-app/components/RoomMap.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import Link from 'next/link'
@@ -15,6 +15,7 @@ import { formatFriendlyTime, formatISODate, formatISOTime } from '../lib/date-ut
import { useLocation } from '../lib/hooks/geolocation'
import styles from '../styles/RoomMap.module.css'
+import { useTranslation } from 'next-i18next'
const SPECIAL_ROOMS = {
G308: { text: 'Linux PC-Pool', color: '#F5BD0C' }
@@ -52,9 +53,18 @@ export default function RoomMap ({ highlight, roomData }) {
const searchField = useRef()
const location = useLocation()
const userKind = useUserKind()
- const [searchText, setSearchText] = useState(highlight ? highlight.toUpperCase() : '')
+ const [searchText, setSearchText] = useState(highlight || '')
const [availableRooms, setAvailableRooms] = useState(null)
+ const { t, i18n } = useTranslation(['rooms', 'api-translations'])
+
+ const getTranslatedFunction = useCallback((room) => {
+ const roomFunctionCleaned = room?.properties?.Funktion?.replace(/\s+/g, ' ')?.trim() ?? ''
+
+ const roomFunction = t(`apiTranslations.roomFunctions.${roomFunctionCleaned}`, { ns: 'api-translations' })
+ return roomFunction === `apiTranslations.roomFunctions.${roomFunctionCleaned}` ? roomFunctionCleaned : roomFunction
+ }, [t])
+
/**
* Preprocessed room data for Leaflet.
*/
@@ -91,10 +101,19 @@ export default function RoomMap ({ highlight, roomData }) {
return [allRooms, DEFAULT_CENTER]
}
- const getProp = (room, prop) => room.properties[prop]?.toUpperCase()
- const fullTextSearcher = room => SEARCHED_PROPERTIES.some(x => getProp(room, x)?.includes(searchText))
- const roomOnlySearcher = room => getProp(room, 'Raum').startsWith(searchText)
- const filtered = allRooms.filter(/^[A-Z](G|[0-9E]\.)?\d*$/.test(searchText) ? roomOnlySearcher : fullTextSearcher)
+ const cleanedText = searchText.toUpperCase().trim()
+
+ const getProp = (room, prop) => {
+ if (prop === 'Funktion') {
+ return getTranslatedFunction(room).toUpperCase()
+ }
+
+ return room.properties[prop]?.toUpperCase()
+ }
+
+ const fullTextSearcher = room => SEARCHED_PROPERTIES.some(x => getProp(room, x)?.includes(cleanedText))
+ const roomOnlySearcher = room => getProp(room, 'Raum').startsWith(cleanedText)
+ const filtered = allRooms.filter(/^[A-Z](G|[0-9E]\.)?\d*$/.test(cleanedText) ? roomOnlySearcher : fullTextSearcher)
let lon = 0
let lat = 0
@@ -107,7 +126,7 @@ export default function RoomMap ({ highlight, roomData }) {
const filteredCenter = count > 0 ? [lon / count, lat / count] : DEFAULT_CENTER
return [filtered, filteredCenter]
- }, [searchText, allRooms])
+ }, [searchText, allRooms, getTranslatedFunction])
useEffect(() => {
async function load () {
@@ -129,6 +148,21 @@ export default function RoomMap ({ highlight, roomData }) {
load()
}, [router, highlight, userKind])
+ /**
+ * Translates the floor name to the current language.
+ * @param {string} floor The floor name as specified in the data
+ * @returns The translated floor name (or the original if not found)
+ */
+ function translateFloors (floor) {
+ const translated = t(`rooms.map.floors.${floor.toLowerCase()}`)
+
+ if (translated.startsWith('rooms.')) {
+ return floor
+ }
+
+ return translated
+ }
+
/**
* Removes focus from the search.
*/
@@ -165,18 +199,19 @@ export default function RoomMap ({ highlight, roomData }) {
{entry.properties.Raum}
- {`, ${entry.properties.Funktion}`}
+ {`, ${getTranslatedFunction(entry, i18n)}`}
{avail && (
<>
- Frei
- {' '}von {formatFriendlyTime(avail.from)}
- {' '}bis {formatFriendlyTime(avail.until)}
+ {t('rooms.map.freeFromUntil', {
+ from: formatFriendlyTime(avail.from),
+ until: formatFriendlyTime(avail.until)
+ })}
>
)}
{!avail && availableRooms && (
<>
- Belegt
+ {t('rooms.map.occupied')}
>
)}
{special?.text}
@@ -216,16 +251,16 @@ export default function RoomMap ({ highlight, roomData }) {
setSearchText(e.target.value.toUpperCase())}
+ onChange={e => setSearchText(e.target.value)}
isInvalid={filteredRooms.length === 0}
ref={searchField}
/>
- Erweiterte Suche
+ {t('rooms.map.extendedSearch')}
{userKind !== USER_GUEST &&
@@ -233,7 +268,7 @@ export default function RoomMap ({ highlight, roomData }) {
<> · >
- Automatische Vorschläge
+ {t('rooms.map.automaticSuggestion')}
>
@@ -253,7 +288,7 @@ export default function RoomMap ({ highlight, roomData }) {
tap={false}
>
{availableRooms && (
<>
- Frei
- Belegt
+ {` ${t('rooms.map.legend.free')}`}
+ {` ${t('rooms.map.legend.occupied')}`}
>
)}
- {!availableRooms && Belegtstatus unbekannt
}
+ {!availableRooms && {` ${t('rooms.map.legend.occupancyUnknown')}`}
}
{SPECIAL_COLORS.map(color => (
))}
- {' '}Sonderausstattung
+ {` ${t('rooms.map.legend.specialEquipment')}`}
@@ -286,7 +321,7 @@ export default function RoomMap ({ highlight, roomData }) {
.map((floorName, i, filteredFloorOrder) => (
diff --git a/rogue-thi-app/components/allCards.js b/rogue-thi-app/components/allCards.js
index 9626301e..0e1ca540 100644
--- a/rogue-thi-app/components/allCards.js
+++ b/rogue-thi-app/components/allCards.js
@@ -21,7 +21,6 @@ export const PLATFORM_MOBILE = 'mobile'
export const ALL_DASHBOARD_CARDS = [
{
key: 'install',
- label: 'Installation',
removable: true,
default: [PLATFORM_MOBILE, USER_STUDENT, USER_EMPLOYEE, USER_GUEST],
card: hidePromptCard => (
@@ -33,98 +32,88 @@ export const ALL_DASHBOARD_CARDS = [
},
{
key: 'timetable',
- label: 'Stundenplan',
removable: true,
default: [PLATFORM_DESKTOP, USER_STUDENT, USER_EMPLOYEE],
card: () =>
},
{
key: 'mensa',
- label: 'Essen',
removable: true,
default: [PLATFORM_DESKTOP, USER_STUDENT, USER_EMPLOYEE, USER_GUEST],
card: () =>
},
{
key: 'mobility',
- label: 'Mobilität',
removable: true,
default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT, USER_EMPLOYEE, USER_GUEST],
card: () =>
},
{
key: 'calendar',
- label: 'Termine',
removable: true,
default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT, USER_EMPLOYEE, USER_GUEST],
card: () =>
},
{
key: 'events',
- label: 'Veranstaltungen',
removable: true,
default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT, USER_EMPLOYEE, USER_GUEST],
card: () =>
},
{
key: 'rooms',
- label: 'Raumplan',
removable: true,
default: [PLATFORM_DESKTOP, USER_STUDENT, USER_EMPLOYEE, USER_GUEST],
card: () =>
},
{
key: 'library',
- label: 'Bibliothek',
removable: true,
default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT, USER_EMPLOYEE],
card: () => (
)
},
{
key: 'grades',
- label: 'Noten & Fächer',
removable: true,
default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT],
card: () => (
)
},
{
key: 'personal',
- label: 'Profil',
removable: false,
default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT, USER_EMPLOYEE, USER_GUEST],
card: () => (
)
},
{
key: 'lecturers',
- label: 'Dozenten',
removable: true,
default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT, USER_EMPLOYEE],
card: () => (
)
diff --git a/rogue-thi-app/components/cards/BaseCard.js b/rogue-thi-app/components/cards/BaseCard.js
index 2615f896..d64aca05 100644
--- a/rogue-thi-app/components/cards/BaseCard.js
+++ b/rogue-thi-app/components/cards/BaseCard.js
@@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import styles from '../../styles/Home.module.css'
+import { useTranslation } from 'next-i18next'
/**
* Base card which all dashboard cards should extend.
@@ -17,7 +18,8 @@ import styles from '../../styles/Home.module.css'
* @param {string} className Class name to attach to the card
* @param {object[]} children Body of the card
*/
-export default function BaseCard ({ link, icon, title, className, children }) {
+export default function BaseCard ({ link, icon, title, i18nKey, className, children }) {
+ const { t } = useTranslation('dashboard')
return (
@@ -25,7 +27,9 @@ export default function BaseCard ({ link, icon, title, className, children }) {
{' '}
- {title}
+ {
+ title || t(`${i18nKey}.title`)
+ }
@@ -37,9 +41,10 @@ export default function BaseCard ({ link, icon, title, className, children }) {
)
}
BaseCard.propTypes = {
- link: PropTypes.string,
- icon: PropTypes.object,
+ link: PropTypes.string.isRequired,
+ icon: PropTypes.object.isRequired,
title: PropTypes.string,
+ i18nKey: PropTypes.string,
className: PropTypes.string,
children: PropTypes.any
}
diff --git a/rogue-thi-app/components/cards/CalendarCard.js b/rogue-thi-app/components/cards/CalendarCard.js
index 9c2abc33..ea3fee4b 100644
--- a/rogue-thi-app/components/cards/CalendarCard.js
+++ b/rogue-thi-app/components/cards/CalendarCard.js
@@ -10,6 +10,7 @@ import { NoSessionError } from '../../lib/backend/thi-session-handler'
import { formatFriendlyRelativeTime } from '../../lib/date-utils'
import { useTime } from '../../lib/hooks/time-hook'
+import { useTranslation } from 'next-i18next'
/**
* Dashboard card for semester and exam dates.
*/
@@ -17,6 +18,7 @@ export default function CalendarCard () {
const router = useRouter()
const time = useTime()
const [mixedCalendar, setMixedCalendar] = useState(calendar)
+ const { t } = useTranslation('dashboard')
useEffect(() => {
async function load () {
@@ -46,7 +48,7 @@ export default function CalendarCard () {
return (
@@ -57,8 +59,8 @@ export default function CalendarCard () {
{(x.end && x.begin < time)
- ? 'endet ' + formatFriendlyRelativeTime(x.end)
- : 'beginnt ' + formatFriendlyRelativeTime(x.begin)}
+ ? t('calendar.date.ends') + ' ' + formatFriendlyRelativeTime(x.end)
+ : t('calendar.date.starts') + ' ' + formatFriendlyRelativeTime(x.begin)}
))}
diff --git a/rogue-thi-app/components/cards/ElectionPrompt.js b/rogue-thi-app/components/cards/ElectionPrompt.js
index 65231317..7cdd4489 100644
--- a/rogue-thi-app/components/cards/ElectionPrompt.js
+++ b/rogue-thi-app/components/cards/ElectionPrompt.js
@@ -7,6 +7,7 @@ import { faCheckToSlot, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import styles from '../../styles/Home.module.css'
+import { useTranslation } from 'next-i18next'
const electionUrl = process.env.NEXT_PUBLIC_ELECTION_URL
@@ -15,24 +16,25 @@ const electionUrl = process.env.NEXT_PUBLIC_ELECTION_URL
* @param {object} onHide Invoked when the user wants to hide the prompt
*/
export default function ElectionPrompt ({ onHide }) {
+ const { t } = useTranslation(['dashboard'])
return (
{' '}
- Hochschulwahlen
+ {t('election.title')}
onHide()}>
-
+
- Aktuell finden die Hochschulwahlen statt. Deine Teilnahme ist wichtig, um die demokratischen Strukturen an unserer Hochschule zu stärken.
+ {t('election.text')}
- Stimme online abgeben
+ {t('election.button')}
diff --git a/rogue-thi-app/components/cards/EventsCard.js b/rogue-thi-app/components/cards/EventsCard.js
index 15b2c6ff..f36fc13f 100644
--- a/rogue-thi-app/components/cards/EventsCard.js
+++ b/rogue-thi-app/components/cards/EventsCard.js
@@ -8,12 +8,15 @@ import BaseCard from './BaseCard'
import NeulandAPI from '../../lib/backend/neuland-api'
+import { useTranslation } from 'next-i18next'
+
/**
* Dashboard card for CL events.
*/
export default function EventsCard () {
const router = useRouter()
const [calendar, setCalendar] = useState([])
+ const { t } = useTranslation(['dashboard'])
useEffect(() => {
async function load () {
@@ -31,7 +34,7 @@ export default function EventsCard () {
return (
@@ -41,7 +44,7 @@ export default function EventsCard () {
{x.title}
- von {x.organizer}
+ {t('events.organizer.attribute')} {x.organizer}
))}
diff --git a/rogue-thi-app/components/cards/FoodCard.js b/rogue-thi-app/components/cards/FoodCard.js
index 76db399f..904635f5 100644
--- a/rogue-thi-app/components/cards/FoodCard.js
+++ b/rogue-thi-app/components/cards/FoodCard.js
@@ -7,7 +7,7 @@ import BaseCard from './BaseCard'
import { FoodFilterContext } from '../../pages/_app'
import { formatISODate } from '../../lib/date-utils'
import { loadFoodEntries } from '../../lib/backend-utils/food-utils'
-
+import { useTranslation } from 'next-i18next'
/**
* Dashboard card for Mensa and Reimanns food plans.
*/
@@ -20,18 +20,30 @@ export default function FoodCard () {
preferencesSelection,
allergenSelection
} = useContext(FoodFilterContext)
+ const { i18n, t } = useTranslation(['dashboard', 'food'])
useEffect(() => {
async function load () {
const restaurants = localStorage.selectedRestaurants
? JSON.parse(localStorage.selectedRestaurants)
: ['mensa']
- if (restaurants.length === 1 && restaurants[0] === 'mensa') {
- setFoodCardTitle('Mensa')
- } else if (restaurants.length === 1 && restaurants[0] === 'reimanns') {
- setFoodCardTitle('Reimanns')
+ if (restaurants.length !== 1) {
+ setFoodCardTitle('food')
} else {
- setFoodCardTitle('Essen')
+ switch (restaurants[0]) {
+ case 'mensa':
+ setFoodCardTitle('cafeteria')
+ break
+ case 'reimanns':
+ setFoodCardTitle('reimanns')
+ break
+ case 'canisius':
+ setFoodCardTitle('canisius')
+ break
+ default:
+ setFoodCardTitle('food')
+ break
+ }
}
const today = formatISODate(new Date())
@@ -52,7 +64,7 @@ export default function FoodCard () {
const todayEntries = entries
.find(x => x.timestamp === today)
?.meals
- .filter(x => x.category !== 'Suppe' && selectedRestaurants.includes(x.restaurant.toLowerCase()))
+ .filter(x => (x.category !== 'Suppe' && x.category !== 'Salat') && selectedRestaurants.includes(x.restaurant.toLowerCase()))
todayEntries?.sort((a, b) => userMealRating(b) - userMealRating(a))
@@ -60,11 +72,11 @@ export default function FoodCard () {
setFoodEntries([])
} else if (todayEntries.length > 2) {
setFoodEntries([
- todayEntries[0].name,
- `und ${todayEntries.length - 1} weitere Gerichte`
+ todayEntries[0].name[i18n.languages[0]],
+ `${t('food.text.additional', { count: todayEntries.length - 1 })}`
])
} else {
- setFoodEntries(todayEntries.map(x => x.name))
+ setFoodEntries(todayEntries.map(x => x.name[i18n.languages[0]]))
}
} catch (e) {
console.error(e)
@@ -72,12 +84,12 @@ export default function FoodCard () {
}
}
load()
- }, [selectedRestaurants, preferencesSelection, allergenSelection])
+ }, [selectedRestaurants, preferencesSelection, allergenSelection, t, i18n])
return (
@@ -89,12 +101,11 @@ export default function FoodCard () {
))}
{foodEntries && foodEntries.length === 0 &&
- Der heutige Speiseplan ist leer.
+ {t('food.error.empty')}
}
{foodError &&
- Fehler beim Abruf des Speiseplans.
- Irgendetwas scheint kaputt zu sein.
+ {t('food.error.generic')}
}
diff --git a/rogue-thi-app/components/cards/InstallPrompt.js b/rogue-thi-app/components/cards/InstallPrompt.js
index 14d0b7e8..d07efd35 100644
--- a/rogue-thi-app/components/cards/InstallPrompt.js
+++ b/rogue-thi-app/components/cards/InstallPrompt.js
@@ -7,6 +7,7 @@ import { faDownload, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { OS_ANDROID, OS_IOS, useOperatingSystem } from '../../lib/hooks/os-hook'
+import { Trans, useTranslation } from 'next-i18next'
import styles from '../../styles/Home.module.css'
@@ -17,6 +18,7 @@ import styles from '../../styles/Home.module.css'
export default function InstallPrompt ({ onHide }) {
const [showPrompt, setShowPrompt] = useState(false)
const os = useOperatingSystem()
+ const { t } = useTranslation(['dashboard'])
useEffect(() => {
if (Capacitor.isNativePlatform()) {
@@ -38,30 +40,42 @@ export default function InstallPrompt ({ onHide }) {
}
return showPrompt && (
-
-
-
-
- {' '}
- Installation
- close()}>
-
-
-
+
+
+
+
+ {' '}
+ {t('install.title')}
+ close()}>
+
+
+
+
+ }}
+ />
+
+ {showPrompt === OS_IOS &&
- Möchtest du diese App auf deinem Smartphone installieren?
+ }}
+ />
- {showPrompt === OS_IOS &&
-
- Drücke in Safari auf Teilen und dann auf Zum Home-Bildschirm .
-
- }
- {showPrompt === OS_ANDROID &&
-
- Öffne in Chrome das Menü und drücke dann auf Zum Startbildschirm zufügen .
-
- }
-
-
+ }
+ {showPrompt === OS_ANDROID &&
+
+ }}
+ />
+
+ }
+
+
)
}
diff --git a/rogue-thi-app/components/cards/MobilityCard.js b/rogue-thi-app/components/cards/MobilityCard.js
index e6af8d84..701a3c02 100644
--- a/rogue-thi-app/components/cards/MobilityCard.js
+++ b/rogue-thi-app/components/cards/MobilityCard.js
@@ -10,15 +10,16 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import {
+ RenderMobilityEntry,
getMobilityEntries,
getMobilityLabel,
- getMobilitySettings,
- renderMobilityEntry
+ getMobilitySettings
} from '../../lib/backend-utils/mobility-utils'
import BaseCard from './BaseCard'
import { useTime } from '../../lib/hooks/time-hook'
import styles from '../../styles/Home.module.css'
+import { useTranslation } from 'next-i18next'
const MAX_STATION_LENGTH = 20
const MOBILITY_ICONS = {
@@ -36,13 +37,14 @@ export default function MobilityCard () {
const [mobility, setMobility] = useState(null)
const [mobilityError, setMobilityError] = useState(null)
const [mobilitySettings, setMobilitySettings] = useState(null)
+ const { t } = useTranslation(['dashboard', 'mobility'])
const mobilityIcon = useMemo(() => {
return mobilitySettings ? MOBILITY_ICONS[mobilitySettings.kind] : faBus
}, [mobilitySettings])
const mobilityLabel = useMemo(() => {
- return mobilitySettings ? getMobilityLabel(mobilitySettings.kind, mobilitySettings.station) : 'Mobilität'
- }, [mobilitySettings])
+ return mobilitySettings ? getMobilityLabel(mobilitySettings.kind, mobilitySettings.station, t) : t('transport.title.unknown')
+ }, [mobilitySettings, t])
useEffect(() => {
setMobilitySettings(getMobilitySettings())
@@ -58,31 +60,31 @@ export default function MobilityCard () {
setMobility(await getMobilityEntries(mobilitySettings.kind, mobilitySettings.station))
} catch (e) {
console.error(e)
- setMobilityError('Fehler beim Abruf.')
+ setMobilityError(t('transport.error.retrieval'))
}
}
load()
- }, [mobilitySettings, time])
+ }, [mobilitySettings, time, t])
return (
{mobility && mobility.slice(0, 4).map((entry, i) =>
- {renderMobilityEntry(mobilitySettings.kind, entry, MAX_STATION_LENGTH, styles, false)}
+
)}
{mobility && mobility.length === 0 &&
- Keine Elemente.
+ {t('transport.error.empty')}
}
{mobilityError &&
- Fehler beim Abruf.
+ {t('transport.error.generic')}
}
diff --git a/rogue-thi-app/components/cards/RoomCard.js b/rogue-thi-app/components/cards/RoomCard.js
index 1a763d96..672758a3 100644
--- a/rogue-thi-app/components/cards/RoomCard.js
+++ b/rogue-thi-app/components/cards/RoomCard.js
@@ -14,6 +14,7 @@ import ReactPlaceholder from 'react-placeholder/lib'
import { USER_STUDENT, useUserKind } from '../../lib/hooks/user-kind'
import Link from 'next/link'
+import { useTranslation } from 'next-i18next'
/**
* Dashboard card for semester and exam dates.
@@ -22,6 +23,7 @@ export default function RoomCard () {
const router = useRouter()
const [filterResults, setFilterResults] = useState(null)
const userKind = useUserKind()
+ const { t } = useTranslation('dashboard')
useEffect(() => {
async function load () {
@@ -71,7 +73,7 @@ export default function RoomCard () {
return (
@@ -85,7 +87,7 @@ export default function RoomCard () {
{x.room}
- Frei von {formatFriendlyTime(x.from)} bis {formatFriendlyTime(x.until)}
+ {t('rooms.text', { from: formatFriendlyTime(x.from), until: formatFriendlyTime(x.until) })}
diff --git a/rogue-thi-app/components/cards/SurveyPrompt.js b/rogue-thi-app/components/cards/SurveyPrompt.js
index 573ed77f..1130e970 100644
--- a/rogue-thi-app/components/cards/SurveyPrompt.js
+++ b/rogue-thi-app/components/cards/SurveyPrompt.js
@@ -7,6 +7,7 @@ import { faPoll, faTimes } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import styles from '../../styles/Home.module.css'
+import { useTranslation } from 'next-i18next'
const SURVEY_URL = process.env.NEXT_PUBLIC_SURVEY_URL
@@ -15,6 +16,8 @@ const SURVEY_URL = process.env.NEXT_PUBLIC_SURVEY_URL
* @param {object} onHide Invoked when the user wants to hide the prompt
*/
export default function SurveyPrompt ({ onHide }) {
+ const { t } = useTranslation(['dashboard'])
+
return (
@@ -23,7 +26,7 @@ export default function SurveyPrompt ({ onHide }) {
{' '}
Umfrage
onHide()}>
-
+
diff --git a/rogue-thi-app/components/cards/TimetableCard.js b/rogue-thi-app/components/cards/TimetableCard.js
index 0d7111c1..64251540 100644
--- a/rogue-thi-app/components/cards/TimetableCard.js
+++ b/rogue-thi-app/components/cards/TimetableCard.js
@@ -9,6 +9,7 @@ import { formatFriendlyTime, formatNearDate } from '../../lib/date-utils'
import { getFriendlyTimetable, getTimetableEntryName } from '../../lib/backend-utils/timetable-utils'
import BaseCard from './BaseCard'
import { NoSessionError } from '../../lib/backend/thi-session-handler'
+import { useTranslation } from 'next-i18next'
/**
* Dashboard card for the timetable.
@@ -18,6 +19,7 @@ export default function TimetableCard () {
const [timetable, setTimetable] = useState(null)
const [timetableError, setTimetableError] = useState(null)
const [currentTime, setCurrentTime] = useState(new Date())
+ const { t } = useTranslation('dashboard')
useEffect(() => {
async function loadTimetable () {
@@ -43,7 +45,7 @@ export default function TimetableCard () {
return (
@@ -56,13 +58,18 @@ export default function TimetableCard () {
let text = null
if (isEndingSoon) {
- text = Endet in {Math.ceil((x.endDate - currentTime) / 1000 / 60)} min
+ text = {t('timetable.text.endingSoon', { mins: Math.ceil((x.endDate - currentTime) / 1000 / 60) })}
} else if (isOngoing) {
- text = Endet um {formatFriendlyTime(x.endDate)}
+ text = {t('timetable.text.ongoing', { time: formatFriendlyTime(x.endDate) })}
} else if (isSoon) {
- text = Beginnt in {Math.ceil((x.startDate - currentTime) / 1000 / 60)} min
+ text =
+ {t('timetable.text.startingSoon', {
+ mins: Math.ceil((x.startDate - currentTime) / 1000 / 60)
+ }
+ )}
+
} else if (isNotSoonOrOngoing) {
- text = {formatNearDate(x.startDate)} um {formatFriendlyTime(x.startDate)}
+ text = {formatNearDate(x.startDate)} {t('timetable.text.future')} {formatFriendlyTime(x.startDate)}
}
return (
diff --git a/rogue-thi-app/components/modal/DashboardModal.js b/rogue-thi-app/components/modal/DashboardModal.js
index 1f2095f5..e6a32921 100644
--- a/rogue-thi-app/components/modal/DashboardModal.js
+++ b/rogue-thi-app/components/modal/DashboardModal.js
@@ -1,11 +1,12 @@
+import { DashboardContext, ShowDashboardModal } from '../../pages/_app'
import { faChevronDown, faChevronUp, faTrash, faTrashRestore } from '@fortawesome/free-solid-svg-icons'
import { useContext, useRef } from 'react'
import Button from 'react-bootstrap/Button'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import ListGroup from 'react-bootstrap/ListGroup'
import Modal from 'react-bootstrap/Modal'
-import { DashboardContext, ShowDashboardModal } from '../../pages/_app'
import styles from '../../styles/Personalize.module.css'
+import { useTranslation } from 'next-i18next'
/**
* A modal component that allows users to personalize their experience by changing the dashboard layout
@@ -24,33 +25,35 @@ export default function DashboardModal () {
const [showDashboardModal, setShowDashboardModal] = useContext(ShowDashboardModal)
const themeModalBody = useRef()
+ const { t } = useTranslation('common')
+
return (
setShowDashboardModal(false)}>
- Dashboard
+ {t('dashboard.orderModal.title')}
- Hier kannst du die Reihenfolge der im Dashboard angezeigten Einträge verändern.
+ {t('dashboard.orderModal.body')}
{shownDashboardEntries.map((entry, i) => (
- {entry.label}
+ {t(`cards.${entry.key}`)}
{entry.removable &&
hideDashboardEntry(entry.key)}>
-
+
}
moveDashboardEntry(i, -1)}>
-
+
moveDashboardEntry(i, +1)}>
-
+
@@ -58,16 +61,16 @@ export default function DashboardModal () {
- Ausgeblendete Elemente
+ {t('dashboard.orderModal.hiddenCards')}
{hiddenDashboardEntries.map((entry, i) => (
- {entry.label}
+ {t(`cards.${entry.key}`)}
bringBackDashboardEntry(i)}>
-
+
@@ -79,7 +82,7 @@ export default function DashboardModal () {
variant="secondary"
onClick={() => resetOrder()}
>
- Reihenfolge zurücksetzen
+ {t('dashboard.orderModal.resetOrder')}
diff --git a/rogue-thi-app/components/modal/FilterFoodModal.js b/rogue-thi-app/components/modal/FilterFoodModal.js
index 205d6d42..6a3417d4 100644
--- a/rogue-thi-app/components/modal/FilterFoodModal.js
+++ b/rogue-thi-app/components/modal/FilterFoodModal.js
@@ -12,6 +12,8 @@ import allergenMap from '../../data/allergens.json'
import flagMap from '../../data/mensa-flags.json'
import styles from '../../styles/FilterFoodModal.module.css'
+import { useTranslation } from 'next-i18next'
+
Object.keys(allergenMap)
.filter(key => key.startsWith('_'))
.forEach(key => delete allergenMap[key])
@@ -36,37 +38,41 @@ export default function FilterFoodModal () {
} = useContext(FoodFilterContext)
const [showAllergenSelection, setShowAllergenSelection] = useState(false)
const [showPreferencesSelection, setShowPreferencesSelection] = useState(false)
+ const { i18n, t } = useTranslation(['common'])
+ const currentLocale = i18n.languages[0]
+
+ const filteredFlagMap = Object.fromEntries(Object.entries(flagMap).filter(([key]) => key !== '_source'))
return (
<>
setShowFoodFilterModal(false)}>
- Filter
+ {t('food.filterModal.header')}
- Restaurants
+ {t('food.filterModal.restaurants.title')}
toggleSelectedRestaurant('mensa')}
/>
toggleSelectedRestaurant('reimanns')}
/>
toggleSelectedRestaurant('canisius')}
/>
@@ -76,43 +82,43 @@ export default function FilterFoodModal () {
- Allergien
+ {t('food.filterModal.allergens.title')}
{
setShowAllergenSelection(true)
setShowFoodFilterModal(false)
}}>
-
+
- <>Ausgewählt:{' '}>
- {Object.entries(allergenSelection).filter(x => x[1]).map(x => allergenMap[x[0]]).join(', ') || 'Keine'}.
+ <>{t('food.filterModal.allergens.selected')}:{' '}>
+ {Object.entries(allergenSelection).filter(x => x[1]).map(x => allergenMap[x[0]][currentLocale]).join(', ') || `${t('food.filterModal.allergens.empty')}`}
- Essenspräferenzen
+ {t('food.filterModal.preferences.title')}
{
setShowPreferencesSelection(true)
setShowFoodFilterModal(false)
}}>
-
+
- <>Ausgewählt:{' '}>
- {Object.entries(preferencesSelection).filter(x => x[1]).map(x => flagMap[x[0]]).join(', ') || 'Keine'}.
+ <>{t('food.filterModal.preferences.selected')}:{' '}>
+ {Object.entries(preferencesSelection).filter(x => x[1]).map(x => flagMap[x[0]][currentLocale]).join(', ') || `${t('food.filterModal.preferences.empty')}`}
- Deine Angaben werden nur lokal auf deinem Gerät gespeichert und an niemanden übermittelt.
+ {t('food.filterModal.info')}
@@ -124,7 +130,7 @@ export default function FilterFoodModal () {
setShowAllergenSelection(false)}>
- Allergene auswählen
+ {t('food.allergensModal')}
@@ -133,7 +139,7 @@ export default function FilterFoodModal () {
{key} {' – '}{value}}
+ label={{key} {' – '}{value[currentLocale]} }
checked={allergenSelection[key] || false}
onChange={e => setAllergenSelection({ ...allergenSelection, [key]: e.target.checked })}
/>
@@ -152,16 +158,16 @@ export default function FilterFoodModal () {
setShowPreferencesSelection(false)}>
- Präferenzen auswählen
+ {t('food.preferencesModal')}
{value}}
+ label={{value[currentLocale]} }
checked={preferencesSelection[key] || false}
onChange={e => setPreferencesSelection({ ...preferencesSelection, [key]: e.target.checked })}
/>
diff --git a/rogue-thi-app/components/modal/LanguageModal.js b/rogue-thi-app/components/modal/LanguageModal.js
new file mode 100644
index 00000000..039c8802
--- /dev/null
+++ b/rogue-thi-app/components/modal/LanguageModal.js
@@ -0,0 +1,58 @@
+import { useContext, useRef } from 'react'
+import { ShowLanguageModal } from '../../pages/_app'
+
+import Button from 'react-bootstrap/Button'
+import Form from 'react-bootstrap/Form'
+import Modal from 'react-bootstrap/Modal'
+import languages from '../../data/languages.json'
+import styles from '../../styles/Personalize.module.css'
+import { useRouter } from 'next/router'
+
+import { useTranslation } from 'next-i18next'
+
+/**
+ * A modal component that allows users to personalize their experience by changing the language
+ * @returns {JSX.Element} The LanguageModal component
+ * @constructor
+ */
+export default function LanguageModal () {
+ const [showLanguageModal, setShowLanguageModal] = useContext(ShowLanguageModal)
+ const languageModalBody = useRef()
+ const router = useRouter()
+
+ const { t, i18n } = useTranslation('personal')
+
+ /**
+ * Changes the current language.
+ * @param {string} languageKey Language key
+ */
+ function changeLanguage (languageKey) {
+ setShowLanguageModal(false)
+ i18n.changeLanguage(languageKey)
+ router.replace('/', '', { locale: i18n.language })
+ }
+
+ return (
+ setShowLanguageModal(false)}>
+
+ {t('personal.modals.language.title')}
+
+
+
+
+
+ )
+}
diff --git a/rogue-thi-app/components/modal/PersonalDataModal.js b/rogue-thi-app/components/modal/PersonalDataModal.js
index 9ac200cf..09562a6a 100644
--- a/rogue-thi-app/components/modal/PersonalDataModal.js
+++ b/rogue-thi-app/components/modal/PersonalDataModal.js
@@ -6,25 +6,30 @@ import ReactPlaceholder from 'react-placeholder'
import { ShowPersonalDataModal } from '../../pages/_app'
import styles from '../../styles/PersonalDataModal.module.css'
+import { getAdjustedLocale } from '../../lib/locale-utils'
+import { useTranslation } from 'next-i18next'
+
export default function PersonalDataModal ({ userdata }) {
const [showPersonalDataModal, setShowPersonalDataModal] = useContext(ShowPersonalDataModal)
+ const { t } = useTranslation('personal')
+
/**
* Displays a row with the users information.
- * @param {string} label Pretty row name
+ * @param {string} i18nKey Translation key for the row label
* @param {string} name Row name as returned by the backend
* @param {object} render Function returning the data to be displayed. If set, the `name` parameter will be ignored.
*/
- function renderPersonalEntry (label, name, render) {
+ function renderPersonalEntry (i18nKey, name, render) {
return (
{
- if (label === 'Prüfungsordnung') {
+ if (i18nKey === 'examRegulations') {
navigator.clipboard.writeText('SPO: ' + userdata.pvers)
} else {
navigator.clipboard.writeText(userdata[name])
}
}}>
- {label}
+ {t(`personal.modals.personalData.${i18nKey}`)}
{userdata && render && render()}
@@ -35,6 +40,8 @@ export default function PersonalDataModal ({ userdata }) {
)
}
+ const formatNum = (new Intl.NumberFormat(getAdjustedLocale(), { minimumFractionDigits: 2, maximumFractionDigits: 2 })).format
+
return (
setShowPersonalDataModal(false)}>
@@ -44,12 +51,12 @@ export default function PersonalDataModal ({ userdata }) {
- {renderPersonalEntry('Matrikelnummer', 'mtknr')}
- {renderPersonalEntry('Bibliotheksnummer', 'bibnr')}
- {renderPersonalEntry('Druckguthaben', 'pcounter')}
- {renderPersonalEntry('Studiengang', 'fachrich')}
- {renderPersonalEntry('Fachsemester', 'stgru')}
- {renderPersonalEntry('Prüfungsordnung', null, () => (
+ {renderPersonalEntry('matriculationNumber', 'mtknr')}
+ {renderPersonalEntry('libraryNumber', 'bibnr')}
+ {renderPersonalEntry('printerBalance', null, () => `${formatNum(userdata.pcounter.replace('€', ''))}€`)}
+ {renderPersonalEntry('fieldOfStudy', 'fachrich')}
+ {renderPersonalEntry('semester', 'stgru')}
+ {renderPersonalEntry('examRegulations', null, () => (
))}
- {renderPersonalEntry('E-Mail', 'email')}
- {renderPersonalEntry('THI E-Mail', 'fhmail')}
- {renderPersonalEntry('Telefon', null, () => userdata.telefon || 'N/A')}
- {renderPersonalEntry('Vorname', 'vname')}
- {renderPersonalEntry('Nachname', 'name')}
- {renderPersonalEntry('Straße', 'str')}
- {renderPersonalEntry('Ort', null, () => userdata.plz && userdata.ort && `${userdata.plz} ${userdata.ort}`)}
+ {renderPersonalEntry('email', 'email')}
+ {renderPersonalEntry('thiEmail', 'fhmail')}
+ {renderPersonalEntry('phone', null, () => userdata.telefon || 'N/A')}
+ {renderPersonalEntry('firstName', 'vname')}
+ {renderPersonalEntry('lastName', 'name')}
+ {renderPersonalEntry('street', 'str')}
+ {renderPersonalEntry('city', null, () => userdata.plz && userdata.ort && `${userdata.plz} ${userdata.ort}`)}
diff --git a/rogue-thi-app/components/modal/ThemeModal.js b/rogue-thi-app/components/modal/ThemeModal.js
index 62146f53..69824c63 100644
--- a/rogue-thi-app/components/modal/ThemeModal.js
+++ b/rogue-thi-app/components/modal/ThemeModal.js
@@ -8,11 +8,13 @@ import styles from '../../styles/Personalize.module.css'
import themes from '../../data/themes.json'
import { useDashboard } from '../../lib/hooks/dashboard'
+import { Trans, useTranslation } from 'next-i18next'
+
const CTF_URL = process.env.NEXT_PUBLIC_CTF_URL
/**
* A modal component that allows users to personalize their experience by changing the theme
- * @returns {JSX.Element} The ThemeModal compontent
+ * @returns {JSX.Element} The ThemeModal component
* @constructor
*/
export default function ThemeModal () {
@@ -22,6 +24,7 @@ export default function ThemeModal () {
const [showThemeModal, setShowThemeModal] = useContext(ShowThemeModal)
const [theme, setTheme] = useContext(ThemeContext)
const themeModalBody = useRef()
+ const { t, i18n } = useTranslation('personal')
/**
* Changes the current theme.
@@ -33,11 +36,25 @@ export default function ThemeModal () {
setShowThemeModal(false)
}
+ /**
+ * Workaround for using next/link and i18n together
+ * See: https://github.com/i18next/react-i18next/issues/1090
+ * @param {string} href The link to the page
+ * @param {string} children The children of the link
+ */
+ function TransLink ({ href, children }) {
+ return (
+
+ {children}
+
+ )
+ }
+
return (
setShowThemeModal(false)}>
- Theme
+ {t('personal.modals.theme.title')}
- Um das Hackerman -Design freizuschalten, musst du mindestens vier Aufgaben unseres Übungs-CTFs lösen.
- Wenn du so weit bist, kannst du es hier freischalten.
+ ,
+ aCtf: ,
+ aHackerman:
+ }}
+ />
diff --git a/rogue-thi-app/components/page/AppNavbar.js b/rogue-thi-app/components/page/AppNavbar.js
index 761d358b..7b8d8901 100644
--- a/rogue-thi-app/components/page/AppNavbar.js
+++ b/rogue-thi-app/components/page/AppNavbar.js
@@ -13,6 +13,7 @@ import useMediaQuery from '@restart/hooks/useMediaQuery'
import { useRouter } from 'next/router'
import styles from '../../styles/AppNavbar.module.css'
+import { useTranslation } from 'next-i18next'
/**
* Navigation bar to be displayed at the top of the screen.
@@ -21,6 +22,8 @@ export default function AppNavbar ({ title, showBack, children }) {
const router = useRouter()
const isDesktop = useMediaQuery('(min-width: 768px)')
+ const { t } = useTranslation('common')
+
/**
* Indicates whether a back button should be shown.
*/
@@ -44,7 +47,7 @@ export default function AppNavbar ({ title, showBack, children }) {
{showBackEffective && (
router.back()} className={styles.back}>
-
+
)}
@@ -86,10 +89,12 @@ AppNavbar.Button = AppNavbarButton
* Overflow menu to be displayed in the navbar.
*/
function AppNavbarOverflow ({ children }) {
+ const { t } = useTranslation('common')
+
return (
-
+
diff --git a/rogue-thi-app/data/allergens.json b/rogue-thi-app/data/allergens.json
index 40612fb8..a7f13a40 100644
--- a/rogue-thi-app/data/allergens.json
+++ b/rogue-thi-app/data/allergens.json
@@ -1,39 +1,150 @@
{
- "_source": "https://www.werkswelt.de/?id=ingo",
- "1": "Farbstoff",
- "2": "Coffein",
- "4": "Konservierungsstoff",
- "5": "Süßungsmittel",
- "7": "Antioxidationsmittel",
- "8": "Geschmacksverstärker",
- "9": "geschwefelt",
- "10": "geschwärzt",
- "12": "Phosphat",
- "13": "enthält eine Phenylalaninquelle",
- "30": "Fettglasur",
- "Wz": "Weizen(Dinkel,Kamut)",
- "Gf": "Glutenfrei",
- "Ro": "Roggen",
- "Ge": "Gerste",
- "Hf": "Hafer",
- "Kr": "Krebstiere",
- "Ei": "Eier",
- "Fi": "Fisch",
- "Er": "Erdnüsse",
- "So": "Sojabohnen",
- "Mi": "Milch/Laktose",
- "Man": "Mandeln",
- "Hs": "Haselnüsse",
- "Wa": "Walnüsse",
- "Ka": "Kaschu(Cashew)nüsse",
- "Pe": "Pekannüsse",
- "Pa": "Paranüsse",
- "Pi": "Schalenfrüchte Pistazien",
- "Mac": "Macadamianüsse",
- "Sel": "Sellerie",
- "Sen": "Senf",
- "Ses": "Sesam",
- "Su": "Schwefeldioxid u. Sulfite",
- "Lu": "Lupinen",
- "We": "Weichtiere"
-}
+ "_source": {
+ "de": "https://www.werkswelt.de/?id=ingo",
+ "en": "translated from https://www.werkswelt.de/?id=ingo"
+ },
+ "1": {
+ "de": "Farbstoff",
+ "en": "Artificial coloring"
+ },
+ "2": {
+ "de": "Coffein",
+ "en": "Caffeine"
+ },
+ "4": {
+ "de": "Konservierungsstoff",
+ "en": "Preservatives"
+ },
+ "5": {
+ "de": "Süßungsmittel",
+ "en": "Sweeteners"
+ },
+ "7": {
+ "de": "Antioxidationsmittel",
+ "en": "Antioxidants"
+ },
+ "8": {
+ "de": "Geschmacksverstärker",
+ "en": "Flavor enhancers"
+ },
+ "9": {
+ "de": "geschwefelt",
+ "en": "Sulfurized"
+ },
+ "10": {
+ "de": "geschwärzt",
+ "en": "Blackened"
+ },
+ "12": {
+ "de": "Phosphat",
+ "en": "Phosphate"
+ },
+ "13": {
+ "de": "enthält eine Phenylalaninquelle",
+ "en": "Contains phenylalanine source"
+ },
+ "30": {
+ "de": "Fettglasur",
+ "en": "Fat glaze"
+ },
+ "Wz": {
+ "de": "Weizen(Dinkel,Kamut)",
+ "en": "Wheat (spelt, kamut)"
+ },
+ "Gf": {
+ "de": "Glutenfrei",
+ "en": "Gluten-free"
+ },
+ "Ro": {
+ "de": "Roggen",
+ "en": "Rye"
+ },
+ "Ge": {
+ "de": "Gerste",
+ "en": "Barley"
+ },
+ "Hf": {
+ "de": "Hafer",
+ "en": "Oats"
+ },
+ "Kr": {
+ "de": "Krebstiere",
+ "en": "Crustaceans"
+ },
+ "Ei": {
+ "de": "Eier",
+ "en": "Eggs"
+ },
+ "Fi": {
+ "de": "Fisch",
+ "en": "Fish"
+ },
+ "Er": {
+ "de": "Erdnüsse",
+ "en": "Peanuts"
+ },
+ "So": {
+ "de": "Sojabohnen",
+ "en": "Soybeans"
+ },
+ "Mi": {
+ "de": "Milch/Laktose",
+ "en": "Milk/lactose"
+ },
+ "Man": {
+ "de": "Mandeln",
+ "en": "Almonds"
+ },
+ "Hs": {
+ "de": "Haselnüsse",
+ "en": "Hazelnuts"
+ },
+ "Wa": {
+ "de": "Walnüsse",
+ "en": "Walnuts"
+ },
+ "Ka": {
+ "de": "Kaschu(Cashew)nüsse",
+ "en": "Cashew nuts"
+ },
+ "Pe": {
+ "de": "Pekannüsse",
+ "en": "Pecans"
+ },
+ "Pa": {
+ "de": "Paranüsse",
+ "en": "Brazil nuts"
+ },
+ "Pi": {
+ "de": "Schalenfrüchte Pistazien",
+ "en": "Pistachios"
+ },
+ "Mac": {
+ "de": "Macadamianüsse",
+ "en": "Macadamia nuts"
+ },
+ "Sel": {
+ "de": "Sellerie",
+ "en": "Celery"
+ },
+ "Sen": {
+ "de": "Senf",
+ "en": "Mustard"
+ },
+ "Ses": {
+ "de": "Sesam",
+ "en": "Sesame"
+ },
+ "Su": {
+ "de": "Schwefeldioxid u. Sulfite",
+ "en": "Sulfur dioxide and sulfites"
+ },
+ "Lu": {
+ "de": "Lupinen",
+ "en": "Lupins"
+ },
+ "We": {
+ "de": "Weichtiere",
+ "en": "Molluscs"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/data/languages.json b/rogue-thi-app/data/languages.json
new file mode 100644
index 00000000..fb2457f1
--- /dev/null
+++ b/rogue-thi-app/data/languages.json
@@ -0,0 +1,4 @@
+[
+ { "name": { "en": "German", "de": "Deutsch" }, "key": "de" },
+ { "name": { "en": "English", "de": "Englisch" }, "key": "en" }
+]
diff --git a/rogue-thi-app/data/mensa-flags.json b/rogue-thi-app/data/mensa-flags.json
index dface479..d353f3dd 100644
--- a/rogue-thi-app/data/mensa-flags.json
+++ b/rogue-thi-app/data/mensa-flags.json
@@ -1,17 +1,66 @@
{
- "B": "Bio",
- "CO2": "CO2-Arm",
- "F": "Fisch",
- "G": "Geflügel",
- "Gf": "Glutenfrei",
- "L": "Lamm",
- "MSC": "MSC Fisch",
- "MV": "Mensa Vital",
- "R": "Rind",
- "S": "Schwein",
- "V": "Vegetarisch",
- "W": "Wild",
- "veg": "Vegan",
- "TODO0": "Reh",
- "TODO1": "Alkohol"
-}
\ No newline at end of file
+ "_source": {
+ "de": "https://www.werkswelt.de/?id=ingo",
+ "en": "translated from https://www.werkswelt.de/?id=ingo"
+ },
+ "B": {
+ "de": "Bio",
+ "en": "Organic"
+ },
+ "CO2": {
+ "de": "CO2-Arm",
+ "en": "Low CO2 emissions"
+ },
+ "F": {
+ "de": "Fisch",
+ "en": "Fish"
+ },
+ "G": {
+ "de": "Geflügel",
+ "en": "Poultry"
+ },
+ "Gf": {
+ "de": "Glutenfrei",
+ "en": "Gluten-free"
+ },
+ "L": {
+ "de": "Lamm",
+ "en": "Lamb"
+ },
+ "MSC": {
+ "de": "MSC Fisch",
+ "en": "MSC fish"
+ },
+ "MV": {
+ "de": "Mensa Vital",
+ "en": "Mensa Vital"
+ },
+ "R": {
+ "de": "Rind",
+ "en": "Beef"
+ },
+ "S": {
+ "de": "Schwein",
+ "en": "Pork"
+ },
+ "V": {
+ "de": "Vegetarisch",
+ "en": "Vegetarian"
+ },
+ "W": {
+ "de": "Wild",
+ "en": "Wild"
+ },
+ "veg": {
+ "de": "Vegan",
+ "en": "Vegan"
+ },
+ "TODO0": {
+ "de": "Reh",
+ "en": "Deer"
+ },
+ "TODO1": {
+ "de": "Alkohol",
+ "en": "Alcohol"
+ }
+}
diff --git a/rogue-thi-app/data/themes.json b/rogue-thi-app/data/themes.json
index 880c3c2a..963ca339 100644
--- a/rogue-thi-app/data/themes.json
+++ b/rogue-thi-app/data/themes.json
@@ -1,10 +1,10 @@
[
- { "name": "Automatisch", "style": "default" },
- { "name": "Hell", "style": "light" },
- { "name": "Dunkel", "style": "dark" },
- { "name": "Barbie & Ken", "style": "barbie" },
- { "name": "Retro", "style": "retro" },
- { "name": "Windows 95", "style": "95" },
- { "name": "Pride", "style": "pride" },
- { "name": "Hackerman", "style": "hacker", "requiresToken": true }
+ { "name": { "en": "Automatic", "de": "Automatisch" }, "style": "default" },
+ { "name": { "en": "Light", "de": "Hell" }, "style": "light" },
+ { "name": { "en": "Dark", "de": "Dunkel" }, "style": "dark" },
+ { "name": { "en": "Barbie & Ken", "de": "Barbie & Ken" }, "style": "barbie" },
+ { "name": { "en": "Retro", "de": "Retro" }, "style": "retro" },
+ { "name": { "en": "Windows 95", "de": "Windows 95" }, "style": "95" },
+ { "name": { "en": "Pride", "de": "Pride" }, "style": "pride" },
+ { "name": { "en": "Hackerman", "de": "Hackerman" }, "style": "hacker", "requiresToken": true }
]
diff --git a/rogue-thi-app/lib/backend-utils/food-utils.js b/rogue-thi-app/lib/backend-utils/food-utils.js
index 650f0bb7..c801e237 100644
--- a/rogue-thi-app/lib/backend-utils/food-utils.js
+++ b/rogue-thi-app/lib/backend-utils/food-utils.js
@@ -3,7 +3,8 @@ import NeulandAPI from '../backend/neuland-api'
/**
* Fetches and parses the meal plan
- * @param {string[]} restaurants Requested restaurants (`mensa` or `reimanns`)
+ * @param {string[]} restaurants Requested restaurants
+ * @param {string} language Language code
* @returns {object[]}
*/
export async function loadFoodEntries (restaurants) {
diff --git a/rogue-thi-app/lib/backend-utils/mobility-utils.js b/rogue-thi-app/lib/backend-utils/mobility-utils.js
index e1f7358b..f39c31fa 100644
--- a/rogue-thi-app/lib/backend-utils/mobility-utils.js
+++ b/rogue-thi-app/lib/backend-utils/mobility-utils.js
@@ -1,5 +1,3 @@
-import React from 'react'
-
import { faEuroSign, faKey } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCreativeCommonsNcEu } from '@fortawesome/free-brands-svg-icons'
@@ -9,6 +7,7 @@ import API from '../backend/authenticated-api'
import NeulandAPI from '../backend/neuland-api'
import stations from '../../data/mobility.json'
+import { useTranslation } from 'next-i18next'
/**
* Retrieves the users mobility preferences.
@@ -25,21 +24,25 @@ export function getMobilitySettings () {
* Determines the title of the mobility card / page.
* @param {string} kind Mobility type (`bus`, `train`, `parking` or `charging`)
* @param {string} station Station name (only for `bus` or `train`)
+ * @param {object} t Translation object
* @returns {string}
*/
-export function getMobilityLabel (kind, station) {
- if (kind === 'bus') {
- const entry = stations.bus.stations.find(x => x.id === station)
- return `Bus (${entry ? entry.name : '?'})`
- } else if (kind === 'train') {
- const entry = stations.train.stations.find(x => x.id === station)
- return `Bahn (${entry ? entry.name : '?'})`
- } else if (kind === 'parking') {
- return 'Parkplätze'
- } else if (kind === 'charging') {
- return 'Ladestationen'
- } else {
- return 'Mobilität'
+export function getMobilityLabel (kind, station, t) {
+ switch (kind) {
+ case 'bus': {
+ const busEntry = stations.bus.stations.find(x => x.id === station)
+ return t('transport.title.bus', { station: busEntry ? busEntry.name : '?' })
+ }
+ case 'train': {
+ const trainEntry = stations.train.stations.find(x => x.id === station)
+ return t('transport.title.train', { station: trainEntry ? trainEntry.name : '?' })
+ }
+ case 'parking':
+ return t('transport.title.parking')
+ case 'charging':
+ return t('transport.title.charging')
+ default:
+ return t('transport.title.unknown')
}
}
@@ -58,7 +61,7 @@ async function getAndConvertCampusParkingData () {
}
return {
- name: 'Congressgarage (Mitarbeiter)',
+ name: 'Congressgarage',
available
}
}
@@ -112,7 +115,9 @@ export async function getMobilityEntries (kind, station) {
* @param {number} maxLen Truncate the string after this many characters
* @param {string} styles CSS object
*/
-export function renderMobilityEntry (kind, item, maxLen, styles, detailed) {
+export function RenderMobilityEntry ({ kind, item, maxLen, styles, detailed }) {
+ const { t } = useTranslation('mobility')
+
if (kind === 'bus') {
const timeString = formatTimes(item.time, 30, 30)
@@ -151,15 +156,15 @@ export function renderMobilityEntry (kind, item, maxLen, styles, detailed) {
{item.priceLevel && (
{item.priceLevel === 'free' && (
-
+
)}
{item.priceLevel === 'restricted' && (
-
+
)}
{item.priceLevel > 0 && new Array(item.priceLevel)
.fill(0)
.map((_, i) => (
-
+
))}
)}
@@ -168,8 +173,8 @@ export function renderMobilityEntry (kind, item, maxLen, styles, detailed) {
{typeof item.available === 'number'
- ? item.available + ' frei'
- : 'n/a'}
+ ? t('transport.details.parking.available', { available: item.available })
+ : t('transport.details.parking.unknown') }
>
)
@@ -180,7 +185,7 @@ export function renderMobilityEntry (kind, item, maxLen, styles, detailed) {
{item.name}
- {item.available} von {item.total} frei
+ {t('transport.details.charging.available', { available: item.available, total: item.total })}
>
)
diff --git a/rogue-thi-app/lib/backend-utils/translation-utils.js b/rogue-thi-app/lib/backend-utils/translation-utils.js
new file mode 100644
index 00000000..0f9de416
--- /dev/null
+++ b/rogue-thi-app/lib/backend-utils/translation-utils.js
@@ -0,0 +1,58 @@
+const DEEPL_ENDPOINT = process.env.NEXT_PUBLIC_DEEPL_ENDPOINT || ''
+const DEEPL_API_KEY = process.env.DEEPL_API_KEY || ''
+
+/**
+ * Translates a text using DeepL.
+ * @param {String} text The text to translate
+ * @param {String} target The target language
+ * @returns {String}
+ */
+async function translate (text, target) {
+ if (!DEEPL_ENDPOINT || !DEEPL_API_KEY) {
+ console.error('DeepL is not configured. Please set DEEPL_ENDPOINT and DEEPL_API_KEY in your .env.local file. Using fallback translation.')
+ return `(TRANSLATION_PLACEHOLDER) ${text}`
+ }
+
+ const resp = await fetch(`${DEEPL_ENDPOINT}`,
+ {
+ method: 'POST',
+ mode: 'cors',
+ headers: {
+ Authorization: `DeepL-Auth-Key ${DEEPL_API_KEY}`,
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ body: `text=${encodeURI(text)}&target_lang=${target}`
+ })
+
+ if (resp.status === 200) {
+ const result = await resp.json()
+ return result.translations.map(x => x.text)[0]
+ } else {
+ throw new Error('DeepL returned an error: ' + await resp.text())
+ }
+}
+
+/**
+ * Translates all meals in the given plan using DeepL.
+ * @param {Object} meals The meal plan
+ * @returns {Object} The translated meal plan
+ */
+export async function translateMeals (meals) {
+ return await Promise.all(meals.map(async (day) => {
+ const meals = await Promise.all(day.meals.map(async (meal) => {
+ return {
+ ...meal,
+ name: {
+ de: meal.name,
+ en: await translate(meal.name, 'EN')
+ },
+ originalLanguage: 'de'
+ }
+ }))
+
+ return {
+ ...day,
+ meals
+ }
+ }))
+}
diff --git a/rogue-thi-app/lib/date-utils.js b/rogue-thi-app/lib/date-utils.js
index 03bec567..cf71ae33 100644
--- a/rogue-thi-app/lib/date-utils.js
+++ b/rogue-thi-app/lib/date-utils.js
@@ -1,10 +1,5 @@
-
-const WORD_TODAY = 'Heute'
-const WORD_TOMORROW = 'Morgen'
-export const WORD_THIS_WEEK = 'Diese Woche'
-export const WORD_NEXT_WEEK = 'Nächste Woche'
-
-export const DATE_LOCALE = 'de-DE'
+import { getAdjustedLocale } from './locale-utils'
+import { i18n } from 'next-i18next'
/**
* Formats a date like "Mo., 1.10.2020"
@@ -21,11 +16,11 @@ export function formatFriendlyDate (datetime) {
tomorrow.setDate(today.getDate() + 1)
if (datetime.toDateString() === today.toDateString()) {
- return WORD_TODAY
+ return t('common.dates.today')
} else if (datetime.toDateString() === tomorrow.toDateString()) {
- return WORD_TOMORROW
+ return t('common.dates.tomorrow')
} else {
- return datetime.toLocaleString(DATE_LOCALE, { weekday: 'short', day: 'numeric', month: '2-digit', year: 'numeric' })
+ return datetime.toLocaleString(getAdjustedLocale(), { weekday: 'short', day: 'numeric', month: '2-digit', year: 'numeric' })
}
}
@@ -53,7 +48,7 @@ export function formatFriendlyTime (datetime) {
datetime = new Date(datetime)
}
- return datetime.toLocaleTimeString(DATE_LOCALE, { hour: 'numeric', minute: '2-digit' })
+ return datetime.toLocaleTimeString(getAdjustedLocale(), { hour: 'numeric', minute: '2-digit' })
}
/**
@@ -101,11 +96,11 @@ export function formatNearDate (datetime) {
tomorrow.setDate(today.getDate() + 1)
if (datetime.toDateString() === today.toDateString()) {
- return WORD_TODAY
+ return i18n.t('common.dates.today', { ns: 'common' })
} else if (datetime.toDateString() === tomorrow.toDateString()) {
- return WORD_TOMORROW
+ return i18n.t('common.dates.tomorrow', { ns: 'common' })
} else {
- return datetime.toLocaleString(DATE_LOCALE, { weekday: 'long', day: 'numeric', month: 'numeric' })
+ return datetime.toLocaleString(getAdjustedLocale(), { weekday: 'long', day: 'numeric', month: 'numeric' })
}
}
@@ -119,8 +114,8 @@ export function buildLinedWeekdaySpan (datetime) {
datetime = new Date(datetime)
}
- const weekday = datetime.toLocaleString(DATE_LOCALE, { weekday: 'short' })
- const date = datetime.toLocaleString(DATE_LOCALE, { day: 'numeric', month: 'numeric' })
+ const weekday = datetime.toLocaleString(getAdjustedLocale(), { weekday: 'short' })
+ const date = datetime.toLocaleString(getAdjustedLocale(), { day: 'numeric', month: 'numeric' })
return {weekday} {date}
}
@@ -131,7 +126,7 @@ export function buildLinedWeekdaySpan (datetime) {
* @returns {string}
*/
function formatFriendlyTimeDelta (delta) {
- const rtl = new Intl.RelativeTimeFormat(DATE_LOCALE, {
+ const rtl = new Intl.RelativeTimeFormat(getAdjustedLocale(), {
numeric: 'auto',
style: 'long'
})
@@ -262,16 +257,16 @@ export function getFriendlyWeek (date) {
const [currStart, currEnd] = getWeek(new Date())
const [nextStart, nextEnd] = getWeek(addWeek(new Date(), 1))
if (date >= currStart && date < currEnd) {
- return WORD_THIS_WEEK
+ return t('common.dates.thisWeek')
} else if (date >= nextStart && date < nextEnd) {
- return WORD_NEXT_WEEK
+ return t('common.dates.nextWeek')
} else {
const monday = getMonday(date)
const sunday = new Date(monday)
sunday.setDate(sunday.getDate() + 6)
- return monday.toLocaleString(DATE_LOCALE, { day: 'numeric', month: 'numeric' }) +
- ' – ' + sunday.toLocaleString(DATE_LOCALE, { day: 'numeric', month: 'numeric' })
+ return monday.toLocaleString(getAdjustedLocale(), { day: 'numeric', month: 'numeric' }) +
+ ' – ' + sunday.toLocaleString(getAdjustedLocale(), { day: 'numeric', month: 'numeric' })
}
}
@@ -305,3 +300,7 @@ export function getAdjustedDay (date) {
export function isSameDay (a, b) {
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
}
+
+function t (...args) {
+ return i18n.t(...args, { ns: 'common' })
+}
diff --git a/rogue-thi-app/lib/hooks/dashboard.js b/rogue-thi-app/lib/hooks/dashboard.js
index 7ffba026..40fc1ede 100644
--- a/rogue-thi-app/lib/hooks/dashboard.js
+++ b/rogue-thi-app/lib/hooks/dashboard.js
@@ -70,7 +70,7 @@ export function useDashboard () {
ALL_DASHBOARD_CARDS.forEach(card => {
if (!entries.find(x => x.key === card.key) && !hiddenEntries.find(x => x.key === card.key)) {
- // new (previosly unknown) card
+ // new (previously unknown) card
entries.splice(0, 0, card)
}
})
diff --git a/rogue-thi-app/lib/locale-utils.js b/rogue-thi-app/lib/locale-utils.js
new file mode 100644
index 00000000..73131db1
--- /dev/null
+++ b/rogue-thi-app/lib/locale-utils.js
@@ -0,0 +1,8 @@
+import { i18n } from 'next-i18next'
+
+export function getAdjustedLocale () {
+ if (i18n.languages[0] === 'en') {
+ return 'en-UK'
+ }
+ return i18n.language
+}
diff --git a/rogue-thi-app/next-i18next.config.js b/rogue-thi-app/next-i18next.config.js
new file mode 100644
index 00000000..aecb139c
--- /dev/null
+++ b/rogue-thi-app/next-i18next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next-i18next').UserConfig} */
+module.exports = {
+ i18n: {
+ defaultLocale: 'en',
+ locales: ['en', 'de'],
+ localeDetection: true
+ }
+}
diff --git a/rogue-thi-app/next.config.js b/rogue-thi-app/next.config.js
index 98df6b29..153a8e0a 100644
--- a/rogue-thi-app/next.config.js
+++ b/rogue-thi-app/next.config.js
@@ -26,8 +26,13 @@ const permissionPolicyFeatures = [
]
const isDev = process.env.NODE_ENV === 'development'
+const DEEPL_ENDPOINT = process.env.NEXT_PUBLIC_DEEPL_ENDPOINT || ''
module.exports = {
+ i18n: {
+ locales: ['en', 'de'],
+ defaultLocale: 'en'
+ },
async headers () {
return [
{
@@ -68,7 +73,7 @@ module.exports = {
value: `default-src 'none';
img-src 'self' data: https://tile.openstreetmap.org;
font-src 'self';
- connect-src 'self' wss://proxy.neuland.app;
+ connect-src 'self' wss://proxy.neuland.app ${DEEPL_ENDPOINT};
style-src 'self' 'unsafe-inline';
script-src 'self'${isDev ? ' \'unsafe-eval\'' : ''};
manifest-src 'self';
diff --git a/rogue-thi-app/package-lock.json b/rogue-thi-app/package-lock.json
index 14e7c039..59fd4600 100644
--- a/rogue-thi-app/package-lock.json
+++ b/rogue-thi-app/package-lock.json
@@ -19,10 +19,12 @@
"dompurify": "^2.3.4",
"fetch-bypass-cors": "github:neuland-ingolstadt/fetch-bypass-cors#v1.0.3",
"fetch-cookie": "^2.0.1",
+ "i18next": "^22.5.0",
"ical-generator": "^3.6.1",
"idb": "^7.0.0",
"leaflet": "^1.7.1",
"next": "^12.1.0",
+ "next-i18next": "^13.2.2",
"node-fetch": "^3.2.3",
"pdf-parse": "^1.1.1",
"postgres-array": "^3.0.1",
@@ -30,6 +32,7 @@
"react": "^17.0.2",
"react-bootstrap": "^1.6.4",
"react-dom": "^17.0.2",
+ "react-i18next": "^12.3.1",
"react-leaflet": "^3.2.2",
"react-placeholder": "^4.1.0",
"react-swipeable-views": "^0.14.0",
@@ -707,6 +710,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+ "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
+ "dependencies": {
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0"
+ }
+ },
"node_modules/@types/invariant": {
"version": "2.2.35",
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
@@ -1375,6 +1387,16 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
+ "node_modules/core-js": {
+ "version": "3.30.2",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz",
+ "integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg==",
+ "hasInstallScript": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -2888,6 +2910,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
"node_modules/htmlparser2": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz",
@@ -2906,6 +2944,33 @@
"entities": "^4.3.0"
}
},
+ "node_modules/i18next": {
+ "version": "22.5.0",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.0.tgz",
+ "integrity": "sha512-sqWuJFj+wJAKQP2qBQ+b7STzxZNUmnSxrehBCCj9vDOW9RDYPfqCaK1Hbh2frNYQuPziz6O2CGoJPwtzY3vAYA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "dependencies": {
+ "@babel/runtime": "^7.20.6"
+ }
+ },
+ "node_modules/i18next-fs-backend": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.2.tgz",
+ "integrity": "sha512-y9vl8HC8b1ayqZELzKvaKgnphrxgbaGGSNQjPU0JoTVP1M3NI6C69SwiAAXi6xuF1FSySJG52EdQZdMUETlwRA=="
+ },
"node_modules/ical-generator": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-3.6.1.tgz",
@@ -3770,6 +3835,41 @@
}
}
},
+ "node_modules/next-i18next": {
+ "version": "13.2.2",
+ "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-13.2.2.tgz",
+ "integrity": "sha512-t0WU6K+HJoq2nVQ0n6OiiEZja9GyMqtDSU74FmOafgk4ljns+iZ18bsNJiI8rOUXfFfkW96ea1N7D5kbMyT+PA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ }
+ ],
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "core-js": "^3",
+ "hoist-non-react-statics": "^3.3.2",
+ "i18next-fs-backend": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "i18next": "^22.0.6",
+ "next": ">= 12.0.0",
+ "react": ">= 17.0.2",
+ "react-i18next": "^12.2.0"
+ }
+ },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -4349,6 +4449,27 @@
"react": "17.0.2"
}
},
+ "node_modules/react-i18next": {
+ "version": "12.3.1",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz",
+ "integrity": "sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==",
+ "dependencies": {
+ "@babel/runtime": "^7.20.6",
+ "html-parse-stringify": "^3.0.1"
+ },
+ "peerDependencies": {
+ "i18next": ">= 19.0.0",
+ "react": ">= 16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -5272,6 +5393,14 @@
"resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz",
"integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ=="
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
diff --git a/rogue-thi-app/package.json b/rogue-thi-app/package.json
index 46d94cbf..54c5902f 100644
--- a/rogue-thi-app/package.json
+++ b/rogue-thi-app/package.json
@@ -25,12 +25,15 @@
"bootstrap": "^4.6.1",
"cheerio": "^1.0.0-rc.10",
"dompurify": "^2.3.4",
+ "eslint-plugin-n": "^16.0.0",
"fetch-bypass-cors": "github:neuland-ingolstadt/fetch-bypass-cors#v1.0.3",
"fetch-cookie": "^2.0.1",
+ "i18next": "^22.5.0",
"ical-generator": "^3.6.1",
"idb": "^7.0.0",
"leaflet": "^1.7.1",
"next": "^12.1.0",
+ "next-i18next": "^13.2.2",
"node-fetch": "^3.2.3",
"pdf-parse": "^1.1.1",
"postgres-array": "^3.0.1",
@@ -38,6 +41,7 @@
"react": "^17.0.2",
"react-bootstrap": "^1.6.4",
"react-dom": "^17.0.2",
+ "react-i18next": "^12.3.1",
"react-leaflet": "^3.2.2",
"react-placeholder": "^4.1.0",
"react-swipeable-views": "^0.14.0",
diff --git a/rogue-thi-app/pages/_app.js b/rogue-thi-app/pages/_app.js
index 1ef34ff2..fdbba441 100644
--- a/rogue-thi-app/pages/_app.js
+++ b/rogue-thi-app/pages/_app.js
@@ -2,6 +2,8 @@ import { React, createContext, useEffect, useMemo, useState } from 'react'
import Head from 'next/head'
import { useRouter } from 'next/router'
+import { appWithTranslation } from 'next-i18next'
+
import PropTypes from 'prop-types'
import TheMatrixAnimation from './../components/TheMatrixAnimation'
import { useFoodFilter } from '../lib/hooks/food-filter'
@@ -20,6 +22,7 @@ export const ShowDashboardModal = createContext(false)
export const ShowPersonalDataModal = createContext(false)
export const ShowThemeModal = createContext(false)
export const DashboardContext = createContext({})
+export const ShowLanguageModal = createContext(false)
config.autoAddCss = false
@@ -29,6 +32,7 @@ function MyApp ({ Component, pageProps }) {
const [showThemeModal, setShowThemeModal] = useState(false)
const [showDashboardModal, setShowDashboardModal] = useState(false)
const [showPersonalDataModal, setShowPersonalDataModal] = useState(false)
+ const [showLanguageModal, setShowLanguageModal] = useState(false)
const foodFilter = useFoodFilter()
const {
shownDashboardEntries,
@@ -62,104 +66,107 @@ function MyApp ({ Component, pageProps }) {
}, [theme, router.pathname])
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* generated using npx pwa-asset-generator (run via Dockerfile) */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {computedTheme === 'hacker' && (
-
-
-
- )}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* generated using npx pwa-asset-generator (run via Dockerfile) */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {computedTheme === 'hacker' && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
)
}
@@ -169,4 +176,4 @@ MyApp.propTypes = {
theme: PropTypes.string
}
-export default MyApp
+export default appWithTranslation(MyApp)
diff --git a/rogue-thi-app/pages/api/canisius.js b/rogue-thi-app/pages/api/canisius.js
index 4b7493dd..11e5fe9c 100644
--- a/rogue-thi-app/pages/api/canisius.js
+++ b/rogue-thi-app/pages/api/canisius.js
@@ -1,4 +1,5 @@
import AsyncMemoryCache from '../../lib/cache/async-memory-cache'
+import { translateMeals } from '../../lib/backend-utils/translation-utils'
const pdf = require('pdf-parse')
@@ -82,7 +83,7 @@ export default async function handler (_, res) {
try {
const data = await cache.get('canisius', async () => {
const pdfBuffer = await getPdf()
- return pdf(pdfBuffer).then(function (data) {
+ const mealPlan = await pdf(pdfBuffer).then(function (data) {
const text = data.text.replace(NEW_LINE_REGEX, ' ')
let days = text.split(TITLE_REGEX)
@@ -104,9 +105,8 @@ export default async function handler (_, res) {
// trim whitespace and split into dishes
const dishes = days.map(getMealsFromBlock)
-
- return Object.keys(days).map(day => {
- const dayDishes = dishes[day].map((dish) => ({
+ return dishes.map((day, index) => {
+ const dayDishes = day.map((dish) => ({
name: dish.name,
category: 'Essen',
prices: dish.prices,
@@ -117,6 +117,7 @@ export default async function handler (_, res) {
const daySalads = salads.map((salad) => ({
name: salad.name,
+ originalLanguage: 'de',
category: 'Salat',
prices: salad.prices,
allergens: null,
@@ -125,11 +126,13 @@ export default async function handler (_, res) {
}))
return {
- timestamp: dates[day],
+ timestamp: dates[index],
meals: dayDishes.length > 0 ? [...dayDishes, ...daySalads] : []
}
})
})
+
+ return translateMeals(mealPlan)
})
sendJson(res, 200, data)
diff --git a/rogue-thi-app/pages/api/mensa.js b/rogue-thi-app/pages/api/mensa.js
index 345b6848..b1dc1341 100644
--- a/rogue-thi-app/pages/api/mensa.js
+++ b/rogue-thi-app/pages/api/mensa.js
@@ -2,10 +2,11 @@ import xmljs from 'xml-js'
import AsyncMemoryCache from '../../lib/cache/async-memory-cache'
import { formatISODate } from '../../lib/date-utils'
+import { translateMeals } from '../../lib/backend-utils/translation-utils'
const CACHE_TTL = 60 * 60 * 1000 // 60m
const URL_DE = 'https://www.max-manager.de/daten-extern/sw-erlangen-nuernberg/xml/mensa-ingolstadt.xml'
-const URL_EN = 'https://www.max-manager.de/daten-extern/sw-erlangen-nuernberg/xml/en/mensa-ingolstadt.xml'
+// const URL_EN = 'https://www.max-manager.de/daten-extern/sw-erlangen-nuernberg/xml/en/mensa-ingolstadt.xml'
const cache = new AsyncMemoryCache({ ttl: CACHE_TTL })
@@ -116,21 +117,15 @@ function parseDataFromXml (xml) {
/**
* Fetches and parses the mensa plan.
- * @param {string} lang Requested language (`de` or `en`)
* @returns {object[]}
*/
-async function fetchPlan (lang) {
- if (lang && lang !== 'de' && lang !== 'en') {
- throw new Error('unknown/unsupported language')
- }
-
- const url = lang && lang === 'en' ? URL_EN : URL_DE
-
- const plan = await cache.get(lang, async () => {
- const resp = await fetch(url)
+async function fetchPlan () {
+ const plan = await cache.get('mensa', async () => {
+ const resp = await fetch(URL_DE)
if (resp.status === 200) {
- return parseDataFromXml(await resp.text())
+ const mealPlan = parseDataFromXml(await resp.text())
+ return await translateMeals(mealPlan)
} else {
throw new Error('Data source returned an error: ' + await resp.text())
}
@@ -144,7 +139,7 @@ export default async function handler (req, res) {
try {
res.statusCode = 200
- const plan = await fetchPlan(req.query.lang)
+ const plan = await fetchPlan()
res.end(JSON.stringify(plan))
} catch (e) {
console.error(e)
diff --git a/rogue-thi-app/pages/api/reimanns.js b/rogue-thi-app/pages/api/reimanns.js
index 51ac71c9..18069c5c 100644
--- a/rogue-thi-app/pages/api/reimanns.js
+++ b/rogue-thi-app/pages/api/reimanns.js
@@ -1,6 +1,7 @@
import cheerio from 'cheerio'
import AsyncMemoryCache from '../../lib/cache/async-memory-cache'
+import { translateMeals } from '../../lib/backend-utils/translation-utils'
const CACHE_TTL = 10 * 60 * 1000 // 10m
const URL = 'http://reimanns.in/mittagsgerichte-wochenkarte/'
@@ -77,7 +78,7 @@ export default async function handler (req, res) {
})
// convert format to the same as /api/mensa
- return Object.keys(days).map(day => ({
+ const mealPlan = Object.keys(days).map(day => ({
timestamp: day,
meals: days[day].map(meal => ({
name: meal,
@@ -92,6 +93,8 @@ export default async function handler (req, res) {
nutrition: null
}))
}))
+
+ return translateMeals(mealPlan)
})
sendJson(res, 200, data)
diff --git a/rogue-thi-app/pages/calendar.js b/rogue-thi-app/pages/calendar.js
index f5187c06..be89031c 100644
--- a/rogue-thi-app/pages/calendar.js
+++ b/rogue-thi-app/pages/calendar.js
@@ -28,6 +28,9 @@ import { useTime } from '../lib/hooks/time-hook'
import styles from '../styles/Calendar.module.css'
+import { Trans, useTranslation } from 'next-i18next'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+
/**
* Page containing the semester and exam dates.
*/
@@ -38,6 +41,8 @@ export default function Calendar () {
const [focusedExam, setFocusedExam] = useState(null)
const userKind = useUserKind()
+ const { i18n, t } = useTranslation('calendar')
+
useEffect(() => {
async function load () {
try {
@@ -58,9 +63,21 @@ export default function Calendar () {
}
}, [router, userKind])
+ function InformationNotice () {
+ return (
+ :
+ }}
+ />
+ )
+ }
+
return (
-
+
setFocusedExam(null)}>
@@ -68,26 +85,29 @@ export default function Calendar () {
{focusedExam && focusedExam.titel}
- Art : {focusedExam && focusedExam.pruefungs_art}
- Raum : {focusedExam && (focusedExam.exam_rooms || 'TBD')}
- Sitzplatz : {focusedExam && (focusedExam.exam_seat || 'TBD')}
- Termin : {focusedExam && (focusedExam.date ? formatFriendlyDateTime(focusedExam.date) : 'TBD')}
- Anmerkung : {focusedExam && focusedExam.anmerkung}
- Prüfer : {focusedExam && focusedExam.pruefer_namen}
- Studiengang : {focusedExam && focusedExam.stg}
- Angemeldet : {focusedExam && formatFriendlyDateTime(focusedExam.anmeldung)}
- Hilfsmittel : {focusedExam && focusedExam.allowed_helpers.map((helper, i) =>
- {helper}
)}
+ {t('calendar.modals.exams.type')} : {focusedExam && focusedExam.pruefungs_art}
+ {t('calendar.modals.exams.room')} : {focusedExam && (focusedExam.exam_rooms || 'TBD')}
+ {t('calendar.modals.exams.seat')} : {focusedExam && (focusedExam.exam_seat || 'TBD')}
+ {t('calendar.modals.exams.date')} : {focusedExam && (focusedExam.date ? formatFriendlyDateTime(focusedExam.date) : 'TBD')}
+ {t('calendar.modals.exams.notes')} : {focusedExam && focusedExam.anmerkung}
+ {t('calendar.modals.exams.examiner')} : {focusedExam && focusedExam.pruefer_namen}
+ {t('calendar.modals.exams.courseOfStudies')} : {focusedExam && focusedExam.stg}
+ {t('calendar.modals.exams.registerDate')} : {focusedExam && formatFriendlyDateTime(focusedExam.anmeldung)}
+ {t('calendar.modals.exams.tools')} :
+
+ {focusedExam && focusedExam.allowed_helpers.map((helper, i) =>
+ {helper} )}
+
setFocusedExam(null)}>
- Schließen
+ {t('calendar.modals.exams.actions.close')}
-
+
{calendar.map((item, idx) =>
@@ -109,7 +129,7 @@ export default function Calendar () {
{(item.end && item.begin < now)
- ? 'bis ' + formatFriendlyRelativeTime(item.end)
+ ? `${t('calendar.dates.until')} ${formatFriendlyRelativeTime(item.end)}`
: formatFriendlyRelativeTime(item.begin)}
@@ -117,18 +137,17 @@ export default function Calendar () {
-
+
{exams && exams.length === 0 && (
- Es sind derzeit keine Prüfungstermine verfügbar.
+ {t('calendar.noExams')}
)}
{exams && exams.map((item, idx) =>
@@ -142,23 +161,22 @@ export default function Calendar () {
{' '}({formatFriendlyRelativeTime(item.date)})
>}
- Raum: {item.exam_rooms || 'TBD'}
- {item.exam_seat && `Sitzplatz: ${item.exam_seat}`}
+ {t('calendar.modals.exams.room')}: {item.exam_rooms || 'TBD'}
+ {item.exam_seat && `${t('calendar.modals.exams.seat')}: ${item.exam_seat}`}
)}
{userKind === USER_GUEST && (
- Prüfungstermine sind als Gast nicht verfügbar.
+ {t('calendar.guestNotice')}
)}
@@ -169,3 +187,12 @@ export default function Calendar () {
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'calendar',
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/events.js b/rogue-thi-app/pages/events.js
index f2ae3ea3..d010940a 100644
--- a/rogue-thi-app/pages/events.js
+++ b/rogue-thi-app/pages/events.js
@@ -22,6 +22,17 @@ import { useTime } from '../lib/hooks/time-hook'
import styles from '../styles/Calendar.module.css'
import clubs from '../data/clubs.json'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import { useTranslation } from 'next-i18next'
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'events',
+ 'common'
+ ]))
+ }
+})
/**
* Page containing the CL events.
@@ -30,6 +41,8 @@ export default function Events () {
const now = useTime()
const [events, setEvents] = useState(null)
+ const { t } = useTranslation('events')
+
useEffect(() => {
async function load () {
const campusLifeEvents = await NeulandAPI.getCampusLifeEvents()
@@ -48,14 +61,14 @@ export default function Events () {
return (
-
+
{events && events.length === 0 && (
- Es sind derzeit keine Veranstaltungstermine verfügbar.
+ {t('events.noEvents')}
)}
{events && events.map((item, idx) => {
@@ -98,7 +111,7 @@ export default function Events () {
{(item.end && item.begin < now)
- ? 'bis ' + formatFriendlyRelativeTime(item.end)
+ ? `${t('events.dates.until')} ${formatFriendlyRelativeTime(item.end)}`
: formatFriendlyRelativeTime(item.begin)}
diff --git a/rogue-thi-app/pages/food.js b/rogue-thi-app/pages/food.js
index 6258a109..03a84292 100644
--- a/rogue-thi-app/pages/food.js
+++ b/rogue-thi-app/pages/food.js
@@ -6,7 +6,7 @@ import Modal from 'react-bootstrap/Modal'
import Nav from 'react-bootstrap/Nav'
import ReactPlaceholder from 'react-placeholder'
-import { faChevronLeft, faChevronRight, faExclamationTriangle, faFilter, faThumbsUp, faUtensils } from '@fortawesome/free-solid-svg-icons'
+import { faChevronLeft, faChevronRight, faExclamationTriangle, faFilter, faThumbsUp, faUtensils, faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import AppBody from '../components/page/AppBody'
@@ -26,11 +26,13 @@ import flagMap from '../data/mensa-flags.json'
import styles from '../styles/Mensa.module.css'
import SwipeableViews from 'react-swipeable-views'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import { useRouter } from 'next/router'
+import { useTranslation } from 'next-i18next'
const CURRENCY_LOCALE = 'de'
const COLOR_WARN = '#bb0000'
const COLOR_GOOD = '#00bb00'
-const FALLBACK_ALLERGEN = 'Unbekannt (Das ist schlecht.)'
// delete comments
Object.keys(allergenMap)
@@ -54,6 +56,9 @@ export default function Mensa () {
const [showMealDetails, setShowMealDetails] = useState(null)
const [week, setWeek] = useState(0)
const userKind = useUserKind()
+ const router = useRouter()
+ const { i18n, t } = useTranslation('food')
+ const currentLocale = i18n.languages[0]
useEffect(() => {
async function load () {
@@ -71,7 +76,7 @@ export default function Mensa () {
}
load()
- }, [selectedRestaurants])
+ }, [selectedRestaurants, router])
/**
* Checks whether the user should be allergens.
@@ -149,24 +154,30 @@ export default function Mensa () {
>
- {meal.name}
+ {/* {isTranslated(meal) && (
+ <>
+
+ {' '}
+ >
+ )} */}
+ {meal.name[i18n.languages[0]]}
- {!meal.allergens && 'Unbekannte Zutaten / Allergene'}
+ {!meal.allergens && t('warning.unknownIngredients.text')}
{containsSelectedAllergen(meal.allergens) && (
-
+
{' '}
)}
{!containsSelectedAllergen(meal.allergens) && containsSelectedPreference(meal.flags) && (
-
+
{' '}
)}
- {meal.flags && meal.flags.map(flag => flagMap[flag]).join(', ')}
+ {meal.flags && meal.flags.map(flag => flagMap[flag]?.[currentLocale]).join(', ')}
{meal.flags?.length > 0 && meal.allergens?.length > 0 && '; '}
{meal.allergens && meal.allergens.join(', ')}
@@ -200,11 +211,11 @@ export default function Mensa () {
{mensa.length > 0 && (
<>
- Mensa
+ {t('list.titles.cafeteria')}
{mensaFood.length > 0 && (
<>
{mensaSoups.length > 0 && (
- Gerichte
+ {t('list.titles.meals')}
)}
{mensaFood.map((meal, idx) => renderMealEntry(meal, `food-${idx}`))}
@@ -214,7 +225,7 @@ export default function Mensa () {
{mensaSoups.length > 0 && (
<>
{mensaFood.length > 0 && (
- Suppen
+ {t('list.titles.soups')}
)}
{mensaSoups.map((meal, idx) => renderMealEntry(meal, `soup-${idx}`))}
@@ -239,7 +250,7 @@ export default function Mensa () {
{canisiusFood.length > 0 && (
<>
{canisiusSalads.length > 0 && (
- Gerichte
+ {t('list.titles.meals')}
)}
{canisiusFood.map((meal, idx) => renderMealEntry(meal, `food-${idx}`))}
@@ -249,7 +260,7 @@ export default function Mensa () {
{canisiusSalads.length > 0 && (
<>
{canisiusFood.length > 0 && (
- Salate
+ {t('list.titles.meals')}
)}
{canisiusSalads.map((meal, idx) => renderMealEntry(meal, `soup-${idx}`))}
@@ -263,32 +274,34 @@ export default function Mensa () {
- Keine Daten verfügbar
+ {t('error.dataUnavailable')}
)}
)
}
+ const isTranslated = (meal) => meal?.originalLanguage !== i18n.languages[0]
+
return (
-
+
setShowFoodFilterModal(true)}>
-
+
setWeek(0)} disabled={week === 0}>
-
+
{week === 0 && getFriendlyWeek(new Date(currentFoodDays?.[0]?.timestamp))}
{week === 1 && getFriendlyWeek(new Date(futureFoodDays?.[0]?.timestamp))}
setWeek(1)} disabled={week === 1}>
-
+
@@ -303,13 +316,13 @@ export default function Mensa () {
setShowMealDetails(null)}>
- Erläuterung
+ {t('foodModal.header')}
- Anmerkungen
- {showMealDetails?.flags === null && 'Unbekannt.'}
- {showMealDetails?.flags?.length === 0 && 'Keine.'}
+ {t('foodModal.flags.title')}
+ {showMealDetails?.flags === null && `${t('foodModal.flags.unkown')}`}
+ {showMealDetails?.flags?.length === 0 && `${t('foodModal.flags.empty')}`}
{showMealDetails?.flags?.map(flag => (
@@ -321,14 +334,14 @@ export default function Mensa () {
{' '}
{flag}
{' – '}
- {flagMap[flag] || FALLBACK_ALLERGEN}
+ {flagMap[flag]?.[currentLocale] || `${t('foodModal.allergens.fallback')}`}
))}
- Allergene
- {showMealDetails?.allergens === null && 'Unbekannt.'}
- {showMealDetails?.allergens?.length === 0 && 'Keine.'}
+ {t('foodModal.allergens.title')}
+ {showMealDetails?.allergens === null && `${t('foodModal.allergens.unkown')}`}
+ {showMealDetails?.allergens?.length === 0 && `${t('foodModal.flags.empty')}`}
{showMealDetails?.allergens?.map(key => (
@@ -341,70 +354,90 @@ export default function Mensa () {
{' '}
{key}
{' – '}
- {allergenMap[key] || FALLBACK_ALLERGEN}
+ {allergenMap[key]?.[currentLocale] || `${t('foodModal.allergens.fallback')}`}
))}
- Nährwerte
+ {t('foodModal.nutrition.title')}
{(showMealDetails?.nutrition && (
- Energie :{' '}
+ {t('foodModal.nutrition.energy.title')} :{' '}
{showMealDetails?.nutrition.kj ? showMealDetails?.nutrition.kj + ' kJ' : ''} /
{showMealDetails?.nutrition.kcal ? showMealDetails?.nutrition.kcal + ' kcal' : ''}
- Fett :{' '}
+ {t('foodModal.nutrition.fat.title')} :{' '}
{formatGram(showMealDetails?.nutrition.fat)}
- davon gesättigte
- Fettsäuren : {formatGram(showMealDetails?.nutrition.fatSaturated)}
+ {t('foodModal.nutrition.fat.saturated')} : {formatGram(showMealDetails?.nutrition.fatSaturated)}
- Kohlenhydrate :{' '}
+ {t('foodModal.nutrition.carbohydrates.title')} :{' '}
{formatGram(showMealDetails?.nutrition.carbs)}
- davon Zucker : {formatGram(showMealDetails?.nutrition.sugar)}
+ {t('foodModal.nutrition.carbohydrates.sugar')} : {formatGram(showMealDetails?.nutrition.sugar)}
- Ballaststoffe :{' '}
+ {t('foodModal.nutrition.fiber.title')} :{' '}
{formatGram(showMealDetails?.nutrition.fiber)}
- Eiweiß :{' '}
+ {t('foodModal.nutrition.protein.title')} :{' '}
{formatGram(showMealDetails?.nutrition.protein)}
- Salz :{' '}
+ {t('foodModal.nutrition.salt.title')} :{' '}
{formatGram(showMealDetails?.nutrition.salt)}
)) || (
- Unbekannt.
+ {t('foodModal.nutrition.unkown.title')}
)}
- Preise
+ {t('foodModal.prices.title')}
- Studierende :{' '}
+ {t('foodModal.prices.students')} :{' '}
{formatPrice(showMealDetails?.prices.student)}
- Mitarbeitende :{' '}
+ {t('foodModal.prices.employees')} :{' '}
{formatPrice(showMealDetails?.prices.employee)}
- Gäste :{' '}
+ {t('foodModal.prices.guests')} :{' '}
{formatPrice(showMealDetails?.prices.guest)}
- Angaben ohne Gewähr.
+ {t('foodModal.warning.title')}
- Bitte prüfe die Angaben auf den Infobildschirmen, bevor du etwas konsumiert.
- Die Nährwertangaben beziehen sich auf eine durchschnittliche Portion.
+ {t('foodModal.warning.text')}
+
+ {isTranslated(showMealDetails) && (
+
+
+ {` ${t('foodModal.translation.title')}`}
+
+
+ {` ${t('foodModal.translation.warning')}`}
+
+
+
+
+ {t('foodModal.translation.originalName')} :{' '}
+ {showMealDetails?.name[showMealDetails?.originalLanguage]}
+
+
+ {t('foodModal.translation.translatedName')} :{' '}
+ {showMealDetails?.name[i18n.languages[0]]}
+
+
+
+ )}
@@ -442,3 +475,12 @@ export default function Mensa () {
}
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'food',
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/grades.js b/rogue-thi-app/pages/grades.js
index 2a4a9201..b3e343df 100644
--- a/rogue-thi-app/pages/grades.js
+++ b/rogue-thi-app/pages/grades.js
@@ -12,9 +12,22 @@ import AppTabbar from '../components/page/AppTabbar'
import { NoSessionError, UnavailableSessionError } from '../lib/backend/thi-session-handler'
import { loadGradeAverage, loadGrades } from '../lib/backend-utils/grades-utils'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import { useTranslation } from 'next-i18next'
+
+import { getAdjustedLocale } from '../lib/locale-utils'
import styles from '../styles/Grades.module.css'
-const formatNum = (new Intl.NumberFormat('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })).format
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'grades',
+ 'common'
+ ]))
+ }
+})
+
+const GRADE_REGEX = /\d+[.,]\d+/
/**
* Page showing the users grades.
@@ -25,6 +38,20 @@ export default function Grades () {
const [missingGrades, setMissingGrades] = useState(null)
const [gradeAverage, setGradeAverage] = useState(null)
+ const { t } = useTranslation('grades')
+
+ const formatNum = (new Intl.NumberFormat(getAdjustedLocale(), { minimumFractionDigits: 1, maximumFractionDigits: 1 })).format
+
+ function getLocalizedGrade (grade) {
+ const match = grade.match(GRADE_REGEX)
+ if (!match) {
+ return grade
+ }
+
+ const num = parseFloat(match[0].replace(',', '.'))
+ return grade.replace(match[0], formatNum(num)).replace(/\*/g, t('grades.credited'))
+ }
+
useEffect(() => {
async function load () {
try {
@@ -42,7 +69,7 @@ export default function Grades () {
// means that the transcripts are currently being updated
console.error(e)
- alert('Noten sind vorübergehend nicht verfügbar.')
+ alert(t('grades.alerts.temporarilyUnavailable'))
} else {
console.error(e)
alert(e)
@@ -50,7 +77,7 @@ export default function Grades () {
}
}
load()
- }, [router])
+ }, [router, t])
/**
* Copies the formula for calculating the grade average to the users clipboard.
@@ -70,25 +97,25 @@ export default function Grades () {
.join(' + ')
await navigator.clipboard.writeText(`(${inner}) / ${weight}`)
- alert('Copied to Clipboard!')
+ alert(t('grades.alerts.copyToClipboard'))
}
/**
* Downloads the users grades as a CSV file.
*/
function downloadGradeCSV () {
- alert('Not yet implemented :(')
+ alert(t('grades.alerts.notImplemented'))
}
return (
-
+
copyGradeFormula()}>
- Notenschnitt Formel kopieren
+ {t('grades.appbar.overflow.copyFormula')}
downloadGradeCSV()}>
- Noten als CSV exportieren
+ {t('grades.appbar.overflow.exportCsv')}
@@ -98,7 +125,7 @@ export default function Grades () {
{gradeAverage && gradeAverage.entries.length > 0 && (
- Notenschnitt
+ {t('grades.summary.title')}
@@ -108,8 +135,10 @@ export default function Grades () {
{gradeAverage.resultMin !== gradeAverage.resultMax && (
- Der genaue Notenschnitt kann nicht ermittelt werden und liegt zwischen
- {' '}{formatNum(gradeAverage.resultMin)} und {formatNum(gradeAverage.resultMax)}
+ {t('grades.summary.disclaimer', {
+ minAverage: formatNum(gradeAverage.resultMin),
+ maxAverage: formatNum(gradeAverage.resultMax)
+ })}
)}
@@ -119,7 +148,7 @@ export default function Grades () {
- Noten
+ {t('grades.gradesList.title')}
@@ -129,8 +158,8 @@ export default function Grades () {
{item.titel}
- Note: {item.note.replace('*', ' (angerechnet)')}
- ECTS: {item.ects || '(keine)'}
+ {t('grades.grade')}: {getLocalizedGrade(item.note)}
+ {t('grades.ects')}: {item.ects || t('grades.none')}
@@ -140,7 +169,7 @@ export default function Grades () {
- Ausstehende Fächer
+ {t('grades.pendingList.title')}
@@ -150,8 +179,8 @@ export default function Grades () {
{item.titel} ({item.stg})
- Frist: {item.frist || '(keine)'}
- ECTS: {item.ects || '(keine)'}
+ {t('grades.deadline')}: {item.frist || t('grades.none')}
+ {t('grades.ects')}: {item.ects || t('grades.none')}
diff --git a/rogue-thi-app/pages/imprint.js b/rogue-thi-app/pages/imprint.js
index 5a500ada..de0e0cd1 100644
--- a/rogue-thi-app/pages/imprint.js
+++ b/rogue-thi-app/pages/imprint.js
@@ -9,24 +9,33 @@ import AppContainer from '../components/page/AppContainer'
import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import styles from '../styles/Imprint.module.css'
+import { useTranslation } from 'next-i18next'
const IMPRINT_URL = process.env.NEXT_PUBLIC_IMPRINT_URL
-export async function getStaticProps () {
+export async function getStaticProps ({ locale }) {
+ const locales = await serverSideTranslations(locale ?? 'en', [
+ 'imprint',
+ 'common'
+ ])
+
try {
const res = await fetch(IMPRINT_URL)
const html = await res.text()
return {
props: {
- neulandImprint: html
+ neulandImprint: html,
+ ...locales
}
}
} catch (e) {
console.error(e)
return {
props: {
- neulandImprint: `Laden fehlgeschlagen! Bitte hier klicken `
+ neulandImprint: `Laden fehlgeschlagen! Bitte hier klicken `,
+ ...locales
}
}
}
@@ -39,6 +48,8 @@ export default function Imprint ({ neulandImprint: unsanitizedNeulandImprint })
const [neulandImprint, setNeulandImprint] = useState('Lädt...')
const [debugUnlockProgress, setDebugUnlockProgress] = useState(0)
+ const { t } = useTranslation('imprint')
+
useEffect(() => {
setNeulandImprint(DOMPurify.sanitize(unsanitizedNeulandImprint))
}, [unsanitizedNeulandImprint])
@@ -61,12 +72,12 @@ export default function Imprint ({ neulandImprint: unsanitizedNeulandImprint })
return (
-
+
- Wir würden uns über euer Feedback freuen.{' '}
+ {`${t('imprint.feedback.title')} `}
:)
@@ -75,30 +86,30 @@ export default function Imprint ({ neulandImprint: unsanitizedNeulandImprint })
app-feedback@informatik.sexy
- Webseite:{' '}
+ {`${t('imprint.feedback.email')}: `}
https://neuland-ingolstadt.de
- Instagram:{' '}
+ {`${t('imprint.feedback.instagram')}: `}
@neuland_ingolstadt
- Quellcode auf GitHub:{' '}
+ {`${t('imprint.feedback.sourceCode')}: `}
neuland-ingolstadt/neuland.app
- Jetzt Mitglied werden und die Entwicklung unterstützen!
+ {t('imprint.feedback.joinNeuland')}
- Rechtliche Hinweise von Neuland Ingolstadt e.V.
+ {t('imprint.legal.title')}
diff --git a/rogue-thi-app/pages/index.js b/rogue-thi-app/pages/index.js
index 8fd22676..e606b25c 100644
--- a/rogue-thi-app/pages/index.js
+++ b/rogue-thi-app/pages/index.js
@@ -5,11 +5,14 @@ import AppContainer from '../components/page/AppContainer'
import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
-import styles from '../styles/Home.module.css'
+import { DashboardContext, ShowDashboardModal } from './_app'
+import DashboardModal from '../components/modal/DashboardModal'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+
import { faPen } from '@fortawesome/free-solid-svg-icons'
-import { ShowDashboardModal, DashboardContext } from './_app'
-import DashboardModal from '../components/modal/DashboardModal'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import styles from '../styles/Home.module.css'
+import { useTranslation } from 'next-i18next'
/**
* Main page.
@@ -23,11 +26,13 @@ export default function Home () {
hideDashboardEntry
} = useContext(DashboardContext)
+ const { t } = useTranslation('common')
+
return (
setShowDashboardModal(true)}>
-
+
@@ -42,3 +47,13 @@ export default function Home () {
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'dashboard',
+ 'mobility',
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/lecturers.js b/rogue-thi-app/pages/lecturers.js
index c3ef2522..f01723b0 100644
--- a/rogue-thi-app/pages/lecturers.js
+++ b/rogue-thi-app/pages/lecturers.js
@@ -19,6 +19,19 @@ import { normalizeLecturers } from '../lib/backend-utils/lecturers-utils'
import styles from '../styles/Lecturers.module.css'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import { useTranslation } from 'next-i18next'
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'lecturers',
+ 'common',
+ 'api-translations'
+ ]))
+ }
+})
+
/**
* Page containing the lecturer search and details.
*/
@@ -31,6 +44,8 @@ export default function Lecturers () {
const [search, setSearch] = useState('')
const [focusedLecturer, setFocusedLecturer] = useState(null)
+ const { t } = useTranslation(['lecturers', 'api-translations'])
+
useEffect(() => {
async function load () {
try {
@@ -80,7 +95,7 @@ export default function Lecturers () {
}
}
- const normalizedSearch = search.toLowerCase()
+ const normalizedSearch = search.toLowerCase().trim()
const checkField = value => value && value.toString().toLowerCase().includes(normalizedSearch)
const filtered = allLecturers
.filter(x => checkField(x.name) ||
@@ -96,9 +111,17 @@ export default function Lecturers () {
load()
}, [router, didFetch, search, personalLecturers, allLecturers])
+ const getTranslatedFunction = (lecturer) => {
+ return t(`apiTranslations.lecturerFunctions.${lecturer.funktion}`, { ns: 'api-translations' })
+ }
+
+ const getTranslatedOrganization = (lecturer) => {
+ return t(`apiTranslations.lecturerOrganizations.${lecturer.organisation}`, { ns: 'api-translations' })
+ }
+
return (
-
+
setFocusedLecturer(null)}>
@@ -108,22 +131,22 @@ export default function Lecturers () {
{focusedLecturer.titel} {focusedLecturer.vorname} {focusedLecturer.name}
- Titel : {focusedLecturer.titel}
- Name : {focusedLecturer.name}
- Vorname : {focusedLecturer.vorname}
- Organisation : {focusedLecturer.organisation || 'N/A'}
- Funktion : {focusedLecturer.funktion}
+ {t('lecturers.modals.details.title')} : {focusedLecturer.titel}
+ {t('lecturers.modals.details.surname')} : {focusedLecturer.name}
+ {t('lecturers.modals.details.forename')} : {focusedLecturer.vorname}
+ {t('lecturers.modals.details.organization')} : {getTranslatedOrganization(focusedLecturer)}
+ {t('lecturers.modals.details.function')} : {getTranslatedFunction(focusedLecturer)}
- Raum :{' '}
+ {t('lecturers.modals.details.room')} :{' '}
{focusedLecturer.room_short && (
{focusedLecturer.raum}
)}
- {focusedLecturer.room_short ? '' : (focusedLecturer.raum || 'N/A')}
+ {focusedLecturer.room_short ? '' : (focusedLecturer.raum || t('lecturers.modals.details.notAvailable'))}
- E-Mail :{' '}
+ {t('lecturers.modals.details.email')} :{' '}
{focusedLecturer.email.includes('@') && (
{focusedLecturer.email}
@@ -131,26 +154,26 @@ export default function Lecturers () {
)}
{!focusedLecturer.email.includes('@') && (
<>
- {focusedLecturer.email || 'N/A'}
+ {focusedLecturer.email || t('lecturers.modals.details.notAvailable')}
>
)}
- Telefon :{' '}
+ {t('lecturers.modals.details.phone')} :{' '}
{focusedLecturer.tel_dienst && (
{focusedLecturer.tel_dienst}
)}
- {!focusedLecturer.tel_dienst && 'N/A'}
+ {!focusedLecturer.tel_dienst && t('lecturers.modals.details.notAvailable')}
- Sprechstunde : {focusedLecturer.sprechstunde}
- Einsichtnahme : {focusedLecturer.einsichtnahme}
+ {t('lecturers.modals.details.officeHours')} : {focusedLecturer.sprechstunde}
+ {t('lecturers.modals.details.insights')} : {focusedLecturer.einsichtnahme}
setFocusedLecturer(null)}>
- Schließen
+ {t('lecturers.modals.details.actions.close')}
>
@@ -161,7 +184,7 @@ export default function Lecturers () {
setSearch(e.target.value)}
/>
@@ -169,7 +192,7 @@ export default function Lecturers () {
- {search ? 'Suchergebnisse' : 'Persönliche Dozenten'}
+ {search ? t('lecturers.search.searchResults') : t('lecturers.search.personalLecturers')}
@@ -180,13 +203,13 @@ export default function Lecturers () {
{x.vorname} {x.name}
- {x.funktion}
+ {getTranslatedFunction(x)}
{x.raum && (
<>
- Raum:{' '}
+ {t('lecturers.body.room')}:{' '}
{x.room_short && (
{x.raum}
diff --git a/rogue-thi-app/pages/library.js b/rogue-thi-app/pages/library.js
index 104afd47..19aa689d 100644
--- a/rogue-thi-app/pages/library.js
+++ b/rogue-thi-app/pages/library.js
@@ -21,6 +21,10 @@ import API from '../lib/backend/authenticated-api'
import styles from '../styles/Library.module.css'
+import { Trans, useTranslation } from 'next-i18next'
+
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+
/**
* Page for reserving library seats.
*/
@@ -32,6 +36,7 @@ export default function Library () {
const [reservationRoom, setReservationRoom] = useState(1)
const [reservationSeat, setReservationSeat] = useState(-1)
const router = useRouter()
+ const { t } = useTranslation('library')
/**
* Fetches and displays the reservation data.
@@ -98,19 +103,20 @@ export default function Library () {
return (
-
+
- Sitzplatz reservieren
+ {t('library.modal.title')}
- Tag: {reservationDay && reservationDay.date}
- Start: {reservationTime && reservationTime.from}
- Ende: {reservationTime && reservationTime.to}
+ {t('library.modal.details.day')}: {reservationDay && reservationDay.date}
+ {t('library.modal.details.start')}: {reservationTime && reservationTime.from}
+ {t('library.modal.details.end')}: {reservationTime && reservationTime.to}
+
- Ort:
+ {t('library.modal.details.location')}:
setReservationRoom(event.target.value)}>
{reservationTime && Object.entries(reservationTime.resources).map(([roomId, room], idx) =>
@@ -120,9 +126,9 @@ export default function Library () {
- Sitz:
+ {t('library.modal.details.seat')}:
setReservationSeat(event.target.value)}>
- Egal
+ {t('library.modal.seatSelection.any')}
{reservationTime && reservationRoom &&
Object.values(reservationTime.resources[reservationRoom].seats).map((x, idx) =>
@@ -135,33 +141,45 @@ export default function Library () {
- Reservieren
+ {t('library.modal.actions.reserve')}
- Abbrechen
+ {t('library.modal.actions.cancel')}
- Deine Reservierungen
+ {t('library.yourReservations')}
{reservations && reservations.length === 0 &&
- Du hast keine Reservierungen.
+ {t('library.details.noReservations')}
}
{reservations && reservations.map((x, i) =>
deleteReservation(x.reservation_id)}>
-
+
- {x.rcategory} , Platz {x.resource}, Reservierung {x.reservation_id}
+
+ }}
+ />
+
{formatNearDate(x.start)}: {formatFriendlyTime(x.start)} - {formatFriendlyTime(x.end)}
)}
@@ -169,7 +187,7 @@ export default function Library () {
- Verfügbare Plätze
+ {t('library.availableSeats')}
0}>
@@ -180,7 +198,7 @@ export default function Library () {
{
setReservationDay(day)
setReservationTime(time)
- }}>Reservieren
+ }}>{t('library.actions.reserve')}
{formatNearDate(new Date(day.date + 'T' + time.from))}
{', '}
@@ -189,10 +207,10 @@ export default function Library () {
{formatFriendlyTime(new Date(day.date + 'T' + time.to))}
- {Object.values(time.resources).reduce((acc, room) => acc + room.num_seats, 0)}
- {' / '}
- {Object.values(time.resources).reduce((acc, room) => acc + room.maxnum_seats, 0)}
- {' verfügbar'}
+ {t('library.details.seatsAvailable', {
+ available: Object.values(time.resources).reduce((acc, room) => acc + room.num_seats, 0),
+ total: Object.values(time.resources).reduce((acc, room) => acc + room.maxnum_seats, 0)
+ })}
)
@@ -205,3 +223,12 @@ export default function Library () {
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'library',
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/login.js b/rogue-thi-app/pages/login.js
index ffbb53b2..bea569df 100644
--- a/rogue-thi-app/pages/login.js
+++ b/rogue-thi-app/pages/login.js
@@ -11,10 +11,12 @@ import AppNavbar from '../components/page/AppNavbar'
import { createGuestSession, createSession } from '../lib/backend/thi-session-handler'
+import { Trans, useTranslation } from 'next-i18next'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+
import styles from '../styles/Login.module.css'
const ORIGINAL_ERROR_WRONG_CREDENTIALS = 'Wrong credentials'
-const FRIENDLY_ERROR_WRONG_CREDENTIALS = 'Deine Zugangsdaten sind falsch.'
const IMPRINT_URL = process.env.NEXT_PUBLIC_IMPRINT_URL
const PRIVACY_URL = process.env.NEXT_PUBLIC_PRIVACY_URL
@@ -29,6 +31,7 @@ export default function Login () {
const [password, setPassword] = useState('')
const [saveCredentials, setSaveCredentials] = useState(false)
const [failure, setFailure] = useState(false)
+ const { t } = useTranslation('login')
/**
* Temporary workaround for #208.
@@ -51,9 +54,9 @@ export default function Login () {
router.replace('/' + (redirect || ''))
} catch (e) {
if (e.message.includes(ORIGINAL_ERROR_WRONG_CREDENTIALS)) {
- setFailure(FRIENDLY_ERROR_WRONG_CREDENTIALS)
+ setFailure(t('error.wrongCredentials'))
} else {
- setFailure('Bei der Verbindung zum Server ist ein Fehler aufgetreten.')
+ setFailure(t('error.generic'))
}
}
}
@@ -84,19 +87,19 @@ export default function Login () {
{!failure && redirect &&
- Für diese Funktion musst du eingeloggt sein.
+ {t('alert')}
}
{GUEST_ONLY &&
- Die App kann derzeit nur als Gast verwendet werden. Weitere Informationen findet ihr unten.
+ {t('guestOnly.warning')}
}
{!GUEST_ONLY &&
<>
- THI-Benutzername
+ {t('form.username')}
- Passwort
+ {t('form.password')}
setSaveCredentials(e.target.checked)}
/>
- Einloggen
+ {t('form.login')}
>
@@ -139,7 +142,7 @@ export default function Login () {
- Als Gast einloggen
+ {t('form.guest')}
@@ -147,38 +150,44 @@ export default function Login () {
{GUEST_ONLY &&
<>
-
Warum kann ich mich nicht einloggen?
+
{t('guestOnly.title')}
- Die Hochschule hat uns dazu angewiesen, die Login-Funktion zu deaktivieren.
- Wir arbeiten an einer Lösung, allerdings ist nicht abzusehen, wann es so weit sein wird.
- Vor einer Nutzung der offiziellen THI-App raten wir aus Sicherheitsgründen ab.
+ {t('guestOnly.details')}
- Der Speiseplan, die Semester- und Veranstaltungstermine, die Raumkarte, die Bus- und Zugabfahrtszeiten sowie die Parkplatzinformationen können weiterhin über den Gastmodus genutzt werden.
-
+ {t('guestOnly.details2')}
+
>
}
-
Was ist das?
+
{t('notes.title1')}
- Das ist eine inoffizielle Alternative zur THI-App, welche eine verbesserte Benutzererfahrung bieten soll.
- Sie wird bei von Studierenden bei Neuland Ingolstadt e.V. für Studierende entwickelt und ist kein Angebot der Technischen Hochschule Ingolstadt.
+
+ }}
+ />
-
Sind meine Daten sicher?
+
{t('notes.title2')}
- Ja.
- Deine Daten werden direkt auf deinem Gerät verschlüsselt, in verschlüsselter Form über unseren Proxy an die THI übermittelt
- und erst dort wieder entschlüsselt.
- Nur du und die THI haben Zugriff auf deine Zugangsdaten und deine persönlichen Daten.
+
+ }}
+ />
- Hier findest du weitere Informationen zur Sicherheit.
+ {t('links.security')}
- Impressum
+ {t('links.imprint')}
<> – >
- Datenschutzerklärung
+ {t('links.privacy')}
<> – >
- Quellcode auf GitHub
+ {t('links.github')}
@@ -186,3 +195,12 @@ export default function Login () {
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'login',
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/mobility.js b/rogue-thi-app/pages/mobility.js
index 29e61cfb..b912b320 100644
--- a/rogue-thi-app/pages/mobility.js
+++ b/rogue-thi-app/pages/mobility.js
@@ -10,15 +10,18 @@ import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
import {
+ RenderMobilityEntry,
getMobilityEntries,
getMobilityLabel,
- getMobilitySettings,
- renderMobilityEntry
+ getMobilitySettings
} from '../lib/backend-utils/mobility-utils'
import stations from '../data/mobility.json'
import { useTime } from '../lib/hooks/time-hook'
import styles from '../styles/Mobility.module.css'
+import { useTranslation } from 'next-i18next'
+
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
export default function Bus () {
const time = useTime()
@@ -26,6 +29,7 @@ export default function Bus () {
const [station, setStation] = useState(null)
const [data, setData] = useState(null)
const [dataError, setDataError] = useState(null)
+ const { t } = useTranslation('mobility')
useEffect(() => {
const { kind, station } = getMobilitySettings()
@@ -72,40 +76,40 @@ export default function Bus () {
return (
-
+
- Verkehrsmittel
+ {t('form.type.label')}
changeKind(e.target.value)}
>
- Bus
- Bahn
- Auto
- E-Auto
+ {t('form.type.option.bus')}
+ {t('form.type.option.train')}
+ {t('form.type.option.parking')}
+ {t('form.type.option.charging')} ç
{(kind === 'bus' || kind === 'train') && (
-
- Bahnhof / Haltestelle
-
- setStation(e.target.value)}
- >
- {kind && stations[kind].stations.map(station =>
- {station.name}
- )}
-
-
+
+ {kind === 'bus' ? t('form.station.label.bus') : t('form.station.label.train')}
+
+ setStation(e.target.value)}
+ >
+ {kind && stations[kind].stations.map(station =>
+ {station.name}
+ )}
+
+
)}
@@ -113,18 +117,18 @@ export default function Bus () {
{dataError && (
- Fehler beim Abruf!
+ {t('transport.error')}
{dataError}
)}
{data && data.length === 0 &&
- Keine Elemente.
+ {t('transport.details.noElements')}
}
{data && data.map((item, idx) => (
- {renderMobilityEntry(kind, item, 200, styles, true)}
+
))}
@@ -135,3 +139,12 @@ export default function Bus () {
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'mobility',
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/personal.js b/rogue-thi-app/pages/personal.js
index fb818804..1171064a 100644
--- a/rogue-thi-app/pages/personal.js
+++ b/rogue-thi-app/pages/personal.js
@@ -11,6 +11,7 @@ import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
import DashboardModal from '../components/modal/DashboardModal'
import FilterFoodModal from '../components/modal/FilterFoodModal'
+import LanguageModal from '../components/modal/LanguageModal'
import PersonalDataModal from '../components/modal/PersonalDataModal'
import ThemeModal from '../components/modal/ThemeModal'
@@ -25,7 +26,7 @@ import {
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { FoodFilterContext, ShowDashboardModal, ShowPersonalDataModal, ShowThemeModal, ThemeContext } from './_app'
+import { FoodFilterContext, ShowDashboardModal, ShowLanguageModal, ShowPersonalDataModal, ShowThemeModal, ThemeContext } from './_app'
import { NoSessionError, UnavailableSessionError, forgetSession } from '../lib/backend/thi-session-handler'
import { USER_EMPLOYEE, USER_GUEST, USER_STUDENT, useUserKind } from '../lib/hooks/user-kind'
import { calculateECTS, loadGradeAverage, loadGrades } from '../lib/backend-utils/grades-utils'
@@ -34,6 +35,9 @@ import API from '../lib/backend/authenticated-api'
import styles from '../styles/Personal.module.css'
import themes from '../data/themes.json'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import { useTranslation } from 'next-i18next'
+
const PRIVACY_URL = process.env.NEXT_PUBLIC_PRIVACY_URL
export default function Personal () {
@@ -47,8 +51,11 @@ export default function Personal () {
const { setShowFoodFilterModal } = useContext(FoodFilterContext)
const [, setShowPersonalDataModal] = useContext(ShowPersonalDataModal)
const [, setShowThemeModal] = useContext(ShowThemeModal)
+ const [, setShowLanguageModal] = useContext(ShowLanguageModal)
const theme = useContext(ThemeContext)
const router = useRouter()
+ const { t, i18n } = useTranslation('personal')
+
const userKind = useUserKind()
const CopyableField = ({ label, value }) => {
@@ -56,7 +63,7 @@ export default function Personal () {
const handleCopy = async () => {
await navigator.clipboard.writeText(value)
- alert(`${label} in die Zwischenablage kopiert.`)
+ alert(t('personal.overview.copiedToClipboard', { label }))
}
return (
@@ -71,9 +78,9 @@ export default function Personal () {
{value
? (
<>
-
- {value}
-
+
+ {value}
+
>
)
: null}
@@ -119,7 +126,7 @@ export default function Personal () {
}, [router, userKind])
return (
-
+
@@ -128,20 +135,20 @@ export default function Personal () {
setShowPersonalDataModal(true)}>
-
+
- {userdata && userdata.name + ', ' + userdata.vname}
+ {userdata && userdata.name + ', ' + userdata.vname}
{userdata && userdata.fachrich}
- {userdata && userdata.stgru + '. Semester'}
+ {userdata && `${userdata.stgru}. ${t('personal.semester')}`}
{userdata && (
<>
-
-
+
+
>
)}
@@ -151,137 +158,156 @@ export default function Personal () {
{grades && missingGrades && grades.length + '/' + (grades.length + missingGrades.length)}
- {' Noten '}
-
+ {` ${t('personal.overview.grades')} `}
+
- {ects !== null && ects + ' ECTS'}
+ {ects !== null && `${ects} ${t('personal.overview.ects')} `}
{!isNaN(average?.result) && ' · '}
{!isNaN(average?.result) && '∅ ' + average.result.toFixed(2).toString().replace('.', ',')}
- {average?.missingWeight === 1 && ' (' + average.missingWeight + ' Gewichtung fehlt)'}
- {average?.missingWeight > 1 && ' (' + average.missingWeight + ' Gewichtungen fehlen)'}
+ {average?.missingWeight === 1 && ` (${average.missingWeight} ${t('personal.grades.missingWeightSingle')})`}
+ {average?.missingWeight > 1 && ` (${average.missingWeight} ${t('personal.grades.missingWeightMultiple')})`}
}
-
+
-
+
{themes.filter(item => item.style.includes(theme[0])).map(item => (
setShowThemeModal(true)} key={item.style}>
-
- {item.name}{' '}
-
-
+
+ {`${item.name[i18n.languages[0]]} `}
+
+
- Theme
+ {t('personal.theme')}
))}
setShowDashboardModal(true)}>
-
-
-
+
+
+
- Dashboard
+ {t('personal.dashboard')}
+
+
+ setShowLanguageModal(true)}>
+
+
+
+
+
+ {t('personal.language')}
setShowFoodFilterModal(true)}>
-
-
-
+
+
+
- Essenspräferenzen
+ {t('personal.foodPreferences')}
-
+
window.open('https://www3.primuss.de/cgi-bin/login/index.pl?FH=fhin', '_blank')}>
-
+ onClick={() => window.open('https://www3.primuss.de/cgi-bin/login/index.pl?FH=fhin', '_blank')}>
+
Primuss
window.open('https://moodle.thi.de/moodle', '_blank')}>
-
+
Moodle
window.open('https://outlook.thi.de/', '_blank')}>
-
+
E-Mail
{userKind === USER_EMPLOYEE &&
window.open('https://mythi.de', '_blank')}>
-
+
MyTHI
}
-
+
{showDebug && (
router.push('/debug')}>
-
- API Spielwiese
+
+ {t('personal.debug')}
)}
window.open(PRIVACY_URL, '_blank')}>
-
- Datenschutzerklärung
+
+ {t('personal.privacy')}
router.push('/imprint')}>
-
- Impressum
+
+ {t('personal.imprint')}
-
+
{userKind === USER_GUEST && (
forgetSession(router)}>
- {'Login '}
-
+ {`${t('personal.login')} `}
+
)}
{userKind !== USER_GUEST && (
forgetSession(router)}>
- {'Logout '}
-
+ {`${t('personal.logout')} `}
+
)}
-
-
-
-
-
+
+
+
+
+
+
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'personal',
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/rooms/index.js b/rogue-thi-app/pages/rooms/index.js
index 53237066..4a556706 100644
--- a/rogue-thi-app/pages/rooms/index.js
+++ b/rogue-thi-app/pages/rooms/index.js
@@ -11,16 +11,24 @@ import AppTabbar from '../../components/page/AppTabbar'
import 'leaflet/dist/leaflet.css'
import styles from '../../styles/Rooms.module.css'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import { useTranslation } from 'next-i18next'
+
const ROOMDATA_URL = 'https://assets.neuland.app/rooms_neuland.geojson'
// import RoomMap without SSR because react-leaflet really does not like SSR
const RoomMap = dynamic(() => import('../../components/RoomMap'), { ssr: false })
-export async function getStaticProps () {
+export async function getStaticProps ({ locale }) {
const response = await fetch(ROOMDATA_URL)
const roomData = await response.json()
return {
props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'rooms',
+ 'common',
+ 'api-translations'
+ ])),
roomData
}
}
@@ -33,15 +41,17 @@ export default function Rooms ({ roomData }) {
const router = useRouter()
const { highlight } = router.query
+ const { t } = useTranslation('rooms')
+
return (
-
+
- Stündlicher Plan
+ {t('rooms.overflow.hourlyPlan')}
window.open('https://ophase.neuland.app/', '_blank')}>
- Campus- {'&'} Stadtführung
+ {t('rooms.overflow.campusCityTour')}
diff --git a/rogue-thi-app/pages/rooms/list.js b/rogue-thi-app/pages/rooms/list.js
index 0b0be2da..3c5ab976 100644
--- a/rogue-thi-app/pages/rooms/list.js
+++ b/rogue-thi-app/pages/rooms/list.js
@@ -19,8 +19,20 @@ import API from '../../lib/backend/authenticated-api'
import styles from '../../styles/RoomsList.module.css'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+import { useTranslation } from 'next-i18next'
+
const TUX_ROOMS = ['G308']
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'rooms',
+ 'common'
+ ]))
+ }
+})
+
/**
* Page containing a textual representation of the room openings.
*/
@@ -28,6 +40,8 @@ export default function RoomList () {
const router = useRouter()
const [freeRooms, setFreeRooms] = useState(null)
+ const { t } = useTranslation('rooms')
+
useEffect(() => {
async function load () {
try {
@@ -73,7 +87,7 @@ export default function RoomList () {
return (
-
+
diff --git a/rogue-thi-app/pages/rooms/search.js b/rogue-thi-app/pages/rooms/search.js
index 0bc3ffe8..8891ea00 100644
--- a/rogue-thi-app/pages/rooms/search.js
+++ b/rogue-thi-app/pages/rooms/search.js
@@ -21,10 +21,22 @@ import { formatFriendlyTime, formatISODate, formatISOTime } from '../../lib/date
import styles from '../../styles/RoomsSearch.module.css'
+import { Trans, useTranslation } from 'next-i18next'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+
const BUILDINGS = ['A', 'B', 'BN', 'C', 'CN', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'M', 'P', 'W', 'Z']
const DURATIONS = ['00:15', '00:30', '00:45', '01:00', '01:15', '01:30', '01:45', '02:00', '02:15', '02:30', '02:45', '03:00', '03:15', '03:30', '03:45', '04:00', '04:15', '04:30', '04:45', '05:00', '05:15', '05:30', '05:45', '06:00']
const TUX_ROOMS = ['G308']
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'rooms',
+ 'common'
+ ]))
+ }
+})
+
/**
* Page containing the room search.
*/
@@ -40,6 +52,8 @@ export default function RoomSearch () {
const [searching, setSearching] = useState(false)
const [filterResults, setFilterResults] = useState(null)
+ const { t } = useTranslation('rooms')
+
/**
* Searches and displays rooms with the specified filters.
*/
@@ -71,27 +85,27 @@ export default function RoomSearch () {
return (
-
+
@@ -144,14 +158,23 @@ export default function RoomSearch () {
- frei ab {formatFriendlyTime(result.from)}
- bis {formatFriendlyTime(result.until)}
+
+ }}
+ />
)}
{filterResults && filterResults.length === 0 &&
- Keine freien Räume gefunden.
+ {t('rooms.search.results.noAvailableRooms')}
}
diff --git a/rogue-thi-app/pages/rooms/suggestions.js b/rogue-thi-app/pages/rooms/suggestions.js
index 5f9d9ec1..012a8a01 100644
--- a/rogue-thi-app/pages/rooms/suggestions.js
+++ b/rogue-thi-app/pages/rooms/suggestions.js
@@ -23,9 +23,21 @@ import styles from '../../styles/RoomsSearch.module.css'
import { USER_GUEST, useUserKind } from '../../lib/hooks/user-kind'
import { getFriendlyTimetable, getTimetableEntryName, getTimetableGaps } from '../../lib/backend-utils/timetable-utils'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+
+import { Trans, useTranslation } from 'next-i18next'
const TUX_ROOMS = ['G308']
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'rooms',
+ 'common'
+ ]))
+ }
+})
+
/**
* Page containing the room search.
*/
@@ -36,6 +48,8 @@ export default function RoomSearch () {
const userKind = useUserKind()
+ const { t } = useTranslation('rooms')
+
useEffect(() => {
async function load () {
try {
@@ -86,7 +100,7 @@ export default function RoomSearch () {
return (
-
+
@@ -112,14 +126,23 @@ export default function RoomSearch () {
- frei ab {formatFriendlyTime(roomResult.from)}
- bis {formatFriendlyTime(roomResult.until)}
+
+ }}
+ />
)}
{result.rooms.length === 0 &&
- Keine freien Räume gefunden.
+ {t('rooms.suggestions.noAvailableRooms')}
}
@@ -131,7 +154,7 @@ export default function RoomSearch () {
- Keine Vorschläge verfügbar
+ {t('rooms.suggestions.noSuggestions')}
}
@@ -149,18 +172,25 @@ export default function RoomSearch () {
function GapHeader ({ result }) {
if (result.gap.endLecture) {
return (
- <>
- {result.gap.startLecture ? getTimetableEntryName(result.gap.startLecture).shortName : 'Jetzt'}
-
- {getTimetableEntryName(result.gap.endLecture).shortName}
- {` (${result.gap.endLecture.raum})`}
- >
+
+ }}
+ values={{
+ from: result.gap.startLecture ? getTimetableEntryName(result.gap.startLecture).shortName : 'Jetzt',
+ until: getTimetableEntryName(result.gap.endLecture).shortName,
+ room: result.gap.endLecture.raum
+ }}
+ />
)
} else {
return (
- <>
- Freie Räume
- >
+
)
}
}
@@ -173,18 +203,25 @@ function GapHeader ({ result }) {
function GapSubtitle ({ result }) {
if (result.gap.endLecture) {
return (
- <>
- {'Pause von '}
- {result.gap.startLecture ? formatFriendlyTime(result.gap.startLecture.endDate) : 'Jetzt'}
- {' bis '}
- {formatFriendlyTime(result.gap.endLecture.startDate)}
- >
+
)
} else {
return (
- <>
- Räume von {formatFriendlyTime(result.gap.startDate)} bis {formatFriendlyTime(result.gap.endDate)}
- >
+
)
}
}
diff --git a/rogue-thi-app/pages/timetable.js b/rogue-thi-app/pages/timetable.js
index 6302ed7c..244271cb 100644
--- a/rogue-thi-app/pages/timetable.js
+++ b/rogue-thi-app/pages/timetable.js
@@ -19,15 +19,28 @@ import AppContainer from '../components/page/AppContainer'
import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
-import { DATE_LOCALE, addWeek, formatFriendlyTime, getFriendlyWeek, getWeek } from '../lib/date-utils'
import { NoSessionError, UnavailableSessionError } from '../lib/backend/thi-session-handler'
import { OS_IOS, useOperatingSystem } from '../lib/hooks/os-hook'
+import { addWeek, formatFriendlyTime, getFriendlyWeek, getWeek } from '../lib/date-utils'
import { getFriendlyTimetable, getTimetableEntryName } from '../lib/backend-utils/timetable-utils'
import styles from '../styles/Timetable.module.css'
+import { Trans, useTranslation } from 'next-i18next'
+import { getAdjustedLocale } from '../lib/locale-utils'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+
const VirtualizeSwipeableViews = virtualize(SwipeableViews)
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'timetable',
+ 'common'
+ ]))
+ }
+})
+
/**
* Groups timetable entries by date.
* @param {object[]} timetable
@@ -74,7 +87,7 @@ function isInWeek (date, start, end) {
* @returns {string}
*/
function getDay (date) {
- return new Date(date).toLocaleDateString(DATE_LOCALE, { day: 'numeric' })
+ return new Date(date).toLocaleDateString(getAdjustedLocale(), { day: 'numeric' })
}
/**
@@ -83,13 +96,15 @@ function getDay (date) {
* @returns {string}
*/
function getWeekday (date) {
- return new Date(date).toLocaleDateString(DATE_LOCALE, { weekday: 'short' })
+ return new Date(date).toLocaleDateString(getAdjustedLocale(), { weekday: 'short' })
}
/**
* Page displaying the users timetable.
*/
export default function Timetable () {
+ const { t } = useTranslation('timetable')
+
const router = useRouter()
const os = useOperatingSystem()
const [timetable, setTimetable] = useState(null)
@@ -207,13 +222,14 @@ export default function Timetable () {
{current && current.length === 0 &&
}
@@ -221,15 +237,27 @@ export default function Timetable () {
)
}
+ function ExplanationListElement ({ i18nKey }) {
+ return (
+
+ }}
+ />
+
+ )
+ }
+
return (
-
+
setShowTimetableExplanation(true)}>
- Fächer bearbeiten
+ {t('timetable.overflow.editLectures')}
setShowICalExplanation(true)}>
- Kalender abonnieren
+ {t('timetable.overflow.subscribeCalendar')}
@@ -237,18 +265,18 @@ export default function Timetable () {
setShowTimetableExplanation(false)}>
- Fächer bearbeiten
+ {t('timetable.modals.timetableExplanation.title')}
- Aktuell können die Fächer für den persönlichen Stundenplan leider nur in Primuss bearbeitet werden:
+ {t('timetable.modals.timetableExplanation.body.header')}
- In myStundenplan einloggen
- Links auf Fächerauswahl klicken
- Studiengang auswählen und unten abspeichern
- Oben auf Studiengruppen klicken
- Semestergruppe auswählen und unten abspeichern
- Oben auf den Studiengang klicken
- Fächer auswählen und unten abspeichern
+
+
+
+
+
+
+
{/* TODO: Video? */}
@@ -256,40 +284,40 @@ export default function Timetable () {
- Zu "myStundenplan"
+ {t('timetable.modals.timetableExplanation.actions.toMyTimetable')}
setShowTimetableExplanation(false)}>
- Schließen
+ {t('timetable.modals.timetableExplanation.actions.close')}
setShowICalExplanation(false)}>
- Kalender abonnieren
+ {t('timetable.modals.subscriptionExplanation.title')}
- Dein Stundenplan kann als Abonnement in eine Kalender-App integriert werden.
+ {t('timetable.modals.subscriptionExplanation.body.header')}
- Die URL findest du aktuell nur in Primuss:
+ {t('timetable.modals.subscriptionExplanation.body.url')}
- In myStundenplan einloggen
- Links auf Aktueller Stundenplan klicken
- Oben auf Extern klicken
- Unter Termine Abonnieren auf Link anzeigen klicken
+
+
+
+
{os === OS_IOS &&
- Die URL kannst du unter iOS wie folgt importieren:
+ {t('timetable.modals.subscriptionExplanation.body.ios.header')}
- Einstellungen-App öffnen
- Auf Kalender > Accounts > Account hinzufügen > Kalenderabo hinzufügen drücken
- Aus Primuss kopierten Link einfügen
- Auf Weiter > Sichern drücken
+
+
+
+
}
@@ -297,11 +325,11 @@ export default function Timetable () {
- Zu "myStundenplan"
+ {t('timetable.modals.subscriptionExplanation.actions.toMyTimetable')}
setShowICalExplanation(false)}>
- Schließen
+ {t('timetable.modals.subscriptionExplanation.actions.close')}
@@ -311,63 +339,63 @@ export default function Timetable () {
{focusedEntry && getTimetableEntryName(focusedEntry).name}
- Allgemein
+ {t('timetable.modals.lectureDetails.general')}
- Dozent : {focusedEntry && focusedEntry.dozent}
- Kürzel : {focusedEntry && getTimetableEntryName(focusedEntry).shortName}
- Prüfung : {focusedEntry && focusedEntry.pruefung}
- Studiengang : {focusedEntry && focusedEntry.stg}
- Studiengruppe : {focusedEntry && focusedEntry.stgru}
- Semesterwochenstunden : {focusedEntry && focusedEntry.sws}
- ECTS : {focusedEntry && focusedEntry.ectspoints}
+ {t('timetable.modals.lectureDetails.lecturer')} : {focusedEntry && focusedEntry.dozent}
+ {t('timetable.modals.lectureDetails.abbreviation')} : {focusedEntry && getTimetableEntryName(focusedEntry).shortName}
+ {t('timetable.modals.lectureDetails.exam')} : {focusedEntry && focusedEntry.pruefung}
+ {t('timetable.modals.lectureDetails.courseOfStudies')} : {focusedEntry && focusedEntry.stg}
+ {t('timetable.modals.lectureDetails.studyGroup')} : {focusedEntry && focusedEntry.stgru}
+ {t('timetable.modals.lectureDetails.semesterWeeklyHours')} : {focusedEntry && focusedEntry.sws}
+ {t('timetable.modals.lectureDetails.ects')} : {focusedEntry && focusedEntry.ectspoints}
- Ziel
+ {t('timetable.modals.lectureDetails.goal')}
{focusedEntry && focusedEntry.ziel && (
)}
{focusedEntry && !focusedEntry.ziel && (
- Keine Angabe
+ {t('timetable.modals.lectureDetails.notSpecified')}
)}
- Inhalt
+ {t('timetable.modals.lectureDetails.content')}
{focusedEntry && focusedEntry.inhalt && (
)}
{focusedEntry && !focusedEntry.inhalt && (
- Keine Angabe
+ {t('timetable.modals.lectureDetails.notSpecified')}
)}
- Literatur
+ {t('timetable.modals.lectureDetails.literature')}
{focusedEntry && focusedEntry.literatur && (
)}
{focusedEntry && !focusedEntry.literatur && (
- Keine Angabe
+ {t('timetable.modals.lectureDetails.notSpecified')}
)}
setFocusedEntry(null)}>
- Schließen
+ {t('timetable.modals.lectureDetails.actions.close')}
setPage(idx => idx - 1)}>
-
+
{getFriendlyWeek(week[0])}
setPage(idx => idx + 1)}>
-
+
diff --git a/rogue-thi-app/public/locales/de/api-translations.json b/rogue-thi-app/public/locales/de/api-translations.json
new file mode 100644
index 00000000..b6972b4b
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/api-translations.json
@@ -0,0 +1,104 @@
+{
+ "__source": "Generated using the thi-translator script",
+ "apiTranslations": {
+ "lecturerFunctions": {
+ "Laboringenieur(in)": "Laboringenieur(in)",
+ "Lehrbeauftragte(r)": "Lehrbeauftragte(r)",
+ "Lehrkraft für besondere Aufgaben": "Lehrkraft für besondere Aufgaben",
+ "Professor(in)": "Professor(in)",
+ "Wiss. Mitarbeiter(in) mit Deputat": "Wiss. Mitarbeiter(in) mit Deputat",
+ "Wissenschaftl. Mitarbeiter(in)": "Wissenschaftl. Mitarbeiter(in)",
+ "unbestimmt": "unbestimmt"
+ },
+ "lecturerOrganizations": {
+ "Fakultät Business School": "Fakultät Business School",
+ "Fakultät Elektro- und Informationstechnik": "Fakultät Elektro- und Informationstechnik",
+ "Fakultät Informatik": "Fakultät Informatik",
+ "Fakultät Maschinenbau": "Fakultät Maschinenbau",
+ "Fakultät Wirtschaftsingenieurwesen": "Fakultät Wirtschaftsingenieurwesen",
+ "Institut für Akademische Weiterbildung": "Institut für Akademische Weiterbildung",
+ "Nachhaltige Infrastruktur": "Nachhaltige Infrastruktur",
+ "Sprachenzentrum": "Sprachenzentrum",
+ "Verwaltung": "Verwaltung",
+ "Zentrum für Angewandte Forschung": "Zentrum für Angewandte Forschung"
+ },
+ "roomFunctions": {
+ "Aufenthalt Enterpreneur": "Aufenthalt Entrepreneur",
+ "Aufenthaltsraum": "Aufenthaltsraum",
+ "Außentreppe": "Außentreppe",
+ "Behinderten-WC": "Behinderten-WC",
+ "Besprechung": "Besprechung",
+ "Besprechungszimmer": "Besprechungszimmer",
+ "Bibliothek": "Bibliothek",
+ "Büro": "Büro",
+ "Büro 1": "Büro",
+ "Büro 2": "Büro",
+ "Büro 3": "Büro",
+ "Carrels 14-20": "Carrels 14-20",
+ "ELT-UV / Kopier": "ELT-UV / Kopierer",
+ "Erste Hilfe": "Erste Hilfe",
+ "Flur": "Flur",
+ "Flur Enterpreneur": "Flur",
+ "Großer Hörsaal (80-200 Plätze)": "Großer Hörsaal (80-200 Plätze)",
+ "Gruppenarbeitsraum": "Gruppenarbeitsraum",
+ "HA / Elektro": "HA / Elektro",
+ "HUB 01 Enterpreneur": "Entrepreneur Hub",
+ "HUB 02 Enterpreneur": "Entrepreneur Hub",
+ "HUB 03 Enterpreneur": "Entrepreneur Hub",
+ "Hörsaal": "Hörsaal",
+ "Kleiner Hörsaal (40-79 Plätze)": "Kleiner Hörsaal (40-79 Plätze)",
+ "Labor": "Labor",
+ "Labor/PC-Pool": "Labor/PC-Pool",
+ "Leuchtmittel": "Leuchtmittel",
+ "Mensa": "Mensa",
+ "PC-Labor": "PC-Labor",
+ "PC-Pool": "PC-Pool",
+ "Pausenraum": "Pausenraum",
+ "Personal": "Personal",
+ "Pumi": "Pumi",
+ "Putzraum": "Putzraum",
+ "Raum": "Raum",
+ "Seminar": "Seminar",
+ "Seminarraum (< 40 Plätze)": "Seminarraum (< 40 Plätze)",
+ "Seminarraum 1": "Seminarraum",
+ "Seminarraum 10": "Seminarraum",
+ "Seminarraum 11": "Seminarraum",
+ "Seminarraum 12": "Seminarraum",
+ "Seminarraum 2": "Seminarraum",
+ "Seminarraum 3": "Seminarraum",
+ "Seminarraum 4": "Seminarraum",
+ "Seminarraum 5": "Seminarraum",
+ "Seminarraum 6": "Seminarraum",
+ "Seminarraum 7": "Seminarraum",
+ "Seminarraum 8": "Seminarraum",
+ "Seminarraum 9": "Seminarraum",
+ "Servicepoint": "Servicepoint",
+ "Sonstiger Raum": "Sonstiger Raum",
+ "Sozialraum": "Sozialraum",
+ "TRH": "TRH",
+ "Teaching Library": "Teaching Library",
+ "Technik": "Technik",
+ "Technik / HLS": "Technik / HLS",
+ "Toilette": "Toilette",
+ "Treppe": "Treppe",
+ "Treppenhaus": "Treppenhaus",
+ "Treppenhaus D2": "Treppenhaus",
+ "Unbestimmt": "Unbestimmt",
+ "Versuchslabor": "Versuchslabor",
+ "Vorbereitung": "Vorbereitung",
+ "Vorlesung": "Vorlesungssaal",
+ "WC": "WC",
+ "WC BEH": "Behinderten-WC",
+ "WC Bf": "WC",
+ "WC D": "WC Damen",
+ "WC Damen": "WC Damen",
+ "WC H": "WC Herren",
+ "WC Herren": "WC Herren",
+ "WC-Beh": "Behinderten-WC",
+ "WC-Damen": "WC Damen",
+ "corridor": "Flur",
+ "room": "Raum",
+ "yes": "yes"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/become-hackerman.json b/rogue-thi-app/public/locales/de/become-hackerman.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/become-hackerman.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/calendar.json b/rogue-thi-app/public/locales/de/calendar.json
new file mode 100644
index 00000000..5f155bfc
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/calendar.json
@@ -0,0 +1,33 @@
+{
+ "calendar": {
+ "appbar": {
+ "title": "Termine"
+ },
+ "modals": {
+ "exams": {
+ "type": "Art",
+ "room": "Raum",
+ "seat": "Sitzplatz",
+ "date": "Termin",
+ "notes": "Anmerkungen",
+ "examiner": "Prüfer:in",
+ "courseOfStudies": "Studiengang",
+ "registerDate": "Angemeldet",
+ "tools": "Hilfsmittel",
+ "actions": {
+ "close": "Schließen"
+ }
+ }
+ },
+ "notice": "Alle Angaben ohne Gewähr. Verbindliche Informationen gibt es nur direkt auf der Webseite der Hochschule .",
+ "tabs": {
+ "semester": "Semester",
+ "exams": "Prüfungen"
+ },
+ "dates": {
+ "until": "bis"
+ },
+ "noExams": "Es sind derzeit keine Prüfungstermine verfügbar.",
+ "guestNotice": "Prüfungstermine sind als Gast nicht verfügbar."
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/common.json b/rogue-thi-app/public/locales/de/common.json
new file mode 100644
index 00000000..cbb0474f
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/common.json
@@ -0,0 +1,71 @@
+{
+ "common": {
+ "dates": {
+ "today": "Heute",
+ "tomorrow": "Morgen",
+ "thisWeek": "Diese Woche",
+ "nextWeek": "Nächste Woche"
+ }
+ },
+ "food": {
+ "filterModal": {
+ "header": "Filter",
+ "restaurants": {
+ "title": "Restaurants",
+ "showMensa": "Mensa anzeigen",
+ "showReimanns": "Reimanns anzeigen",
+ "showCanisius": "Canisiuskonvikt anzeigen"
+ },
+ "allergens": {
+ "title": "Allergien",
+ "iconTitle": "Allergene",
+ "selected": "Ausgewählt",
+ "empty": "Keine"
+ },
+ "preferences": {
+ "title": "Essenspräferenzen",
+ "iconTitle": "Präferenzen",
+ "selected": "Ausgewählt",
+ "empty": "Keine"
+ },
+ "info": "Deine Angaben werden nur lokal auf deinem Gerät gespeichert und an niemanden übermittelt."
+ },
+ "allergensModal" : "Allergene auswählen",
+ "preferencesModal" : "Präferenzen auswählen"
+ },
+ "dashboard": {
+ "orderModal": {
+ "title": "Dashboard",
+ "body": "Hier kannst du die Reihenfolge der im Dashboard angezeigten Einträge verändern.",
+ "hiddenCards": "Ausgeblendete Elemente",
+ "resetOrder": "Reihenfolge zurücksetzen",
+ "icons": {
+ "moveUp": "Nach oben",
+ "moveDown": "Nach unten",
+ "remove": "Entfernen",
+ "restore": "Wiederherstellen",
+ "personalize": "Personalisieren"
+ }
+ }
+ },
+ "cards": {
+ "install": "Installation",
+ "timetable": "Stundenplan",
+ "mensa": "Essen",
+ "mobility": "Mobilität",
+ "calendar": "Termine",
+ "events": "Veranstaltungen",
+ "rooms": "Raumplan",
+ "library": "Bibliothek",
+ "grades": "Noten & Fächer",
+ "personal": "Profil",
+ "lecturers": "Dozenten"
+ },
+ "prompts": {
+ "close": "Schließen"
+ },
+ "appbar": {
+ "back": "Zurück",
+ "overflow": "Mehr Optionen"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/dashboard.json b/rogue-thi-app/public/locales/de/dashboard.json
new file mode 100644
index 00000000..5e99ba61
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/dashboard.json
@@ -0,0 +1,85 @@
+{
+ "timetable": {
+ "title": "Stundenplan",
+ "text": {
+ "endingSoon": "endet in {{mins}} min",
+ "ongoing": "endet um {{time}}",
+ "startingSoon": "beginnt in {{mins}} min",
+ "future": "um"
+ }
+ },
+ "food": {
+ "location": {
+ "food": {
+ "title": "Essen"
+ },
+ "cafeteria": {
+ "title": "Mensa"
+ },
+ "reimanns": {
+ "title": "Reimanns"
+ },
+ "canisius": {
+ "title": "Canisiuskonvikt"
+ }
+ },
+ "error": {
+ "empty": "Der heutige Speiseplan ist leer.",
+ "generic": "Fehler beim Abruf des Speiseplans. Irgendetwas scheint kaputt zu sein."
+ },
+ "text.additional": "und {{count}} weitere Gerichte"
+ },
+ "transport": {
+ "title": {
+ "train": "Bahn ({{station}})",
+ "bus": "Bus ({{station}})",
+ "parking": "Parkplätze",
+ "charging": "Ladestationen"
+ },
+ "error": {
+ "empty": "Keine Elemente.",
+ "generic": "Fehler beim Abruf."
+ }
+ },
+ "calendar": {
+ "title": "Kalender",
+ "date": {
+ "ends": "endet",
+ "starts": "beginnt"
+ }
+ },
+ "events": {
+ "title": "Veranstaltungen",
+ "organizer.attribute": "von"
+ },
+ "rooms": {
+ "title": "Räume",
+ "text": "Frei von {{from}} bis {{until}}"
+ },
+ "library": {
+ "title": "Bibliothek"
+ },
+ "grades": {
+ "title": "Noten & Fächer"
+ },
+ "personal": {
+ "title": "Persönliche Daten"
+ },
+ "lecturers": {
+ "title": "Dozenten"
+ },
+ "election": {
+ "title": "Hochschulwahlen",
+ "text": "Aktuell finden die Hochschulwahlen statt. Deine Teilnahme ist wichtig, um die demokratischen Strukturen an unserer Hochschule zu stärken.",
+ "button": "Stimme online abgeben",
+ "icon.close": "Schließen"
+ },
+ "install": {
+ "title": "Installation",
+ "text": {
+ "question": "Möchtest du diese App auf deinem Smartphone installieren ?",
+ "ios": "Drücke in Safari auf Teilen und dann auf Zum Home-Bildschirm .",
+ "android": "Öffne in Chrome das Menü und drücke dann auf Zum Startbildschirm zufügen ."
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/debug.json b/rogue-thi-app/public/locales/de/debug.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/debug.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/events.json b/rogue-thi-app/public/locales/de/events.json
new file mode 100644
index 00000000..23fe4b6c
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/events.json
@@ -0,0 +1,11 @@
+{
+ "events": {
+ "appbar": {
+ "title": "Veranstaltungen"
+ }
+ },
+ "noEvents": "Es sind derzeit keine Veranstaltungstermine verfügbar.",
+ "dates": {
+ "until": "bis"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/food.json b/rogue-thi-app/public/locales/de/food.json
new file mode 100644
index 00000000..e6eac915
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/food.json
@@ -0,0 +1,76 @@
+{
+ "warning": {
+ "unknownIngredients": {
+ "text": "Unbekannte Zutaten / Allergene'",
+ "iconTitle": "Allergiewarnung"
+ }
+ },
+ "filter": "Filter",
+ "preferences": {
+ "iconTitle": "Bevorzugtes Essen"
+ },
+ "list": {
+ "titles": {
+ "cafeteria": "Mensa",
+ "meals": "Gerichte",
+ "soups": "Suppen",
+ "salads": "Salate"
+
+ }
+ },
+ "error": {
+ "dataUnavailable": "Keine Daten verfügbar"
+ },
+ "navigation": {
+ "weeks": {
+ "previous": "Woche zurück",
+ "next": "Woche vor"
+ }
+ },
+ "foodModal": {
+ "header": "Erläuterung",
+ "flags": {
+ "title": "Anmerkungen",
+ "unkown": "Unbekannt",
+ "empty": "Keine Anmerkungen vorhanden."
+ },
+ "allergens": {
+ "title": "Allergene",
+ "unkown": "Unbekannt",
+ "empty": "Keine Allergene vorhanden.",
+ "fallback" : "Unbekannt (Das ist schlecht)"
+ },
+ "nutrition": {
+ "title": "Nährwerte",
+ "energy.title": "Energie",
+ "fat": {
+ "title": "Fett",
+ "saturated": "davon gesättigte Fettsäuren"
+ },
+ "carbohydrates": {
+ "title": "Kohlenhydrate",
+ "sugar": "davon Zucker"
+ },
+ "fiber.title": "Ballaststoffe",
+ "protein.title": "Eiweiß",
+ "salt.title": "Salz",
+ "unkown.title": "Unbekannt."
+ },
+ "prices": {
+ "title": "Preise",
+ "students": "Studierende",
+ "employees": "Mitarbeitende",
+ "guests": "Gäste"
+ },
+ "warning": {
+ "title": "Angaben ohne Gewähr.",
+ "text": "Bitte prüfe die Angaben auf den Infobildschirmen, bevor du etwas konsumiert. Die Nährwertangaben beziehen sich auf eine durchschnittliche Portion."
+ },
+ "translation": {
+ "title": "Übersetzung",
+ "warning": "Dieses Gericht wurde automatisch übersetzt. Bitte überprüfen Sie die Informationen auf den Informationsbildschirmen, bevor du etwas konsumierst.",
+ "originalName": "Originalname",
+ "translatedName": "Übersetzter Name"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/grades.json b/rogue-thi-app/public/locales/de/grades.json
new file mode 100644
index 00000000..a6985520
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/grades.json
@@ -0,0 +1,31 @@
+{
+ "grades": {
+ "appbar": {
+ "title": "Noten & Fächer",
+ "overflow": {
+ "copyFormula": "Notenschnitt Formel kopieren",
+ "exportCsv": "Noten als CSV exportieren"
+ }
+ },
+ "alerts": {
+ "temporarilyUnavailable": "Noten sind vorübergehend nicht verfügbar.",
+ "copyToClipboard": "In Zwischenablage kopiert",
+ "notImplemented": "Diese Funktion ist noch nicht verfügbar."
+ },
+ "summary": {
+ "title": "Notenschnitt",
+ "disclaimer": "Der genaue Notenschnitt kann nicht ermittelt werden und liegt zwischen {{minAverage} und {{maxAverage}}"
+ },
+ "gradesList": {
+ "title": "Noten"
+ },
+ "pendingList": {
+ "title": "Ausstehende Fächer"
+ },
+ "grade": "Note",
+ "deadline": "Frist",
+ "ects": "ECTS",
+ "none": "(keine)",
+ "credited": "(angerechnet)"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/imprint.json b/rogue-thi-app/public/locales/de/imprint.json
new file mode 100644
index 00000000..897ee55b
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/imprint.json
@@ -0,0 +1,18 @@
+{
+ "imprint": {
+ "appbar": {
+ "title": "Impressum"
+ },
+ "feedback": {
+ "title": "Wir würden uns über euer Feedback freuen.",
+ "email": "E-Mail",
+ "website": "Webseite",
+ "instagram": "Instagram",
+ "sourceCode": "Quellcode auf GitHub",
+ "joinNeuland": "Jetzt Mitglied werden und die Entwicklung unterstützen!"
+ },
+ "legal": {
+ "title": "Rechtliche Hinweise von Neuland Ingolstadt e.V."
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/index.json b/rogue-thi-app/public/locales/de/index.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/index.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/lecturers.json b/rogue-thi-app/public/locales/de/lecturers.json
new file mode 100644
index 00000000..ad82da56
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/lecturers.json
@@ -0,0 +1,33 @@
+{
+ "lecturers": {
+ "appbar": {
+ "title": "Dozenten"
+ },
+ "modals": {
+ "details": {
+ "title": "Titel",
+ "surname": "Name",
+ "forename": "Vorname",
+ "organization": "Organisation",
+ "function": "Funktion",
+ "room": "Raum",
+ "email": "E-Mail",
+ "phone": "Telefon",
+ "officeHours": "Sprechzeiten",
+ "insights": "Einsichtnahme",
+ "actions": {
+ "close": "Schließen"
+ },
+ "notAvailable": "N/A"
+ }
+ },
+ "search": {
+ "placeholder": "Alle Dozenten durchsuchen...",
+ "personalLecturers": "Persönliche Dozenten",
+ "searchResults": "Suchergebnisse"
+ },
+ "body": {
+ "room": "Raum"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/library.json b/rogue-thi-app/public/locales/de/library.json
new file mode 100644
index 00000000..3c8c627b
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/library.json
@@ -0,0 +1,38 @@
+{
+ "library": {
+ "title": "Bibliothek",
+ "yourReservations": "Deine Reservierungen",
+ "availableSeats": "Verfügbare Plätze",
+ "actions": {
+ "reserve": "Reservieren",
+ "delete": "Löschen"
+ },
+ "details": {
+ "seatsAvailable": "{{available}} / {{total}} verfügbar",
+ "noReservations": "Du hast keine Reservierungen",
+ "reservationDetails": "{{category}} , Platz {{seat}}, Reservierung {{reservation_id}}"
+ },
+ "modal": {
+ "title": "Sitzplatz reservieren",
+ "details": {
+ "day": "Tag",
+ "start": "Start",
+ "end": "Ende",
+ "location": "Ort",
+ "seat": "Sitz"
+ },
+ "seatSelection": {
+ "any": "Egal"
+ },
+ "librarySelection": {
+ "libraryNorth": "Lesesaal Nord (alte Bibliothek)",
+ "librarySouth": "Lesesaal Süd (neue Bibliothek)",
+ "libraryGallery": "Lesesaal Galerie"
+ },
+ "actions": {
+ "cancel": "Abbrechen",
+ "reserve": "Reservieren"
+ }
+ }
+ }
+}
diff --git a/rogue-thi-app/public/locales/de/login.json b/rogue-thi-app/public/locales/de/login.json
new file mode 100644
index 00000000..3b9165fb
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/login.json
@@ -0,0 +1,32 @@
+{
+ "error": {
+ "wrongCredentials": "Deine Zugangsdaten sind falsch.",
+ "generic": "Bei der Verbindung zum Server ist ein Fehler aufgetreten."
+ },
+ "alert": "Für diese Funktion musst du eingeloggt sein.",
+ "guestOnly": {
+ "warning": "Die App kann derzeit nur als Gast verwendet werden. Weitere Informationen findet ihr unten.",
+ "title": "Warum kann ich mich nicht einloggen?",
+ "details": "Die Hochschule hat uns dazu angewiesen, die Login-Funktion zu deaktivieren. Wir arbeiten an einer Lösung, allerdings ist nicht abzusehen, wann es so weit sein wird. Vor einer Nutzung der offiziellen THI-App raten wir aus Sicherheitsgründen ab.",
+ "details2": "Der Speiseplan, die Semester- und Veranstaltungstermine, die Raumkarte, die Bus- und Zugabfahrtszeiten sowie die Parkplatzinformationen können weiterhin über den Gastmodus genutzt werden."
+ },
+ "notes": {
+ "title1": "Was ist das?",
+ "text1": "Das ist eine inoffizielle Alternative zur THI-App, welche eine verbesserte Benutzererfahrung bieten soll. Sie wird bei von Studierenden bei Neuland Ingolstadt e.V. für Studierende entwickelt und ist kein Angebot der Technischen Hochschule Ingolstadt.",
+ "title2": "Sind meine Daten sicher?",
+ "text2": "Ja. Deine Daten werden direkt auf deinem Gerät verschlüsselt, in verschlüsselter Form über unseren Proxy an die THI übermittelt und erst dort wieder entschlüsselt. Nur du und die THI haben Zugriff auf deine Zugangsdaten und deine persönlichen Daten."
+ },
+ "links": {
+ "security": "Hier findest du weitere Informationen zur Sicherheit.",
+ "imprint": "Impressum",
+ "privacy": "Datenschutzerklärung",
+ "github": "Quellcode auf GitHub"
+ },
+ "form": {
+ "username": "THI-Benutzername",
+ "password": "Passwort",
+ "login": "Anmelden",
+ "guest": "Als Gast fortfahren",
+ "save": "Angemeldet bleiben"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/mobility.json b/rogue-thi-app/public/locales/de/mobility.json
new file mode 100644
index 00000000..b13d6500
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/mobility.json
@@ -0,0 +1,45 @@
+{
+ "form": {
+ "type": {
+ "label": "Verkehrsmittel",
+ "option": {
+ "bus": "Bus",
+ "train": "Bahn",
+ "parking": "Auto",
+ "charging": "E-Auto"
+ }
+ },
+ "station": {
+ "label": {
+ "bus": "Haltestelle",
+ "train": "Bahnhof"
+ }
+ }
+ },
+ "transport": {
+ "title": {
+ "bus": "Bus ({{station}})",
+ "train": "Bahn ({{station}})",
+ "parking": "Parkplätze",
+ "charging": "Ladestationen",
+ "unkown": "Mobilität"
+ },
+ "details": {
+ "charging": {
+ "available": "{{available}} von {{total}} frei"
+ },
+ "parking": {
+ "available": "{{available}} frei",
+ "unknown": "n/a",
+ "employees": "Mitarbeiter",
+ "free": "Kostenlos",
+ "paid": "Kostenpflichtig",
+ "restricted": "Zugangsbeschränkt"
+ },
+ "noElements": "Keine Elemente."
+ },
+ "error": {
+ "retrieval": "Fehler beim Abruf."
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/personal.json b/rogue-thi-app/public/locales/de/personal.json
new file mode 100644
index 00000000..cc7bc5df
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/personal.json
@@ -0,0 +1,51 @@
+{
+ "personal": {
+ "title": "Profil",
+ "grades": {
+ "title": "Noten",
+ "missingWeightSingle": " Gewichtung fehlt",
+ "missingWeightMultiple": " Gewichtungen fehlen"
+ },
+ "overview": {
+ "grades": "Noten",
+ "ects": "ECTS",
+ "matriculationNumber": "Mat.-Nr",
+ "libraryNumber": "Bib.-Nr",
+ "copiedToClipboard": "{{label}} in Zwischenablage kopiert."
+ },
+ "semester": "Semester",
+ "foodPreferences": "Essenspräferenzen",
+ "debug": "API Spielwiese",
+ "privacy": "Datenschutzerklärung",
+ "imprint": "Impressum",
+ "language": "Sprache ändern",
+ "login": "Anmelden",
+ "logout": "Ausloggen",
+ "dashboard": "Dashboard",
+ "theme": "Theme",
+ "modals": {
+ "theme": {
+ "title": "Theme",
+ "hackerman": "Um das Hackerman -Design freizuschalten, musst du mindestens vier Aufgaben unseres Übungs-CTFs lösen. Wenn du so weit bist, kannst du es hier freischalten."
+ },
+ "language": {
+ "title": "Sprache"
+ },
+ "personalData": {
+ "matriculationNumber": "Matrikelnummer",
+ "libraryNumber": "Bibliotheksnummer",
+ "printerBalance": "Druckguthaben",
+ "fieldOfStudy": "Studiengang",
+ "semester": "Fachsemester",
+ "examRegulations": "Prüfungsordnung",
+ "email": "E-Mail",
+ "thiEmail": "THI E-Mail",
+ "phone": "Telefon",
+ "firstName": "Vorname",
+ "lastName": "Nachname",
+ "street": "Straße",
+ "city": "Ort"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/rooms.json b/rogue-thi-app/public/locales/de/rooms.json
new file mode 100644
index 00000000..329938e7
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/rooms.json
@@ -0,0 +1,68 @@
+{
+ "rooms": {
+ "overflow": {
+ "hourlyPlan": "Stündlicher Plan",
+ "campusCityTour": "Campus- & Stadtführung"
+ },
+ "map": {
+ "appbar": {
+ "title": "Raumplan"
+ },
+ "searchPlaceholder": "Suche nach 'W003', 'Toilette', 'Bibliothek', ...",
+ "extendedSearch": "Erweiterte Suche",
+ "automaticSuggestion": "Automatische Vorschläge",
+ "occupied": "Belegt",
+ "freeFromUntil": "Frei von {{from}} bis {{until}}",
+ "attribution": "© OpenStreetMap -Mitwirkende",
+ "legend": {
+ "free": "Frei",
+ "occupied": "Belegt",
+ "specialEquipment": "Sonderausstattung",
+ "occupancyUnknown": "Belegung unbekannt"
+ },
+ "floors": {
+ "eg": "EG"
+ }
+ },
+ "search": {
+ "appbar": {
+ "title": "Erweiterte Raumsuche"
+ },
+ "building": "Gebäude",
+ "date": "Datum",
+ "time": "Uhrzeit",
+ "duration": "Dauer",
+ "search": "Suchen",
+ "buildingsAll": "Alle",
+ "results": {
+ "availableFromUntil": "frei ab {{from}} bis {{until}}",
+ "noAvailableRooms": "Keine freien Räume gefunden"
+ }
+ },
+ "suggestions": {
+ "appbar": {
+ "title": "Raumvorschläge"
+ },
+ "noSuggestions": "Keine Vorschläge verfügbar.",
+ "noAvailableRooms": "Keine freien Räume gefunden.",
+ "gaps": {
+ "header": {
+ "general": "Freie Räume",
+ "specific": "{{from}} {{until}}"
+ },
+ "subtitle": {
+ "general": "Pause von {{from}} bis {{until}}",
+ "specific": "Räume von {{from}} bis {{until}}"
+ }
+ }
+ },
+ "list": {
+ "appbar": {
+ "title": "Stündlicher Raumplan"
+ }
+ },
+ "common": {
+ "availableFromUntil": "frei ab {{from}} bis {{until}}"
+ }
+ }
+}
diff --git a/rogue-thi-app/public/locales/de/timetable.json b/rogue-thi-app/public/locales/de/timetable.json
new file mode 100644
index 00000000..db8e92c3
--- /dev/null
+++ b/rogue-thi-app/public/locales/de/timetable.json
@@ -0,0 +1,77 @@
+{
+ "timetable": {
+ "appbar": {
+ "title": "Stundenplan"
+ },
+ "overview": {
+ "noLectures": "Keine Veranstaltungen. 🎉",
+ "configureTimetable": "Du kannst deinen Stundenplan im Stundenplantool der THI zusammenstellen , dann erscheinen hier die Fächer."
+ },
+ "overflow": {
+ "editLectures": "Fächer bearbeiten",
+ "subscribeCalendar": "Kalender abonnieren"
+ },
+ "weekSelection": {
+ "weekBack": "Woche zurück",
+ "weekForward": "Woche vor"
+ },
+ "modals": {
+ "timetableExplanation": {
+ "title": "Fächer bearbeiten",
+ "body": {
+ "header": "Aktuell können die Fächer für den persönlichen Stundenplan leider nur in Primuss bearbeitet werden:",
+ "login": "In myStundenplan einloggen",
+ "subjects": "Links auf Fächerauswahl klicken",
+ "courseOfStudies": "Studiengang auswählen und unten abspeichern",
+ "studyGroups": "Oben auf Studiengruppen klicken",
+ "semesterGroup": "Semestergruppe auswählen und unten abspeichern",
+ "clickOnStudy": "Oben auf den Studiengang klicken",
+ "selectSubjects": "Fächer auswählen und unten abspeichern"
+ },
+ "actions": {
+ "close": "Schließen",
+ "toMyTimetable": "Zu \"myStundenplan\""
+ }
+ },
+ "subscriptionExplanation": {
+ "title": "Kalender abonnieren",
+ "body": {
+ "header": "Dein Stundenplan kann als Abonnement in eine Kalender-App integriert werden.",
+ "url": "Die URL findest du aktuell nur in Primuss:",
+ "login": "In myStundenplan einloggen",
+ "timetable": "Links auf Aktueller Stundenplan klicken",
+ "externalCalendar": "Oben auf Extern klicken",
+ "subscribe": "Unter Termine Abonnieren auf Link anzeigen klicken",
+ "ios": {
+ "header": "Die URL kannst du unter iOS wie folgt importieren:",
+ "openSettings": "Einstellungen-App öffnen",
+ "addCalendarSubscription": "Auf Kalender > Accounts > Account hinzufügen > Kalenderabo hinzufügen drücken",
+ "pasteUrl": "Aus Primuss kopierten Link einfügen",
+ "save": "Auf Weiter > Sichern drücken"
+ }
+ },
+ "actions": {
+ "close": "Schließen",
+ "toMyTimetable": "Zu \"myStundenplan\""
+ }
+ },
+ "lectureDetails": {
+ "general": "Allgemein",
+ "lecturer": "Dozent:in",
+ "abbreviation": "Kürzel",
+ "exam": "Prüfung",
+ "courseOfStudies": "Studiengang",
+ "studyGroup": "Studiengruppe",
+ "semesterWeeklyHours": "Semesterwochenstunden",
+ "ects": "ECTS",
+ "goal": "Ziel",
+ "content": "Inhalt",
+ "literature": "Literatur",
+ "notSpecified": "Keine Angabe",
+ "actions": {
+ "close": "Schließen"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/api-translations.json b/rogue-thi-app/public/locales/en/api-translations.json
new file mode 100644
index 00000000..cb7f38cb
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/api-translations.json
@@ -0,0 +1,104 @@
+{
+ "__source": "Generated using the thi-translator script",
+ "apiTranslations": {
+ "lecturerFunctions": {
+ "Laboringenieur(in)": "Laboratory Engineer",
+ "Lehrbeauftragte(r)": "Lecturer",
+ "Lehrkraft für besondere Aufgaben": "Teacher for special tasks",
+ "Professor(in)": "Professor",
+ "Wiss. Mitarbeiter(in) mit Deputat": "Scientific staff with Deputat",
+ "Wissenschaftl. Mitarbeiter(in)": "Research associate",
+ "unbestimmt": "indeterminate"
+ },
+ "lecturerOrganizations": {
+ "Fakultät Business School": "Faculty Business School",
+ "Fakultät Elektro- und Informationstechnik": "Faculty of Electrical Engineering and Information Technology",
+ "Fakultät Informatik": "Faculty of Informatics",
+ "Fakultät Maschinenbau": "Faculty of Mechanical Engineering",
+ "Fakultät Wirtschaftsingenieurwesen": "Faculty of Industrial Engineering",
+ "Institut für Akademische Weiterbildung": "Institute for Academic Continuing Education",
+ "Nachhaltige Infrastruktur": "Sustainable infrastructure",
+ "Sprachenzentrum": "Language Center",
+ "Verwaltung": "Management",
+ "Zentrum für Angewandte Forschung": "Center for Applied Research"
+ },
+ "roomFunctions": {
+ "Aufenthalt Enterpreneur": "Entrepreneur Lounge",
+ "Aufenthaltsraum": "Common Room",
+ "Außentreppe": "Outdoor Staircase",
+ "Behinderten-WC": "Accessible Toilet",
+ "Besprechung": "Meeting",
+ "Besprechungszimmer": "Meeting Room",
+ "Bibliothek": "Library",
+ "Büro": "Office",
+ "Büro 1": "Office",
+ "Büro 2": "Office",
+ "Büro 3": "Office",
+ "Carrels 14-20": "Carrels 14-20",
+ "ELT-UV / Kopier": "ELT-UV / Copy",
+ "Erste Hilfe": "First Aid",
+ "Flur": "Corridor",
+ "Flur Enterpreneur": "Corridor",
+ "Großer Hörsaal (80-200 Plätze)": "Large Lecture Hall (80-200 Seats)",
+ "Gruppenarbeitsraum": "Group Study Room",
+ "HA / Elektro": "HA / Electrical",
+ "HUB 01 Enterpreneur": "Entrepreneur Hub",
+ "HUB 02 Enterpreneur": "Entrepreneur Hub",
+ "HUB 03 Enterpreneur": "Entrepreneur Hub",
+ "Hörsaal": "Lecture Hall",
+ "Kleiner Hörsaal (40-79 Plätze)": "Small Lecture Hall (40-79 Seats)",
+ "Labor": "Laboratory",
+ "Labor/PC-Pool": "Laboratory / PC Pool",
+ "Leuchtmittel": "Lighting",
+ "Mensa": "Cafeteria",
+ "PC-Labor": "PC Lab",
+ "PC-Pool": "PC Pool",
+ "Pausenraum": "Break Room",
+ "Personal": "Staff",
+ "Pumi": "Pumi",
+ "Putzraum": "Cleaning Room",
+ "Raum": "Room",
+ "Seminar": "Seminar",
+ "Seminarraum (< 40 Plätze)": "Seminar Room (< 40 Seats)",
+ "Seminarraum 1": "Seminar Room",
+ "Seminarraum 10": "Seminar Room",
+ "Seminarraum 11": "Seminar Room",
+ "Seminarraum 12": "Seminar Room",
+ "Seminarraum 2": "Seminar Room",
+ "Seminarraum 3": "Seminar Room",
+ "Seminarraum 4": "Seminar Room",
+ "Seminarraum 5": "Seminar Room",
+ "Seminarraum 6": "Seminar Room",
+ "Seminarraum 7": "Seminar Room",
+ "Seminarraum 8": "Seminar Room",
+ "Seminarraum 9": "Seminar Room",
+ "Servicepoint": "Service Point",
+ "Sonstiger Raum": "Other Room",
+ "Sozialraum": "Social Room",
+ "TRH": "TRH",
+ "Teaching Library": "Teaching Library",
+ "Technik": "Technical Room",
+ "Technik / HLS": "Technical / HVAC",
+ "Toilette": "Toilet",
+ "Treppe": "Staircase",
+ "Treppenhaus": "Stairwell",
+ "Treppenhaus D2": "Stairwell",
+ "Unbestimmt": "Undefined",
+ "Versuchslabor": "Experimental Laboratory",
+ "Vorbereitung": "Preparation",
+ "Vorlesung": "Lecture Hall",
+ "WC": "Toilet",
+ "WC BEH": "Accessible Toilet",
+ "WC Bf": "Toilet",
+ "WC D": "Women's Toilet",
+ "WC Damen": "Women's Toilet",
+ "WC H": "Men's Toilet",
+ "WC Herren": "Men's Toilet",
+ "WC-Beh": "Accessible Toilet",
+ "WC-Damen": "Women's Toilet",
+ "corridor": "Corridor",
+ "room": "Room",
+ "yes": "yes"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/become-hackerman.json b/rogue-thi-app/public/locales/en/become-hackerman.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/become-hackerman.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/calendar.json b/rogue-thi-app/public/locales/en/calendar.json
new file mode 100644
index 00000000..c484ecd2
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/calendar.json
@@ -0,0 +1,33 @@
+{
+ "calendar": {
+ "appbar": {
+ "title": "Appointments"
+ },
+ "modals": {
+ "exams": {
+ "type": "Type",
+ "room": "Room",
+ "seat": "Seat",
+ "date": "Date",
+ "notes": "Notes",
+ "examiner": "Examiner",
+ "courseOfStudies": "Course of Studies",
+ "registerDate": "Registered",
+ "tools": "Tools",
+ "actions": {
+ "close": "Close"
+ }
+ }
+ },
+ "notice": "All information without guarantee. Binding information is only available directly on the university website .",
+ "tabs": {
+ "semester": "Semester",
+ "exams": "Exams"
+ },
+ "dates": {
+ "until": "until"
+ },
+ "noExams": "There are currently no exam dates available.",
+ "guestNotice": "Exam dates are not available for guests."
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/common.json b/rogue-thi-app/public/locales/en/common.json
new file mode 100644
index 00000000..de8af714
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/common.json
@@ -0,0 +1,71 @@
+{
+ "common": {
+ "dates": {
+ "today": "Today",
+ "tomorrow": "Tomorrow",
+ "thisWeek": "This Week",
+ "nextWeek": "Next Week"
+ }
+ },
+ "food": {
+ "filterModal": {
+ "header": "Filters",
+ "restaurants": {
+ "title": "Restaurants",
+ "showMensa": "Show Cafeteria",
+ "showReimanns": "Show Reimanns",
+ "showCanisius": "Show Canisius Convict"
+ },
+ "allergens": {
+ "title": "Allergies",
+ "iconTitle": "Allergens",
+ "selected": "Selected",
+ "empty": "None"
+ },
+ "preferences": {
+ "title": "Food Preferences",
+ "iconTitle": "Preferences",
+ "selected": "Selected",
+ "empty": "None"
+ },
+ "info": "Your data will only be stored locally on your device and not transmitted to anyone."
+ },
+ "allergensModal": "Select Allergens",
+ "preferencesModal": "Select Preferences"
+ },
+ "dashboard": {
+ "orderModal": {
+ "title": "Dashboard",
+ "body": "Here you can change the order of the entries displayed on the dashboard.",
+ "hiddenCards": "Hidden Cards",
+ "resetOrder": "Reset Order",
+ "icons": {
+ "moveUp": "Move Up",
+ "moveDown": "Move Down",
+ "remove": "Remove",
+ "restore": "Restore",
+ "personalize": "Personalize"
+ }
+ }
+ },
+ "cards": {
+ "install": "Installation",
+ "timetable": "Timetable",
+ "mensa": "Cafeteria",
+ "mobility": "Mobility",
+ "calendar": "Calendar",
+ "events": "Events",
+ "rooms": "Room Plan",
+ "library": "Library",
+ "grades": "Grades & Subjects",
+ "personal": "Profile",
+ "lecturers": "Lecturers"
+ },
+ "prompts": {
+ "close": "Close"
+ },
+ "appbar": {
+ "back": "Back",
+ "overflow": "More options"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/dashboard.json b/rogue-thi-app/public/locales/en/dashboard.json
new file mode 100644
index 00000000..84471a2a
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/dashboard.json
@@ -0,0 +1,85 @@
+{
+ "timetable": {
+ "title": "Timetable",
+ "text": {
+ "endingSoon": "ends in {{mins}} min",
+ "ongoing": "ends at {{time}}",
+ "startingSoon": "starts in {{mins}} min",
+ "future": "at"
+ }
+ },
+ "food": {
+ "location": {
+ "food": {
+ "title": "Food"
+ },
+ "cafeteria": {
+ "title": "Cafeteria"
+ },
+ "reimanns": {
+ "title": "Reimanns"
+ },
+ "canisius": {
+ "title": "Canisiuskonvikt"
+ }
+ },
+ "error": {
+ "empty": "Today's menu is empty.",
+ "generic": "Error retrieving the menu. Something seems to be broken."
+ },
+ "text.additional": "and {{count}} more dishes"
+ },
+ "transport": {
+ "title": {
+ "train": "Train ({{station}})",
+ "bus": "Bus ({{station}})",
+ "parking": "Parking",
+ "charging": "Charging Stations"
+ },
+ "error": {
+ "empty": "No items.",
+ "generic": "Error retrieving."
+ }
+ },
+ "calendar": {
+ "title": "Calendar",
+ "date": {
+ "ends": "ends",
+ "starts": "starts"
+ }
+ },
+ "events": {
+ "title": "Events",
+ "organizer.attribute": "by"
+ },
+ "rooms": {
+ "title": "Rooms",
+ "text": "Available from {{from}} to {{until}}"
+ },
+ "library": {
+ "title": "Library"
+ },
+ "grades": {
+ "title": "Grades & Subjects"
+ },
+ "personal": {
+ "title": "Personal Data"
+ },
+ "lecturers": {
+ "title": "Lecturers"
+ },
+ "election": {
+ "title": "University Elections",
+ "text": "The university elections are currently taking place. Your participation is important to strengthen the democratic structures at our university.",
+ "button": "Vote Online",
+ "icon.close": "Close"
+ },
+ "install": {
+ "title": "Installation",
+ "text": {
+ "question": "Want to install this app on your smartphone?",
+ "ios": "In Safari, tap Share and then tap Add to Home Screen .",
+ "android": "In Chrome, tap the Menu button and then tap Add to Home Screen ."
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/debug.json b/rogue-thi-app/public/locales/en/debug.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/debug.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/events.json b/rogue-thi-app/public/locales/en/events.json
new file mode 100644
index 00000000..5fe2f54f
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/events.json
@@ -0,0 +1,11 @@
+{
+ "events": {
+ "appbar": {
+ "title": "Events"
+ }
+ },
+ "noEvents": "There are currently no event dates available.",
+ "dates": {
+ "until": "until"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/food.json b/rogue-thi-app/public/locales/en/food.json
new file mode 100644
index 00000000..bd4c42df
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/food.json
@@ -0,0 +1,75 @@
+{
+ "warning": {
+ "unknownIngredients": {
+ "text": "Unknown ingredients / allergens",
+ "iconTitle": "Allergy Warning"
+ }
+ },
+ "filter": "Filter",
+ "preferences": {
+ "iconTitle": "Preferred Food"
+ },
+ "list": {
+ "titles": {
+ "cafeteria": "Cafeteria",
+ "meals": "Meals",
+ "soups": "Soups",
+ "salads": "Salads"
+ }
+ },
+ "error": {
+ "dataUnavailable": "Data unavailable"
+ },
+ "navigation": {
+ "weeks": {
+ "previous": "Previous week",
+ "next": "Next week"
+ }
+ },
+ "foodModal": {
+ "header": "Details",
+ "flags": {
+ "title": "Notes",
+ "unkown": "Unknown",
+ "empty": "No notes available"
+ },
+ "allergens": {
+ "title": "Allergens",
+ "unkown": "Unknown",
+ "empty": "No allergens present",
+ "fallback": "Unknown (This is bad)"
+ },
+ "nutrition": {
+ "title": "Nutritional Values",
+ "energy.title": "Energy",
+ "fat": {
+ "title": "Fat",
+ "saturated": "of which saturated fat"
+ },
+ "carbohydrates": {
+ "title": "Carbohydrates",
+ "sugar": "of which sugar"
+ },
+ "fiber.title": "Fiber",
+ "protein.title": "Protein",
+ "salt.title": "Salt",
+ "unkown.title": "Unknown"
+ },
+ "prices": {
+ "title": "Prices",
+ "students": "Students",
+ "employees": "Employees",
+ "guests": "Guests"
+ },
+ "warning": {
+ "title": "Disclaimer",
+ "text": "Please check the information on the information screens before consuming anything. The nutritional information is based on an average portion."
+ },
+ "translation": {
+ "title": "Translation",
+ "warning": "This meal has been automatically translated. Please verify the information on the information screens before consuming anything.",
+ "originalName": "Original name",
+ "translatedName": "Translated name"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/grades.json b/rogue-thi-app/public/locales/en/grades.json
new file mode 100644
index 00000000..f130a261
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/grades.json
@@ -0,0 +1,31 @@
+{
+ "grades": {
+ "appbar": {
+ "title": "Grades & Subjects",
+ "overflow": {
+ "copyFormula": "Copy Grade Average Formula",
+ "exportCsv": "Export Grades as CSV"
+ }
+ },
+ "alerts": {
+ "temporarilyUnavailable": "Grades are temporarily unavailable.",
+ "copyToClipboard": "Copied to clipboard",
+ "notImplemented": "This feature is not yet available."
+ },
+ "summary": {
+ "title": "Grade Average",
+ "disclaimer": "The exact grade average cannot be determined and ranges between {{minAverage}} and {{maxAverage}}"
+ },
+ "gradesList": {
+ "title": "Grades"
+ },
+ "pendingList": {
+ "title": "Pending Subjects"
+ },
+ "grade": "Grade",
+ "deadline": "Deadline",
+ "ects": "ECTS",
+ "none": "(none)",
+ "credited": "(credited)"
+ }
+}
diff --git a/rogue-thi-app/public/locales/en/imprint.json b/rogue-thi-app/public/locales/en/imprint.json
new file mode 100644
index 00000000..bd1377ed
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/imprint.json
@@ -0,0 +1,18 @@
+{
+ "imprint": {
+ "appbar": {
+ "title": "Imprint"
+ },
+ "feedback": {
+ "title": "We would appreciate your feedback.",
+ "email": "Email",
+ "website": "Website",
+ "instagram": "Instagram",
+ "sourceCode": "Source Code on GitHub",
+ "joinNeuland": "Join now and support the development!"
+ },
+ "legal": {
+ "title": "Legal Notice from Neuland Ingolstadt e.V."
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/index.json b/rogue-thi-app/public/locales/en/index.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/index.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/lecturers.json b/rogue-thi-app/public/locales/en/lecturers.json
new file mode 100644
index 00000000..1a578e56
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/lecturers.json
@@ -0,0 +1,33 @@
+{
+ "lecturers": {
+ "appbar": {
+ "title": "Lecturers"
+ },
+ "modals": {
+ "details": {
+ "title": "Title",
+ "surname": "Surname",
+ "forename": "Forename",
+ "organization": "Organization",
+ "function": "Function",
+ "room": "Room",
+ "email": "Email",
+ "phone": "Phone",
+ "officeHours": "Office Hours",
+ "insights": "Insights",
+ "actions": {
+ "close": "Close"
+ },
+ "notAvailable": "N/A"
+ }
+ },
+ "search": {
+ "placeholder": "Search all lecturers...",
+ "personalLecturers": "Personal Lecturers",
+ "searchResults": "Search Results"
+ },
+ "body": {
+ "room": "Room"
+ }
+ }
+}
diff --git a/rogue-thi-app/public/locales/en/library.json b/rogue-thi-app/public/locales/en/library.json
new file mode 100644
index 00000000..10015c7a
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/library.json
@@ -0,0 +1,38 @@
+{
+ "library": {
+ "title": "Library",
+ "yourReservations": "Your reservations",
+ "availableSeats": "Available seats",
+ "actions": {
+ "reserve": "Reserve",
+ "delete": "Delete"
+ },
+ "details": {
+ "seatsAvailable": "{{available}} / {{total}} available",
+ "noReservations": "You have no reservations",
+ "reservationDetails": "{{category}} , Seat {{seat}}, Reservation {{reservation_id}}"
+ },
+ "modal": {
+ "title": "Reserve a seat",
+ "details": {
+ "day": "Day",
+ "start": "Start",
+ "end": "End",
+ "location": "Location",
+ "seat": "Seat"
+ },
+ "seatSelection": {
+ "any": "Any"
+ },
+ "librarySelection": {
+ "libraryNorth": "Lesesaal Nord (alte Bibliothek)",
+ "librarySouth": "Lesesaal Süd (neue Bibliothek)",
+ "libraryGallery": "Lesesaal Galerie"
+ },
+ "actions": {
+ "cancel": "Cancel",
+ "reserve": "Reserve"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/login.json b/rogue-thi-app/public/locales/en/login.json
new file mode 100644
index 00000000..ec63d109
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/login.json
@@ -0,0 +1,32 @@
+{
+ "error": {
+ "wrongCredentials": "Your login credentials are incorrect.",
+ "generic": "An error occurred while connecting to the server."
+ },
+ "alert": "You must be logged in to use this feature.",
+ "guestOnly": {
+ "warning": "The app can currently only be used as a guest. More information can be found below.",
+ "title": "Why can't I log in?",
+ "details": "The university has instructed us to disable the login function. We are working on a solution, but it is not clear when it will be ready. We advise against using the official THI app for security reasons.",
+ "details2": "The menu plan, semester and event dates, room map, bus and train departure times, and parking information can still be accessed through the guest mode."
+ },
+ "notes": {
+ "title1": "What is this?",
+ "text1": "This is an unofficial alternative to the THI app, which is intended to provide an improved user experience. It is being developed by students at Neuland Ingolstadt e.V. for students and is not an offering of the Technische Hochschule Ingolstadt.",
+ "title2": "Are my data secure?",
+ "text2": "Yes. Your data is encrypted directly on your device, transmitted in encrypted form through our proxy to the THI, and decrypted only there. Only you and the THI have access to your login credentials and personal data."
+ },
+ "links": {
+ "security": "Here you can find more information about security.",
+ "imprint": "Imprint",
+ "privacy": "Privacy Policy",
+ "github": "Source code on GitHub"
+ },
+ "form": {
+ "username": "THI username",
+ "password": "Password",
+ "login": "Log in",
+ "guest": "Continue as guest",
+ "save": "Stay logged in"
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/mobility.json b/rogue-thi-app/public/locales/en/mobility.json
new file mode 100644
index 00000000..be85bca1
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/mobility.json
@@ -0,0 +1,46 @@
+{
+ "form": {
+ "type": {
+ "label": "Transportation",
+ "option": {
+ "bus": "Bus",
+ "train": "Train",
+ "parking": "Parking",
+ "charging": "Charging Stations"
+ }
+ },
+ "station": {
+ "label": {
+ "bus": "Stop",
+ "train": "Station"
+ }
+ }
+ },
+ "transport": {
+ "title": {
+ "bus": "Bus ({{station}})",
+ "train": "Train ({{station}})",
+ "parking": "Parking",
+ "charging": "Charging Stations",
+ "unknown": "Mobility"
+ },
+ "details": {
+ "charging": {
+ "available": "{{available}} of {{total}} available"
+ },
+ "parking": {
+ "available": "{{available}} available",
+ "unknown": "n/a",
+ "employees": "Employees",
+ "free": "Free",
+ "paid": "Paid",
+ "restricted": "Restricted"
+ },
+ "noElements": "No elements."
+ },
+ "error": {
+ "retrieval": "Error retrieving data."
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/personal.json b/rogue-thi-app/public/locales/en/personal.json
new file mode 100644
index 00000000..cc53a201
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/personal.json
@@ -0,0 +1,51 @@
+{
+ "personal": {
+ "title": "Profile",
+ "grades": {
+ "title": "Grades",
+ "missingWeightSingle": " weight missing",
+ "missingWeightMultiple": " weights missing"
+ },
+ "overview": {
+ "grades": "grades",
+ "ects": "ECTS",
+ "matriculationNumber": "Mat. No",
+ "libraryNumber": "Lib. No",
+ "copiedToClipboard": "Copied {{label}} to clipboard"
+ },
+ "semester": "semester",
+ "foodPreferences": "Food Preferences",
+ "debug": "API Playground",
+ "privacy": "Privacy Policy",
+ "imprint": "Imprint",
+ "language": "Change Language",
+ "login": "Log In",
+ "logout": "Log Out",
+ "dashboard": "Dashboard",
+ "theme": "Theme",
+ "modals": {
+ "theme": {
+ "title": "Theme",
+ "hackerman": "To unlock the Hackerman design, you need to solve at least four tasks in our Practice CTF . Once you're there, you can unlock it here ."
+ },
+ "language": {
+ "title": "Language"
+ },
+ "personalData": {
+ "matriculationNumber": "Matriculation Number",
+ "libraryNumber": "Library Number",
+ "printerBalance": "Printer Balance",
+ "fieldOfStudy": "Field of Study",
+ "semester": "Semester",
+ "examRegulations": "Exam Regulations",
+ "email": "Email",
+ "thiEmail": "THI Email",
+ "phone": "Phone",
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "street": "Street",
+ "city": "City"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/rooms.json b/rogue-thi-app/public/locales/en/rooms.json
new file mode 100644
index 00000000..bb71e574
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/rooms.json
@@ -0,0 +1,68 @@
+{
+ "rooms": {
+ "overflow": {
+ "hourlyPlan": "Hourly Plan",
+ "campusCityTour": "Campus & City Tour"
+ },
+ "map": {
+ "appbar": {
+ "title": "Room Map"
+ },
+ "searchPlaceholder": "Search for 'W003', 'Toilet', 'Library', ...",
+ "extendedSearch": "Extended Search",
+ "automaticSuggestion": "Automatic Suggestions",
+ "occupied": "Occupied",
+ "freeFromUntil": "Free from {{from}} to {{until}}",
+ "attribution": "© OpenStreetMap contributors",
+ "legend": {
+ "free": "Free",
+ "occupied": "Occupied",
+ "specialEquipment": "Special Equipment",
+ "occupancyUnknown": "Occupancy Unknown"
+ },
+ "floors": {
+ "eg": "GF"
+ }
+ },
+ "search": {
+ "appbar": {
+ "title": "Advanced Room Search"
+ },
+ "building": "Building",
+ "date": "Date",
+ "time": "Time",
+ "duration": "Duration",
+ "search": "Search",
+ "buildingsAll": "All",
+ "results": {
+ "availableFromUntil": "Available from {{from}} to {{until}}",
+ "noAvailableRooms": "No available rooms found"
+ }
+ },
+ "suggestions": {
+ "appbar": {
+ "title": "Room Suggestions"
+ },
+ "noSuggestions": "No suggestions available.",
+ "noAvailableRooms": "No available rooms found.",
+ "gaps": {
+ "header": {
+ "general": "Available Rooms",
+ "specific": "{{from}} {{until}}"
+ },
+ "subtitle": {
+ "general": "Break from {{from}} to {{until}}",
+ "specific": "Rooms from {{from}} to {{until}}"
+ }
+ }
+ },
+ "list": {
+ "appbar": {
+ "title": "Hourly Room Plan"
+ }
+ },
+ "common": {
+ "availableFromUntil": "Available from {{from}} to {{until}}"
+ }
+ }
+}
diff --git a/rogue-thi-app/public/locales/en/timetable.json b/rogue-thi-app/public/locales/en/timetable.json
new file mode 100644
index 00000000..f6a6cc3e
--- /dev/null
+++ b/rogue-thi-app/public/locales/en/timetable.json
@@ -0,0 +1,77 @@
+{
+ "timetable": {
+ "appbar": {
+ "title": "Timetable"
+ },
+ "overview": {
+ "noLectures": "No lectures. 🎉",
+ "configureTimetable": "You can create your timetable using the THI Timetable Tool , then your subjects will appear here."
+ },
+ "overflow": {
+ "editLectures": "Edit Subjects",
+ "subscribeCalendar": "Subscribe to Calendar"
+ },
+ "weekSelection": {
+ "weekBack": "Next week",
+ "weekForward": "Previous week"
+ },
+ "modals": {
+ "timetableExplanation": {
+ "title": "Edit Subjects",
+ "body": {
+ "header": "Currently, you can only edit the subjects for your personal timetable in Primuss:",
+ "login": "Login to myStundenplan ",
+ "subjects": "Click on Subject Selection ",
+ "courseOfStudies": "Select your course of studies and save at the bottom",
+ "studyGroups": "Click on Study Groups at the top",
+ "semesterGroup": "Select your semester group and save at the bottom",
+ "clickOnStudy": "Click on your course of studies at the top",
+ "selectSubjects": "Select subjects and save at the bottom"
+ },
+ "actions": {
+ "close": "Close",
+ "toMyTimetable": "Go to \"myStundenplan\""
+ }
+ },
+ "subscriptionExplanation": {
+ "title": "Subscribe to Calendar",
+ "body": {
+ "header": "Your timetable can be integrated as a subscription into a calendar app.",
+ "url": "Currently, you can only find the URL in Primuss:",
+ "login": "Login to myStundenplan ",
+ "timetable": "Click on Current Timetable ",
+ "externalCalendar": "Click on External at the top",
+ "subscribe": "Click on Show Link under Subscribe to Events ",
+ "ios": {
+ "header": "You can import the URL in iOS as follows:",
+ "openSettings": "Open the Settings app",
+ "addCalendarSubscription": "Tap on Calendar > Accounts > Add Account > Add Calendar Subscription ",
+ "pasteUrl": "Paste the copied link from Primuss",
+ "save": "Tap on Next > Save "
+ }
+ },
+ "actions": {
+ "close": "Close",
+ "toMyTimetable": "Go to \"myStundenplan\""
+ }
+ },
+ "lectureDetails": {
+ "general": "General",
+ "lecturer": "Lecturer",
+ "abbreviation": "Abbreviation",
+ "exam": "Exam",
+ "courseOfStudies": "Course of Studies",
+ "studyGroup": "Study Group",
+ "semesterWeeklyHours": "Weekly Semester Hours",
+ "ects": "ECTS",
+ "goal": "Goal",
+ "content": "Content",
+ "literature": "Literature",
+ "notSpecified": "Not specified",
+ "actions": {
+ "close": "Close"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rogue-thi-app/styles/Mensa.module.css b/rogue-thi-app/styles/Mensa.module.css
index 697cbe2f..12cc64d5 100644
--- a/rogue-thi-app/styles/Mensa.module.css
+++ b/rogue-thi-app/styles/Mensa.module.css
@@ -57,3 +57,8 @@
margin-top: 75px;
text-align: center;
}
+
+.translated {
+ color: var(--primary);
+ font-size: .8rem;
+}
diff --git a/rogue-thi-app/styles/Personal.module.css b/rogue-thi-app/styles/Personal.module.css
index 7f29ceec..845045f5 100644
--- a/rogue-thi-app/styles/Personal.module.css
+++ b/rogue-thi-app/styles/Personal.module.css
@@ -1,4 +1,4 @@
-.personal_data {
+.personalData {
width: 400px;
max-width: 95vw;
}
diff --git a/thi-translator/.gitignore b/thi-translator/.gitignore
new file mode 100644
index 00000000..4c49bd78
--- /dev/null
+++ b/thi-translator/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/thi-translator/README.md b/thi-translator/README.md
new file mode 100644
index 00000000..c01abf5b
--- /dev/null
+++ b/thi-translator/README.md
@@ -0,0 +1,37 @@
+# THI Translator
+
+## Description
+
+This scripts aims to translate all german words used in the API results of the THI to english. It uses the [DeepL API](https://www.deepl.com/docs-api/) to translate the words.
+
+## Usage
+
+For simplicity this script is not embedded to the dockerfile. To use it you have to run it manually and copy the main project.
+
+In the future this script should be embedded to the dockerfile and run automatically.
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+```
+
+- Create a .env file in the root directory of the project
+- Enter your DeepL API key in the .env file
+
+ ```bash
+ # .env
+ DEEPL_API_KEY=your-api-key
+ ```
+
+- Enter your THI credentials in the .env file
+
+ ```bash
+ # .env
+ THI_USERNAME=your-username
+ THI_PASSWORD=your-password
+ ```
+
+```bash
+# Run the script
+python thi-translator.py
+```
diff --git a/thi-translator/requirements.txt b/thi-translator/requirements.txt
new file mode 100644
index 00000000..1cf6d19a
--- /dev/null
+++ b/thi-translator/requirements.txt
@@ -0,0 +1,3 @@
+deepl>=1.15.0
+python-dotenv>=1.0.0
+Requests>=2.31.0
diff --git a/thi-translator/thi-translator.py b/thi-translator/thi-translator.py
new file mode 100644
index 00000000..135c5fe4
--- /dev/null
+++ b/thi-translator/thi-translator.py
@@ -0,0 +1,255 @@
+import requests
+import os
+from dotenv import load_dotenv
+import json
+import deepl
+from pathlib import Path
+import re
+from shutil import copy
+
+API_URL = "https://hiplan.thi.de/webservice/production2/index.php"
+DEEPL_API_URL = "https://api.deepl.com/v2/translate"
+MAIN_DIR = Path(__file__).parent.parent / "rogue-thi-app" / "public" / "locales"
+
+DEEPL_API_KEY = os.getenv("DEEPL_API_KEY")
+THI_USERNAME = os.getenv("THI_USERNAME")
+THI_PASSWORD = os.getenv("THI_PASSWORD")
+
+GENDER_REGEX = re.compile(r"\(\w+\)")
+CLEAN_REGEX = re.compile(r"\s+")
+
+LANGUAGES = ["EN-US"]
+
+MAP_URL = "https://assets.neuland.app/rooms_neuland.geojson"
+
+
+class ThiTranslator:
+ def __init__(self):
+ load_dotenv()
+
+ self.DEEPL_API_KEY = os.getenv("DEEPL_API_KEY")
+ self.THI_USERNAME = os.getenv("THI_USERNAME")
+ self.THI_PASSWORD = os.getenv("THI_PASSWORD")
+
+ self.__check_env()
+
+ self.translator = deepl.Translator(self.DEEPL_API_KEY)
+ self.__check_deepL()
+
+ self.session_id = self.__open_session()
+ self.path = Path(__file__).parent / "data"
+
+ if not self.path.exists():
+ self.path.mkdir()
+
+ print(f'Opened session with id "{self.session_id}"')
+ self.output = {}
+
+ def __check_env(self):
+ """Checks if the environment variables are set"""
+ if not self.DEEPL_API_KEY:
+ raise ValueError("DEEPL_API_KEY is not set")
+
+ if not self.THI_USERNAME:
+ raise ValueError("THI_USERNAME is not set")
+
+ if not self.THI_PASSWORD:
+ raise ValueError("THI_PASSWORD is not set")
+
+ def __check_deepL(self):
+ try:
+ self.translator.translate_text("test", target_lang="EN-US")
+ except Exception as e:
+ print(self.DEEPL_API_KEY)
+ raise ValueError("DeepL API key is not valid")
+
+ def __open_session(self):
+ """Opens a session with the THI API and returns the session id"""
+ data = {
+ "method": "open",
+ "service": "session",
+ "username": self.THI_USERNAME,
+ "passwd": self.THI_PASSWORD,
+ "format": "json",
+ }
+
+ session_req = requests.post(API_URL, data=data)
+ return session_req.json()["data"][0]
+
+ def __close_session(self):
+ """Closes the session with the given session id"""
+ data = {
+ "method": "close",
+ "service": "session",
+ "session": self.session_id,
+ "format": "json",
+ }
+
+ session_req = requests.post(API_URL, data=data)
+ return session_req.json()["data"]
+
+ def add_to_output(self, data, key):
+ """Adds the data to the output"""
+ self.output[key] = data
+
+ def __get_lecturers(self):
+ """Returns a list of all lecturers"""
+ data = {
+ "method": "lecturers",
+ "service": "thiapp",
+ "session": self.session_id,
+ "format": "json",
+ "from": "a",
+ "to": "z",
+ }
+
+ lecturers_req = requests.post(API_URL, data=data)
+
+ return lecturers_req.json()["data"][1]
+
+ def __extract_all_functions(self, lecturers):
+ """Extracts all functions from the lecturers"""
+ functions = [
+ lecturer["funktion"] for lecturer in lecturers if lecturer["funktion"] != ""
+ ]
+
+ # remove (in) or (r) from functions (e.g. Professor(in) -> Professor)
+ functions = list(set(functions))
+ cleaned_function = [GENDER_REGEX.sub("", function) for function in functions]
+
+ return functions, cleaned_function
+
+ def __translate(self, text):
+ """Translates the function to english using the DeepL API"""
+ results = {"de": text}
+
+ for lang in LANGUAGES:
+ result = self.translator.translate_text(text, target_lang=lang)
+ results[lang.split("-")[0].lower()] = result.text
+
+ return results
+
+ def __translate_genders(self, text, cleaned_text):
+ """
+ Translates the function to english using the DeepL API
+ The output dict will contain the original text and and use the cleaned text for the translation.
+ """
+ results = {"de": text}
+
+ for lang in LANGUAGES:
+ result = self.translator.translate_text(cleaned_text, target_lang=lang)
+ results[lang.split("-")[0].lower()] = result.text
+
+ return results
+
+ def translate_room_functions(self):
+ """Translates the map properties to english using the DeepL API"""
+ response = requests.get(MAP_URL)
+ data = response.json()["features"]
+
+ room_properties = [feature["properties"]["Funktion"] for feature in data]
+ room_properties = list(set(room_properties))
+
+ room_properties = [
+ property
+ for property in room_properties
+ if property is not None and property != ""
+ ]
+
+ room_properties = [CLEAN_REGEX.sub(" ", property).strip() for property in room_properties]
+
+ translated = [self.__translate(property) for property in room_properties]
+
+ return dict(zip(room_properties, translated))
+
+ def translate_lecturer_functions(self):
+ """
+ Extracts all functions from the lecturers and translates them to english.
+ Returns a dict with the original functions and the translated functions nested in a dict with the language as key.
+ """
+
+ lecturers = self.__get_lecturers()
+ functions, cleaned_functions = self.__extract_all_functions(lecturers)
+
+ translated = [
+ self.__translate_genders(function, cleaned)
+ for function, cleaned in zip(functions, cleaned_functions)
+ ]
+
+ return dict(zip(functions, translated))
+
+ def translate_lecturer_organizations(self):
+ """
+ Extracts all organizations from the lecturers and translates them to english.
+ Returns a dict with the original organizations and the translated organizations nested in a dict with the language as key.
+ """
+
+ lecturers = self.__get_lecturers()
+ organizations = [
+ lecturer["organisation"]
+ for lecturer in lecturers
+ if lecturer["organisation"] != ""
+ ]
+ organizations = [lecturer for lecturer in organizations if lecturer is not None]
+ organizations = list(set(organizations))
+
+ translated = [self.__translate(organization) for organization in organizations]
+
+ return dict(zip(organizations, translated))
+
+ def close(self):
+ """Closes the session"""
+ self.__close_session()
+
+ print(f'Closed session with id "{self.session_id}"')
+
+ def save_file(self, data, name):
+ """Saves the data to a file with the given name"""
+
+
+ def export_files(self):
+ """Creates to localizations files for each language"""
+ languages = LANGUAGES + ["DE"]
+
+ for lang in languages:
+ lang_short = lang.split("-")[0].lower()
+
+ content = {
+ "__source": "Generated using the thi-translator script",
+ "apiTranslations": {}
+ }
+
+ for key in self.output.keys():
+ content["apiTranslations"][key] = {}
+ for item_key, value in self.output[key].items():
+ content["apiTranslations"][key][item_key] = value[lang_short]
+
+ with open(MAIN_DIR / lang_short / 'api-translations.json', "w+", encoding="utf-8") as f:
+ f.write(json.dumps(content, indent=4, ensure_ascii=False, sort_keys=True))
+
+
+
+def main():
+ translator = ThiTranslator()
+
+ # Functions
+ translator.add_to_output(
+ translator.translate_lecturer_functions(), "lecturerFunctions"
+ )
+
+ # Organizations
+ translator.add_to_output(
+ translator.translate_lecturer_organizations(), "lecturerOrganizations"
+ )
+
+ # Map
+ translator.add_to_output(
+ translator.translate_room_functions(), "roomFunctions"
+ )
+
+ translator.close()
+ translator.export_files()
+
+
+if __name__ == "__main__":
+ main()
From 771728a6d94f94ab9e5b1218f1ab43c7a9b90afb Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Sat, 24 Jun 2023 00:53:18 +0200
Subject: [PATCH 04/34] Save user language preference (#290)
---
rogue-thi-app/components/modal/LanguageModal.js | 1 +
rogue-thi-app/next.config.js | 8 ++++----
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/rogue-thi-app/components/modal/LanguageModal.js b/rogue-thi-app/components/modal/LanguageModal.js
index 039c8802..bab6bfd2 100644
--- a/rogue-thi-app/components/modal/LanguageModal.js
+++ b/rogue-thi-app/components/modal/LanguageModal.js
@@ -30,6 +30,7 @@ export default function LanguageModal () {
setShowLanguageModal(false)
i18n.changeLanguage(languageKey)
router.replace('/', '', { locale: i18n.language })
+ document.cookie = `NEXT_LOCALE=${i18n.language}; path=/`
}
return (
diff --git a/rogue-thi-app/next.config.js b/rogue-thi-app/next.config.js
index 153a8e0a..92ddda3c 100644
--- a/rogue-thi-app/next.config.js
+++ b/rogue-thi-app/next.config.js
@@ -1,3 +1,5 @@
+const { i18n } = require('./next-i18next.config')
+
// https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md
const permissionPolicyFeatures = [
'accelerometer',
@@ -29,10 +31,8 @@ const isDev = process.env.NODE_ENV === 'development'
const DEEPL_ENDPOINT = process.env.NEXT_PUBLIC_DEEPL_ENDPOINT || ''
module.exports = {
- i18n: {
- locales: ['en', 'de'],
- defaultLocale: 'en'
- },
+ i18n,
+ trailingSlash: true,
async headers () {
return [
{
From c55d30071190c5c31435412983aafef85edb795d Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Mon, 26 Jun 2023 10:50:00 +0200
Subject: [PATCH 05/34] =?UTF-8?q?=F0=9F=A7=B9=20General=20improvements=20(?=
=?UTF-8?q?#292)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 🐛Remove unnecessary dots from student clubs (#286)
* 🌐 Localize semester dates
* 🐛 fix food number formatting; small localization error
* 🚸 change food preferences icon
* 🌐 use official translation for semester dates
* ✨ add meal translation caching
---
.../components/cards/CalendarCard.js | 4 +-
rogue-thi-app/data/calendar.json | 41 +++++++++++++++----
.../lib/backend-utils/translation-utils.js | 24 ++++++++++-
rogue-thi-app/pages/api/cl-events.js | 10 +++--
rogue-thi-app/pages/calendar.js | 4 +-
rogue-thi-app/pages/food.js | 19 +++++----
6 files changed, 76 insertions(+), 26 deletions(-)
diff --git a/rogue-thi-app/components/cards/CalendarCard.js b/rogue-thi-app/components/cards/CalendarCard.js
index ea3fee4b..f2b57349 100644
--- a/rogue-thi-app/components/cards/CalendarCard.js
+++ b/rogue-thi-app/components/cards/CalendarCard.js
@@ -10,7 +10,7 @@ import { NoSessionError } from '../../lib/backend/thi-session-handler'
import { formatFriendlyRelativeTime } from '../../lib/date-utils'
import { useTime } from '../../lib/hooks/time-hook'
-import { useTranslation } from 'next-i18next'
+import { i18n, useTranslation } from 'next-i18next'
/**
* Dashboard card for semester and exam dates.
*/
@@ -55,7 +55,7 @@ export default function CalendarCard () {
{mixedCalendar && mixedCalendar.slice(0, 2).map((x, i) => (
- {x.name}
+ {x.name[i18n.languages[0]]}
{(x.end && x.begin < time)
diff --git a/rogue-thi-app/data/calendar.json b/rogue-thi-app/data/calendar.json
index 55a2a86b..d1365818 100644
--- a/rogue-thi-app/data/calendar.json
+++ b/rogue-thi-app/data/calendar.json
@@ -143,7 +143,10 @@
"end": "2023-05-04"
},
{
- "name": "Prüfungsabmeldung",
+ "name": {
+ "de": "Prüfungsabmeldung",
+ "en": "Exam withdrawal"
+ },
"begin": "2023-04-25",
"end": "2023-06-26"
},
@@ -167,36 +170,58 @@
"hasHours": true
},
{
- "name": "Rückmeldung zum Wintersemester 2023/2024",
+ "name": {
+ "de": "Rückmeldung zum Wintersemester 2023/2024",
+ "en": "Re-registration for winter semester 2023/2024"
+ },
+
"begin": "2023-07-02",
"end": "2023-08-07"
},
{
- "name": "Erweiterter Prüfungszeitraum",
+ "name": {
+ "de": "Erweiterter Prüfungszeitraum",
+ "en": "Additional exam period"
+ },
"begin": "2023-07-04",
"end": "2023-07-07"
},
{
- "name": "Prüfungszeitraum",
+ "name": {
+ "de": "Prüfungszeitraum",
+ "en": "Exam period"
+ },
"begin": "2023-07-08",
"end": "2023-07-20"
},
{
- "name": "Ende der Vorlesungszeit",
+ "name": {
+ "de": "Ende der Vorlesungszeit",
+ "en": "End of lecture period"
+ },
"begin": "2023-07-07"
},
{
- "name": "Notenmeldung",
+ "name": {
+ "de": "Notenmeldung",
+ "en": "Recording grades"
+ },
"begin": "2023-07-25T09:00",
"hasHours": true
},
{
- "name": "Notenbekanntgabe",
+ "name": {
+ "de": "Notenbekanntgabe",
+ "en": "Announcement of grades"
+ },
"begin": "2023-07-31T13:00",
"hasHours": true
},
{
- "name": "Semesterferien",
+ "name": {
+ "de": "Semesterferien",
+ "en": "Semester break"
+ },
"begin": "2023-08-01",
"end": "2023-09-30"
}
diff --git a/rogue-thi-app/lib/backend-utils/translation-utils.js b/rogue-thi-app/lib/backend-utils/translation-utils.js
index 0f9de416..e434e8ad 100644
--- a/rogue-thi-app/lib/backend-utils/translation-utils.js
+++ b/rogue-thi-app/lib/backend-utils/translation-utils.js
@@ -1,11 +1,31 @@
+import AsyncMemoryCache from '../cache/async-memory-cache'
+
const DEEPL_ENDPOINT = process.env.NEXT_PUBLIC_DEEPL_ENDPOINT || ''
const DEEPL_API_KEY = process.env.DEEPL_API_KEY || ''
+const CACHE_TTL = 60 * 60 * 24 * 1000// 24h
+
+const cache = new AsyncMemoryCache({ ttl: CACHE_TTL })
+
+/**
+ * Gets a translation from the cache or translates it using DeepL.
+ * @param {String} text The text to translate
+ * @param {String} target The target language
+ * @returns {String} The translated text
+ * @throws {Error} If DeepL is not configured or returns an error
+ **/
+async function getTranslation (text, target) {
+ return await cache.get(`${text}__${target}`, async () => {
+ return await translate(text, target)
+ })
+}
+
/**
* Translates a text using DeepL.
* @param {String} text The text to translate
* @param {String} target The target language
- * @returns {String}
+ * @returns {String} The translated text
+ * @throws {Error} If DeepL is not configured or returns an error
*/
async function translate (text, target) {
if (!DEEPL_ENDPOINT || !DEEPL_API_KEY) {
@@ -44,7 +64,7 @@ export async function translateMeals (meals) {
...meal,
name: {
de: meal.name,
- en: await translate(meal.name, 'EN')
+ en: await getTranslation(meal.name, 'EN')
},
originalLanguage: 'de'
}
diff --git a/rogue-thi-app/pages/api/cl-events.js b/rogue-thi-app/pages/api/cl-events.js
index fedcdea0..ce743fda 100644
--- a/rogue-thi-app/pages/api/cl-events.js
+++ b/rogue-thi-app/pages/api/cl-events.js
@@ -19,6 +19,8 @@ const EVENT_LIST_URL = 'https://moodle.thi.de/mod/dataform/view.php?id=162869'
const EVENT_DETAILS_PREFIX = 'https://moodle.thi.de/mod/dataform/view.php'
const EVENT_STORE = `${process.env.STORE}/cl-events.json`
+const isDev = process.env.NODE_ENV === 'development'
+
const cache = new AsyncMemoryCache({ ttl: CACHE_TTL })
/**
@@ -153,7 +155,7 @@ export async function getAllEventDetails (username, password) {
// since it may contain sensitive information
remoteEvents.push({
id: crypto.createHash('sha256').update(url).digest('hex'),
- organizer: details.Verein,
+ organizer: details.Verein.trim().replace(/( \.)$/g, ''),
title: details.Event,
begin: details.Start ? parseLocalDateTime(details.Start) : null,
end: details.Ende ? parseLocalDateTime(details.Ende) : null
@@ -161,7 +163,7 @@ export async function getAllEventDetails (username, password) {
}
const now = new Date()
- let events = await loadEvents()
+ let events = !isDev ? await loadEvents() : []
if (remoteEvents.length > 0) {
// remove all events which disappeared from the server
@@ -175,7 +177,9 @@ export async function getAllEventDetails (username, password) {
// we need to persist the events because they disappear on monday
// even if the event has not passed yet
- await saveEvents(events)
+ if (!isDev) {
+ await saveEvents(events)
+ }
return events
}
diff --git a/rogue-thi-app/pages/calendar.js b/rogue-thi-app/pages/calendar.js
index be89031c..5f04a51d 100644
--- a/rogue-thi-app/pages/calendar.js
+++ b/rogue-thi-app/pages/calendar.js
@@ -112,10 +112,10 @@ export default function Calendar () {
{calendar.map((item, idx) =>
- {!item.url && item.name}
+ {!item.url && item.name[i18n.languages[0]]}
{item.url && (
- {item.name}
+ {item.name[i18n.languages[0]]}
{' '}
diff --git a/rogue-thi-app/pages/food.js b/rogue-thi-app/pages/food.js
index 03a84292..bed99f99 100644
--- a/rogue-thi-app/pages/food.js
+++ b/rogue-thi-app/pages/food.js
@@ -6,7 +6,7 @@ import Modal from 'react-bootstrap/Modal'
import Nav from 'react-bootstrap/Nav'
import ReactPlaceholder from 'react-placeholder'
-import { faChevronLeft, faChevronRight, faExclamationTriangle, faFilter, faThumbsUp, faUtensils, faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
+import { faChevronLeft, faChevronRight, faExclamationTriangle, faFilter, faHeartCircleCheck, faUtensils, faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import AppBody from '../components/page/AppBody'
@@ -30,7 +30,8 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
-const CURRENCY_LOCALE = 'de'
+import { getAdjustedLocale } from '../lib/locale-utils'
+
const COLOR_WARN = '#bb0000'
const COLOR_GOOD = '#00bb00'
@@ -103,7 +104,7 @@ export default function Mensa () {
* @returns {string}
*/
function formatPrice (x) {
- return x?.toLocaleString(CURRENCY_LOCALE, { style: 'currency', currency: 'EUR' })
+ return x?.toLocaleString(getAdjustedLocale(), { style: 'currency', currency: 'EUR' })
}
/**
@@ -130,12 +131,12 @@ export default function Mensa () {
}
/**
- * Formats a float for the German locale.
+ * Formats a float for the current locale.
* @param {number} x
* @returns {string}
*/
function formatFloat (x) {
- return x?.toString().replace('.', ',')
+ return (new Intl.NumberFormat(getAdjustedLocale(), { minimumFractionDigits: 1, maximumFractionDigits: 2 })).format(x)
}
/**
@@ -173,7 +174,7 @@ export default function Mensa () {
)}
{!containsSelectedAllergen(meal.allergens) && containsSelectedPreference(meal.flags) && (
-
+
{' '}
)}
@@ -328,7 +329,7 @@ export default function Mensa () {
{containsSelectedPreference([flag]) && (
- {' '}
+ {' '}
)}
{' '}
@@ -347,9 +348,9 @@ export default function Mensa () {
{containsSelectedAllergen([key]) && (
-
+
{' '}
-
+
)}
{' '}
{key}
From 26d0c0b42884d8b71b6d23e4bbed78f22dfdec72 Mon Sep 17 00:00:00 2001
From: Alexander Horn
Date: Fri, 30 Jun 2023 21:46:38 +0200
Subject: [PATCH 06/34] Fix debug page and remove obtainSession method
---
.../lib/backend/thi-session-handler.js | 47 -----------------
rogue-thi-app/pages/debug.js | 50 +++++++++++--------
2 files changed, 30 insertions(+), 67 deletions(-)
diff --git a/rogue-thi-app/lib/backend/thi-session-handler.js b/rogue-thi-app/lib/backend/thi-session-handler.js
index 4bc96062..142b2646 100644
--- a/rogue-thi-app/lib/backend/thi-session-handler.js
+++ b/rogue-thi-app/lib/backend/thi-session-handler.js
@@ -124,53 +124,6 @@ export async function callWithSession (method) {
}
}
-/**
- * Obtains a session, either directly from localStorage or by logging in
- * using saved credentials.
- *
- * If a session can not be obtained, the user is redirected to /login.
- *
- * @param {object} router Next.js router object
- */
-export async function obtainSession (router) {
- let session = localStorage.session
- const age = parseInt(localStorage.sessionCreated)
-
- const credStore = new CredentialStorage(CRED_NAME)
- const { username, password } = await credStore.read(CRED_ID) || {}
-
- // invalidate expired session
- if (age + SESSION_EXPIRES < Date.now() || !await API.isAlive(session)) {
- console.log('Invalidating session')
-
- session = null
- }
-
- // try to log in again
- if (!session && username && password) {
- try {
- console.log('Logging in again')
- const { session: newSession, isStudent } = await API.login(username, password)
- session = newSession
-
- localStorage.session = session
- localStorage.sessionCreated = Date.now()
- localStorage.isStudent = isStudent
- } catch (e) {
- console.log('Failed to log in again')
-
- console.error(e)
- }
- }
-
- if (session) {
- return session
- } else {
- router.replace('/login')
- return null
- }
-}
-
/**
* Logs out the user by deleting the session from localStorage.
*
diff --git a/rogue-thi-app/pages/debug.js b/rogue-thi-app/pages/debug.js
index c3b31f0d..36109a77 100644
--- a/rogue-thi-app/pages/debug.js
+++ b/rogue-thi-app/pages/debug.js
@@ -13,8 +13,8 @@ import AppContainer from '../components/page/AppContainer'
import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
+import { NoSessionError, UnavailableSessionError, callWithSession } from '../lib/backend/thi-session-handler'
import API from '../lib/backend/anonymous-api'
-import { obtainSession } from '../lib/backend/thi-session-handler'
import styles from '../styles/Common.module.css'
@@ -30,26 +30,36 @@ export default function Debug () {
useEffect(() => {
async function load () {
- const initialParams = [
- {
- name: 'service',
- value: 'thiapp'
- },
- {
- name: 'method',
- value: 'persdata'
- },
- {
- name: 'session',
- value: await obtainSession(router)
- },
- {
- name: 'format',
- value: 'json'
- }
- ]
+ try {
+ const initialParams = [
+ {
+ name: 'service',
+ value: 'thiapp'
+ },
+ {
+ name: 'method',
+ value: 'persdata'
+ },
+ {
+ name: 'session',
+ // don't do anything with the session key, just return it
+ value: await callWithSession(session => session)
+ },
+ {
+ name: 'format',
+ value: 'json'
+ }
+ ]
- setParameters(initialParams)
+ setParameters(initialParams)
+ } catch (e) {
+ if (e instanceof NoSessionError || e instanceof UnavailableSessionError) {
+ router.replace('/login?redirect=debug')
+ } else {
+ console.error(e)
+ alert(e)
+ }
+ }
}
load()
}, [router])
From 50a1b83e3b85080ce9b505f43043df0020ec3ed2 Mon Sep 17 00:00:00 2001
From: Alexander Horn
Date: Sat, 1 Jul 2023 17:12:31 +0200
Subject: [PATCH 07/34] Upgrade to Node.js 18 LTS Fixes #297
---
Dockerfile | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 5bc5fe31..8ebe6105 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,7 +42,7 @@ RUN mkdir ./splash && npx pwa-asset-generator --no-sandbox=true --path-override
-FROM node:16
+FROM node:18
WORKDIR /opt/next
@@ -54,7 +54,8 @@ ENV NEXT_PUBLIC_ELECTION_URL $NEXT_PUBLIC_ELECTION_URL
ENV NEXT_PUBLIC_GUEST_ONLY $NEXT_PUBLIC_GUEST_ONLY
COPY rogue-thi-app/package.json rogue-thi-app/package-lock.json ./
-RUN npm install
+# OpenSSL legacy provider needed to build node-forge
+RUN NODE_OPTIONS=--openssl-legacy-provider npm install
COPY rogue-thi-app/ .
COPY --from=spo /opt/spo-grade-weights.json data/
COPY --from=distances /opt/room-distances.json data/
From fdb4fd5b4945e373633bbd43076b88ce892a4a57 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Tue, 4 Jul 2023 15:19:20 +0200
Subject: [PATCH 08/34] =?UTF-8?q?=E2=9C=A8=20add=20suggestion=20preference?=
=?UTF-8?q?s?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 🐛 Fix small translation error
* ✏️ Fix typos
* ✨ add suggestions preferences
* 📱 Wrap duration buttons on small screens
* 🌐 Translate room functions (search page) and room names
* 🐛 Validate date on room search page
* 🐛 show translated room names in RoomCard
* 🐛 filter suggestions correctly in edge cases
* 🚸 show general suggestions if first gap is too far in future
---
rogue-thi-app/components/RoomMap.js | 17 +-
rogue-thi-app/components/cards/RoomCard.js | 4 +-
.../lib/backend-utils/rooms-utils.js | 96 +++++-
rogue-thi-app/lib/hooks/building-filter.js | 24 ++
rogue-thi-app/pages/rooms/search.js | 16 +-
rogue-thi-app/pages/rooms/suggestions.js | 294 ++++++++++++------
rogue-thi-app/public/locales/de/common.json | 3 +
rogue-thi-app/public/locales/de/rooms.json | 30 +-
.../public/locales/en/api-translations.json | 2 +-
rogue-thi-app/public/locales/en/common.json | 3 +
rogue-thi-app/public/locales/en/rooms.json | 24 +-
rogue-thi-app/styles/RoomsSearch.module.css | 5 +
room-distances/calculate-distances.py | 10 +-
13 files changed, 388 insertions(+), 140 deletions(-)
create mode 100644 rogue-thi-app/lib/hooks/building-filter.js
diff --git a/rogue-thi-app/components/RoomMap.js b/rogue-thi-app/components/RoomMap.js
index 61c831a8..e12e025d 100644
--- a/rogue-thi-app/components/RoomMap.js
+++ b/rogue-thi-app/components/RoomMap.js
@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import React, { useEffect, useMemo, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import Link from 'next/link'
@@ -10,7 +10,7 @@ import { AttributionControl, CircleMarker, FeatureGroup, LayerGroup, LayersContr
import { NoSessionError, UnavailableSessionError } from '../lib/backend/thi-session-handler'
import { USER_GUEST, useUserKind } from '../lib/hooks/user-kind'
-import { filterRooms, getNextValidDate } from '../lib/backend-utils/rooms-utils'
+import { filterRooms, getNextValidDate, getTranslatedRoomFunction } from '../lib/backend-utils/rooms-utils'
import { formatFriendlyTime, formatISODate, formatISOTime } from '../lib/date-utils'
import { useLocation } from '../lib/hooks/geolocation'
@@ -58,13 +58,6 @@ export default function RoomMap ({ highlight, roomData }) {
const { t, i18n } = useTranslation(['rooms', 'api-translations'])
- const getTranslatedFunction = useCallback((room) => {
- const roomFunctionCleaned = room?.properties?.Funktion?.replace(/\s+/g, ' ')?.trim() ?? ''
-
- const roomFunction = t(`apiTranslations.roomFunctions.${roomFunctionCleaned}`, { ns: 'api-translations' })
- return roomFunction === `apiTranslations.roomFunctions.${roomFunctionCleaned}` ? roomFunctionCleaned : roomFunction
- }, [t])
-
/**
* Preprocessed room data for Leaflet.
*/
@@ -105,7 +98,7 @@ export default function RoomMap ({ highlight, roomData }) {
const getProp = (room, prop) => {
if (prop === 'Funktion') {
- return getTranslatedFunction(room).toUpperCase()
+ return getTranslatedRoomFunction(room?.properties?.Funktion).toUpperCase()
}
return room.properties[prop]?.toUpperCase()
@@ -126,7 +119,7 @@ export default function RoomMap ({ highlight, roomData }) {
const filteredCenter = count > 0 ? [lon / count, lat / count] : DEFAULT_CENTER
return [filtered, filteredCenter]
- }, [searchText, allRooms, getTranslatedFunction])
+ }, [searchText, allRooms])
useEffect(() => {
async function load () {
@@ -199,7 +192,7 @@ export default function RoomMap ({ highlight, roomData }) {
{entry.properties.Raum}
- {`, ${getTranslatedFunction(entry, i18n)}`}
+ {`, ${getTranslatedRoomFunction(entry?.properties?.Funktion, i18n)}`}
{avail && (
<>
{t('rooms.map.freeFromUntil', {
diff --git a/rogue-thi-app/components/cards/RoomCard.js b/rogue-thi-app/components/cards/RoomCard.js
index 672758a3..bc24bdeb 100644
--- a/rogue-thi-app/components/cards/RoomCard.js
+++ b/rogue-thi-app/components/cards/RoomCard.js
@@ -6,7 +6,7 @@ import { faDoorOpen } from '@fortawesome/free-solid-svg-icons'
import BaseCard from './BaseCard'
-import { findSuggestedRooms, getEmptySuggestions } from '../../lib/backend-utils/rooms-utils'
+import { findSuggestedRooms, getEmptySuggestions, getTranslatedRoomName } from '../../lib/backend-utils/rooms-utils'
import { formatFriendlyTime, isSameDay } from '../../lib/date-utils'
import { getFriendlyTimetable, getTimetableGaps } from '../../lib/backend-utils/timetable-utils'
import { NoSessionError } from '../../lib/backend/thi-session-handler'
@@ -84,7 +84,7 @@ export default function RoomCard () {
- {x.room}
+ {getTranslatedRoomName(x.room)}
{t('rooms.text', { from: formatFriendlyTime(x.from), until: formatFriendlyTime(x.until) })}
diff --git a/rogue-thi-app/lib/backend-utils/rooms-utils.js b/rogue-thi-app/lib/backend-utils/rooms-utils.js
index cdeee207..bb443adf 100644
--- a/rogue-thi-app/lib/backend-utils/rooms-utils.js
+++ b/rogue-thi-app/lib/backend-utils/rooms-utils.js
@@ -1,13 +1,16 @@
import API from '../backend/authenticated-api'
-import { formatISODate } from '../date-utils'
+
+import { formatISODate, getWeek } from '../date-utils'
import { getFriendlyTimetable } from './timetable-utils'
+import { i18n } from 'next-i18next'
import roomDistances from '../../data/room-distances.json'
const IGNORE_GAPS = 15
+export const BUILDINGS = ['A', 'B', 'BN', 'C', 'CN', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'M', 'P', 'W', 'Z']
export const BUILDINGS_ALL = 'Alle'
export const DURATION_PRESET = '01:00'
-const SUGGESTION_DURATION_PRESET = 90
+export const SUGGESTION_DURATION_PRESET = 90
/**
* Adds minutes to a date object.
@@ -211,6 +214,19 @@ function sortRoomsByDistance (room, rooms) {
return rooms
}
+/**
+ * Returns all buildings filtered by Neuburg or Ingolstadt using the timetable.
+ */
+export async function getAllUserBuildings () {
+ const majorityRoom = await getMajorityRoom()
+
+ if (majorityRoom) {
+ return BUILDINGS.filter(building => building.includes('N') === majorityRoom.includes('N'))
+ }
+
+ return BUILDINGS
+}
+
/**
* Finds rooms that are close to the given room and are available for the given time.
* @param {string} room Room name (e.g. `G215`)
@@ -235,27 +251,91 @@ export async function findSuggestedRooms (room, startDate, endDate) {
* @returns {string} Room name (e.g. `G215`)
*/
async function getMajorityRoom () {
- const timetable = await getFriendlyTimetable(new Date(), false)
+ const date = new Date()
+ if (date.getDay() === 0) {
+ date.setDate(date.getDate() + 1)
+ }
+
+ const week = getWeek(date)
+
+ const timetable = await getFriendlyTimetable(week[0], false)
const rooms = timetable.map(x => x.raum)
return mode(rooms)
}
+/**
+ * Translates the room function to the current language.
+ * @param {string} roomFunction Room function (e.g. `Seminarraum`)
+ * @returns {string} Translated room function
+ */
+export function getTranslatedRoomFunction (roomFunction) {
+ const roomFunctionCleaned = roomFunction?.replace(/\s+/g, ' ')?.trim() ?? ''
+
+ const translatedRoomFunction = i18n.t(`apiTranslations.roomFunctions.${roomFunctionCleaned}`, { ns: 'api-translations' })
+ return translatedRoomFunction === `apiTranslations.roomFunctions.${roomFunctionCleaned}` ? roomFunctionCleaned : translatedRoomFunction
+}
+
+/**
+ * Translates the room name to the current language.
+ * This is only used for some special cases like 'alle Räume'.
+ * @param {string} room Room name (e.g. `G215`)
+ * @returns {string} Translated room name
+ */
+export function getTranslatedRoomName (room) {
+ switch (room) {
+ case 'alle Räume':
+ return i18n.t('rooms.allRooms', { ns: 'common' })
+ default:
+ return room
+ }
+}
+
/**
* Finds empty rooms for the current time with the given duration.
* @param {boolean} [asGap] Whether to return the result as a gap with start and end date or only the rooms
* @param {number} [duration] Duration of the gap in minutes
* @returns {Array}
**/
-export async function getEmptySuggestions (asGap = false, duration = SUGGESTION_DURATION_PRESET) {
- const endDate = addMinutes(new Date(), duration)
+export async function getEmptySuggestions (asGap = false) {
+ const userDurationStorage = localStorage.getItem('suggestion-duration')
+ const userDuration = userDurationStorage ? parseInt(userDurationStorage) : SUGGESTION_DURATION_PRESET
+
+ const endDate = addMinutes(new Date(), userDuration)
let rooms = await searchRooms(new Date(), endDate)
+ const buildingFilter = localStorage.buildingPreferences
+
+ if (buildingFilter) {
+ // test if any of the rooms is in any of the user's preferred buildings
+ const userBuildings = JSON.parse(buildingFilter)
+ const filteredBuildings = Object.keys(userBuildings).filter(x => userBuildings[x])
+
+ if (filteredBuildings.length !== 0) {
+ const filteredRooms = rooms.filter(x => filteredBuildings.some(y => isInBuilding(x.room, y)))
+
+ if (filteredRooms.length >= 4) {
+ // enough rooms in preferred buildings -> filter out other buildings
+ rooms = filteredRooms
+ } else {
+ // not enough rooms in preferred buildings -> show all rooms but sort by preferred buildings
+ rooms = rooms.sort(x => filteredBuildings.some(y => isInBuilding(x.room, y)))
+ }
+ }
+ }
+
+ // no preferred buildings -> search rooms near majority room
+ // preferred buildings -> filter by preferred buildings and sort by distance to majority room
const majorityRoom = await getMajorityRoom()
- rooms = sortRoomsByDistance(majorityRoom, rooms)
- // hide Neuburg buildings if next lecture is not in Neuburg
- rooms = rooms.filter(x => x.room.includes('N') === majorityRoom.includes('N'))
+ // if majority room is undefined -> do not filter
+ if (majorityRoom) {
+ rooms = sortRoomsByDistance(majorityRoom, rooms)
+
+ // hide Neuburg buildings if next lecture is not in Neuburg
+ rooms = rooms.filter(x => x.room.includes('N') === majorityRoom.includes('N'))
+ }
+
rooms = rooms.slice(0, 4)
if (asGap) {
diff --git a/rogue-thi-app/lib/hooks/building-filter.js b/rogue-thi-app/lib/hooks/building-filter.js
new file mode 100644
index 00000000..0ddbb70f
--- /dev/null
+++ b/rogue-thi-app/lib/hooks/building-filter.js
@@ -0,0 +1,24 @@
+import { useEffect, useState } from 'react'
+
+export function useBuildingFilter () {
+ const [buildingPreferences, setBuildingPreferences] = useState({})
+
+ useEffect(() => {
+ if (localStorage.buildingPreferences) {
+ setBuildingPreferences(JSON.parse(localStorage.buildingPreferences))
+ }
+ }, [])
+
+ /**
+ * Persists the building preferences to localStorage.
+ */
+ function saveBuildingPreferences () {
+ localStorage.buildingPreferences = JSON.stringify(buildingPreferences)
+ }
+
+ return {
+ buildingPreferences,
+ setBuildingPreferences,
+ saveBuildingPreferences
+ }
+}
diff --git a/rogue-thi-app/pages/rooms/search.js b/rogue-thi-app/pages/rooms/search.js
index 8891ea00..6c3aaa83 100644
--- a/rogue-thi-app/pages/rooms/search.js
+++ b/rogue-thi-app/pages/rooms/search.js
@@ -15,7 +15,7 @@ import AppContainer from '../../components/page/AppContainer'
import AppNavbar from '../../components/page/AppNavbar'
import AppTabbar from '../../components/page/AppTabbar'
-import { BUILDINGS_ALL, DURATION_PRESET, filterRooms, getNextValidDate } from '../../lib/backend-utils/rooms-utils'
+import { BUILDINGS, BUILDINGS_ALL, DURATION_PRESET, filterRooms, getNextValidDate, getTranslatedRoomFunction, getTranslatedRoomName } from '../../lib/backend-utils/rooms-utils'
import { NoSessionError, UnavailableSessionError } from '../../lib/backend/thi-session-handler'
import { formatFriendlyTime, formatISODate, formatISOTime } from '../../lib/date-utils'
@@ -24,7 +24,6 @@ import styles from '../../styles/RoomsSearch.module.css'
import { Trans, useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
-const BUILDINGS = ['A', 'B', 'BN', 'C', 'CN', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'M', 'P', 'W', 'Z']
const DURATIONS = ['00:15', '00:30', '00:45', '01:00', '01:15', '01:30', '01:45', '02:00', '02:15', '02:30', '02:45', '03:00', '03:15', '03:30', '03:45', '04:00', '04:15', '04:30', '04:45', '05:00', '05:15', '05:30', '05:45', '06:00']
const TUX_ROOMS = ['G308']
@@ -32,7 +31,8 @@ export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale ?? 'en', [
'rooms',
- 'common'
+ 'common',
+ 'api-translations'
]))
}
})
@@ -58,6 +58,12 @@ export default function RoomSearch () {
* Searches and displays rooms with the specified filters.
*/
const filter = useCallback(async () => {
+ // when entering dates on desktop, for a short time the date is invalid (e.g. 2023-07-00) when the user is still typing
+ const validateDate = new Date(date)
+ if (isNaN(validateDate.getTime())) {
+ return
+ }
+
setSearching(true)
setFilterResults(null)
@@ -150,11 +156,11 @@ export default function RoomSearch () {
- {result.room}
+ {getTranslatedRoomName(result.room)}
{TUX_ROOMS.includes(result.room) && <>
>}
- {result.type}
+ {getTranslatedRoomFunction(result.type)}
diff --git a/rogue-thi-app/pages/rooms/suggestions.js b/rogue-thi-app/pages/rooms/suggestions.js
index 012a8a01..8552be33 100644
--- a/rogue-thi-app/pages/rooms/suggestions.js
+++ b/rogue-thi-app/pages/rooms/suggestions.js
@@ -5,6 +5,12 @@ import { useRouter } from 'next/router'
import ListGroup from 'react-bootstrap/ListGroup'
import ReactPlaceholder from 'react-placeholder'
+import Button from 'react-bootstrap/Button'
+import ButtonGroup from 'react-bootstrap/ButtonGroup'
+import Container from 'react-bootstrap/Container'
+import Form from 'react-bootstrap/Form'
+import Modal from 'react-bootstrap/Modal'
+
import { faArrowRight, faCalendar } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faLinux } from '@fortawesome/free-brands-svg-icons'
@@ -17,7 +23,7 @@ import AppTabbar from '../../components/page/AppTabbar'
import { NoSessionError, UnavailableSessionError } from '../../lib/backend/thi-session-handler'
import { formatFriendlyTime, isSameDay } from '../../lib/date-utils'
-import { findSuggestedRooms, getEmptySuggestions } from '../../lib/backend-utils/rooms-utils'
+import { SUGGESTION_DURATION_PRESET, findSuggestedRooms, getAllUserBuildings, getEmptySuggestions, getTranslatedRoomFunction, getTranslatedRoomName } from '../../lib/backend-utils/rooms-utils'
import styles from '../../styles/RoomsSearch.module.css'
@@ -26,6 +32,7 @@ import { getFriendlyTimetable, getTimetableEntryName, getTimetableGaps } from '.
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import { Trans, useTranslation } from 'next-i18next'
+import { useBuildingFilter } from '../../lib/hooks/building-filter'
const TUX_ROOMS = ['G308']
@@ -33,6 +40,7 @@ export const getStaticProps = async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale ?? 'en', [
'rooms',
+ 'api-translations',
'common'
]))
}
@@ -45,44 +53,60 @@ export default function RoomSearch () {
const router = useRouter()
const [suggestions, setSuggestions] = useState(null)
+ const [showEditDuration, setShowEditDuration] = useState(false)
+ const [buildings, setBuildings] = useState([])
+
+ const { buildingPreferences, setBuildingPreferences, saveBuildingPreferences } = useBuildingFilter()
const userKind = useUserKind()
const { t } = useTranslation('rooms')
- useEffect(() => {
- async function load () {
- try {
- // get timetable and filter for today
- const timetable = await getFriendlyTimetable(new Date(), false)
- const today = timetable.filter(x => isSameDay(x.startDate, new Date()))
-
- if (today.length < 1) {
- // no lectures today -> general room search
- const suggestions = await getEmptySuggestions(true)
- setSuggestions(suggestions)
- return
- }
+ async function getSuggestions () {
+ // get timetable and filter for today
+ const timetable = await getFriendlyTimetable(new Date(), false)
+ const today = timetable.filter(x => isSameDay(x.startDate, new Date()))
+
+ if (today.length < 1) {
+ // no lectures today -> general room search
+ return await getEmptySuggestions(true)
+ }
- const gaps = getTimetableGaps(today)
- if (gaps.length < 1) {
- const suggestions = await getEmptySuggestions(true)
- setSuggestions(suggestions)
- return
+ const gaps = getTimetableGaps(today)
+ if (gaps.length < 1) {
+ return await getEmptySuggestions(true)
+ }
+
+ const suggestions = await Promise.all(gaps.map(async (gap) => {
+ const rooms = await findSuggestedRooms(gap.endLecture.raum, gap.startDate, gap.endDate)
+
+ return (
+ {
+ gap,
+ rooms: rooms.slice(0, 4)
}
+ )
+ }))
- const suggestions = await Promise.all(gaps.map(async (gap) => {
- const rooms = await findSuggestedRooms(gap.endLecture.raum, gap.startDate, gap.endDate)
+ // if first gap is in too far in the future (now + suggestion duration), show empty suggestions as well
+ const deltaTime = suggestions[0].gap.startDate.getTime() - new Date().getTime()
+ const suggestionDuration = parseInt(localStorage.getItem('suggestion-duration') ?? `${SUGGESTION_DURATION_PRESET}`)
- return (
- {
- gap,
- rooms: rooms.slice(0, 4)
- }
- )
- }))
+ if (deltaTime > suggestionDuration * 60 * 1000) {
+ const emptySuggestions = await getEmptySuggestions(true)
+ suggestions.unshift(emptySuggestions[0])
+ }
- setSuggestions(suggestions)
+ return suggestions
+ }
+
+ useEffect(() => {
+ async function load () {
+ try {
+ // load buildings
+ setBuildings(await getAllUserBuildings())
+
+ setSuggestions(await getSuggestions())
} catch (e) {
if (e instanceof NoSessionError || e instanceof UnavailableSessionError) {
router.replace('/login?redirect=rooms%2Fsuggestions')
@@ -98,11 +122,155 @@ export default function RoomSearch () {
}
}, [router, userKind])
+ /**
+ * Closes the modal and saves the preferences.
+ */
+ async function closeModal () {
+ setShowEditDuration(false)
+ saveBuildingPreferences()
+
+ const suggestions = await getSuggestions()
+ setSuggestions(suggestions)
+ }
+
+ /**
+ * Returns the header for the given result like `Jetzt -> KI_ML3 (K015)`
+ * @param {object} result Gap result object
+ * @returns {JSX.Element} Header
+ */
+ function GapHeader ({ result }) {
+ if (result.gap.endLecture) {
+ return (
+
+ }}
+ values={{
+ from: result.gap.startLecture ? getTimetableEntryName(result.gap.startLecture).shortName : t('rooms.suggestions.gaps.now'),
+ until: getTimetableEntryName(result.gap.endLecture).shortName,
+ room: result.gap.endLecture.raum
+ }}
+ />
+ )
+ } else {
+ return (
+
+ )
+ }
+ }
+
+ /**
+ * Returns the subtitle for the given result like `Pause von 10:00 bis 10:15`
+ * @param {object} result Gap result object
+ * @returns {JSX.Element} Subtitle
+ **/
+ function GapSubtitle ({ result }) {
+ if (result.gap.endLecture) {
+ return (
+
+ )
+ } else {
+ return (
+
+ )
+ }
+ }
+
+ /**
+ * A button to select a duration.
+ * @param {int} duration Duration in minutes
+ * @returns {JSX.Element} Button
+ */
+ function DurationButton ({ duration }) {
+ const variant = (localStorage.getItem('suggestion-duration') ?? `${SUGGESTION_DURATION_PRESET}`) === `${duration}` ? 'primary' : 'outline-primary'
+ return
{
+ localStorage.setItem('suggestion-duration', duration)
+ } }>
+
+ {duration}
+
+ {t('rooms.suggestions.modals.suggestionPreferences.minutes')}
+
+ }
+
return (
-
+
+
+ setShowEditDuration(true)}>
+ {t('rooms.suggestions.appbar.overflow.suggestionPreferences')}
+
+
+
+
+
+ {t('rooms.suggestions.modals.suggestionPreferences.title')}
+
+
+ {t('rooms.suggestions.modals.suggestionPreferences.description')}
+
+
+
+ {t('rooms.suggestions.modals.suggestionPreferences.duration')}
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('rooms.suggestions.modals.suggestionPreferences.preferredBuildings')}
+
+
+ setBuildingPreferences({ ...buildingPreferences, [building]: e.target.checked })}
+ />
+ )}
+
+
+
+
+
+ {t('rooms.suggestions.modals.suggestionPreferences.close')}
+
+
+
+
{suggestions && suggestions.map((result, idx) =>
@@ -118,11 +286,11 @@ export default function RoomSearch () {
- {roomResult.room}
+ {getTranslatedRoomName(roomResult.room)}
{TUX_ROOMS.includes(roomResult.room) && <>
>}
- {roomResult.type}
+ {getTranslatedRoomFunction(roomResult.type)}
@@ -163,65 +331,3 @@ export default function RoomSearch () {
)
}
-
-/**
- * Returns the header for the given result like `Jetzt -> KI_ML3 (K015)`
- * @param {object} result Gap result object
- * @returns {JSX.Element} Header
- */
-function GapHeader ({ result }) {
- if (result.gap.endLecture) {
- return (
-
- }}
- values={{
- from: result.gap.startLecture ? getTimetableEntryName(result.gap.startLecture).shortName : 'Jetzt',
- until: getTimetableEntryName(result.gap.endLecture).shortName,
- room: result.gap.endLecture.raum
- }}
- />
- )
- } else {
- return (
-
- )
- }
-}
-
-/**
- * Returns the subtitle for the given result like `Pause von 10:00 bis 10:15`
- * @param {object} result Gap result object
- * @returns {JSX.Element} Subtitle
- **/
-function GapSubtitle ({ result }) {
- if (result.gap.endLecture) {
- return (
-
- )
- } else {
- return (
-
- )
- }
-}
diff --git a/rogue-thi-app/public/locales/de/common.json b/rogue-thi-app/public/locales/de/common.json
index cbb0474f..69cf780c 100644
--- a/rogue-thi-app/public/locales/de/common.json
+++ b/rogue-thi-app/public/locales/de/common.json
@@ -67,5 +67,8 @@
"appbar": {
"back": "Zurück",
"overflow": "Mehr Optionen"
+ },
+ "rooms": {
+ "allRooms": "Alle Räume"
}
}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/de/rooms.json b/rogue-thi-app/public/locales/de/rooms.json
index 329938e7..59872069 100644
--- a/rogue-thi-app/public/locales/de/rooms.json
+++ b/rogue-thi-app/public/locales/de/rooms.json
@@ -41,28 +41,42 @@
},
"suggestions": {
"appbar": {
- "title": "Raumvorschläge"
+ "title": "Raumvorschläge",
+ "overflow": {
+ "suggestionPreferences": "Vorschlags-Präferenzen bearbeiten"
+ }
},
"noSuggestions": "Keine Vorschläge verfügbar.",
"noAvailableRooms": "Keine freien Räume gefunden.",
"gaps": {
"header": {
"general": "Freie Räume",
- "specific": "{{from}}
{{until}}"
+ "specific": "{{from}}
{{until}} ({{room}})"
},
"subtitle": {
- "general": "Pause von {{from}} bis {{until}}",
- "specific": "Räume von {{from}} bis {{until}}"
+ "general": "Räume bis {{until}} ({{duration}} Minuten)",
+ "specific": "Pause von {{from}} bis {{until}}"
+ },
+ "now": "Jetzt"
+ },
+ "modals": {
+ "suggestionPreferences": {
+ "title": "Vorschlags-Präferenzen",
+ "description": "Hier kannst du die Dauer und Gebäude der Raumvorschläge anpassen.",
+ "duration": "Dauer",
+ "preferredBuildings": "Bevorzugte Gebäude",
+ "minutes": "Minuten",
+ "close": "Schließen"
}
}
},
"list": {
- "appbar": {
- "title": "Stündlicher Raumplan"
- }
+ "appbar": {
+ "title": "Stündlicher Raumplan"
+ }
},
"common": {
"availableFromUntil": "frei ab {{from}}
bis {{until}}"
}
}
-}
+}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/api-translations.json b/rogue-thi-app/public/locales/en/api-translations.json
index cb7f38cb..8801a56c 100644
--- a/rogue-thi-app/public/locales/en/api-translations.json
+++ b/rogue-thi-app/public/locales/en/api-translations.json
@@ -13,7 +13,7 @@
"lecturerOrganizations": {
"Fakultät Business School": "Faculty Business School",
"Fakultät Elektro- und Informationstechnik": "Faculty of Electrical Engineering and Information Technology",
- "Fakultät Informatik": "Faculty of Informatics",
+ "Fakultät Informatik": "Faculty of Computer Science",
"Fakultät Maschinenbau": "Faculty of Mechanical Engineering",
"Fakultät Wirtschaftsingenieurwesen": "Faculty of Industrial Engineering",
"Institut für Akademische Weiterbildung": "Institute for Academic Continuing Education",
diff --git a/rogue-thi-app/public/locales/en/common.json b/rogue-thi-app/public/locales/en/common.json
index de8af714..a16f2465 100644
--- a/rogue-thi-app/public/locales/en/common.json
+++ b/rogue-thi-app/public/locales/en/common.json
@@ -67,5 +67,8 @@
"appbar": {
"back": "Back",
"overflow": "More options"
+ },
+ "rooms": {
+ "allRooms": "All rooms"
}
}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/rooms.json b/rogue-thi-app/public/locales/en/rooms.json
index bb71e574..b057d0e0 100644
--- a/rogue-thi-app/public/locales/en/rooms.json
+++ b/rogue-thi-app/public/locales/en/rooms.json
@@ -41,18 +41,32 @@
},
"suggestions": {
"appbar": {
- "title": "Room Suggestions"
+ "title": "Room Suggestions",
+ "overflow": {
+ "suggestionPreferences": "Change suggestion preferences"
+ }
},
"noSuggestions": "No suggestions available.",
"noAvailableRooms": "No available rooms found.",
"gaps": {
"header": {
"general": "Available Rooms",
- "specific": "{{from}}
{{until}}"
+ "specific": "{{from}}
{{until}} ({{room}})"
},
"subtitle": {
- "general": "Break from {{from}} to {{until}}",
- "specific": "Rooms from {{from}} to {{until}}"
+ "general": "Rooms until {{until}} ({{duration}} minutes)",
+ "specific": "Break from {{from}} until {{until}}"
+ },
+ "now": "Now"
+ },
+ "modals": {
+ "suggestionPreferences": {
+ "title": "Suggestion preferences",
+ "description": "Here you can adjust the duration and buildings of room suggestions.",
+ "duration": "Duration",
+ "preferredBuildings": "Preferred buildings",
+ "minutes": "minutes",
+ "close": "Close"
}
}
},
@@ -65,4 +79,4 @@
"availableFromUntil": "Available from {{from}}
to {{until}}"
}
}
-}
+}
\ No newline at end of file
diff --git a/rogue-thi-app/styles/RoomsSearch.module.css b/rogue-thi-app/styles/RoomsSearch.module.css
index fe644e10..a1721855 100644
--- a/rogue-thi-app/styles/RoomsSearch.module.css
+++ b/rogue-thi-app/styles/RoomsSearch.module.css
@@ -60,3 +60,8 @@
margin-top: 75px;
text-align: center;
}
+
+.container {
+ margin: 10px 0px 15px 0px;
+ flex-wrap: wrap;
+}
diff --git a/room-distances/calculate-distances.py b/room-distances/calculate-distances.py
index ddc26a88..03238caf 100644
--- a/room-distances/calculate-distances.py
+++ b/room-distances/calculate-distances.py
@@ -50,14 +50,14 @@ def main():
# filter rooms by type where a room type is partly in 'Funktion'
rooms = [room for room in all_rooms if any([room_type in str(room['properties']['Funktion']) for room_type in ROOM_TYPES])]
- stairscases = [room for room in all_rooms if any([staircase_type in str(room['properties']['Funktion']) for staircase_type in STAIRCASE_TYPES])]
+ staircases = [room for room in all_rooms if any([staircase_type in str(room['properties']['Funktion']) for staircase_type in STAIRCASE_TYPES])]
# add centers to rooms
for room in rooms:
room['center'] = calculate_center(room)
# add centers to staircases
- for staircase in stairscases:
+ for staircase in staircases:
staircase['center'] = calculate_center(staircase)
# calculate distances between rooms
@@ -78,14 +78,14 @@ def main():
elif room['properties']['Gebaeude'] != room2['properties']['Gebaeude']:
total_distance = 0
# find nearest staircase
- distance1, nearestStaircase1 = findNearestStaircase(room, stairscases)
+ distance1, nearestStaircase1 = findNearestStaircase(room, staircases)
total_distance += distance1
# add distance inside staircase (naive assumption)
total_distance += float(room['properties']['Ebene']) * 5
# find staircase in other building
- distance2, nearestStaircase2 = findNearestStaircase(room2, stairscases)
+ distance2, nearestStaircase2 = findNearestStaircase(room2, staircases)
total_distance += distance2
# add distance inside staircase (naive assumption)
@@ -100,7 +100,7 @@ def main():
elif room['properties']['Ebene'] != room2['properties']['Ebene']:
total_distance = 0
# find nearest staircase
- staircase_distance, nearest_staircase = findNearestStaircase(room, stairscases)
+ staircase_distance, nearest_staircase = findNearestStaircase(room, staircases)
total_distance += staircase_distance
# add distance inside staircase (naive assumption)
From b6f2087ca0f6595d6df74dd3e9f778eb0817ca17 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Tue, 4 Jul 2023 23:39:26 +0200
Subject: [PATCH 09/34] =?UTF-8?q?=F0=9F=90=9B=20Localize=20NavBar?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 🐛 add missing localizations on TimetableCard
* 🐛 Localize NavBar
* 🐛 Added localization to debug and hackerman page
---
rogue-thi-app/components/cards/TimetableCard.js | 4 ++--
rogue-thi-app/components/page/AppTabbar.js | 10 ++++++----
rogue-thi-app/pages/become-hackerman.js | 9 +++++++++
rogue-thi-app/pages/debug.js | 10 ++++++++++
rogue-thi-app/public/locales/de/common.json | 1 +
rogue-thi-app/public/locales/de/dashboard.json | 4 +++-
rogue-thi-app/public/locales/en/common.json | 3 ++-
rogue-thi-app/public/locales/en/dashboard.json | 4 +++-
8 files changed, 36 insertions(+), 9 deletions(-)
diff --git a/rogue-thi-app/components/cards/TimetableCard.js b/rogue-thi-app/components/cards/TimetableCard.js
index 64251540..f1f3ba03 100644
--- a/rogue-thi-app/components/cards/TimetableCard.js
+++ b/rogue-thi-app/components/cards/TimetableCard.js
@@ -82,9 +82,9 @@ export default function TimetableCard () {
)
})) ||
(timetable && timetable.length === 0 &&
- Du hast heute keine Vorlesungen mehr. ) ||
+ {t('timetable.text.noLectures')} ) ||
(timetableError &&
- Fehler beim Abruf des Stundenplans. )}
+ {t('timetable.text.error')} )}
diff --git a/rogue-thi-app/components/page/AppTabbar.js b/rogue-thi-app/components/page/AppTabbar.js
index 278c4e20..d7ba315f 100644
--- a/rogue-thi-app/components/page/AppTabbar.js
+++ b/rogue-thi-app/components/page/AppTabbar.js
@@ -15,6 +15,8 @@ import { USER_GUEST, useUserKind } from '../../lib/hooks/user-kind'
import styles from '../../styles/AppTabbar.module.css'
+import { i18n } from 'next-i18next'
+
/**
* Tab bar to be displayed at the bottom of the screen.
*/
@@ -31,27 +33,27 @@ export default function AppTabbar () {
router.replace('/')} className={[styles.tab, router.pathname === '/' && styles.tabActive]}>
- Home
+ {i18n.t('cards.home')}
{userKind !== USER_GUEST && (
router.replace('/timetable')} className={[styles.tab, router.pathname === '/timetable' && styles.tabActive]}>
- Stundenplan
+ {i18n.t('cards.timetable')}
)}
router.replace('/rooms')} className={[styles.tab, router.pathname === '/rooms' && styles.tabActive]}>
- Raumplan
+ {i18n.t('cards.rooms')}
router.replace('/food')} className={[styles.tab, router.pathname === '/food' && styles.tabActive]}>
- Essen
+ {i18n.t('cards.mensa')}
diff --git a/rogue-thi-app/pages/become-hackerman.js b/rogue-thi-app/pages/become-hackerman.js
index f110c077..caa4ad5b 100644
--- a/rogue-thi-app/pages/become-hackerman.js
+++ b/rogue-thi-app/pages/become-hackerman.js
@@ -9,6 +9,7 @@ import AppBody from '../components/page/AppBody'
import AppContainer from '../components/page/AppContainer'
import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
// somone could try bruteforcing these, but that way he wont learn how to hack ;)
const FLAG_CSV = process.env.NEXT_PUBLIC_HACKERMAN_FLAGS || ''
@@ -120,3 +121,11 @@ export default function BecomeHackerman () {
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/pages/debug.js b/rogue-thi-app/pages/debug.js
index 36109a77..b53247e6 100644
--- a/rogue-thi-app/pages/debug.js
+++ b/rogue-thi-app/pages/debug.js
@@ -18,6 +18,8 @@ import API from '../lib/backend/anonymous-api'
import styles from '../styles/Common.module.css'
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
+
const GIT_URL = process.env.NEXT_PUBLIC_GIT_URL
/**
@@ -160,3 +162,11 @@ export default function Debug () {
)
}
+
+export const getStaticProps = async ({ locale }) => ({
+ props: {
+ ...(await serverSideTranslations(locale ?? 'en', [
+ 'common'
+ ]))
+ }
+})
diff --git a/rogue-thi-app/public/locales/de/common.json b/rogue-thi-app/public/locales/de/common.json
index 69cf780c..1095019b 100644
--- a/rogue-thi-app/public/locales/de/common.json
+++ b/rogue-thi-app/public/locales/de/common.json
@@ -49,6 +49,7 @@
}
},
"cards": {
+ "home": "Home",
"install": "Installation",
"timetable": "Stundenplan",
"mensa": "Essen",
diff --git a/rogue-thi-app/public/locales/de/dashboard.json b/rogue-thi-app/public/locales/de/dashboard.json
index 5e99ba61..44e66ee6 100644
--- a/rogue-thi-app/public/locales/de/dashboard.json
+++ b/rogue-thi-app/public/locales/de/dashboard.json
@@ -5,7 +5,9 @@
"endingSoon": "endet in {{mins}} min",
"ongoing": "endet um {{time}}",
"startingSoon": "beginnt in {{mins}} min",
- "future": "um"
+ "future": "um",
+ "noLectures": "Du hast heute keine Vorlesungen mehr.",
+ "error": "Fehler beim Abruf des Stundenplans."
}
},
"food": {
diff --git a/rogue-thi-app/public/locales/en/common.json b/rogue-thi-app/public/locales/en/common.json
index a16f2465..f2908bf9 100644
--- a/rogue-thi-app/public/locales/en/common.json
+++ b/rogue-thi-app/public/locales/en/common.json
@@ -49,9 +49,10 @@
}
},
"cards": {
+ "home": "Home",
"install": "Installation",
"timetable": "Timetable",
- "mensa": "Cafeteria",
+ "mensa": "Food",
"mobility": "Mobility",
"calendar": "Calendar",
"events": "Events",
diff --git a/rogue-thi-app/public/locales/en/dashboard.json b/rogue-thi-app/public/locales/en/dashboard.json
index 84471a2a..9a1fad88 100644
--- a/rogue-thi-app/public/locales/en/dashboard.json
+++ b/rogue-thi-app/public/locales/en/dashboard.json
@@ -5,7 +5,9 @@
"endingSoon": "ends in {{mins}} min",
"ongoing": "ends at {{time}}",
"startingSoon": "starts in {{mins}} min",
- "future": "at"
+ "future": "at",
+ "noLectures": "You have no more lectures today.",
+ "error": "Error retrieving timetable."
}
},
"food": {
From abaa0ff19ea493aa50890c2f94973a6f9cc31c53 Mon Sep 17 00:00:00 2001
From: Robert Eggl
Date: Thu, 6 Jul 2023 14:29:20 +0200
Subject: [PATCH 10/34] Fixes cookie deletion on browser session end (#300)
---
rogue-thi-app/components/modal/LanguageModal.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rogue-thi-app/components/modal/LanguageModal.js b/rogue-thi-app/components/modal/LanguageModal.js
index bab6bfd2..46d0d95d 100644
--- a/rogue-thi-app/components/modal/LanguageModal.js
+++ b/rogue-thi-app/components/modal/LanguageModal.js
@@ -30,7 +30,7 @@ export default function LanguageModal () {
setShowLanguageModal(false)
i18n.changeLanguage(languageKey)
router.replace('/', '', { locale: i18n.language })
- document.cookie = `NEXT_LOCALE=${i18n.language}; path=/`
+ document.cookie = `NEXT_LOCALE=${i18n.language}; path=/; expires=${new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 5).toUTCString()}`
}
return (
From 2a820c658998d11d4dc08319d32ccad231642e8c Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Thu, 6 Jul 2023 23:59:37 +0200
Subject: [PATCH 11/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20UI=20error=20on=20Fo?=
=?UTF-8?q?odCard=20(#303)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/components/cards/FoodCard.js | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/rogue-thi-app/components/cards/FoodCard.js b/rogue-thi-app/components/cards/FoodCard.js
index 904635f5..cc81edf2 100644
--- a/rogue-thi-app/components/cards/FoodCard.js
+++ b/rogue-thi-app/components/cards/FoodCard.js
@@ -3,11 +3,12 @@ import ListGroup from 'react-bootstrap/ListGroup'
import ReactPlaceholder from 'react-placeholder'
import { faUtensils } from '@fortawesome/free-solid-svg-icons'
+import { Trans, useTranslation } from 'next-i18next'
import BaseCard from './BaseCard'
import { FoodFilterContext } from '../../pages/_app'
import { formatISODate } from '../../lib/date-utils'
import { loadFoodEntries } from '../../lib/backend-utils/food-utils'
-import { useTranslation } from 'next-i18next'
+
/**
* Dashboard card for Mensa and Reimanns food plans.
*/
@@ -105,7 +106,11 @@ export default function FoodCard () {
}
{foodError &&
- {t('food.error.generic')}
+ }}
+ />
}
From 650de54b268420bd06ff3360da44ea730c042e75 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Fri, 7 Jul 2023 00:02:59 +0200
Subject: [PATCH 12/34] =?UTF-8?q?=F0=9F=A5=85=20Improve=20translation=20fa?=
=?UTF-8?q?llback=20and=20development=20(#302)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../lib/backend-utils/translation-utils.js | 70 +++-
rogue-thi-app/package-lock.json | 394 +++++++++---------
rogue-thi-app/package.json | 1 +
3 files changed, 255 insertions(+), 210 deletions(-)
diff --git a/rogue-thi-app/lib/backend-utils/translation-utils.js b/rogue-thi-app/lib/backend-utils/translation-utils.js
index e434e8ad..286bfba8 100644
--- a/rogue-thi-app/lib/backend-utils/translation-utils.js
+++ b/rogue-thi-app/lib/backend-utils/translation-utils.js
@@ -1,18 +1,23 @@
+import * as deepl from 'deepl-node'
import AsyncMemoryCache from '../cache/async-memory-cache'
const DEEPL_ENDPOINT = process.env.NEXT_PUBLIC_DEEPL_ENDPOINT || ''
const DEEPL_API_KEY = process.env.DEEPL_API_KEY || ''
+const ENABLE_DEV_TRANSLATIONS = process.env.ENABLE_DEV_TRANSLATIONS === 'true' || false
-const CACHE_TTL = 60 * 60 * 24 * 1000// 24h
+const CACHE_TTL = 60 * 60 * 24 * 7 * 1000 // 7 days
const cache = new AsyncMemoryCache({ ttl: CACHE_TTL })
+const isDev = process.env.NODE_ENV !== 'production'
+
+const translator = DEEPL_API_KEY ? new deepl.Translator(DEEPL_API_KEY) : null
+const SOURCE_LANG = 'DE'
/**
* Gets a translation from the cache or translates it using DeepL.
* @param {String} text The text to translate
* @param {String} target The target language
- * @returns {String} The translated text
- * @throws {Error} If DeepL is not configured or returns an error
+ * @returns {String} The translated text or the original text if DeepL is not configured or returns an error
**/
async function getTranslation (text, target) {
return await cache.get(`${text}__${target}`, async () => {
@@ -28,28 +33,37 @@ async function getTranslation (text, target) {
* @throws {Error} If DeepL is not configured or returns an error
*/
async function translate (text, target) {
- if (!DEEPL_ENDPOINT || !DEEPL_API_KEY) {
- console.error('DeepL is not configured. Please set DEEPL_ENDPOINT and DEEPL_API_KEY in your .env.local file. Using fallback translation.')
- return `(TRANSLATION_PLACEHOLDER) ${text}`
+ try {
+ return (await translator.translateText(text, SOURCE_LANG, target)).text
+ } catch (err) {
+ console.error(err)
+ return isDev ? `FALLBACK: ${text}` : text
}
+}
- const resp = await fetch(`${DEEPL_ENDPOINT}`,
- {
- method: 'POST',
- mode: 'cors',
- headers: {
- Authorization: `DeepL-Auth-Key ${DEEPL_API_KEY}`,
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- body: `text=${encodeURI(text)}&target_lang=${target}`
+/**
+ * Brings the given meal plan into the correct format as if it was translated by DeepL.
+ * @param {Object} meals The meal plan
+ * @returns {Object} The translated meal plan
+ **/
+function translateFallback (meals) {
+ return meals.map((day) => {
+ const meals = day.meals.map((meal) => {
+ return {
+ ...meal,
+ name: {
+ de: meal.name,
+ en: isDev ? `FALLBACK: ${meal.name}` : meal.name
+ },
+ originalLanguage: 'de'
+ }
})
- if (resp.status === 200) {
- const result = await resp.json()
- return result.translations.map(x => x.text)[0]
- } else {
- throw new Error('DeepL returned an error: ' + await resp.text())
- }
+ return {
+ ...day,
+ meals
+ }
+ })
}
/**
@@ -58,13 +72,25 @@ async function translate (text, target) {
* @returns {Object} The translated meal plan
*/
export async function translateMeals (meals) {
+ if (isDev && !ENABLE_DEV_TRANSLATIONS) {
+ console.warn('DeepL is disabled in development mode.')
+ console.warn('To enable DeepL in development mode, set ENABLE_DEV_TRANSLATIONS=true in your .env.local file.')
+ return translateFallback(meals)
+ }
+
+ if (!DEEPL_ENDPOINT || !DEEPL_API_KEY || !translator) {
+ console.warn('DeepL is not configured.')
+ console.warn('To enable DeepL, set the DEEPL_API_KEY in your .env.local file.')
+ return translateFallback(meals)
+ }
+
return await Promise.all(meals.map(async (day) => {
const meals = await Promise.all(day.meals.map(async (meal) => {
return {
...meal,
name: {
de: meal.name,
- en: await getTranslation(meal.name, 'EN')
+ en: await getTranslation(meal.name, 'EN-GB')
},
originalLanguage: 'de'
}
diff --git a/rogue-thi-app/package-lock.json b/rogue-thi-app/package-lock.json
index 59fd4600..91db9897 100644
--- a/rogue-thi-app/package-lock.json
+++ b/rogue-thi-app/package-lock.json
@@ -16,7 +16,9 @@
"@restart/hooks": "^0.4.7",
"bootstrap": "^4.6.1",
"cheerio": "^1.0.0-rc.10",
+ "deepl-node": "^1.10.2",
"dompurify": "^2.3.4",
+ "eslint-plugin-n": "^16.0.0",
"fetch-bypass-cors": "github:neuland-ingolstadt/fetch-bypass-cors#v1.0.3",
"fetch-cookie": "^2.0.1",
"i18next": "^22.5.0",
@@ -168,11 +170,32 @@
"@capacitor/core": "^4.7.0"
}
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+ "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
"node_modules/@eslint/eslintrc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
"integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
- "dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@@ -252,7 +275,6 @@
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
- "dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
@@ -266,7 +288,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
"engines": {
"node": ">=12.22"
},
@@ -278,8 +299,7 @@
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
- "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
- "dev": true
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
},
"node_modules/@ionic/cli-framework-output": {
"version": "2.2.5",
@@ -618,7 +638,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -631,7 +650,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
"engines": {
"node": ">= 8"
}
@@ -640,7 +658,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -733,8 +750,7 @@
"node_modules/@types/node": {
"version": "18.11.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
- "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==",
- "devOptional": true
+ "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
@@ -880,7 +896,6 @@
"version": "8.8.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
"integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
- "dev": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -892,7 +907,6 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -901,7 +915,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -917,7 +930,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -926,7 +938,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -952,8 +963,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/aria-query": {
"version": "5.1.3",
@@ -1056,6 +1066,11 @@
"node": ">=8"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -1086,6 +1101,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@@ -1098,8 +1136,7 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
- "dev": true
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -1178,7 +1215,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -1208,8 +1244,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
"integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
- "dev": true,
- "peer": true,
"dependencies": {
"semver": "^7.0.0"
}
@@ -1231,7 +1265,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -1255,7 +1288,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -1358,7 +1390,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1369,8 +1400,18 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
},
"node_modules/commander": {
"version": "9.5.0",
@@ -1384,8 +1425,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/core-js": {
"version": "3.30.2",
@@ -1401,7 +1441,6 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -1460,7 +1499,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -1504,8 +1542,21 @@
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+ },
+ "node_modules/deepl-node": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/deepl-node/-/deepl-node-1.10.2.tgz",
+ "integrity": "sha512-MWif2j54Cb0284FFNO8hJSGB2W9xfF0dfRICYicVtfM2FF1ClSU7Fqdf4ct27i5H9YLe32PLvbcwxVaaT+BiQQ==",
+ "dependencies": {
+ "@types/node": ">=12.0",
+ "axios": ">=0.21.2 <1.2.0 || >=1.2.1",
+ "form-data": "^3.0.0",
+ "loglevel": ">=1.6.2"
+ },
+ "engines": {
+ "node": ">=12.0"
+ }
},
"node_modules/define-lazy-prop": {
"version": "2.0.0",
@@ -1532,6 +1583,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -1556,7 +1615,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
"dependencies": {
"esutils": "^2.0.2"
},
@@ -1778,7 +1836,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"engines": {
"node": ">=10"
},
@@ -1790,7 +1847,6 @@
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.32.0.tgz",
"integrity": "sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ==",
- "dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.4.1",
"@humanwhocodes/config-array": "^0.11.8",
@@ -1980,50 +2036,22 @@
"ms": "^2.1.1"
}
},
- "node_modules/eslint-plugin-es": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz",
- "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==",
- "dev": true,
- "peer": true,
+ "node_modules/eslint-plugin-es-x": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.1.0.tgz",
+ "integrity": "sha512-AhiaF31syh4CCQ+C5ccJA0VG6+kJK8+5mXKKE7Qs1xcPRg02CDPOj3mWlQxuWS/AYtg7kxrDNgW9YW3vc0Q+Mw==",
"dependencies": {
- "eslint-utils": "^2.0.0",
- "regexpp": "^3.0.0"
+ "@eslint-community/eslint-utils": "^4.1.2",
+ "@eslint-community/regexpp": "^4.5.0"
},
"engines": {
- "node": ">=8.10.0"
+ "node": "^14.18.0 || >=16.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/mysticatea"
+ "url": "https://github.com/sponsors/ota-meshi"
},
"peerDependencies": {
- "eslint": ">=4.19.1"
- }
- },
- "node_modules/eslint-plugin-es/node_modules/eslint-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
- "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "eslint-visitor-keys": "^1.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/mysticatea"
- }
- },
- "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true,
- "peer": true,
- "engines": {
- "node": ">=4"
+ "eslint": ">=8"
}
},
"node_modules/eslint-plugin-import": {
@@ -2125,23 +2153,21 @@
}
},
"node_modules/eslint-plugin-n": {
- "version": "15.6.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz",
- "integrity": "sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==",
- "dev": true,
- "peer": true,
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.0.1.tgz",
+ "integrity": "sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA==",
"dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
"builtins": "^5.0.1",
- "eslint-plugin-es": "^4.1.0",
- "eslint-utils": "^3.0.0",
- "ignore": "^5.1.1",
- "is-core-module": "^2.11.0",
+ "eslint-plugin-es-x": "^7.1.0",
+ "ignore": "^5.2.4",
+ "is-core-module": "^2.12.1",
"minimatch": "^3.1.2",
- "resolve": "^1.22.1",
- "semver": "^7.3.8"
+ "resolve": "^1.22.2",
+ "semver": "^7.5.3"
},
"engines": {
- "node": ">=12.22.0"
+ "node": ">=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
@@ -2317,7 +2343,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
- "dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@@ -2330,7 +2355,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
- "dev": true,
"dependencies": {
"eslint-visitor-keys": "^2.0.0"
},
@@ -2348,7 +2372,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
- "dev": true,
"engines": {
"node": ">=10"
}
@@ -2357,7 +2380,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
"integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
- "dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@@ -2366,7 +2388,6 @@
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
"integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
- "dev": true,
"dependencies": {
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
@@ -2383,7 +2404,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
- "dev": true,
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -2395,7 +2415,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -2407,7 +2426,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
"engines": {
"node": ">=4.0"
}
@@ -2416,7 +2434,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2424,8 +2441,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.2.12",
@@ -2458,20 +2474,17 @@
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
},
"node_modules/fastq": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
- "dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
@@ -2528,7 +2541,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
- "dev": true,
"dependencies": {
"flat-cache": "^3.0.4"
},
@@ -2551,7 +2563,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@@ -2567,7 +2578,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
- "dev": true,
"dependencies": {
"flatted": "^3.1.0",
"rimraf": "^3.0.2"
@@ -2579,8 +2589,26 @@
"node_modules/flatted": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
- "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
- "dev": true
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
},
"node_modules/for-each": {
"version": "0.3.3",
@@ -2591,6 +2619,19 @@
"is-callable": "^1.1.3"
}
},
+ "node_modules/form-data": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
+ "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -2644,8 +2685,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
- "dev": true
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.2",
@@ -2663,8 +2703,7 @@
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/function.prototype.name": {
"version": "1.1.5",
@@ -2727,7 +2766,6 @@
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
- "dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2747,7 +2785,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -2759,7 +2796,6 @@
"version": "13.19.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
"integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
- "dev": true,
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -2826,14 +2862,12 @@
"node_modules/grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
- "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
- "dev": true
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@@ -2854,7 +2888,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -3031,7 +3064,6 @@
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
- "dev": true,
"engines": {
"node": ">= 4"
}
@@ -3045,7 +3077,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -3061,7 +3092,6 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
"engines": {
"node": ">=0.8.19"
}
@@ -3070,7 +3100,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -3079,8 +3108,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "3.0.1",
@@ -3195,10 +3223,9 @@
}
},
"node_modules/is-core-module": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
- "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
- "dev": true,
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
+ "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
"dependencies": {
"has": "^1.0.3"
},
@@ -3312,7 +3339,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -3458,8 +3484,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/jquery": {
"version": "3.6.3",
@@ -3471,7 +3496,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
- "dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
@@ -3486,7 +3510,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
- "dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -3497,14 +3520,12 @@
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
},
"node_modules/json5": {
"version": "1.0.2",
@@ -3581,7 +3602,6 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -3594,7 +3614,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
"dependencies": {
"p-locate": "^5.0.0"
},
@@ -3608,8 +3627,19 @@
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ },
+ "node_modules/loglevel": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
+ "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/loglevel"
+ }
},
"node_modules/loose-envify": {
"version": "1.4.0",
@@ -3626,7 +3656,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -3656,11 +3685,29 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
- "dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -3780,8 +3827,7 @@
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
},
"node_modules/next": {
"version": "12.3.4",
@@ -4062,7 +4108,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -4088,7 +4133,6 @@
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
- "dev": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -4105,7 +4149,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -4120,7 +4163,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
@@ -4135,7 +4177,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -4170,7 +4211,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -4179,7 +4219,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4188,7 +4227,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -4196,8 +4234,7 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -4309,7 +4346,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
"engines": {
"node": ">= 0.8.0"
}
@@ -4358,6 +4394,11 @@
"react": ">=0.14.0"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -4380,7 +4421,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -4714,7 +4754,6 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
- "dev": true,
"engines": {
"node": ">=8"
},
@@ -4728,12 +4767,11 @@
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
"node_modules/resolve": {
- "version": "1.22.1",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
- "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
- "dev": true,
+ "version": "1.22.2",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
+ "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
"dependencies": {
- "is-core-module": "^2.9.0",
+ "is-core-module": "^2.11.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -4748,7 +4786,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -4757,7 +4794,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -4767,7 +4803,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -4782,7 +4817,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -4867,10 +4901,9 @@
}
},
"node_modules/semver": {
- "version": "7.3.8",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
- "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
- "dev": true,
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
+ "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -4895,7 +4928,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -4907,7 +4939,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -5073,7 +5104,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -5094,7 +5124,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
"engines": {
"node": ">=8"
},
@@ -5125,7 +5154,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -5137,7 +5165,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -5165,8 +5192,7 @@
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
- "dev": true
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
},
"node_modules/through2": {
"version": "4.0.2",
@@ -5261,7 +5287,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
"dependencies": {
"prelude-ls": "^1.2.1"
},
@@ -5273,7 +5298,6 @@
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
- "dev": true,
"engines": {
"node": ">=10"
},
@@ -5360,7 +5384,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
"dependencies": {
"punycode": "^2.1.0"
}
@@ -5421,7 +5444,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -5491,7 +5513,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5516,8 +5537,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "dev": true
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xml-js": {
"version": "1.6.11",
@@ -5569,8 +5589,7 @@
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yauzl": {
"version": "2.10.0",
@@ -5586,7 +5605,6 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
"engines": {
"node": ">=10"
},
diff --git a/rogue-thi-app/package.json b/rogue-thi-app/package.json
index 54c5902f..36f0c91e 100644
--- a/rogue-thi-app/package.json
+++ b/rogue-thi-app/package.json
@@ -24,6 +24,7 @@
"@restart/hooks": "^0.4.7",
"bootstrap": "^4.6.1",
"cheerio": "^1.0.0-rc.10",
+ "deepl-node": "^1.10.2",
"dompurify": "^2.3.4",
"eslint-plugin-n": "^16.0.0",
"fetch-bypass-cors": "github:neuland-ingolstadt/fetch-bypass-cors#v1.0.3",
From ae16ba1aea660b3a7eb51f6146e54826b188265e Mon Sep 17 00:00:00 2001
From: Alexander Horn
Date: Sat, 8 Jul 2023 13:47:20 +0200
Subject: [PATCH 13/34] Migrate to new API (#295)
Co-authored-by: Philipp Opheys
---
Dockerfile | 4 +
rogue-thi-app/.env | 1 +
rogue-thi-app/.eslintrc.json | 3 +-
.../components/cards/TimetableCard.js | 14 +-
.../lib/backend-utils/calendar-utils.js | 27 +-
.../lib/backend-utils/library-utils.js | 27 ++
.../lib/backend-utils/rooms-utils.js | 14 +-
.../lib/backend-utils/timetable-utils.js | 43 ++-
rogue-thi-app/lib/backend/anonymous-api.js | 19 +-
.../lib/backend/authenticated-api.js | 78 +++--
rogue-thi-app/lib/date-utils.js | 16 +
rogue-thi-app/next.config.js | 5 +-
rogue-thi-app/package-lock.json | 319 ++++++++++--------
rogue-thi-app/package.json | 4 +-
rogue-thi-app/pages/calendar.js | 23 +-
rogue-thi-app/pages/library.js | 72 ++--
rogue-thi-app/pages/rooms/list.js | 10 +-
rogue-thi-app/pages/rooms/suggestions.js | 5 +-
rogue-thi-app/pages/timetable.js | 36 +-
rogue-thi-app/public/locales/de/calendar.json | 1 +
rogue-thi-app/public/locales/de/library.json | 1 +
rogue-thi-app/public/locales/en/calendar.json | 1 +
rogue-thi-app/public/locales/en/library.json | 1 +
23 files changed, 432 insertions(+), 292 deletions(-)
create mode 100644 rogue-thi-app/lib/backend-utils/library-utils.js
diff --git a/Dockerfile b/Dockerfile
index 8ebe6105..6716a2ee 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -49,9 +49,13 @@ WORKDIR /opt/next
ARG NEXT_PUBLIC_HACKERMAN_FLAGS
ARG NEXT_PUBLIC_ELECTION_URL
ARG NEXT_PUBLIC_GUEST_ONLY
+ARG NEXT_PUBLIC_THI_API_MODE
+ARG NEXT_PUBLIC_THI_API_KEY
ENV NEXT_PUBLIC_HACKERMAN_FLAGS $NEXT_PUBLIC_HACKERMAN_FLAGS
ENV NEXT_PUBLIC_ELECTION_URL $NEXT_PUBLIC_ELECTION_URL
ENV NEXT_PUBLIC_GUEST_ONLY $NEXT_PUBLIC_GUEST_ONLY
+ARG NEXT_PUBLIC_THI_API_MODE $NEXT_PUBLIC_THI_API_MODE
+ENV NEXT_PUBLIC_THI_API_KEY $NEXT_PUBLIC_THI_API_KEY
COPY rogue-thi-app/package.json rogue-thi-app/package-lock.json ./
# OpenSSL legacy provider needed to build node-forge
diff --git a/rogue-thi-app/.env b/rogue-thi-app/.env
index ccfceb8e..9b04306d 100644
--- a/rogue-thi-app/.env
+++ b/rogue-thi-app/.env
@@ -1,4 +1,5 @@
NEXT_PUBLIC_PROXY_URL=wss://proxy.neuland.app
+NEXT_PUBLIC_THI_API_HOST=hiplan.thi.de
NEXT_PUBLIC_IMPRINT_URL=https://assets.neuland.app/impressum-app.htm
NEXT_PUBLIC_PRIVACY_URL=https://assets.neuland.app/datenschutzerklaerung-app.pdf
NEXT_PUBLIC_GIT_URL=https://github.com/neuland-ingolstadt/THI-App
diff --git a/rogue-thi-app/.eslintrc.json b/rogue-thi-app/.eslintrc.json
index b7da8bcb..90fb6261 100644
--- a/rogue-thi-app/.eslintrc.json
+++ b/rogue-thi-app/.eslintrc.json
@@ -25,6 +25,7 @@
"import/no-anonymous-default-export": "off",
"sort-imports": ["warn", {
"allowSeparatedGroups": true
- }]
+ }],
+ "dot-notation": "off"
}
}
diff --git a/rogue-thi-app/components/cards/TimetableCard.js b/rogue-thi-app/components/cards/TimetableCard.js
index f1f3ba03..77203e11 100644
--- a/rogue-thi-app/components/cards/TimetableCard.js
+++ b/rogue-thi-app/components/cards/TimetableCard.js
@@ -75,16 +75,18 @@ export default function TimetableCard () {
return (
- {getTimetableEntryName(x).shortName} in {x.raum}
+ {getTimetableEntryName(x).shortName} in {x.rooms.join(', ')}
{text}
)
- })) ||
- (timetable && timetable.length === 0 &&
- {t('timetable.text.noLectures')} ) ||
- (timetableError &&
- {t('timetable.text.error')} )}
+ }))}
+ {(timetable && timetable.length === 0) &&
+ {t('timetable.text.noLectures')}
+ }
+ {(timetableError &&
+ {t('timetable.text.error')} )
+ }
diff --git a/rogue-thi-app/lib/backend-utils/calendar-utils.js b/rogue-thi-app/lib/backend-utils/calendar-utils.js
index c57aa178..cda42db0 100644
--- a/rogue-thi-app/lib/backend-utils/calendar-utils.js
+++ b/rogue-thi-app/lib/backend-utils/calendar-utils.js
@@ -1,5 +1,4 @@
import API from '../backend/authenticated-api'
-import { parse as parsePostgresArray } from 'postgres-array'
import rawCalendar from '../../data/calendar.json'
export const compileTime = new Date()
@@ -21,21 +20,17 @@ export async function loadExamList () {
return examList
// Modus 2 seems to be an indicator for "not real" exams like internships, which still got listed in API.getExams()
.filter((x) => x.modus !== '2')
- .map(x => {
- if (x.exm_date && x.exam_time) {
- const [, day, month, year] = x.exm_date.match(/(\d{1,})\.(\d{1,})\.(\d{4})/)
- x.date = new Date(`${year}-${month}-${day}T${x.exam_time}`)
- } else {
- x.date = null
- }
-
- x.anmeldung = new Date(x.anm_date + 'T' + x.anm_time)
- // hilfsmittel is returned as a string in postgres array syntax
- x.allowed_helpers = parsePostgresArray(x.hilfsmittel)
- .filter((v, i, a) => a.indexOf(v) === i)
-
- return x
- })
+ .map(exam => ({
+ name: exam.titel,
+ type: exam.pruefungs_art,
+ rooms: exam.exam_rooms,
+ seat: exam.exam_seat,
+ notes: exam.anmerkung,
+ examiners: exam.pruefer_namen,
+ date: new Date(exam.exam_ts),
+ enrollment: new Date(exam.anm_ts),
+ aids: exam.hilfsmittel?.filter((v, i, a) => a.indexOf(v) === i) || []
+ }))
// sort list in chronologically order
.sort((a, b) => a.date - b.date)
}
diff --git a/rogue-thi-app/lib/backend-utils/library-utils.js b/rogue-thi-app/lib/backend-utils/library-utils.js
new file mode 100644
index 00000000..e76aed67
--- /dev/null
+++ b/rogue-thi-app/lib/backend-utils/library-utils.js
@@ -0,0 +1,27 @@
+import API from '../backend/authenticated-api'
+import { combineDateTime } from '../date-utils'
+
+/**
+ * Converts the seat list for easier processing.
+ * @returns {object}
+ */
+export async function getFriendlyAvailableLibrarySeats () {
+ const available = await API.getAvailableLibrarySeats()
+ return available
+ .map(day => {
+ const date = day.date.substring(0, 10)
+ return {
+ date,
+ resource: day.resource.map(slot => {
+ const from = combineDateTime(date, slot.from)
+ const to = combineDateTime(date, slot.to)
+
+ return {
+ ...slot,
+ from,
+ to
+ }
+ })
+ }
+ })
+}
diff --git a/rogue-thi-app/lib/backend-utils/rooms-utils.js b/rogue-thi-app/lib/backend-utils/rooms-utils.js
index bb443adf..e03be262 100644
--- a/rogue-thi-app/lib/backend-utils/rooms-utils.js
+++ b/rogue-thi-app/lib/backend-utils/rooms-utils.js
@@ -70,7 +70,7 @@ export function getRoomOpenings (rooms, date) {
date = formatISODate(date)
const openings = {}
// get todays rooms
- rooms.filter(room => room.datum === date)
+ rooms.filter(room => room.datum.startsWith(date))
// flatten room types
.flatMap(room => room.rtypes)
// flatten time slots
@@ -83,12 +83,12 @@ export function getRoomOpenings (rooms, date) {
)
// flatten room list
.flatMap(stunde =>
- stunde.raeume.split(', ')
- .map(room => ({
+ stunde.raeume
+ .map(([,, room]) => ({
room,
type: stunde.type,
- from: new Date(date + 'T' + stunde.von),
- until: new Date(date + 'T' + stunde.bis)
+ from: new Date(stunde.von),
+ until: new Date(stunde.bis)
}))
)
// iterate over every room
@@ -168,7 +168,7 @@ export async function filterRooms (date, time, building = BUILDINGS_ALL, duratio
export async function searchRooms (beginDate, endDate, building = BUILDINGS_ALL) {
const data = await API.getFreeRooms(beginDate)
- const openings = getRoomOpenings(data.rooms, beginDate)
+ const openings = getRoomOpenings(data, beginDate)
return Object.keys(openings)
.flatMap(room =>
openings[room].map(opening => ({
@@ -259,7 +259,7 @@ async function getMajorityRoom () {
const week = getWeek(date)
const timetable = await getFriendlyTimetable(week[0], false)
- const rooms = timetable.map(x => x.raum)
+ const rooms = timetable.flatMap(x => x.rooms)
return mode(rooms)
}
diff --git a/rogue-thi-app/lib/backend-utils/timetable-utils.js b/rogue-thi-app/lib/backend-utils/timetable-utils.js
index 477cc76b..e30fa933 100644
--- a/rogue-thi-app/lib/backend-utils/timetable-utils.js
+++ b/rogue-thi-app/lib/backend-utils/timetable-utils.js
@@ -1,4 +1,5 @@
import API from '../backend/authenticated-api'
+import { combineDateTime } from '../date-utils'
import { getNextValidDate } from './rooms-utils'
/**
@@ -7,18 +8,18 @@ import { getNextValidDate } from './rooms-utils'
* @returns {object}
*/
export function getTimetableEntryName (item) {
- const match = item.veranstaltung.match(/^[A-Z]{2}\S*/)
+ const match = item.shortName.match(/^[A-Z]{2}\S*/)
if (match) {
const [shortName] = match
return {
- name: item.fach,
+ name: item.name,
shortName
}
} else {
// fallback for weird entries like
// "veranstaltung": "„Richtige Studienorganisation und Prüfungsplanung“_durchgeführt von CSS und SCS",
// "fach": "fiktiv für Raumbelegung der Verwaltung E",
- const name = `${item.veranstaltung} - ${item.fach}`
+ const name = `${item.shortName} - ${item.name}`
const shortName = name.length < 10 ? name : name.substr(0, 10) + '…'
return {
name,
@@ -76,24 +77,40 @@ export async function getFriendlyTimetable (date, detailed) {
const { timetable } = await API.getTimetable(date, detailed)
return timetable
+ .flatMap(day =>
+ Object.values(day.hours)
+ .flatMap(hours => hours.map(hour => ({ date: day.date, ...hour })))
+ )
.map(x => {
- // parse dates
- x.startDate = new Date(`${x.datum}T${x.von}`)
- x.endDate = new Date(`${x.datum}T${x.bis}`)
+ const startDate = combineDateTime(x.date, x.von)
+ const endDate = combineDateTime(x.date, x.bis)
// normalize room order
- if (x.raum) {
- x.rooms = x.raum
+ let rooms = []
+ if (x.details.raum) {
+ rooms = x.details.raum
.split(',')
.map(x => x.trim().toUpperCase())
.sort()
- x.raum = x.rooms.join(', ')
- } else {
- x.rooms = []
- x.raum = ''
}
- return x
+ return {
+ date: x.date,
+ startDate,
+ endDate,
+ name: x.details.fach,
+ shortName: x.details.veranstaltung,
+ rooms,
+ lecturer: x.details.dozent,
+ exam: x.details.pruefung,
+ course: x.details.stg,
+ studyGroup: x.details.stgru,
+ sws: x.details.sws,
+ ects: x.details.ectspoints,
+ objective: x.details.ziel,
+ contents: x.details.inhalt,
+ literature: x.details.literatur
+ }
})
.filter(x => x.endDate > date)
.sort((a, b) => a.startDate - b.startDate)
diff --git a/rogue-thi-app/lib/backend/anonymous-api.js b/rogue-thi-app/lib/backend/anonymous-api.js
index 29babf47..cad9e6d5 100644
--- a/rogue-thi-app/lib/backend/anonymous-api.js
+++ b/rogue-thi-app/lib/backend/anonymous-api.js
@@ -7,8 +7,9 @@ const CACHE_NAMESPACE = 'thi-api-client'
const CACHE_TTL = 10 * 60 * 1000
const ENDPOINT_MODE = process.env.NEXT_PUBLIC_THI_API_MODE || 'websocket-proxy'
-const ENDPOINT_HOST = 'hiplan.thi.de'
-const ENDPOINT_URL = '/webservice/production2/index.php'
+const API_KEY = process.env.NEXT_PUBLIC_THI_API_KEY
+const ENDPOINT_HOST = process.env.NEXT_PUBLIC_THI_API_HOST
+const ENDPOINT_URL = '/webservice/zits_s_40_test/index.php'
const PROXY_URL = process.env.NEXT_PUBLIC_PROXY_URL
const GIT_URL = process.env.NEXT_PUBLIC_GIT_URL
const USER_AGENT = `neuland.app/${packageInfo.version} (+${GIT_URL})`
@@ -98,14 +99,18 @@ export class AnonymousAPIClient {
})
}
+ const headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'X-API-KEY': API_KEY
+ }
+ if (ENDPOINT_MODE !== 'direct') {
+ headers['Host'] = ENDPOINT_HOST
+ headers['User-Agent'] = USER_AGENT
+ }
const resp = await this.connection.fetch(`https://${ENDPOINT_HOST}${ENDPOINT_URL}`, {
method: 'POST',
body: new URLSearchParams(params).toString(),
- headers: {
- Host: ENDPOINT_HOST,
- 'Content-Type': 'application/x-www-form-urlencoded',
- 'User-Agent': USER_AGENT
- }
+ headers
})
try {
return await resp.json()
diff --git a/rogue-thi-app/lib/backend/authenticated-api.js b/rogue-thi-app/lib/backend/authenticated-api.js
index 6ae90319..7ac8f1f6 100644
--- a/rogue-thi-app/lib/backend/authenticated-api.js
+++ b/rogue-thi-app/lib/backend/authenticated-api.js
@@ -67,11 +67,17 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
session,
...params
})
- if (res.status === 0) {
- return res
- } else {
+
+ // old status format
+ if (res.status !== 0) {
throw new APIError(res.status, res.data)
}
+ // new status format
+ if (res.data[0] !== 0) {
+ throw new APIError(res.data[0], res.data[1])
+ }
+
+ return res.data[1]
})
}
@@ -100,7 +106,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
format: 'json'
})
- return res.data[1]
+ return res
}
async getFaculty () {
@@ -127,10 +133,9 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
})
return {
- semester: res.data[1],
- holidays: res.data[2],
- events: res.data[2],
- timetable: res.data[3]
+ semester: res[0],
+ holidays: res[1],
+ timetable: res[2]
}
} catch (e) {
// when the user did not select any classes, the timetable returns 'Query not possible'
@@ -149,10 +154,11 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
const res = await this.requestCached(KEY_GET_EXAMS, {
service: 'thiapp',
method: 'exams',
- format: 'json'
+ format: 'json',
+ modus: '1' // what does this mean? if only we knew
})
- return res.data[1]
+ return res
} catch (e) {
// when you have no exams the API sometimes returns "No exam data available"
if (e.data === 'No exam data available' || e.data === 'Query not possible') {
@@ -170,7 +176,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
format: 'json'
})
- return res.data[1]
+ return res
}
async getMensaPlan () {
@@ -180,7 +186,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
format: 'json'
})
- return res.data
+ return res
}
/**
@@ -197,7 +203,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
year: 1900 + date.getYear()
})
- return res.data[1]
+ return res
}
async getCampusParkingData () {
@@ -207,7 +213,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
format: 'json'
})
- return res.data
+ return res
}
async getPersonalLecturers () {
@@ -217,7 +223,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
format: 'json'
})
- return res.data[1]
+ return res
}
/**
@@ -234,7 +240,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
to
})
- return res.data[1]
+ return res
}
async getLibraryReservations () {
@@ -243,13 +249,11 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
service: 'thiapp',
method: 'reservations',
type: 1,
- subtype: 1,
- cmd: 'getreservation',
- data: '',
+ cmd: 'getreservations',
format: 'json'
})
- return res.data[1]
+ return res[1]
} catch (e) {
// as of 2021-06 the API returns "Service not available" when the user has no reservations
// thus we dont alert the error here, but just silently set the reservations to none
@@ -262,17 +266,26 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
}
async getAvailableLibrarySeats () {
- const res = await this.requestAuthenticated({
- service: 'thiapp',
- method: 'reservations',
- type: 1,
- subtype: 1,
- cmd: 'getavailabilities',
- data: '',
- format: 'json'
- })
+ try {
+ const res = await this.requestAuthenticated({
+ service: 'thiapp',
+ method: 'reservations',
+ type: 1,
+ subtype: 1,
+ cmd: 'getavailabilities',
+ format: 'json'
+ })
- return res.data[1]
+ return res[1]
+ } catch (e) {
+ // Unbekannter Fehler means the user has already reserved a spot
+ // and can not reserve additional ones
+ if (e.data === 'Unbekannter Fehler') {
+ return []
+ } else {
+ throw e
+ }
+ }
}
/**
@@ -292,10 +305,11 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
to: end,
place
}),
+ dblslots: 0,
format: 'json'
})
- return res.data[1][0]
+ return res[0]
}
/**
@@ -332,7 +346,7 @@ export class AuthenticatedAPIClient extends AnonymousAPIClient {
format: 'json'
})
- return res.data[1]
+ return res
}
}
diff --git a/rogue-thi-app/lib/date-utils.js b/rogue-thi-app/lib/date-utils.js
index cf71ae33..325bd3b7 100644
--- a/rogue-thi-app/lib/date-utils.js
+++ b/rogue-thi-app/lib/date-utils.js
@@ -301,6 +301,22 @@ export function isSameDay (a, b) {
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
}
+/**
+ * Combines the date from one Date object and the time from another Date object
+ * @param {Date} date
+ * @param {Date} time
+ * @returns {Date}
+ */
+export function combineDateTime (date, time) {
+ date = new Date(date)
+ time = new Date(time)
+ date.setHours(time.getHours())
+ date.setMinutes(time.getMinutes())
+ date.setSeconds(time.getSeconds())
+ date.setMilliseconds(time.getMilliseconds())
+ return date
+}
+
function t (...args) {
return i18n.t(...args, { ns: 'common' })
}
diff --git a/rogue-thi-app/next.config.js b/rogue-thi-app/next.config.js
index 92ddda3c..0771ca5d 100644
--- a/rogue-thi-app/next.config.js
+++ b/rogue-thi-app/next.config.js
@@ -28,7 +28,8 @@ const permissionPolicyFeatures = [
]
const isDev = process.env.NODE_ENV === 'development'
-const DEEPL_ENDPOINT = process.env.NEXT_PUBLIC_DEEPL_ENDPOINT || ''
+const PROXY_URL = process.env.NEXT_PUBLIC_PROXY_URL
+const API_URL = 'https://' + process.env.NEXT_PUBLIC_THI_API_HOST
module.exports = {
i18n,
@@ -73,7 +74,7 @@ module.exports = {
value: `default-src 'none';
img-src 'self' data: https://tile.openstreetmap.org;
font-src 'self';
- connect-src 'self' wss://proxy.neuland.app ${DEEPL_ENDPOINT};
+ connect-src 'self' ${PROXY_URL} ${API_URL};
style-src 'self' 'unsafe-inline';
script-src 'self'${isDev ? ' \'unsafe-eval\'' : ''};
manifest-src 'self';
diff --git a/rogue-thi-app/package-lock.json b/rogue-thi-app/package-lock.json
index 91db9897..28a00e89 100644
--- a/rogue-thi-app/package-lock.json
+++ b/rogue-thi-app/package-lock.json
@@ -18,7 +18,6 @@
"cheerio": "^1.0.0-rc.10",
"deepl-node": "^1.10.2",
"dompurify": "^2.3.4",
- "eslint-plugin-n": "^16.0.0",
"fetch-bypass-cors": "github:neuland-ingolstadt/fetch-bypass-cors#v1.0.3",
"fetch-cookie": "^2.0.1",
"i18next": "^22.5.0",
@@ -29,7 +28,6 @@
"next-i18next": "^13.2.2",
"node-fetch": "^3.2.3",
"pdf-parse": "^1.1.1",
- "postgres-array": "^3.0.1",
"prop-types": "^15.8.0",
"react": "^17.0.2",
"react-bootstrap": "^1.6.4",
@@ -52,7 +50,7 @@
"eslint-config-next": "^12.1.0",
"eslint-config-standard": "^17.0.0-1",
"eslint-plugin-import": "^2.25.4",
- "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.3"
}
@@ -170,32 +168,11 @@
"@capacitor/core": "^4.7.0"
}
},
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
- "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
- "dependencies": {
- "eslint-visitor-keys": "^3.3.0"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
- "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
"node_modules/@eslint/eslintrc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
"integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
+ "dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@@ -275,6 +252,7 @@
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+ "dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
@@ -288,6 +266,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
"engines": {
"node": ">=12.22"
},
@@ -299,7 +278,8 @@
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
- "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA=="
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
},
"node_modules/@ionic/cli-framework-output": {
"version": "2.2.5",
@@ -638,6 +618,7 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -650,6 +631,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
"engines": {
"node": ">= 8"
}
@@ -658,6 +640,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -896,6 +879,7 @@
"version": "8.8.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz",
"integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==",
+ "dev": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -907,6 +891,7 @@
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
@@ -915,6 +900,7 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -930,6 +916,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -938,6 +925,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -963,7 +951,8 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
},
"node_modules/aria-query": {
"version": "5.1.3",
@@ -1136,7 +1125,8 @@
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -1215,6 +1205,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -1244,6 +1235,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
"integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
+ "dev": true,
"dependencies": {
"semver": "^7.0.0"
}
@@ -1265,6 +1257,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
"engines": {
"node": ">=6"
}
@@ -1288,6 +1281,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -1390,6 +1384,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1400,7 +1395,8 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
@@ -1425,7 +1421,8 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
},
"node_modules/core-js": {
"version": "3.30.2",
@@ -1441,6 +1438,7 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -1499,6 +1497,7 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -1542,7 +1541,8 @@
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
},
"node_modules/deepl-node": {
"version": "1.10.2",
@@ -1615,6 +1615,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
"dependencies": {
"esutils": "^2.0.2"
},
@@ -1836,6 +1837,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
"engines": {
"node": ">=10"
},
@@ -1847,6 +1849,7 @@
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.32.0.tgz",
"integrity": "sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ==",
+ "dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.4.1",
"@humanwhocodes/config-array": "^0.11.8",
@@ -2036,22 +2039,47 @@
"ms": "^2.1.1"
}
},
- "node_modules/eslint-plugin-es-x": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.1.0.tgz",
- "integrity": "sha512-AhiaF31syh4CCQ+C5ccJA0VG6+kJK8+5mXKKE7Qs1xcPRg02CDPOj3mWlQxuWS/AYtg7kxrDNgW9YW3vc0Q+Mw==",
+ "node_modules/eslint-plugin-es": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz",
+ "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==",
+ "dev": true,
"dependencies": {
- "@eslint-community/eslint-utils": "^4.1.2",
- "@eslint-community/regexpp": "^4.5.0"
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
},
"engines": {
- "node": "^14.18.0 || >=16.0.0"
+ "node": ">=8.10.0"
},
"funding": {
- "url": "https://github.com/sponsors/ota-meshi"
+ "url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
- "eslint": ">=8"
+ "eslint": ">=4.19.1"
+ }
+ },
+ "node_modules/eslint-plugin-es/node_modules/eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
}
},
"node_modules/eslint-plugin-import": {
@@ -2153,21 +2181,22 @@
}
},
"node_modules/eslint-plugin-n": {
- "version": "16.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.0.1.tgz",
- "integrity": "sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA==",
+ "version": "15.7.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz",
+ "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==",
+ "dev": true,
"dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
"builtins": "^5.0.1",
- "eslint-plugin-es-x": "^7.1.0",
- "ignore": "^5.2.4",
- "is-core-module": "^2.12.1",
+ "eslint-plugin-es": "^4.1.0",
+ "eslint-utils": "^3.0.0",
+ "ignore": "^5.1.1",
+ "is-core-module": "^2.11.0",
"minimatch": "^3.1.2",
- "resolve": "^1.22.2",
- "semver": "^7.5.3"
+ "resolve": "^1.22.1",
+ "semver": "^7.3.8"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">=12.22.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
@@ -2176,78 +2205,6 @@
"eslint": ">=7.0.0"
}
},
- "node_modules/eslint-plugin-node": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
- "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==",
- "dev": true,
- "dependencies": {
- "eslint-plugin-es": "^3.0.0",
- "eslint-utils": "^2.0.0",
- "ignore": "^5.1.1",
- "minimatch": "^3.0.4",
- "resolve": "^1.10.1",
- "semver": "^6.1.0"
- },
- "engines": {
- "node": ">=8.10.0"
- },
- "peerDependencies": {
- "eslint": ">=5.16.0"
- }
- },
- "node_modules/eslint-plugin-node/node_modules/eslint-plugin-es": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
- "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==",
- "dev": true,
- "dependencies": {
- "eslint-utils": "^2.0.0",
- "regexpp": "^3.0.0"
- },
- "engines": {
- "node": ">=8.10.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/mysticatea"
- },
- "peerDependencies": {
- "eslint": ">=4.19.1"
- }
- },
- "node_modules/eslint-plugin-node/node_modules/eslint-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
- "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
- "dev": true,
- "dependencies": {
- "eslint-visitor-keys": "^1.1.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/mysticatea"
- }
- },
- "node_modules/eslint-plugin-node/node_modules/eslint-visitor-keys": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
- "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/eslint-plugin-node/node_modules/semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true,
- "bin": {
- "semver": "bin/semver.js"
- }
- },
"node_modules/eslint-plugin-promise": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
@@ -2343,6 +2300,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
+ "dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@@ -2355,6 +2313,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
"dependencies": {
"eslint-visitor-keys": "^2.0.0"
},
@@ -2372,6 +2331,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
"engines": {
"node": ">=10"
}
@@ -2380,6 +2340,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
"integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
+ "dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@@ -2388,6 +2349,7 @@
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
"integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
+ "dev": true,
"dependencies": {
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
@@ -2404,6 +2366,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
+ "dev": true,
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -2415,6 +2378,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -2426,6 +2390,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
"engines": {
"node": ">=4.0"
}
@@ -2434,6 +2399,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2441,7 +2407,8 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
},
"node_modules/fast-glob": {
"version": "3.2.12",
@@ -2474,17 +2441,20 @@
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
},
"node_modules/fastq": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
@@ -2541,6 +2511,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
"dependencies": {
"flat-cache": "^3.0.4"
},
@@ -2563,6 +2534,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
@@ -2578,6 +2550,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
"dependencies": {
"flatted": "^3.1.0",
"rimraf": "^3.0.2"
@@ -2589,7 +2562,8 @@
"node_modules/flatted": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
- "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.2",
@@ -2685,7 +2659,8 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
@@ -2703,7 +2678,8 @@
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
},
"node_modules/function.prototype.name": {
"version": "1.1.5",
@@ -2766,6 +2742,7 @@
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -2785,6 +2762,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -2796,6 +2774,7 @@
"version": "13.19.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
"integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
+ "dev": true,
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -2862,12 +2841,14 @@
"node_modules/grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
- "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true
},
"node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
"dependencies": {
"function-bind": "^1.1.1"
},
@@ -2888,6 +2869,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -3064,6 +3046,7 @@
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
"engines": {
"node": ">= 4"
}
@@ -3077,6 +3060,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -3092,6 +3076,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
"engines": {
"node": ">=0.8.19"
}
@@ -3100,6 +3085,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@@ -3108,7 +3094,8 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
},
"node_modules/ini": {
"version": "3.0.1",
@@ -3226,6 +3213,7 @@
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
"integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
+ "dev": true,
"dependencies": {
"has": "^1.0.3"
},
@@ -3339,6 +3327,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -3484,7 +3473,8 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
},
"node_modules/jquery": {
"version": "3.6.3",
@@ -3496,6 +3486,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
+ "dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
@@ -3510,6 +3501,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@@ -3520,12 +3512,14 @@
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
},
"node_modules/json5": {
"version": "1.0.2",
@@ -3602,6 +3596,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -3614,6 +3609,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
"dependencies": {
"p-locate": "^5.0.0"
},
@@ -3627,7 +3623,8 @@
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
},
"node_modules/loglevel": {
"version": "1.8.1",
@@ -3656,6 +3653,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -3708,6 +3706,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -3827,7 +3826,8 @@
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
},
"node_modules/next": {
"version": "12.3.4",
@@ -4108,6 +4108,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
"dependencies": {
"wrappy": "1"
}
@@ -4133,6 +4134,7 @@
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@@ -4149,6 +4151,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -4163,6 +4166,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
@@ -4177,6 +4181,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -4211,6 +4216,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -4219,6 +4225,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4227,6 +4234,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -4234,7 +4242,8 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
},
"node_modules/path-type": {
"version": "4.0.0",
@@ -4334,18 +4343,11 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/postgres-array": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.1.tgz",
- "integrity": "sha512-h7i53Dw2Yq3a1uuZ6lbVFAkvMMwssJ8jkzeAg0XaZm1XIFF/t/s+tockdqbWTymyEm07dVenOQbFisEi+kj8uA==",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
"engines": {
"node": ">= 0.8.0"
}
@@ -4421,6 +4423,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -4754,6 +4757,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true,
"engines": {
"node": ">=8"
},
@@ -4770,6 +4774,7 @@
"version": "1.22.2",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
"integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+ "dev": true,
"dependencies": {
"is-core-module": "^2.11.0",
"path-parse": "^1.0.7",
@@ -4786,6 +4791,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
"engines": {
"node": ">=4"
}
@@ -4794,6 +4800,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -4803,6 +4810,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -4817,6 +4825,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -4904,6 +4913,7 @@
"version": "7.5.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
"integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
+ "dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -4928,6 +4938,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -4939,6 +4950,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
"engines": {
"node": ">=8"
}
@@ -5104,6 +5116,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -5124,6 +5137,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
"engines": {
"node": ">=8"
},
@@ -5154,6 +5168,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -5165,6 +5180,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -5192,7 +5208,8 @@
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
},
"node_modules/through2": {
"version": "4.0.2",
@@ -5287,6 +5304,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
"dependencies": {
"prelude-ls": "^1.2.1"
},
@@ -5298,6 +5316,7 @@
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
"engines": {
"node": ">=10"
},
@@ -5384,6 +5403,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
"dependencies": {
"punycode": "^2.1.0"
}
@@ -5444,6 +5464,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"dependencies": {
"isexe": "^2.0.0"
},
@@ -5513,6 +5534,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5537,7 +5559,8 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
},
"node_modules/xml-js": {
"version": "1.6.11",
@@ -5589,7 +5612,8 @@
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
},
"node_modules/yauzl": {
"version": "2.10.0",
@@ -5605,6 +5629,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
"engines": {
"node": ">=10"
},
diff --git a/rogue-thi-app/package.json b/rogue-thi-app/package.json
index 36f0c91e..a4bd2457 100644
--- a/rogue-thi-app/package.json
+++ b/rogue-thi-app/package.json
@@ -26,7 +26,6 @@
"cheerio": "^1.0.0-rc.10",
"deepl-node": "^1.10.2",
"dompurify": "^2.3.4",
- "eslint-plugin-n": "^16.0.0",
"fetch-bypass-cors": "github:neuland-ingolstadt/fetch-bypass-cors#v1.0.3",
"fetch-cookie": "^2.0.1",
"i18next": "^22.5.0",
@@ -37,7 +36,6 @@
"next-i18next": "^13.2.2",
"node-fetch": "^3.2.3",
"pdf-parse": "^1.1.1",
- "postgres-array": "^3.0.1",
"prop-types": "^15.8.0",
"react": "^17.0.2",
"react-bootstrap": "^1.6.4",
@@ -60,7 +58,7 @@
"eslint-config-next": "^12.1.0",
"eslint-config-standard": "^17.0.0-1",
"eslint-plugin-import": "^2.25.4",
- "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.29.3"
}
diff --git a/rogue-thi-app/pages/calendar.js b/rogue-thi-app/pages/calendar.js
index 5f04a51d..47a94e74 100644
--- a/rogue-thi-app/pages/calendar.js
+++ b/rogue-thi-app/pages/calendar.js
@@ -82,20 +82,19 @@ export default function Calendar () {
setFocusedExam(null)}>
- {focusedExam && focusedExam.titel}
+ {focusedExam && focusedExam.name}
- {t('calendar.modals.exams.type')} : {focusedExam && focusedExam.pruefungs_art}
- {t('calendar.modals.exams.room')} : {focusedExam && (focusedExam.exam_rooms || 'TBD')}
- {t('calendar.modals.exams.seat')} : {focusedExam && (focusedExam.exam_seat || 'TBD')}
+ {t('calendar.modals.exams.type')} : {focusedExam && focusedExam.type}
+ {t('calendar.modals.exams.room')} : {focusedExam && (focusedExam.rooms || 'TBD')}
+ {t('calendar.modals.exams.seat')} : {focusedExam && (focusedExam.seat || 'TBD')}
{t('calendar.modals.exams.date')} : {focusedExam && (focusedExam.date ? formatFriendlyDateTime(focusedExam.date) : 'TBD')}
- {t('calendar.modals.exams.notes')} : {focusedExam && focusedExam.anmerkung}
- {t('calendar.modals.exams.examiner')} : {focusedExam && focusedExam.pruefer_namen}
- {t('calendar.modals.exams.courseOfStudies')} : {focusedExam && focusedExam.stg}
- {t('calendar.modals.exams.registerDate')} : {focusedExam && formatFriendlyDateTime(focusedExam.anmeldung)}
+ {t('calendar.modals.exams.notes')} : {focusedExam && (focusedExam.notes || t('calendar.modals.exams.none'))}
+ {t('calendar.modals.exams.examiner')} : {focusedExam && focusedExam.examiners.join('; ')}
+ {t('calendar.modals.exams.registerDate')} : {focusedExam && formatFriendlyDateTime(focusedExam.enrollment)}
{t('calendar.modals.exams.tools')} :
- {focusedExam && focusedExam.allowed_helpers.map((helper, i) =>
+ {focusedExam && focusedExam.aids.map((helper, i) =>
{helper} )}
@@ -153,7 +152,7 @@ export default function Calendar () {
{exams && exams.map((item, idx) =>
setFocusedExam(item)}>
- {item.titel} ({item.stg})
+ {item.name}
{item.date && <>
@@ -161,8 +160,8 @@ export default function Calendar () {
{' '}({formatFriendlyRelativeTime(item.date)})
>}
- {t('calendar.modals.exams.room')}: {item.exam_rooms || 'TBD'}
- {item.exam_seat && `${t('calendar.modals.exams.seat')}: ${item.exam_seat}`}
+ {t('calendar.modals.exams.room')}: {item.rooms || 'TBD'}
+ {item.seat && `${t('calendar.modals.exams.seat')}: ${item.seat}`}
diff --git a/rogue-thi-app/pages/library.js b/rogue-thi-app/pages/library.js
index 19aa689d..a02e4e08 100644
--- a/rogue-thi-app/pages/library.js
+++ b/rogue-thi-app/pages/library.js
@@ -16,8 +16,9 @@ import AppNavbar from '../components/page/AppNavbar'
import AppTabbar from '../components/page/AppTabbar'
import { NoSessionError, UnavailableSessionError } from '../lib/backend/thi-session-handler'
-import { formatFriendlyTime, formatNearDate } from '../lib/date-utils'
+import { formatFriendlyDate, formatFriendlyTime, formatNearDate } from '../lib/date-utils'
import API from '../lib/backend/authenticated-api'
+import { getFriendlyAvailableLibrarySeats } from '../lib/backend-utils/library-utils'
import styles from '../styles/Library.module.css'
@@ -30,7 +31,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
*/
export default function Library () {
const [reservations, setReservations] = useState(null)
- const [available, setAvailable] = useState([])
+ const [available, setAvailable] = useState(null)
const [reservationDay, setReservationDay] = useState(false)
const [reservationTime, setReservationTime] = useState(false)
const [reservationRoom, setReservationRoom] = useState(1)
@@ -42,7 +43,7 @@ export default function Library () {
* Fetches and displays the reservation data.
*/
async function refreshData () {
- const available = await API.getAvailableLibrarySeats()
+ const available = await getFriendlyAvailableLibrarySeats()
setAvailable(available)
const response = await API.getLibraryReservations()
@@ -77,8 +78,9 @@ export default function Library () {
await API.addLibraryReservation(
reservationRoom,
reservationDay.date,
- reservationTime.from,
- reservationTime.to,
+ // this needs to be de-DE regardless of the users locale
+ reservationTime.from.toLocaleTimeString('de-DE', { timeStyle: 'short' }),
+ reservationTime.to.toLocaleTimeString('de-DE', { timeStyle: 'short' }),
reservationSeat
)
await refreshData()
@@ -101,28 +103,44 @@ export default function Library () {
load()
}, [router])
+ /**
+ * Returns a list of available rooms where are more than 0 seats available.
+ * @returns {Array} List of available rooms
+ **/
+ function getAvailableRooms () {
+ return Object.entries(reservationTime.resources).map(([roomId, room], idx) => [roomId, room, idx]).filter(([, room]) => room.num_seats > 0)
+ }
+
+ /**
+ * Returns the first available room.
+ * @returns {string} Room ID
+ * */
+ function getFirstAvailableRoom () {
+ return getAvailableRooms()[0][0][0]
+ }
+
return (
-
+ setReservationRoom(getFirstAvailableRoom())}>
{t('library.modal.title')}
- {t('library.modal.details.day')}: {reservationDay && reservationDay.date}
- {t('library.modal.details.start')}: {reservationTime && reservationTime.from}
- {t('library.modal.details.end')}: {reservationTime && reservationTime.to}
+ {t('library.modal.details.day')}: {reservationDay && formatFriendlyDate(reservationDay.date)}
+ {t('library.modal.details.start')}: {reservationTime && formatFriendlyTime(reservationTime.from)}
+ {t('library.modal.details.end')}: {reservationTime && formatFriendlyTime(reservationTime.to)}
{t('library.modal.details.location')}:
setReservationRoom(event.target.value)}>
- {reservationTime && Object.entries(reservationTime.resources).map(([roomId, room], idx) =>
-
- {room.room_name}
-
- )}
+ {reservationTime && getAvailableRooms().map(([roomId, room, idx]) =>
+
+ {room.room_name}
+
+ )}
@@ -189,22 +207,25 @@ export default function Library () {
{t('library.availableSeats')}
- 0}>
+
{available && available.map((day, i) =>
day.resource.map((time, j) =>
-
- {
- setReservationDay(day)
- setReservationTime(time)
- }}>{t('library.actions.reserve')}
+ {Object.values(time.resources).reduce((acc, room) => acc + room.num_seats, 0) > 0 &&
+ {
+ setReservationDay(day)
+ setReservationTime(time)
+ }}>
+ {t('library.actions.reserve')}
+
+ }
- {formatNearDate(new Date(day.date + 'T' + time.from))}
+ {formatNearDate(time.from)}
{', '}
- {formatFriendlyTime(new Date(day.date + 'T' + time.from))}
+ {formatFriendlyTime(time.from)}
{' - '}
- {formatFriendlyTime(new Date(day.date + 'T' + time.to))}
+ {formatFriendlyTime(time.to)}
{t('library.details.seatsAvailable', {
@@ -215,6 +236,11 @@ export default function Library () {
)
)}
+ {available && available.length === 0 &&
+
+ {t('library.details.noMoreReservations')}
+
+ }
diff --git a/rogue-thi-app/pages/rooms/list.js b/rogue-thi-app/pages/rooms/list.js
index 3c5ab976..5e358697 100644
--- a/rogue-thi-app/pages/rooms/list.js
+++ b/rogue-thi-app/pages/rooms/list.js
@@ -48,24 +48,24 @@ export default function RoomList () {
const now = new Date()
const data = await API.getFreeRooms(now)
- const days = data.rooms.map(day => {
+ const days = data.map(day => {
const result = {}
result.date = new Date(day.datum)
result.hours = {}
day.rtypes.forEach(roomType => Object.entries(roomType.stunden).forEach(([hIndex, hour]) => {
- const to = new Date(day.datum + 'T' + hour.bis)
+ const to = new Date(hour.bis)
if (to < now) { return }
if (!result.hours[hIndex]) {
result.hours[hIndex] = {
- from: new Date(day.datum + 'T' + hour.von),
- to: new Date(day.datum + 'T' + hour.bis),
+ from: new Date(hour.von),
+ to: new Date(hour.bis),
roomTypes: {}
}
}
- result.hours[hIndex].roomTypes[roomType.raumtyp] = hour.raeume.split(', ')
+ result.hours[hIndex].roomTypes[roomType.raumtyp] = hour.raeume.map(([,, room]) => room)
}))
return result
diff --git a/rogue-thi-app/pages/rooms/suggestions.js b/rogue-thi-app/pages/rooms/suggestions.js
index 8552be33..5ee7252a 100644
--- a/rogue-thi-app/pages/rooms/suggestions.js
+++ b/rogue-thi-app/pages/rooms/suggestions.js
@@ -204,8 +204,11 @@ export default function RoomSearch () {
*/
function DurationButton ({ duration }) {
const variant = (localStorage.getItem('suggestion-duration') ?? `${SUGGESTION_DURATION_PRESET}`) === `${duration}` ? 'primary' : 'outline-primary'
- return {
+ return {
localStorage.setItem('suggestion-duration', duration)
+
+ const suggestions = await getSuggestions()
+ setSuggestions(suggestions)
} }>
{duration}
diff --git a/rogue-thi-app/pages/timetable.js b/rogue-thi-app/pages/timetable.js
index 244271cb..24632318 100644
--- a/rogue-thi-app/pages/timetable.js
+++ b/rogue-thi-app/pages/timetable.js
@@ -48,13 +48,13 @@ export const getStaticProps = async ({ locale }) => ({
function groupTimetableEntries (timetable) {
// get all available dates and remove duplicates
const dates = timetable
- .map(x => x.datum)
+ .map(x => x.date)
.filter((v, i, a) => a.indexOf(v) === i)
// get events for each date
const groups = dates.map(date => ({
date,
- items: timetable.filter(x => x.datum === date)
+ items: timetable.filter(x => x.date === date)
}))
return groups
@@ -205,7 +205,9 @@ export default function Timetable () {
{item.rooms.map((room, i) => /^[A-Z](G|[0-9E]\.)?\d*$/.test(room)
- ?
e.stopPropagation()}>{room}
+ ?
+
e.stopPropagation()}>{room}
+
:
{room}
)}
@@ -341,41 +343,41 @@ export default function Timetable () {
{t('timetable.modals.lectureDetails.general')}
- {t('timetable.modals.lectureDetails.lecturer')} : {focusedEntry && focusedEntry.dozent}
+ {t('timetable.modals.lectureDetails.lecturer')} : {focusedEntry && focusedEntry.lecturer}
{t('timetable.modals.lectureDetails.abbreviation')} : {focusedEntry && getTimetableEntryName(focusedEntry).shortName}
- {t('timetable.modals.lectureDetails.exam')} : {focusedEntry && focusedEntry.pruefung}
- {t('timetable.modals.lectureDetails.courseOfStudies')} : {focusedEntry && focusedEntry.stg}
- {t('timetable.modals.lectureDetails.studyGroup')} : {focusedEntry && focusedEntry.stgru}
+ {t('timetable.modals.lectureDetails.exam')} : {focusedEntry && focusedEntry.exam}
+ {t('timetable.modals.lectureDetails.courseOfStudies')} : {focusedEntry && focusedEntry.course}
+ {t('timetable.modals.lectureDetails.studyGroup')} : {focusedEntry && focusedEntry.studyGroup}
{t('timetable.modals.lectureDetails.semesterWeeklyHours')} : {focusedEntry && focusedEntry.sws}
- {t('timetable.modals.lectureDetails.ects')} : {focusedEntry && focusedEntry.ectspoints}
+ {t('timetable.modals.lectureDetails.ects')} : {focusedEntry && focusedEntry.ects}
{t('timetable.modals.lectureDetails.goal')}
- {focusedEntry && focusedEntry.ziel && (
-
+ {focusedEntry && focusedEntry.objective && (
+
)}
- {focusedEntry && !focusedEntry.ziel && (
+ {focusedEntry && !focusedEntry.objective && (
{t('timetable.modals.lectureDetails.notSpecified')}
)}
{t('timetable.modals.lectureDetails.content')}
- {focusedEntry && focusedEntry.inhalt && (
-
+ {focusedEntry && focusedEntry.contents && (
+
)}
- {focusedEntry && !focusedEntry.inhalt && (
+ {focusedEntry && !focusedEntry.contents && (
{t('timetable.modals.lectureDetails.notSpecified')}
)}
{t('timetable.modals.lectureDetails.literature')}
- {focusedEntry && focusedEntry.literatur && (
-
+ {focusedEntry && focusedEntry.literature && (
+
)}
- {focusedEntry && !focusedEntry.literatur && (
+ {focusedEntry && !focusedEntry.literature && (
{t('timetable.modals.lectureDetails.notSpecified')}
)}
diff --git a/rogue-thi-app/public/locales/de/calendar.json b/rogue-thi-app/public/locales/de/calendar.json
index 5f155bfc..b8f63a85 100644
--- a/rogue-thi-app/public/locales/de/calendar.json
+++ b/rogue-thi-app/public/locales/de/calendar.json
@@ -14,6 +14,7 @@
"courseOfStudies": "Studiengang",
"registerDate": "Angemeldet",
"tools": "Hilfsmittel",
+ "none": "keine",
"actions": {
"close": "Schließen"
}
diff --git a/rogue-thi-app/public/locales/de/library.json b/rogue-thi-app/public/locales/de/library.json
index 3c8c627b..4799fd81 100644
--- a/rogue-thi-app/public/locales/de/library.json
+++ b/rogue-thi-app/public/locales/de/library.json
@@ -10,6 +10,7 @@
"details": {
"seatsAvailable": "{{available}} / {{total}} verfügbar",
"noReservations": "Du hast keine Reservierungen",
+ "noMoreReservations": "Du kannst keine weiteren Plätze reservieren",
"reservationDetails": "{{category}} , Platz {{seat}}, Reservierung {{reservation_id}}"
},
"modal": {
diff --git a/rogue-thi-app/public/locales/en/calendar.json b/rogue-thi-app/public/locales/en/calendar.json
index c484ecd2..bee72686 100644
--- a/rogue-thi-app/public/locales/en/calendar.json
+++ b/rogue-thi-app/public/locales/en/calendar.json
@@ -14,6 +14,7 @@
"courseOfStudies": "Course of Studies",
"registerDate": "Registered",
"tools": "Tools",
+ "none": "none",
"actions": {
"close": "Close"
}
diff --git a/rogue-thi-app/public/locales/en/library.json b/rogue-thi-app/public/locales/en/library.json
index 10015c7a..9d64a520 100644
--- a/rogue-thi-app/public/locales/en/library.json
+++ b/rogue-thi-app/public/locales/en/library.json
@@ -10,6 +10,7 @@
"details": {
"seatsAvailable": "{{available}} / {{total}} available",
"noReservations": "You have no reservations",
+ "noMoreReservations": "You can not reserve any more seats",
"reservationDetails": "{{category}} , Seat {{seat}}, Reservation {{reservation_id}}"
},
"modal": {
From 642cf20b103ed9429fee576ea772d88578b56635 Mon Sep 17 00:00:00 2001
From: Alexander Horn
Date: Sun, 9 Jul 2023 13:46:19 +0200
Subject: [PATCH 14/34] Fix room 0 being displayed (#305)
---
rogue-thi-app/lib/backend-utils/rooms-utils.js | 6 ++++--
rogue-thi-app/pages/rooms/list.js | 5 +++--
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/rogue-thi-app/lib/backend-utils/rooms-utils.js b/rogue-thi-app/lib/backend-utils/rooms-utils.js
index e03be262..8179a2a5 100644
--- a/rogue-thi-app/lib/backend-utils/rooms-utils.js
+++ b/rogue-thi-app/lib/backend-utils/rooms-utils.js
@@ -9,6 +9,7 @@ const IGNORE_GAPS = 15
export const BUILDINGS = ['A', 'B', 'BN', 'C', 'CN', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'M', 'P', 'W', 'Z']
export const BUILDINGS_ALL = 'Alle'
+export const ROOMS_ALL = 'Alle'
export const DURATION_PRESET = '01:00'
export const SUGGESTION_DURATION_PRESET = 90
@@ -85,7 +86,8 @@ export function getRoomOpenings (rooms, date) {
.flatMap(stunde =>
stunde.raeume
.map(([,, room]) => ({
- room,
+ // 0 indicates that every room is free
+ room: room === 0 ? ROOMS_ALL : room,
type: stunde.type,
from: new Date(stunde.von),
until: new Date(stunde.bis)
@@ -284,7 +286,7 @@ export function getTranslatedRoomFunction (roomFunction) {
*/
export function getTranslatedRoomName (room) {
switch (room) {
- case 'alle Räume':
+ case ROOMS_ALL:
return i18n.t('rooms.allRooms', { ns: 'common' })
default:
return room
diff --git a/rogue-thi-app/pages/rooms/list.js b/rogue-thi-app/pages/rooms/list.js
index 5e358697..a4134249 100644
--- a/rogue-thi-app/pages/rooms/list.js
+++ b/rogue-thi-app/pages/rooms/list.js
@@ -14,6 +14,7 @@ import AppNavbar from '../../components/page/AppNavbar'
import AppTabbar from '../../components/page/AppTabbar'
import { NoSessionError, UnavailableSessionError } from '../../lib/backend/thi-session-handler'
+import { ROOMS_ALL, getTranslatedRoomName } from '../../lib/backend-utils/rooms-utils'
import { formatFriendlyTime, formatNearDate } from '../../lib/date-utils'
import API from '../../lib/backend/authenticated-api'
@@ -65,7 +66,7 @@ export default function RoomList () {
}
}
- result.hours[hIndex].roomTypes[roomType.raumtyp] = hour.raeume.map(([,, room]) => room)
+ result.hours[hIndex].roomTypes[roomType.raumtyp] = hour.raeume.map(([,, room]) => room === 0 ? ROOMS_ALL : room)
}))
return result
@@ -108,7 +109,7 @@ export default function RoomList () {
{rooms.map((room, idx) =>
- {room}
+ {getTranslatedRoomName(room)}
{TUX_ROOMS.includes(room) && <> >}
{idx === rooms.length - 1 ? '' : ', '}
From d542b4538031a9e3afba85d6aa3a636c48027567 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Wed, 12 Jul 2023 21:20:47 +0200
Subject: [PATCH 15/34] =?UTF-8?q?=E2=9C=A8=20add=20ExamsCard=20(#306)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/components/SwipeableTabs.js | 14 +++-
rogue-thi-app/components/allCards.js | 7 ++
.../components/cards/CalendarCard.js | 4 +-
rogue-thi-app/components/cards/ExamsCard.js | 77 +++++++++++++++++++
rogue-thi-app/pages/calendar.js | 5 +-
rogue-thi-app/public/locales/de/common.json | 3 +-
.../public/locales/de/dashboard.json | 6 +-
rogue-thi-app/public/locales/en/common.json | 3 +-
.../public/locales/en/dashboard.json | 6 +-
9 files changed, 114 insertions(+), 11 deletions(-)
create mode 100644 rogue-thi-app/components/cards/ExamsCard.js
diff --git a/rogue-thi-app/components/SwipeableTabs.js b/rogue-thi-app/components/SwipeableTabs.js
index 75c07f37..99d6b389 100644
--- a/rogue-thi-app/components/SwipeableTabs.js
+++ b/rogue-thi-app/components/SwipeableTabs.js
@@ -1,16 +1,21 @@
-import React, { useState } from 'react'
+import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import Nav from 'react-bootstrap/Nav'
import SwipeableViews from 'react-swipeable-views'
/**
- * A `Tabs`-like component that is swipable on mobile.
+ * A `Tabs`-like component that is swipeable on mobile.
* @param {string} className Class name to attach to the entire object
* @param {object} children Array of `SwipeableTab` objects
+ * @param {number} defaultPage Index of the tab to show by default
*/
-export default function SwipeableTabs ({ className, children }) {
- const [page, setPage] = useState(0)
+export default function SwipeableTabs ({ className, children, defaultPage }) {
+ const [page, setPage] = useState(defaultPage || 0)
+
+ useEffect(() => {
+ setPage(defaultPage || 0)
+ }, [defaultPage])
return (
@@ -31,6 +36,7 @@ export default function SwipeableTabs ({ className, children }) {
}
SwipeableTabs.propTypes = {
className: PropTypes.string,
+ defaultPage: PropTypes.number,
children: PropTypes.any
}
diff --git a/rogue-thi-app/components/allCards.js b/rogue-thi-app/components/allCards.js
index 0e1ca540..79b07368 100644
--- a/rogue-thi-app/components/allCards.js
+++ b/rogue-thi-app/components/allCards.js
@@ -1,6 +1,7 @@
import BaseCard from './cards/BaseCard'
import CalendarCard from './cards/CalendarCard'
import EventsCard from './cards/EventsCard'
+import ExamsCard from './cards/ExamsCard'
import FoodCard from './cards/FoodCard'
import InstallPrompt from './cards/InstallPrompt'
import MobilityCard from './cards/MobilityCard'
@@ -30,6 +31,12 @@ export const ALL_DASHBOARD_CARDS = [
/>
)
},
+ {
+ key: 'exams',
+ removable: true,
+ default: [PLATFORM_DESKTOP, PLATFORM_MOBILE, USER_STUDENT],
+ card: () =>
+ },
{
key: 'timetable',
removable: true,
diff --git a/rogue-thi-app/components/cards/CalendarCard.js b/rogue-thi-app/components/cards/CalendarCard.js
index f2b57349..63e4b87a 100644
--- a/rogue-thi-app/components/cards/CalendarCard.js
+++ b/rogue-thi-app/components/cards/CalendarCard.js
@@ -26,7 +26,7 @@ export default function CalendarCard () {
try {
exams = (await loadExamList())
.filter(x => !!x.date) // remove exams without a date
- .map(x => ({ name: `Prüfung ${x.titel}`, begin: x.date }))
+ .map(x => ({ name: `${t('calendar.exam')} ${x.titel}`, begin: x.date }))
} catch (e) {
if (e instanceof NoSessionError) {
router.replace('/login')
@@ -43,7 +43,7 @@ export default function CalendarCard () {
setMixedCalendar(combined)
}
load()
- }, [router])
+ }, [router, t])
return (
{
+ async function load () {
+ try {
+ let examList = await loadExamList()
+
+ // filter out exams that are already over
+ examList = examList.filter(x => x.date > Date.now())
+
+ // filter out exams that are not more than 30 days in the future
+ examList = examList.filter(x => x.date < Date.now() + 1000 * 60 * 60 * 24 * 30)
+
+ setExams(examList)
+ } catch (e) {
+ if (e instanceof NoSessionError) {
+ router.replace('/login')
+ } else {
+ console.error(e)
+ alert(e)
+ }
+ }
+ }
+
+ if (userKind === USER_STUDENT) {
+ load()
+ }
+ }, [router, userKind])
+
+ // return nothing if there are no exams
+ if (!exams || exams.length === 0) {
+ return null
+ }
+
+ return (
+
+
+ {exams && exams.slice(0, 2).map((x, i) => (
+
+
+ {x.name}
+
+
+ {x.seat}
+ { ' - ' }
+ {formatFriendlyRelativeTime(x.date, time)}
+
+
+ ))}
+
+
+ )
+}
diff --git a/rogue-thi-app/pages/calendar.js b/rogue-thi-app/pages/calendar.js
index 47a94e74..c32ce61b 100644
--- a/rogue-thi-app/pages/calendar.js
+++ b/rogue-thi-app/pages/calendar.js
@@ -75,6 +75,9 @@ export default function Calendar () {
)
}
+ const { focus } = router.query
+ const page = focus === 'exams' ? 1 : 0
+
return (
@@ -105,7 +108,7 @@ export default function Calendar () {
-
+
{calendar.map((item, idx) =>
diff --git a/rogue-thi-app/public/locales/de/common.json b/rogue-thi-app/public/locales/de/common.json
index 1095019b..680ae576 100644
--- a/rogue-thi-app/public/locales/de/common.json
+++ b/rogue-thi-app/public/locales/de/common.json
@@ -60,7 +60,8 @@
"library": "Bibliothek",
"grades": "Noten & Fächer",
"personal": "Profil",
- "lecturers": "Dozenten"
+ "lecturers": "Dozenten",
+ "exams": "Prüfungen"
},
"prompts": {
"close": "Schließen"
diff --git a/rogue-thi-app/public/locales/de/dashboard.json b/rogue-thi-app/public/locales/de/dashboard.json
index 44e66ee6..a38977a4 100644
--- a/rogue-thi-app/public/locales/de/dashboard.json
+++ b/rogue-thi-app/public/locales/de/dashboard.json
@@ -48,7 +48,11 @@
"date": {
"ends": "endet",
"starts": "beginnt"
- }
+ },
+ "exam": "Prüfung"
+ },
+ "exams": {
+ "title": "Prüfungen"
},
"events": {
"title": "Veranstaltungen",
diff --git a/rogue-thi-app/public/locales/en/common.json b/rogue-thi-app/public/locales/en/common.json
index f2908bf9..618a10bf 100644
--- a/rogue-thi-app/public/locales/en/common.json
+++ b/rogue-thi-app/public/locales/en/common.json
@@ -60,7 +60,8 @@
"library": "Library",
"grades": "Grades & Subjects",
"personal": "Profile",
- "lecturers": "Lecturers"
+ "lecturers": "Lecturers",
+ "exams": "Exams"
},
"prompts": {
"close": "Close"
diff --git a/rogue-thi-app/public/locales/en/dashboard.json b/rogue-thi-app/public/locales/en/dashboard.json
index 9a1fad88..5fbf3d3e 100644
--- a/rogue-thi-app/public/locales/en/dashboard.json
+++ b/rogue-thi-app/public/locales/en/dashboard.json
@@ -48,7 +48,11 @@
"date": {
"ends": "ends",
"starts": "starts"
- }
+ },
+ "exam": "Exam"
+ },
+ "exams": {
+ "title": "Exams"
},
"events": {
"title": "Events",
From 6206e2e94cbd250eb308d01f6debfb27b0b33e6e Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Thu, 13 Jul 2023 19:29:03 +0200
Subject: [PATCH 16/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20duplicate=20grade=20?=
=?UTF-8?q?listing=20(#307)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../lib/backend-utils/grades-utils.js | 26 ++++++++++++++++---
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/rogue-thi-app/lib/backend-utils/grades-utils.js b/rogue-thi-app/lib/backend-utils/grades-utils.js
index bb2fd72b..b849c6f2 100644
--- a/rogue-thi-app/lib/backend-utils/grades-utils.js
+++ b/rogue-thi-app/lib/backend-utils/grades-utils.js
@@ -11,6 +11,7 @@ function simplifyName (x) {
*/
async function getGradeList () {
const gradeList = await API.getGrades()
+
gradeList.forEach(x => {
if (x.anrech === '*' && x.note === '') {
x.note = 'E*'
@@ -29,11 +30,28 @@ async function getGradeList () {
export async function loadGrades () {
const gradeList = await getGradeList()
- const deduplicatedGrades = gradeList
- .filter((x, i) => x.ects || !gradeList.some((y, j) => i !== j && x.titel.trim() === y.titel.trim()))
+ const duplicateGrades = gradeList.filter((x, i) => gradeList.some((y, j) => i !== j && x.titel.trim() === y.titel.trim()))
+
+ // group by title and keep the one with the highest ECTS
+ const groupedDuplicates = duplicateGrades.reduce((acc, curr) => {
+ const existing = acc.find(x => x.titel.trim() === curr.titel.trim())
+ if (existing) {
+ if (existing.ects < curr.ects) {
+ existing.ects = curr.ects
+ }
+ } else {
+ acc.push(curr)
+ }
+ return acc
+ }, [])
+
+ const deduplicatedGrades = gradeList.filter(x => !groupedDuplicates.some(y => x.titel.trim() === y.titel.trim())).concat(groupedDuplicates)
+
+ // sort by original index
+ const sortedGrades = deduplicatedGrades.sort((a, b) => gradeList.indexOf(a) - gradeList.indexOf(b))
- const finishedGrades = deduplicatedGrades.filter(x => x.note)
- const missingGrades = deduplicatedGrades.filter(x => !finishedGrades.some(y => x.titel.trim() === y.titel.trim()))
+ const finishedGrades = sortedGrades.filter(x => x.note)
+ const missingGrades = sortedGrades.filter(x => !finishedGrades.some(y => x.titel.trim() === y.titel.trim()))
return {
finished: finishedGrades,
From 609dc305b7e0bfb18420b4d7cd0962f11084697d Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Fri, 14 Jul 2023 17:41:01 +0200
Subject: [PATCH 17/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20canisius=20category?=
=?UTF-8?q?=20and=20unify=20settings=20label=20(#308)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/pages/food.js | 2 +-
rogue-thi-app/public/locales/de/personal.json | 2 +-
rogue-thi-app/public/locales/en/personal.json | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/rogue-thi-app/pages/food.js b/rogue-thi-app/pages/food.js
index bed99f99..1a1e6171 100644
--- a/rogue-thi-app/pages/food.js
+++ b/rogue-thi-app/pages/food.js
@@ -261,7 +261,7 @@ export default function Mensa () {
{canisiusSalads.length > 0 && (
<>
{canisiusFood.length > 0 && (
- {t('list.titles.meals')}
+ {t('list.titles.salads')}
)}
{canisiusSalads.map((meal, idx) => renderMealEntry(meal, `soup-${idx}`))}
diff --git a/rogue-thi-app/public/locales/de/personal.json b/rogue-thi-app/public/locales/de/personal.json
index cc7bc5df..bd615fed 100644
--- a/rogue-thi-app/public/locales/de/personal.json
+++ b/rogue-thi-app/public/locales/de/personal.json
@@ -18,7 +18,7 @@
"debug": "API Spielwiese",
"privacy": "Datenschutzerklärung",
"imprint": "Impressum",
- "language": "Sprache ändern",
+ "language": "Sprache",
"login": "Anmelden",
"logout": "Ausloggen",
"dashboard": "Dashboard",
diff --git a/rogue-thi-app/public/locales/en/personal.json b/rogue-thi-app/public/locales/en/personal.json
index cc53a201..118f60b6 100644
--- a/rogue-thi-app/public/locales/en/personal.json
+++ b/rogue-thi-app/public/locales/en/personal.json
@@ -18,7 +18,7 @@
"debug": "API Playground",
"privacy": "Privacy Policy",
"imprint": "Imprint",
- "language": "Change Language",
+ "language": "Language",
"login": "Log In",
"logout": "Log Out",
"dashboard": "Dashboard",
From 4d98b9438d147248c1243e3ad67fcc3fae96166d Mon Sep 17 00:00:00 2001
From: Robert Eggl
Date: Thu, 20 Jul 2023 00:23:02 +0200
Subject: [PATCH 18/34] fix(mobility): fixes wrong translation key (#310)
---
rogue-thi-app/pages/mobility.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rogue-thi-app/pages/mobility.js b/rogue-thi-app/pages/mobility.js
index b912b320..c97cc015 100644
--- a/rogue-thi-app/pages/mobility.js
+++ b/rogue-thi-app/pages/mobility.js
@@ -117,7 +117,7 @@ export default function Bus () {
{dataError && (
- {t('transport.error')}
+ {t('transport.error.retrieval')}
{dataError}
)}
From a3196b9a00fc81787b2cb4e8b127f19b1d6aee23 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Sun, 23 Jul 2023 00:33:45 +0200
Subject: [PATCH 19/34] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20fix=20translation=20?=
=?UTF-8?q?key=20typo=20(#311)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/pages/food.js | 6 +++---
rogue-thi-app/public/locales/de/food.json | 6 +++---
rogue-thi-app/public/locales/de/mobility.json | 2 +-
rogue-thi-app/public/locales/en/food.json | 6 +++---
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/rogue-thi-app/pages/food.js b/rogue-thi-app/pages/food.js
index 1a1e6171..2810b8d7 100644
--- a/rogue-thi-app/pages/food.js
+++ b/rogue-thi-app/pages/food.js
@@ -322,7 +322,7 @@ export default function Mensa () {
{t('foodModal.flags.title')}
- {showMealDetails?.flags === null && `${t('foodModal.flags.unkown')}`}
+ {showMealDetails?.flags === null && `${t('foodModal.flags.unknown')}`}
{showMealDetails?.flags?.length === 0 && `${t('foodModal.flags.empty')}`}
{showMealDetails?.flags?.map(flag => (
@@ -341,7 +341,7 @@ export default function Mensa () {
{t('foodModal.allergens.title')}
- {showMealDetails?.allergens === null && `${t('foodModal.allergens.unkown')}`}
+ {showMealDetails?.allergens === null && `${t('foodModal.allergens.unknown')}`}
{showMealDetails?.allergens?.length === 0 && `${t('foodModal.flags.empty')}`}
{showMealDetails?.allergens?.map(key => (
@@ -393,7 +393,7 @@ export default function Mensa () {
{formatGram(showMealDetails?.nutrition.salt)}
)) || (
- {t('foodModal.nutrition.unkown.title')}
+ {t('foodModal.nutrition.unknown.title')}
)}
{t('foodModal.prices.title')}
diff --git a/rogue-thi-app/public/locales/de/food.json b/rogue-thi-app/public/locales/de/food.json
index e6eac915..4560f227 100644
--- a/rogue-thi-app/public/locales/de/food.json
+++ b/rogue-thi-app/public/locales/de/food.json
@@ -31,12 +31,12 @@
"header": "Erläuterung",
"flags": {
"title": "Anmerkungen",
- "unkown": "Unbekannt",
+ "unknown": "Unbekannt",
"empty": "Keine Anmerkungen vorhanden."
},
"allergens": {
"title": "Allergene",
- "unkown": "Unbekannt",
+ "unknown": "Unbekannt",
"empty": "Keine Allergene vorhanden.",
"fallback" : "Unbekannt (Das ist schlecht)"
},
@@ -54,7 +54,7 @@
"fiber.title": "Ballaststoffe",
"protein.title": "Eiweiß",
"salt.title": "Salz",
- "unkown.title": "Unbekannt."
+ "unknown.title": "Unbekannt."
},
"prices": {
"title": "Preise",
diff --git a/rogue-thi-app/public/locales/de/mobility.json b/rogue-thi-app/public/locales/de/mobility.json
index b13d6500..03493ad1 100644
--- a/rogue-thi-app/public/locales/de/mobility.json
+++ b/rogue-thi-app/public/locales/de/mobility.json
@@ -22,7 +22,7 @@
"train": "Bahn ({{station}})",
"parking": "Parkplätze",
"charging": "Ladestationen",
- "unkown": "Mobilität"
+ "unknown": "Mobilität"
},
"details": {
"charging": {
diff --git a/rogue-thi-app/public/locales/en/food.json b/rogue-thi-app/public/locales/en/food.json
index bd4c42df..0bf24de1 100644
--- a/rogue-thi-app/public/locales/en/food.json
+++ b/rogue-thi-app/public/locales/en/food.json
@@ -30,12 +30,12 @@
"header": "Details",
"flags": {
"title": "Notes",
- "unkown": "Unknown",
+ "unknown": "Unknown",
"empty": "No notes available"
},
"allergens": {
"title": "Allergens",
- "unkown": "Unknown",
+ "unknown": "Unknown",
"empty": "No allergens present",
"fallback": "Unknown (This is bad)"
},
@@ -53,7 +53,7 @@
"fiber.title": "Fiber",
"protein.title": "Protein",
"salt.title": "Salt",
- "unkown.title": "Unknown"
+ "unknown.title": "Unknown"
},
"prices": {
"title": "Prices",
From ae19233706eef274bee752372a5a868461755357 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Mon, 24 Jul 2023 00:18:00 +0200
Subject: [PATCH 20/34] =?UTF-8?q?=F0=9F=92=84=20small=20UI=20changes=20and?=
=?UTF-8?q?=20fixes=20(#312)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/components/RoomMap.js | 9 +--------
rogue-thi-app/styles/Home.module.css | 15 ++++++++-------
rogue-thi-app/styles/Mobility.module.css | 15 +++++++++------
rogue-thi-app/styles/RoomMap.module.css | 13 ++++++++++++-
rogue-thi-app/styles/Timetable.module.css | 2 +-
5 files changed, 31 insertions(+), 23 deletions(-)
diff --git a/rogue-thi-app/components/RoomMap.js b/rogue-thi-app/components/RoomMap.js
index e12e025d..2b6c9771 100644
--- a/rogue-thi-app/components/RoomMap.js
+++ b/rogue-thi-app/components/RoomMap.js
@@ -179,13 +179,6 @@ export default function RoomMap ({ highlight, roomData }) {
const special = SPECIAL_ROOMS[entry.properties.Raum]
- let color = '#6c757d'
- if (special) {
- color = special.color
- } else if (avail) {
- color = '#8845ef'
- }
-
return (
@@ -213,7 +206,7 @@ export default function RoomMap ({ highlight, roomData }) {
positions={entry.coordinates}
pathOptions={{
...entry.options,
- color
+ className: special ? special.color : (avail ? styles.roomAvailable : styles.roomOccupied)
}}
/>
diff --git a/rogue-thi-app/styles/Home.module.css b/rogue-thi-app/styles/Home.module.css
index c0058a8a..b574654d 100644
--- a/rogue-thi-app/styles/Home.module.css
+++ b/rogue-thi-app/styles/Home.module.css
@@ -72,19 +72,20 @@
padding: .25rem;
border: 0;
display: flex;
+ align-items: center;
}
.mobilityRoute {
- padding: 0px 3px;
- margin-right: 6px;
- min-width: 2.5em;
+ min-width: 3em;
white-space: nowrap;
text-align: center;
- float: left;
- background: #eeeeee;
- border-radius: 2px;
- color: black;
font-weight: bold;
+ color: var(--white);
+ background-image: linear-gradient(70deg, color-mix(in srgb, var(--secondary) 50%, #ffffff00 50%) 30%, color-mix(in srgb, var(--secondary) 30%, color-mix(in srgb, var(--gray) 20%, #ffffff00 80%) 70%));
+ padding: 2px 6px;
+ border-radius: 5px;
+ border: 1px solid var(--secondary);
+ margin-right: 10px;
}
.mobilityDestination {
diff --git a/rogue-thi-app/styles/Mobility.module.css b/rogue-thi-app/styles/Mobility.module.css
index ca77f05c..e7a26cc4 100644
--- a/rogue-thi-app/styles/Mobility.module.css
+++ b/rogue-thi-app/styles/Mobility.module.css
@@ -16,17 +16,20 @@
.mobilityItem {
display: flex;
flex-direction: row;
+ align-items: center;
}
.mobilityRoute {
- padding: 0px 3px;
- margin-right: 6px;
- border-radius: 2px;
- min-width: 2.5em;
+ min-width: 3em;
+ white-space: nowrap;
text-align: center;
- background: #eeeeee;
- color: black;
font-weight: bold;
+ color: var(--white);
+ background-image: linear-gradient(70deg, color-mix(in srgb, var(--secondary) 50%, #ffffff00 50%) 30%, color-mix(in srgb, var(--secondary) 30%, color-mix(in srgb, var(--gray) 20%, #ffffff00 80%) 70%));
+ padding: 2px 6px;
+ border-radius: 5px;
+ border: 1px solid var(--secondary);
+ margin-right: 10px;
}
.mobilityDestination {
diff --git a/rogue-thi-app/styles/RoomMap.module.css b/rogue-thi-app/styles/RoomMap.module.css
index 7c4a5387..1b174cd5 100644
--- a/rogue-thi-app/styles/RoomMap.module.css
+++ b/rogue-thi-app/styles/RoomMap.module.css
@@ -58,7 +58,7 @@
cursor: pointer;
}
.mapContainer :global(.leaflet-control-layers input:checked + span) {
- background-color: var(--primary-color, #8845ef);
+ background-color: var(--primary, #8845ef);
color: var(--inverted-font-color, #ffffff);
}
@@ -76,6 +76,17 @@
background-color: #8845ef;
}
+.roomAvailable {
+ fill: var(--primary);
+ fill-opacity: 0.4;
+ stroke: var(--primary);
+}
+
+.roomOccupied {
+ fill: var(--gray);
+ stroke: var(--gray);
+}
+
.legendTaken::before {
content: '';
display: inline-block;
diff --git a/rogue-thi-app/styles/Timetable.module.css b/rogue-thi-app/styles/Timetable.module.css
index a92f9978..c93becab 100644
--- a/rogue-thi-app/styles/Timetable.module.css
+++ b/rogue-thi-app/styles/Timetable.module.css
@@ -20,7 +20,7 @@
}
.today .heading {
- color: var(--primary-color, #8845ef) !important;
+ color: var(--primary, #8845ef) !important;
}
.day .heading .date {
From 461f85266b5ce98222a9e8cd0e2d7b09cba70e17 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Mon, 24 Jul 2023 00:18:11 +0200
Subject: [PATCH 21/34] =?UTF-8?q?=E2=9C=A8=20Add=20basic=20THI=20light=20t?=
=?UTF-8?q?heme=20(#313)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/data/themes.json | 1 +
rogue-thi-app/scss/themes/thi-light.scss | 24 ++++++++++++++++++++++++
2 files changed, 25 insertions(+)
create mode 100644 rogue-thi-app/scss/themes/thi-light.scss
diff --git a/rogue-thi-app/data/themes.json b/rogue-thi-app/data/themes.json
index 963ca339..2ab5b722 100644
--- a/rogue-thi-app/data/themes.json
+++ b/rogue-thi-app/data/themes.json
@@ -1,6 +1,7 @@
[
{ "name": { "en": "Automatic", "de": "Automatisch" }, "style": "default" },
{ "name": { "en": "Light", "de": "Hell" }, "style": "light" },
+ { "name": { "en": "THI Light", "de": "THI Hell" }, "style": "thi-light" },
{ "name": { "en": "Dark", "de": "Dunkel" }, "style": "dark" },
{ "name": { "en": "Barbie & Ken", "de": "Barbie & Ken" }, "style": "barbie" },
{ "name": { "en": "Retro", "de": "Retro" }, "style": "retro" },
diff --git a/rogue-thi-app/scss/themes/thi-light.scss b/rogue-thi-app/scss/themes/thi-light.scss
new file mode 100644
index 00000000..ad19ddd4
--- /dev/null
+++ b/rogue-thi-app/scss/themes/thi-light.scss
@@ -0,0 +1,24 @@
+$theme-colors: (
+ "primary": #005a9b,
+ "secondary": #009bcd
+);
+
+@import "../../node_modules/bootstrap/scss/bootstrap";
+
+// react-placeholder
+// declare after bootstrap, since we need bootstrap variables
+.text-row, .rect-shape, .round-shape {
+ background: $gray-200 !important;
+}
+
+:root {
+ --navbar-border-image: linear-gradient(90deg, #005a9b, #228cc1, #3a93c3, #6fa2c7, #31ccff);
+ --tabbar-tab-active: #005a9b;
+ // --navbar-border-image: none;
+}
+
+
+.navbar-brand > * {
+ color: #025b9c !important;
+ font-weight: bold !important;
+}
\ No newline at end of file
From 441c2c62a1424d74d8600c88cbf872f9270942e1 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Mon, 24 Jul 2023 11:54:27 +0200
Subject: [PATCH 22/34] =?UTF-8?q?=E2=9C=A8=20add=20static=20Reimanns=20mea?=
=?UTF-8?q?ls=20(#309)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ✨ add static Reimanns meals
* 🎨 unify meal API format
* 💄 redesign food tags; name clipping
* 💄 overhaul food page
* 🚸 add filter for static reimanns meals
* 💄 optimize box label for light mode
* 🐛 remove vegetarian or vegan flag in case of contradiction #234
---
.../components/modal/FilterFoodModal.js | 6 +
rogue-thi-app/data/flag-contradictions.json | 25 +++
rogue-thi-app/data/reimanns-meals.json | 160 +++++++++++++++++
rogue-thi-app/lib/backend-utils/food-utils.js | 45 ++++-
.../lib/backend-utils/translation-utils.js | 5 +-
rogue-thi-app/lib/date-utils.js | 17 ++
rogue-thi-app/lib/hooks/food-filter.js | 2 +-
rogue-thi-app/pages/api/canisius.js | 4 +-
rogue-thi-app/pages/api/mensa.js | 4 +-
rogue-thi-app/pages/api/reimanns.js | 21 ++-
rogue-thi-app/pages/food.js | 164 ++++++++++++------
rogue-thi-app/public/locales/de/common.json | 1 +
rogue-thi-app/public/locales/de/food.json | 4 +-
rogue-thi-app/public/locales/en/common.json | 1 +
rogue-thi-app/public/locales/en/food.json | 4 +-
rogue-thi-app/styles/Mensa.module.css | 72 +++++++-
16 files changed, 466 insertions(+), 69 deletions(-)
create mode 100644 rogue-thi-app/data/flag-contradictions.json
create mode 100644 rogue-thi-app/data/reimanns-meals.json
diff --git a/rogue-thi-app/components/modal/FilterFoodModal.js b/rogue-thi-app/components/modal/FilterFoodModal.js
index 6a3417d4..593c0c23 100644
--- a/rogue-thi-app/components/modal/FilterFoodModal.js
+++ b/rogue-thi-app/components/modal/FilterFoodModal.js
@@ -70,6 +70,12 @@ export default function FilterFoodModal () {
checked={selectedRestaurants.includes('reimanns')}
onChange={() => toggleSelectedRestaurant('reimanns')}
/>
+ toggleSelectedRestaurant('reimanns-static')}
+ />
['reimanns', 'reimanns-static'].includes(x))) {
const data = await NeulandAPI.getReimannsPlan()
const startOfToday = new Date(formatISODate(new Date())).getTime()
@@ -67,3 +69,44 @@ export async function loadFoodEntries (restaurants) {
}
})
}
+
+/**
+ * Cleans the meal flags to remove wrong flags (e.g. "veg" (vegan) and "R" (Beef) at the same time => remove "veg")
+ * @param {string[]} flags Meal flags
+ * @returns {string[]} Cleaned meal flags
+ **/
+function cleanMealFlags (flags) {
+ // find contradictions
+ const contradictions = flags?.filter(x => flagContradictions[x]?.some(y => flags?.includes(y))) || []
+
+ // remove contradictions
+ flags = flags?.filter(x => !contradictions.includes(x)) || []
+
+ return flags
+}
+
+/**
+ * Unifies the meal plan entries to a common format
+ * @param {object[]} entries Meal plan entries
+ * @returns {object[]} Unified meal plan entries
+ */
+export function unifyFoodEntries (entries) {
+ return entries.map(entry => ({
+ timestamp: entry.timestamp,
+ meals: entry.meals.map(meal => ({
+ name: meal.name,
+ category: meal.category,
+ prices: meal.prices || {
+ student: null,
+ employee: null,
+ guest: null
+ },
+ allergens: meal.allergens || null,
+ flags: cleanMealFlags(meal.flags) || [],
+ nutrition: meal.nutrition || null,
+ variations: meal.variations || [],
+ originalLanguage: meal.originalLanguage || 'de',
+ static: meal.static || false
+ }))
+ }))
+}
diff --git a/rogue-thi-app/lib/backend-utils/translation-utils.js b/rogue-thi-app/lib/backend-utils/translation-utils.js
index 286bfba8..0fdbc7e4 100644
--- a/rogue-thi-app/lib/backend-utils/translation-utils.js
+++ b/rogue-thi-app/lib/backend-utils/translation-utils.js
@@ -4,6 +4,7 @@ import AsyncMemoryCache from '../cache/async-memory-cache'
const DEEPL_ENDPOINT = process.env.NEXT_PUBLIC_DEEPL_ENDPOINT || ''
const DEEPL_API_KEY = process.env.DEEPL_API_KEY || ''
const ENABLE_DEV_TRANSLATIONS = process.env.ENABLE_DEV_TRANSLATIONS === 'true' || false
+const DISABLE_FALLBACK_WARNINGS = process.env.DISABLE_FALLBACK_WARNINGS === 'true' || false
const CACHE_TTL = 60 * 60 * 24 * 7 * 1000 // 7 days
@@ -37,7 +38,7 @@ async function translate (text, target) {
return (await translator.translateText(text, SOURCE_LANG, target)).text
} catch (err) {
console.error(err)
- return isDev ? `FALLBACK: ${text}` : text
+ return isDev && !DISABLE_FALLBACK_WARNINGS ? `FALLBACK: ${text}` : text
}
}
@@ -53,7 +54,7 @@ function translateFallback (meals) {
...meal,
name: {
de: meal.name,
- en: isDev ? `FALLBACK: ${meal.name}` : meal.name
+ en: isDev && !DISABLE_FALLBACK_WARNINGS ? `FALLBACK: ${meal.name}` : meal.name
},
originalLanguage: 'de'
}
diff --git a/rogue-thi-app/lib/date-utils.js b/rogue-thi-app/lib/date-utils.js
index 325bd3b7..591b6801 100644
--- a/rogue-thi-app/lib/date-utils.js
+++ b/rogue-thi-app/lib/date-utils.js
@@ -236,6 +236,23 @@ export function getWeek (date) {
return [start, end]
}
+/**
+ * Returns all days between the given dates
+ * @param {Date} begin
+ * @param {Date} end
+ * @returns {Date[]}
+ */
+export function getDays (begin, end) {
+ const days = []
+ const date = new Date(begin)
+ // eslint-disable-next-line no-unmodified-loop-condition
+ while (date < end) {
+ days.push(new Date(date))
+ date.setDate(date.getDate() + 1)
+ }
+ return days
+}
+
/**
* Adds weeks to a date
* @param {Date} date
diff --git a/rogue-thi-app/lib/hooks/food-filter.js b/rogue-thi-app/lib/hooks/food-filter.js
index 332671e0..32825351 100644
--- a/rogue-thi-app/lib/hooks/food-filter.js
+++ b/rogue-thi-app/lib/hooks/food-filter.js
@@ -30,7 +30,7 @@ import { useEffect, useState } from 'react'
* @returns {void}
*/
export function useFoodFilter () {
- const [selectedRestaurants, setSelectedRestaurants] = useState(['mensa'])
+ const [selectedRestaurants, setSelectedRestaurants] = useState(['mensa', 'reimanns', 'reimanns-static'])
const [preferencesSelection, setPreferencesSelection] = useState({})
const [allergenSelection, setAllergenSelection] = useState({})
const [showFoodFilterModal, setShowFoodFilterModal] = useState(false)
diff --git a/rogue-thi-app/pages/api/canisius.js b/rogue-thi-app/pages/api/canisius.js
index 11e5fe9c..fb641ee5 100644
--- a/rogue-thi-app/pages/api/canisius.js
+++ b/rogue-thi-app/pages/api/canisius.js
@@ -1,5 +1,6 @@
import AsyncMemoryCache from '../../lib/cache/async-memory-cache'
import { translateMeals } from '../../lib/backend-utils/translation-utils'
+import { unifyFoodEntries } from '../../lib/backend-utils/food-utils'
const pdf = require('pdf-parse')
@@ -132,7 +133,8 @@ export default async function handler (_, res) {
})
})
- return translateMeals(mealPlan)
+ const translatedMeals = await translateMeals(mealPlan)
+ return unifyFoodEntries(translatedMeals)
})
sendJson(res, 200, data)
diff --git a/rogue-thi-app/pages/api/mensa.js b/rogue-thi-app/pages/api/mensa.js
index b1dc1341..1e1dd1cb 100644
--- a/rogue-thi-app/pages/api/mensa.js
+++ b/rogue-thi-app/pages/api/mensa.js
@@ -3,6 +3,7 @@ import xmljs from 'xml-js'
import AsyncMemoryCache from '../../lib/cache/async-memory-cache'
import { formatISODate } from '../../lib/date-utils'
import { translateMeals } from '../../lib/backend-utils/translation-utils'
+import { unifyFoodEntries } from '../../lib/backend-utils/food-utils'
const CACHE_TTL = 60 * 60 * 1000 // 60m
const URL_DE = 'https://www.max-manager.de/daten-extern/sw-erlangen-nuernberg/xml/mensa-ingolstadt.xml'
@@ -125,7 +126,8 @@ async function fetchPlan () {
if (resp.status === 200) {
const mealPlan = parseDataFromXml(await resp.text())
- return await translateMeals(mealPlan)
+ const translatedMeals = await translateMeals(mealPlan)
+ return unifyFoodEntries(translatedMeals)
} else {
throw new Error('Data source returned an error: ' + await resp.text())
}
diff --git a/rogue-thi-app/pages/api/reimanns.js b/rogue-thi-app/pages/api/reimanns.js
index 18069c5c..c79135c6 100644
--- a/rogue-thi-app/pages/api/reimanns.js
+++ b/rogue-thi-app/pages/api/reimanns.js
@@ -1,7 +1,10 @@
import cheerio from 'cheerio'
+import { addWeek, getDays, getWeek } from '../../lib/date-utils'
import AsyncMemoryCache from '../../lib/cache/async-memory-cache'
+import staticMeals from '../../data/reimanns-meals.json'
import { translateMeals } from '../../lib/backend-utils/translation-utils'
+import { unifyFoodEntries } from '../../lib/backend-utils/food-utils'
const CACHE_TTL = 10 * 60 * 1000 // 10m
const URL = 'http://reimanns.in/mittagsgerichte-wochenkarte/'
@@ -46,7 +49,13 @@ export default async function handler (req, res) {
return content.split('\n')
})
- const days = {}
+ // fill in all days (even if they are not on the website to add static meals)
+ const [weekStart] = getWeek(new Date())
+ const [, nextWeekEnd] = getWeek(addWeek(new Date(), 1))
+ const allDays = getDays(weekStart, nextWeekEnd)
+
+ const days = Object.fromEntries(allDays.map(day => [day.toISOString().split('T')[0], []]))
+
let day = null
lines.forEach(content => {
content = content.trim()
@@ -94,7 +103,15 @@ export default async function handler (req, res) {
}))
}))
- return translateMeals(mealPlan)
+ const scrapedMeals = await translateMeals(mealPlan)
+
+ // add static meals (no need to translate)
+ // TODO: add allergens, flags, nutrition (ask Reimanns for data)
+ scrapedMeals.forEach(day => {
+ day.meals.push(...staticMeals)
+ })
+
+ return unifyFoodEntries(scrapedMeals)
})
sendJson(res, 200, data)
diff --git a/rogue-thi-app/pages/food.js b/rogue-thi-app/pages/food.js
index 2810b8d7..5acbebaa 100644
--- a/rogue-thi-app/pages/food.js
+++ b/rogue-thi-app/pages/food.js
@@ -139,6 +139,32 @@ export default function Mensa () {
return (new Intl.NumberFormat(getAdjustedLocale(), { minimumFractionDigits: 1, maximumFractionDigits: 2 })).format(x)
}
+ /**
+ * Renders the variations of a meal in a list.
+ * @param {object} meal
+ * @returns {JSX.Element}
+ **/
+ function renderFoodVariations (meal) {
+ return meal?.variations?.length > 0 && (
+ {meal?.variations?.map((variant, idx) => (
+
+
+
+ {variant.name[i18n.languages[0]]}
+
+
+
+ {`${variant.additional ? '+ ' : ''}${getUserSpecificPrice(variant)}`}
+
+
+
+
+ ))}
+ )
+ }
+
/**
* Renders a meal entry.
* @param {object} meal
@@ -146,6 +172,9 @@ export default function Mensa () {
* @returns {JSX.Element}
*/
function renderMealEntry (meal, key) {
+ const userAllergens = meal.allergens && meal.allergens.filter(x => allergenSelection[x]).map(x => allergenMap[x]?.[currentLocale])
+ const userPreferences = meal.flags && meal.flags.filter(x => preferencesSelection[x] || ['veg', 'V'].includes(x))?.map(x => flagMap[x]?.[currentLocale])
+
return (
-
- {/* {isTranslated(meal) && (
- <>
-
- {' '}
- >
- )} */}
- {meal.name[i18n.languages[0]]}
+
+
+ {meal.name[i18n.languages[0]]}
+
+
+
+ {getUserSpecificPrice(meal)}
+
-
-
- {!meal.allergens && t('warning.unknownIngredients.text')}
+
+
+
+ {/* {!meal.allergens && t('warning.unknownIngredients.text')} */}
{containsSelectedAllergen(meal.allergens) && (
-
-
- {' '}
+
+
+ {t('preferences.warn')}
)}
{!containsSelectedAllergen(meal.allergens) && containsSelectedPreference(meal.flags) && (
-
-
- {' '}
+
+
+ {t('preferences.match')}
)}
- {meal.flags && meal.flags.map(flag => flagMap[flag]?.[currentLocale]).join(', ')}
- {meal.flags?.length > 0 && meal.allergens?.length > 0 && '; '}
- {meal.allergens && meal.allergens.join(', ')}
-
+ {userPreferences?.join(', ')}
+ {userPreferences?.length > 0 && userAllergens?.length > 0 && ' • '}
+ {userAllergens?.join(', ')}
+
-
- {getUserSpecificPrice(meal)}
-
+
+ {/* Variations of meal */}
+ {renderFoodVariations(meal)}
)
}
@@ -198,13 +228,19 @@ export default function Mensa () {
* @returns {JSX.Element}
*/
function renderMealDay (day, key) {
+ const includeStaticReimanns = selectedRestaurants.includes('reimanns-static')
+
const mensa = day.meals.filter(x => x.restaurant === 'Mensa')
- const mensaSoups = day.meals.filter(x => x.restaurant === 'Mensa' && x.category === 'Suppe')
- const mensaFood = day.meals.filter(x => x.restaurant === 'Mensa' && x.category !== 'Suppe')
- const reimanns = day.meals.filter(x => x.restaurant === 'Reimanns')
+ const mensaSoups = mensa.filter(x => x.category === 'Suppe')
+ const mensaFood = mensa.filter(x => x.category !== 'Suppe')
+
+ const reimanns = day.meals.filter(x => x.restaurant === 'Reimanns').filter(x => !x.static || x.static === includeStaticReimanns)
+ const reimannsFood = reimanns.filter(x => x.category !== 'Salat')
+ const reimannsSalad = reimanns.filter(x => x.category === 'Salat')
+
const canisius = day.meals.filter(x => x.restaurant === 'Canisius')
- const canisiusSalads = day.meals.filter(x => x.restaurant === 'Canisius' && x.category === 'Salat')
- const canisiusFood = day.meals.filter(x => x.restaurant === 'Canisius' && x.category !== 'Salat')
+ const canisiusSalads = canisius.filter(x => x.category === 'Salat')
+ const canisiusFood = canisius.filter(x => x.category !== 'Salat')
const noData = mensa.length === 0 && reimanns.length === 0 && canisius.length === 0
@@ -212,24 +248,20 @@ export default function Mensa () {
{mensa.length > 0 && (
<>
- {t('list.titles.cafeteria')}
+ {t('list.titles.cafeteria')}
{mensaFood.length > 0 && (
<>
- {mensaSoups.length > 0 && (
- {t('list.titles.meals')}
- )}
-
- {mensaFood.map((meal, idx) => renderMealEntry(meal, `food-${idx}`))}
+ {t('list.titles.meals')}
+
+ {mensaFood.map((meal, idx) => renderMealEntry(meal, `mensa-food-${idx}`))}
>
)}
{mensaSoups.length > 0 && (
<>
- {mensaFood.length > 0 && (
- {t('list.titles.soups')}
- )}
-
- {mensaSoups.map((meal, idx) => renderMealEntry(meal, `soup-${idx}`))}
+ {t('list.titles.soups')}
+
+ {mensaSoups.map((meal, idx) => renderMealEntry(meal, `mensa-soup-${idx}`))}
>
)}
@@ -238,33 +270,42 @@ export default function Mensa () {
{reimanns.length > 0 && (
<>
- Reimanns
-
- {reimanns.map((meal, idx) => renderMealEntry(meal, `reimanns-${idx}`))}
-
+ Reimanns
+ {reimannsFood.length > 0 && (
+ <>
+ {t('list.titles.meals')}
+
+ {reimannsFood.map((meal, idx) => renderMealEntry(meal, `reimanns-food-${idx}`))}
+
+ >
+ )}
+ {reimannsSalad.length > 0 && (
+ <>
+ {t('list.titles.salads')}
+
+ {reimannsSalad.map((meal, idx) => renderMealEntry(meal, `reimanns-salad-${idx}`))}
+
+ >
+ )}
>
)}
{canisius.length > 0 && (
<>
- Canisiuskonvikt
+ Canisiuskonvikt
{canisiusFood.length > 0 && (
<>
- {canisiusSalads.length > 0 && (
- {t('list.titles.meals')}
- )}
-
- {canisiusFood.map((meal, idx) => renderMealEntry(meal, `food-${idx}`))}
+ {t('list.titles.meals')}
+
+ {canisiusFood.map((meal, idx) => renderMealEntry(meal, `canisius-food-${idx}`))}
>
)}
{canisiusSalads.length > 0 && (
<>
- {canisiusFood.length > 0 && (
- {t('list.titles.salads')}
- )}
-
- {canisiusSalads.map((meal, idx) => renderMealEntry(meal, `soup-${idx}`))}
+ {t('list.titles.salads')}
+
+ {canisiusSalads.map((meal, idx) => renderMealEntry(meal, `canisius-salad-${idx}`))}
>
)}
@@ -321,6 +362,8 @@ export default function Mensa () {
+ {showMealDetails?.name[i18n.languages[0]]}
+
{t('foodModal.flags.title')}
{showMealDetails?.flags === null && `${t('foodModal.flags.unknown')}`}
{showMealDetails?.flags?.length === 0 && `${t('foodModal.flags.empty')}`}
@@ -412,6 +455,10 @@ export default function Mensa () {
+ {/* Variations of meal */}
+ {renderFoodVariations(showMealDetails)}
+ {showMealDetails?.variations?.length > 0 && ( )}
+
{t('foodModal.warning.title')}
@@ -462,7 +509,12 @@ export default function Mensa () {
*/
function WeekTab ({ foodEntries, index, setIndex }) {
return
-
setIndex(parseInt(key))}>
+ setIndex(parseInt(key))}
+ className={styles.nav}
+ >
{foodEntries && foodEntries.map((child, idx) =>
{buildLinedWeekdaySpan(child.timestamp)}
diff --git a/rogue-thi-app/public/locales/de/common.json b/rogue-thi-app/public/locales/de/common.json
index 680ae576..ae5bcb8a 100644
--- a/rogue-thi-app/public/locales/de/common.json
+++ b/rogue-thi-app/public/locales/de/common.json
@@ -14,6 +14,7 @@
"title": "Restaurants",
"showMensa": "Mensa anzeigen",
"showReimanns": "Reimanns anzeigen",
+ "showReimannsStatic": "Reimanns anzeigen (feste Gerichte)",
"showCanisius": "Canisiuskonvikt anzeigen"
},
"allergens": {
diff --git a/rogue-thi-app/public/locales/de/food.json b/rogue-thi-app/public/locales/de/food.json
index 4560f227..5f0a290d 100644
--- a/rogue-thi-app/public/locales/de/food.json
+++ b/rogue-thi-app/public/locales/de/food.json
@@ -7,7 +7,9 @@
},
"filter": "Filter",
"preferences": {
- "iconTitle": "Bevorzugtes Essen"
+ "iconTitle": "Bevorzugtes Essen",
+ "match": "Bevorzugt",
+ "warn": "Allergene"
},
"list": {
"titles": {
diff --git a/rogue-thi-app/public/locales/en/common.json b/rogue-thi-app/public/locales/en/common.json
index 618a10bf..f969df9c 100644
--- a/rogue-thi-app/public/locales/en/common.json
+++ b/rogue-thi-app/public/locales/en/common.json
@@ -14,6 +14,7 @@
"title": "Restaurants",
"showMensa": "Show Cafeteria",
"showReimanns": "Show Reimanns",
+ "showReimannsStatic": "Show Reimanns (static meals)",
"showCanisius": "Show Canisius Convict"
},
"allergens": {
diff --git a/rogue-thi-app/public/locales/en/food.json b/rogue-thi-app/public/locales/en/food.json
index 0bf24de1..42c01541 100644
--- a/rogue-thi-app/public/locales/en/food.json
+++ b/rogue-thi-app/public/locales/en/food.json
@@ -7,7 +7,9 @@
},
"filter": "Filter",
"preferences": {
- "iconTitle": "Preferred Food"
+ "iconTitle": "Preferred Food",
+ "match": "Match",
+ "warn": "Allergies"
},
"list": {
"titles": {
diff --git a/rogue-thi-app/styles/Mensa.module.css b/rogue-thi-app/styles/Mensa.module.css
index 12cc64d5..ceff092c 100644
--- a/rogue-thi-app/styles/Mensa.module.css
+++ b/rogue-thi-app/styles/Mensa.module.css
@@ -14,14 +14,23 @@
}
.kindHeader {
- margin-top: 10px;
+ color: var(--secondary);
+ font-size: 1.1rem;
+}
+
+.meals {
+ padding-bottom: 15px;
+}
+
+.nav {
+ padding-bottom: 15px;
}
.details {
- flex: 1;
+ margin-left: 5px;
color: gray;
text-align: right;
- align-self: flex-end;
+ align-self: flex-start;
}
.restaurant {
@@ -62,3 +71,60 @@
color: var(--primary);
font-size: .8rem;
}
+
+.variations {
+ margin-top: 10px;
+}
+
+.variation {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+}
+
+.item {
+ padding-top: 15px;
+ padding-bottom: 15px;
+}
+
+.box {
+ color: var(--white);
+ background-image: linear-gradient(70deg, color-mix(in srgb, var(--secondary) 50%, #ffffff00 50%) 30%, color-mix(in srgb, var(--secondary) 30%, color-mix(in srgb, var(--gray) 20%, #ffffff00 80%) 70%));
+ padding: 4px 8px 5px 8px;
+ border-radius: 5px;
+ border: 1px solid var(--secondary);
+ margin: 10px 10px 5px 0;
+ display: inline-block;
+}
+
+.warn {
+ color: var(--white);
+ background-image: linear-gradient(70deg, color-mix(in srgb, var(--danger) 50%, #ffffff00 50%) 30%, color-mix(in srgb, var(--danger) 30%, color-mix(in srgb, var(--gray) 20%, #ffffff00 80%) 70%));
+ border: 1px solid var(--danger);
+}
+
+.match {
+ color: var(--white);
+ background-image: linear-gradient(70deg, color-mix(in srgb, var(--success) 50%, #ffffff00 50%) 30%, color-mix(in srgb, var(--success) 30%, color-mix(in srgb, var(--gray) 20%, #ffffff00 80%) 70%));
+ border: 1px solid var(--success);
+}
+
+.name {
+ max-lines: 2;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.modalTitle {
+ padding-bottom: 5px;
+ margin-bottom: 10px;
+ border-bottom: 3px solid var(--primary);
+}
+
+.icon {
+ margin-right: 6px;
+}
From 75478bc2161f508b34b765b9e45ff8be6eafe252 Mon Sep 17 00:00:00 2001
From: Robert Eggl
Date: Mon, 24 Jul 2023 13:57:05 +0200
Subject: [PATCH 23/34] Adds basic yarn support (#301)
* Adds yarn support
* Update CONTRIBUTING.md
---
.yarnrc.yml | 4 ++++
CONTRIBUTING.md | 2 +-
rogue-thi-app/.gitignore | 1 +
rogue-thi-app/README.md | 13 ++++++++++++-
rogue-thi-app/package.json | 2 +-
5 files changed, 19 insertions(+), 3 deletions(-)
create mode 100644 .yarnrc.yml
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 00000000..58bbd0df
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1,4 @@
+enableTelemetry: false
+
+nodeLinker: node-modules
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 90dd1ffe..54c27958 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,7 +11,7 @@ You should know how to program in object oriented languages. Prior experience wi
You need to install the necessary tools.
* [Git](https://git-scm.com/downloads)
-* [Node.js 14 LTS](https://nodejs.org/en/)
+* [Node.js 18 LTS](https://nodejs.org/en/)
* [Visual Studio Code](https://code.visualstudio.com/)
* [ESLint for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
diff --git a/rogue-thi-app/.gitignore b/rogue-thi-app/.gitignore
index 1d8f1cec..4d260615 100644
--- a/rogue-thi-app/.gitignore
+++ b/rogue-thi-app/.gitignore
@@ -40,6 +40,7 @@ yarn-error.log*
# yarn ignore, they use npm ;/
yarn.lock
+/.yarn
# capacitor
/ios
diff --git a/rogue-thi-app/README.md b/rogue-thi-app/README.md
index 4b5c883b..1838c758 100644
--- a/rogue-thi-app/README.md
+++ b/rogue-thi-app/README.md
@@ -2,7 +2,18 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Getting Started
-First, run the development server:
+First, install the dependencies:
+
+```bash
+npm install
+# or
+yarn
+```
+
+> **Note**
+> For node.js versions 17 and above, you need to set the environment variable `NODE_OPTIONS=--openssl-legacy-provider` to avoid an error when installing the dependencies.
+
+Run the development server:
```bash
npm run dev
diff --git a/rogue-thi-app/package.json b/rogue-thi-app/package.json
index a4bd2457..9cbe91fe 100644
--- a/rogue-thi-app/package.json
+++ b/rogue-thi-app/package.json
@@ -46,7 +46,7 @@
"react-swipeable-views": "^0.14.0",
"react-swipeable-views-utils": "^0.14.0",
"sass": "^1.45.1",
- "win95.css": "github:neuland-ingolstadt/win95.css#477a223",
+ "win95.css": "github:neuland-ingolstadt/win95.css#commit=477a22394b61b57b20ee2c3a83b769c9a6dc8e46",
"xml-js": "^1.6.11"
},
"devDependencies": {
From 15c87030e979ba6633790e36ee09906577eea8f5 Mon Sep 17 00:00:00 2001
From: Robert Eggl
Date: Mon, 24 Jul 2023 14:07:35 +0200
Subject: [PATCH 24/34] fixes previous pr #301 (#314)
---
CONTRIBUTING.md | 7 +++++++
.yarnrc.yml => rogue-thi-app/.yarnrc.yml | 0
2 files changed, 7 insertions(+)
rename .yarnrc.yml => rogue-thi-app/.yarnrc.yml (100%)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 54c27958..e02314fe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,8 +28,13 @@ cd THI-App/rogue-thi-app
Download everything else that is required to run the app:
```bash
npm install
+# or
+yarn
```
+> **Note**
+> For node.js versions 17 and above, you need to set the environment variable `NODE_OPTIONS=--openssl-legacy-provider` to avoid an error when installing the dependencies.
+
Now open the `rogue-thi-app` folder in Visual Studio Code.
## Developing
@@ -37,6 +42,8 @@ Now open the `rogue-thi-app` folder in Visual Studio Code.
Start the application locally:
```bash
npm run dev
+# or
+yarn dev
```
Go to https://localhost:3000 in your browser and voilà, you're ready to go. You can change things in your code and your browser will automatically load the new version.
diff --git a/.yarnrc.yml b/rogue-thi-app/.yarnrc.yml
similarity index 100%
rename from .yarnrc.yml
rename to rogue-thi-app/.yarnrc.yml
From 307f07ff6ff65563bb2fda3e9382655ac635dd65 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Thu, 27 Jul 2023 17:25:01 +0200
Subject: [PATCH 25/34] =?UTF-8?q?=F0=9F=9A=B8=20Improve=20FoodModal=20(#31?=
=?UTF-8?q?5)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 💄 simplify navbar gradient
* 🐛 fix FoodTab border overlapping into next day
* 🚸 Improve FoodModal
---
rogue-thi-app/lib/backend-utils/food-utils.js | 20 +-
rogue-thi-app/pages/food.js | 247 +++++++++++-------
rogue-thi-app/public/locales/de/food.json | 8 +-
rogue-thi-app/public/locales/en/food.json | 8 +-
rogue-thi-app/scss/themes/thi-light.scss | 4 +-
rogue-thi-app/styles/Mensa.module.css | 10 +-
6 files changed, 194 insertions(+), 103 deletions(-)
diff --git a/rogue-thi-app/lib/backend-utils/food-utils.js b/rogue-thi-app/lib/backend-utils/food-utils.js
index 1593e14e..ca5d804a 100644
--- a/rogue-thi-app/lib/backend-utils/food-utils.js
+++ b/rogue-thi-app/lib/backend-utils/food-utils.js
@@ -76,8 +76,10 @@ export async function loadFoodEntries (restaurants) {
* @returns {string[]} Cleaned meal flags
**/
function cleanMealFlags (flags) {
+ if (!flags) return null
+
// find contradictions
- const contradictions = flags?.filter(x => flagContradictions[x]?.some(y => flags?.includes(y))) || []
+ const contradictions = flags.filter(x => flagContradictions[x]?.some(y => flags?.includes(y)))
// remove contradictions
flags = flags?.filter(x => !contradictions.includes(x)) || []
@@ -85,6 +87,18 @@ function cleanMealFlags (flags) {
return flags
}
+/**
+ * Capitalizes the first letter of the meal names
+ * @param {object} mealNames Meal names
+ * @returns {object} Capitalized meal names
+ * @example { de: 'veganer burger', en: 'vegan burger' } => { de: 'Veganer burger', en: 'Vegan burger' }
+ */
+function capitalize (mealNames) {
+ return Object.fromEntries(Object.entries(mealNames).map(([key, value]) =>
+ [key, value.charAt(0).toUpperCase() + value.slice(1)]
+ ))
+}
+
/**
* Unifies the meal plan entries to a common format
* @param {object[]} entries Meal plan entries
@@ -94,7 +108,7 @@ export function unifyFoodEntries (entries) {
return entries.map(entry => ({
timestamp: entry.timestamp,
meals: entry.meals.map(meal => ({
- name: meal.name,
+ name: capitalize(meal.name),
category: meal.category,
prices: meal.prices || {
student: null,
@@ -102,7 +116,7 @@ export function unifyFoodEntries (entries) {
guest: null
},
allergens: meal.allergens || null,
- flags: cleanMealFlags(meal.flags) || [],
+ flags: cleanMealFlags(meal.flags),
nutrition: meal.nutrition || null,
variations: meal.variations || [],
originalLanguage: meal.originalLanguage || 'de',
diff --git a/rogue-thi-app/pages/food.js b/rogue-thi-app/pages/food.js
index 5acbebaa..7015f01f 100644
--- a/rogue-thi-app/pages/food.js
+++ b/rogue-thi-app/pages/food.js
@@ -6,7 +6,7 @@ import Modal from 'react-bootstrap/Modal'
import Nav from 'react-bootstrap/Nav'
import ReactPlaceholder from 'react-placeholder'
-import { faChevronLeft, faChevronRight, faExclamationTriangle, faFilter, faHeartCircleCheck, faUtensils, faWandMagicSparkles } from '@fortawesome/free-solid-svg-icons'
+import { faChevronLeft, faChevronRight, faExclamationTriangle, faFilter, faHeartCircleCheck, faUtensils } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import AppBody from '../components/page/AppBody'
@@ -139,30 +139,48 @@ export default function Mensa () {
return (new Intl.NumberFormat(getAdjustedLocale(), { minimumFractionDigits: 1, maximumFractionDigits: 2 })).format(x)
}
+ /**
+ * Renders styled ListGroup.Item
+ * @param {string} title
+ * @param {string} content
+ * @param {string} key
+ * @returns {JSX.Element}
+ */
+ function ListGroupItem ({ title, content, key }) {
+ return (
+
+
+
+ {title}
+
+
+
+ {content}
+
+
+
+ )
+ }
+
/**
* Renders the variations of a meal in a list.
* @param {object} meal
* @returns {JSX.Element}
**/
function renderFoodVariations (meal) {
- return meal?.variations?.length > 0 && (
- {meal?.variations?.map((variant, idx) => (
-
-
-
- {variant.name[i18n.languages[0]]}
-
-
-
- {`${variant.additional ? '+ ' : ''}${getUserSpecificPrice(variant)}`}
-
-
-
-
- ))}
- )
+ return meal?.variations?.length > 0 && (
+
+
+ {meal?.variations?.map((variant, idx) => (
+
+ ))}
+
+
+ )
}
/**
@@ -245,7 +263,7 @@ export default function Mensa () {
const noData = mensa.length === 0 && reimanns.length === 0 && canisius.length === 0
return (
-
+
{mensa.length > 0 && (
<>
{t('list.titles.cafeteria')}
@@ -364,8 +382,12 @@ export default function Mensa () {
{showMealDetails?.name[i18n.languages[0]]}
- {t('foodModal.flags.title')}
- {showMealDetails?.flags === null && `${t('foodModal.flags.unknown')}`}
+ {t('foodModal.flags.title')}
+ {showMealDetails?.flags === null && (
+
+ {t('foodModal.flags.unknown')}
+
+ )}
{showMealDetails?.flags?.length === 0 && `${t('foodModal.flags.empty')}`}
{showMealDetails?.flags?.map(flag => (
@@ -383,8 +405,12 @@ export default function Mensa () {
))}
- {t('foodModal.allergens.title')}
- {showMealDetails?.allergens === null && `${t('foodModal.allergens.unknown')}`}
+ {t('foodModal.allergens.title')}
+ {showMealDetails?.allergens === null && (
+
+ {t('foodModal.allergens.unknown')}
+
+ )}
{showMealDetails?.allergens?.length === 0 && `${t('foodModal.flags.empty')}`}
{showMealDetails?.allergens?.map(key => (
@@ -403,88 +429,123 @@ export default function Mensa () {
))}
- {t('foodModal.nutrition.title')}
+ {t('foodModal.nutrition.title')}
{(showMealDetails?.nutrition && (
-
-
-
- {t('foodModal.nutrition.energy.title')} :{' '}
- {showMealDetails?.nutrition.kj ? showMealDetails?.nutrition.kj + ' kJ' : ''} /
- {showMealDetails?.nutrition.kcal ? showMealDetails?.nutrition.kcal + ' kcal' : ''}
-
-
- {t('foodModal.nutrition.fat.title')} :{' '}
- {formatGram(showMealDetails?.nutrition.fat)}
- {t('foodModal.nutrition.fat.saturated')} : {formatGram(showMealDetails?.nutrition.fatSaturated)}
-
-
- {t('foodModal.nutrition.carbohydrates.title')} :{' '}
- {formatGram(showMealDetails?.nutrition.carbs)}
- {t('foodModal.nutrition.carbohydrates.sugar')} : {formatGram(showMealDetails?.nutrition.sugar)}
-
-
- {t('foodModal.nutrition.fiber.title')} :{' '}
- {formatGram(showMealDetails?.nutrition.fiber)}
-
-
- {t('foodModal.nutrition.protein.title')} :{' '}
- {formatGram(showMealDetails?.nutrition.protein)}
-
-
- {t('foodModal.nutrition.salt.title')} :{' '}
- {formatGram(showMealDetails?.nutrition.salt)}
-
- )) || (
- {t('foodModal.nutrition.unknown.title')}
+
+
+
+
+ {t('foodModal.nutrition.fat.title')}
+ {t('foodModal.nutrition.fat.saturated')}
+ >
+ )}
+ content={(
+ <>
+ {formatGram(showMealDetails?.nutrition.fat)}
+ {formatGram(showMealDetails?.nutrition.fatSaturated)}
+ >
+ )}
+ key={'fat'}
+ />
+
+ {t('foodModal.nutrition.carbohydrates.title')}
+ {t('foodModal.nutrition.carbohydrates.sugar')}
+ >
+ )}
+ content={(
+ <>
+ {formatGram(showMealDetails?.nutrition.carbs)}
+ {formatGram(showMealDetails?.nutrition.sugar)}
+ >
+ )}
+ key={'fat'}
+ />
+
+
+
+
+
+ )) || (
+
+ {t('foodModal.allergens.unknown')}
+
)}
{t('foodModal.prices.title')}
-
-
- {t('foodModal.prices.students')} :{' '}
- {formatPrice(showMealDetails?.prices.student)}
-
-
- {t('foodModal.prices.employees')} :{' '}
- {formatPrice(showMealDetails?.prices.employee)}
-
-
- {t('foodModal.prices.guests')} :{' '}
- {formatPrice(showMealDetails?.prices.guest)}
-
-
+
+
+ {showMealDetails?.prices.student !== null && (
+
+ )}
+ {showMealDetails?.prices.student !== null && (
+
+ )}
+ {showMealDetails?.prices.student !== null && (
+
+ )}
+
+ {(showMealDetails?.prices.student === null && showMealDetails?.prices.employee === null && showMealDetails?.prices.guest === null) && (
+
+ {t('foodModal.prices.unknown')}
+
+ )}
+
{/* Variations of meal */}
+ {showMealDetails?.variations?.length > 0 && (
+ {t('foodModal.variations.title')}
+ )}
{renderFoodVariations(showMealDetails)}
- {showMealDetails?.variations?.length > 0 && ( )}
- {t('foodModal.warning.title')}
-
- {t('foodModal.warning.text')}
+
{t('foodModal.warning.title')}
+ {`${isTranslated(showMealDetails) && `${t('foodModal.translation.warning')} `}${t('foodModal.warning.text')}`}
{isTranslated(showMealDetails) && (
-
-
- {` ${t('foodModal.translation.title')}`}
-
-
- {` ${t('foodModal.translation.warning')}`}
-
-
-
-
- {t('foodModal.translation.originalName')} :{' '}
- {showMealDetails?.name[showMealDetails?.originalLanguage]}
-
-
- {t('foodModal.translation.translatedName')} :{' '}
- {showMealDetails?.name[i18n.languages[0]]}
-
-
-
+
+
+ {t('foodModal.translation.originalName')} :{' '}
+ {showMealDetails?.name[showMealDetails?.originalLanguage]}
+
+
+ {t('foodModal.translation.translatedName')} :{' '}
+ {showMealDetails?.name[i18n.languages[0]]}
+
+
)}
diff --git a/rogue-thi-app/public/locales/de/food.json b/rogue-thi-app/public/locales/de/food.json
index 5f0a290d..e8b24081 100644
--- a/rogue-thi-app/public/locales/de/food.json
+++ b/rogue-thi-app/public/locales/de/food.json
@@ -62,7 +62,8 @@
"title": "Preise",
"students": "Studierende",
"employees": "Mitarbeitende",
- "guests": "Gäste"
+ "guests": "Gäste",
+ "unknown": "Unbekannt"
},
"warning": {
"title": "Angaben ohne Gewähr.",
@@ -70,9 +71,12 @@
},
"translation": {
"title": "Übersetzung",
- "warning": "Dieses Gericht wurde automatisch übersetzt. Bitte überprüfen Sie die Informationen auf den Informationsbildschirmen, bevor du etwas konsumierst.",
+ "warning": "Dieses Gericht wurde automatisch übersetzt.",
"originalName": "Originalname",
"translatedName": "Übersetzter Name"
+ },
+ "variations": {
+ "title": "Varianten"
}
}
}
\ No newline at end of file
diff --git a/rogue-thi-app/public/locales/en/food.json b/rogue-thi-app/public/locales/en/food.json
index 42c01541..af7cdb3d 100644
--- a/rogue-thi-app/public/locales/en/food.json
+++ b/rogue-thi-app/public/locales/en/food.json
@@ -61,7 +61,8 @@
"title": "Prices",
"students": "Students",
"employees": "Employees",
- "guests": "Guests"
+ "guests": "Guests",
+ "unknown": "Unknown"
},
"warning": {
"title": "Disclaimer",
@@ -69,9 +70,12 @@
},
"translation": {
"title": "Translation",
- "warning": "This meal has been automatically translated. Please verify the information on the information screens before consuming anything.",
+ "warning": "This meal has been automatically translated.",
"originalName": "Original name",
"translatedName": "Translated name"
+ },
+ "variations": {
+ "title": "Variations"
}
}
}
\ No newline at end of file
diff --git a/rogue-thi-app/scss/themes/thi-light.scss b/rogue-thi-app/scss/themes/thi-light.scss
index ad19ddd4..6691f0a0 100644
--- a/rogue-thi-app/scss/themes/thi-light.scss
+++ b/rogue-thi-app/scss/themes/thi-light.scss
@@ -1,6 +1,6 @@
$theme-colors: (
"primary": #005a9b,
- "secondary": #009bcd
+ "secondary": #3a93c3
);
@import "../../node_modules/bootstrap/scss/bootstrap";
@@ -12,7 +12,7 @@ $theme-colors: (
}
:root {
- --navbar-border-image: linear-gradient(90deg, #005a9b, #228cc1, #3a93c3, #6fa2c7, #31ccff);
+ --navbar-border-image: linear-gradient(90deg, #005a9b, #00b1ec);
--tabbar-tab-active: #005a9b;
// --navbar-border-image: none;
}
diff --git a/rogue-thi-app/styles/Mensa.module.css b/rogue-thi-app/styles/Mensa.module.css
index ceff092c..d782e96d 100644
--- a/rogue-thi-app/styles/Mensa.module.css
+++ b/rogue-thi-app/styles/Mensa.module.css
@@ -122,9 +122,17 @@
.modalTitle {
padding-bottom: 5px;
margin-bottom: 10px;
- border-bottom: 3px solid var(--primary);
+ border-bottom: 1px solid var(--secondary);
}
.icon {
margin-right: 6px;
}
+
+.container {
+ width: calc(100% - 1px);
+}
+
+.unknown {
+ color: var(--gray);
+}
From d1d8681675c687b1549d5f67de48dd2335fe3fca Mon Sep 17 00:00:00 2001
From: Robert Eggl
Date: Fri, 28 Jul 2023 14:49:37 +0200
Subject: [PATCH 26/34] refactor(app): moves to jsx files (#316)
---
rogue-thi-app/android/.gitkeep | 0
rogue-thi-app/components/{RoomMap.js => RoomMap.jsx} | 0
rogue-thi-app/components/{SwipeableTabs.js => SwipeableTabs.jsx} | 0
.../components/{TheMatrixAnimation.js => TheMatrixAnimation.jsx} | 0
rogue-thi-app/components/{allCards.js => allCards.jsx} | 0
rogue-thi-app/components/cards/{BaseCard.js => BaseCard.jsx} | 0
.../components/cards/{CalendarCard.js => CalendarCard.jsx} | 0
.../components/cards/{ElectionPrompt.js => ElectionPrompt.jsx} | 0
rogue-thi-app/components/cards/{EventsCard.js => EventsCard.jsx} | 0
rogue-thi-app/components/cards/{ExamsCard.js => ExamsCard.jsx} | 0
rogue-thi-app/components/cards/{FoodCard.js => FoodCard.jsx} | 0
.../components/cards/{InstallPrompt.js => InstallPrompt.jsx} | 0
.../components/cards/{MobilityCard.js => MobilityCard.jsx} | 0
rogue-thi-app/components/cards/{RoomCard.js => RoomCard.jsx} | 0
.../components/cards/{SurveyPrompt.js => SurveyPrompt.jsx} | 0
.../components/cards/{TimetableCard.js => TimetableCard.jsx} | 0
.../components/modal/{DashboardModal.js => DashboardModal.jsx} | 0
.../components/modal/{FilterFoodModal.js => FilterFoodModal.jsx} | 0
.../components/modal/{LanguageModal.js => LanguageModal.jsx} | 0
.../modal/{PersonalDataModal.js => PersonalDataModal.jsx} | 0
rogue-thi-app/components/modal/{ThemeModal.js => ThemeModal.jsx} | 0
rogue-thi-app/components/page/{AppBody.js => AppBody.jsx} | 0
.../components/page/{AppContainer.js => AppContainer.jsx} | 0
rogue-thi-app/components/page/{AppNavbar.js => AppNavbar.jsx} | 0
rogue-thi-app/components/page/{AppTabbar.js => AppTabbar.jsx} | 0
.../lib/backend-utils/{mobility-utils.js => mobility-utils.jsx} | 0
rogue-thi-app/lib/{date-utils.js => date-utils.jsx} | 0
rogue-thi-app/pages/{_app.js => _app.jsx} | 0
rogue-thi-app/pages/{become-hackerman.js => become-hackerman.jsx} | 0
rogue-thi-app/pages/{calendar.js => calendar.jsx} | 0
rogue-thi-app/pages/{debug.js => debug.jsx} | 0
rogue-thi-app/pages/{events.js => events.jsx} | 0
rogue-thi-app/pages/{food.js => food.jsx} | 0
rogue-thi-app/pages/{grades.js => grades.jsx} | 0
rogue-thi-app/pages/{imprint.js => imprint.jsx} | 0
rogue-thi-app/pages/{index.js => index.jsx} | 0
rogue-thi-app/pages/{lecturers.js => lecturers.jsx} | 0
rogue-thi-app/pages/{library.js => library.jsx} | 0
rogue-thi-app/pages/{login.js => login.jsx} | 0
rogue-thi-app/pages/{mobility.js => mobility.jsx} | 0
rogue-thi-app/pages/{personal.js => personal.jsx} | 0
rogue-thi-app/pages/rooms/{index.js => index.jsx} | 0
rogue-thi-app/pages/rooms/{list.js => list.jsx} | 0
rogue-thi-app/pages/rooms/{search.js => search.jsx} | 0
rogue-thi-app/pages/rooms/{suggestions.js => suggestions.jsx} | 0
rogue-thi-app/pages/{timetable.js => timetable.jsx} | 0
46 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 rogue-thi-app/android/.gitkeep
rename rogue-thi-app/components/{RoomMap.js => RoomMap.jsx} (100%)
rename rogue-thi-app/components/{SwipeableTabs.js => SwipeableTabs.jsx} (100%)
rename rogue-thi-app/components/{TheMatrixAnimation.js => TheMatrixAnimation.jsx} (100%)
rename rogue-thi-app/components/{allCards.js => allCards.jsx} (100%)
rename rogue-thi-app/components/cards/{BaseCard.js => BaseCard.jsx} (100%)
rename rogue-thi-app/components/cards/{CalendarCard.js => CalendarCard.jsx} (100%)
rename rogue-thi-app/components/cards/{ElectionPrompt.js => ElectionPrompt.jsx} (100%)
rename rogue-thi-app/components/cards/{EventsCard.js => EventsCard.jsx} (100%)
rename rogue-thi-app/components/cards/{ExamsCard.js => ExamsCard.jsx} (100%)
rename rogue-thi-app/components/cards/{FoodCard.js => FoodCard.jsx} (100%)
rename rogue-thi-app/components/cards/{InstallPrompt.js => InstallPrompt.jsx} (100%)
rename rogue-thi-app/components/cards/{MobilityCard.js => MobilityCard.jsx} (100%)
rename rogue-thi-app/components/cards/{RoomCard.js => RoomCard.jsx} (100%)
rename rogue-thi-app/components/cards/{SurveyPrompt.js => SurveyPrompt.jsx} (100%)
rename rogue-thi-app/components/cards/{TimetableCard.js => TimetableCard.jsx} (100%)
rename rogue-thi-app/components/modal/{DashboardModal.js => DashboardModal.jsx} (100%)
rename rogue-thi-app/components/modal/{FilterFoodModal.js => FilterFoodModal.jsx} (100%)
rename rogue-thi-app/components/modal/{LanguageModal.js => LanguageModal.jsx} (100%)
rename rogue-thi-app/components/modal/{PersonalDataModal.js => PersonalDataModal.jsx} (100%)
rename rogue-thi-app/components/modal/{ThemeModal.js => ThemeModal.jsx} (100%)
rename rogue-thi-app/components/page/{AppBody.js => AppBody.jsx} (100%)
rename rogue-thi-app/components/page/{AppContainer.js => AppContainer.jsx} (100%)
rename rogue-thi-app/components/page/{AppNavbar.js => AppNavbar.jsx} (100%)
rename rogue-thi-app/components/page/{AppTabbar.js => AppTabbar.jsx} (100%)
rename rogue-thi-app/lib/backend-utils/{mobility-utils.js => mobility-utils.jsx} (100%)
rename rogue-thi-app/lib/{date-utils.js => date-utils.jsx} (100%)
rename rogue-thi-app/pages/{_app.js => _app.jsx} (100%)
rename rogue-thi-app/pages/{become-hackerman.js => become-hackerman.jsx} (100%)
rename rogue-thi-app/pages/{calendar.js => calendar.jsx} (100%)
rename rogue-thi-app/pages/{debug.js => debug.jsx} (100%)
rename rogue-thi-app/pages/{events.js => events.jsx} (100%)
rename rogue-thi-app/pages/{food.js => food.jsx} (100%)
rename rogue-thi-app/pages/{grades.js => grades.jsx} (100%)
rename rogue-thi-app/pages/{imprint.js => imprint.jsx} (100%)
rename rogue-thi-app/pages/{index.js => index.jsx} (100%)
rename rogue-thi-app/pages/{lecturers.js => lecturers.jsx} (100%)
rename rogue-thi-app/pages/{library.js => library.jsx} (100%)
rename rogue-thi-app/pages/{login.js => login.jsx} (100%)
rename rogue-thi-app/pages/{mobility.js => mobility.jsx} (100%)
rename rogue-thi-app/pages/{personal.js => personal.jsx} (100%)
rename rogue-thi-app/pages/rooms/{index.js => index.jsx} (100%)
rename rogue-thi-app/pages/rooms/{list.js => list.jsx} (100%)
rename rogue-thi-app/pages/rooms/{search.js => search.jsx} (100%)
rename rogue-thi-app/pages/rooms/{suggestions.js => suggestions.jsx} (100%)
rename rogue-thi-app/pages/{timetable.js => timetable.jsx} (100%)
diff --git a/rogue-thi-app/android/.gitkeep b/rogue-thi-app/android/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/rogue-thi-app/components/RoomMap.js b/rogue-thi-app/components/RoomMap.jsx
similarity index 100%
rename from rogue-thi-app/components/RoomMap.js
rename to rogue-thi-app/components/RoomMap.jsx
diff --git a/rogue-thi-app/components/SwipeableTabs.js b/rogue-thi-app/components/SwipeableTabs.jsx
similarity index 100%
rename from rogue-thi-app/components/SwipeableTabs.js
rename to rogue-thi-app/components/SwipeableTabs.jsx
diff --git a/rogue-thi-app/components/TheMatrixAnimation.js b/rogue-thi-app/components/TheMatrixAnimation.jsx
similarity index 100%
rename from rogue-thi-app/components/TheMatrixAnimation.js
rename to rogue-thi-app/components/TheMatrixAnimation.jsx
diff --git a/rogue-thi-app/components/allCards.js b/rogue-thi-app/components/allCards.jsx
similarity index 100%
rename from rogue-thi-app/components/allCards.js
rename to rogue-thi-app/components/allCards.jsx
diff --git a/rogue-thi-app/components/cards/BaseCard.js b/rogue-thi-app/components/cards/BaseCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/BaseCard.js
rename to rogue-thi-app/components/cards/BaseCard.jsx
diff --git a/rogue-thi-app/components/cards/CalendarCard.js b/rogue-thi-app/components/cards/CalendarCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/CalendarCard.js
rename to rogue-thi-app/components/cards/CalendarCard.jsx
diff --git a/rogue-thi-app/components/cards/ElectionPrompt.js b/rogue-thi-app/components/cards/ElectionPrompt.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/ElectionPrompt.js
rename to rogue-thi-app/components/cards/ElectionPrompt.jsx
diff --git a/rogue-thi-app/components/cards/EventsCard.js b/rogue-thi-app/components/cards/EventsCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/EventsCard.js
rename to rogue-thi-app/components/cards/EventsCard.jsx
diff --git a/rogue-thi-app/components/cards/ExamsCard.js b/rogue-thi-app/components/cards/ExamsCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/ExamsCard.js
rename to rogue-thi-app/components/cards/ExamsCard.jsx
diff --git a/rogue-thi-app/components/cards/FoodCard.js b/rogue-thi-app/components/cards/FoodCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/FoodCard.js
rename to rogue-thi-app/components/cards/FoodCard.jsx
diff --git a/rogue-thi-app/components/cards/InstallPrompt.js b/rogue-thi-app/components/cards/InstallPrompt.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/InstallPrompt.js
rename to rogue-thi-app/components/cards/InstallPrompt.jsx
diff --git a/rogue-thi-app/components/cards/MobilityCard.js b/rogue-thi-app/components/cards/MobilityCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/MobilityCard.js
rename to rogue-thi-app/components/cards/MobilityCard.jsx
diff --git a/rogue-thi-app/components/cards/RoomCard.js b/rogue-thi-app/components/cards/RoomCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/RoomCard.js
rename to rogue-thi-app/components/cards/RoomCard.jsx
diff --git a/rogue-thi-app/components/cards/SurveyPrompt.js b/rogue-thi-app/components/cards/SurveyPrompt.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/SurveyPrompt.js
rename to rogue-thi-app/components/cards/SurveyPrompt.jsx
diff --git a/rogue-thi-app/components/cards/TimetableCard.js b/rogue-thi-app/components/cards/TimetableCard.jsx
similarity index 100%
rename from rogue-thi-app/components/cards/TimetableCard.js
rename to rogue-thi-app/components/cards/TimetableCard.jsx
diff --git a/rogue-thi-app/components/modal/DashboardModal.js b/rogue-thi-app/components/modal/DashboardModal.jsx
similarity index 100%
rename from rogue-thi-app/components/modal/DashboardModal.js
rename to rogue-thi-app/components/modal/DashboardModal.jsx
diff --git a/rogue-thi-app/components/modal/FilterFoodModal.js b/rogue-thi-app/components/modal/FilterFoodModal.jsx
similarity index 100%
rename from rogue-thi-app/components/modal/FilterFoodModal.js
rename to rogue-thi-app/components/modal/FilterFoodModal.jsx
diff --git a/rogue-thi-app/components/modal/LanguageModal.js b/rogue-thi-app/components/modal/LanguageModal.jsx
similarity index 100%
rename from rogue-thi-app/components/modal/LanguageModal.js
rename to rogue-thi-app/components/modal/LanguageModal.jsx
diff --git a/rogue-thi-app/components/modal/PersonalDataModal.js b/rogue-thi-app/components/modal/PersonalDataModal.jsx
similarity index 100%
rename from rogue-thi-app/components/modal/PersonalDataModal.js
rename to rogue-thi-app/components/modal/PersonalDataModal.jsx
diff --git a/rogue-thi-app/components/modal/ThemeModal.js b/rogue-thi-app/components/modal/ThemeModal.jsx
similarity index 100%
rename from rogue-thi-app/components/modal/ThemeModal.js
rename to rogue-thi-app/components/modal/ThemeModal.jsx
diff --git a/rogue-thi-app/components/page/AppBody.js b/rogue-thi-app/components/page/AppBody.jsx
similarity index 100%
rename from rogue-thi-app/components/page/AppBody.js
rename to rogue-thi-app/components/page/AppBody.jsx
diff --git a/rogue-thi-app/components/page/AppContainer.js b/rogue-thi-app/components/page/AppContainer.jsx
similarity index 100%
rename from rogue-thi-app/components/page/AppContainer.js
rename to rogue-thi-app/components/page/AppContainer.jsx
diff --git a/rogue-thi-app/components/page/AppNavbar.js b/rogue-thi-app/components/page/AppNavbar.jsx
similarity index 100%
rename from rogue-thi-app/components/page/AppNavbar.js
rename to rogue-thi-app/components/page/AppNavbar.jsx
diff --git a/rogue-thi-app/components/page/AppTabbar.js b/rogue-thi-app/components/page/AppTabbar.jsx
similarity index 100%
rename from rogue-thi-app/components/page/AppTabbar.js
rename to rogue-thi-app/components/page/AppTabbar.jsx
diff --git a/rogue-thi-app/lib/backend-utils/mobility-utils.js b/rogue-thi-app/lib/backend-utils/mobility-utils.jsx
similarity index 100%
rename from rogue-thi-app/lib/backend-utils/mobility-utils.js
rename to rogue-thi-app/lib/backend-utils/mobility-utils.jsx
diff --git a/rogue-thi-app/lib/date-utils.js b/rogue-thi-app/lib/date-utils.jsx
similarity index 100%
rename from rogue-thi-app/lib/date-utils.js
rename to rogue-thi-app/lib/date-utils.jsx
diff --git a/rogue-thi-app/pages/_app.js b/rogue-thi-app/pages/_app.jsx
similarity index 100%
rename from rogue-thi-app/pages/_app.js
rename to rogue-thi-app/pages/_app.jsx
diff --git a/rogue-thi-app/pages/become-hackerman.js b/rogue-thi-app/pages/become-hackerman.jsx
similarity index 100%
rename from rogue-thi-app/pages/become-hackerman.js
rename to rogue-thi-app/pages/become-hackerman.jsx
diff --git a/rogue-thi-app/pages/calendar.js b/rogue-thi-app/pages/calendar.jsx
similarity index 100%
rename from rogue-thi-app/pages/calendar.js
rename to rogue-thi-app/pages/calendar.jsx
diff --git a/rogue-thi-app/pages/debug.js b/rogue-thi-app/pages/debug.jsx
similarity index 100%
rename from rogue-thi-app/pages/debug.js
rename to rogue-thi-app/pages/debug.jsx
diff --git a/rogue-thi-app/pages/events.js b/rogue-thi-app/pages/events.jsx
similarity index 100%
rename from rogue-thi-app/pages/events.js
rename to rogue-thi-app/pages/events.jsx
diff --git a/rogue-thi-app/pages/food.js b/rogue-thi-app/pages/food.jsx
similarity index 100%
rename from rogue-thi-app/pages/food.js
rename to rogue-thi-app/pages/food.jsx
diff --git a/rogue-thi-app/pages/grades.js b/rogue-thi-app/pages/grades.jsx
similarity index 100%
rename from rogue-thi-app/pages/grades.js
rename to rogue-thi-app/pages/grades.jsx
diff --git a/rogue-thi-app/pages/imprint.js b/rogue-thi-app/pages/imprint.jsx
similarity index 100%
rename from rogue-thi-app/pages/imprint.js
rename to rogue-thi-app/pages/imprint.jsx
diff --git a/rogue-thi-app/pages/index.js b/rogue-thi-app/pages/index.jsx
similarity index 100%
rename from rogue-thi-app/pages/index.js
rename to rogue-thi-app/pages/index.jsx
diff --git a/rogue-thi-app/pages/lecturers.js b/rogue-thi-app/pages/lecturers.jsx
similarity index 100%
rename from rogue-thi-app/pages/lecturers.js
rename to rogue-thi-app/pages/lecturers.jsx
diff --git a/rogue-thi-app/pages/library.js b/rogue-thi-app/pages/library.jsx
similarity index 100%
rename from rogue-thi-app/pages/library.js
rename to rogue-thi-app/pages/library.jsx
diff --git a/rogue-thi-app/pages/login.js b/rogue-thi-app/pages/login.jsx
similarity index 100%
rename from rogue-thi-app/pages/login.js
rename to rogue-thi-app/pages/login.jsx
diff --git a/rogue-thi-app/pages/mobility.js b/rogue-thi-app/pages/mobility.jsx
similarity index 100%
rename from rogue-thi-app/pages/mobility.js
rename to rogue-thi-app/pages/mobility.jsx
diff --git a/rogue-thi-app/pages/personal.js b/rogue-thi-app/pages/personal.jsx
similarity index 100%
rename from rogue-thi-app/pages/personal.js
rename to rogue-thi-app/pages/personal.jsx
diff --git a/rogue-thi-app/pages/rooms/index.js b/rogue-thi-app/pages/rooms/index.jsx
similarity index 100%
rename from rogue-thi-app/pages/rooms/index.js
rename to rogue-thi-app/pages/rooms/index.jsx
diff --git a/rogue-thi-app/pages/rooms/list.js b/rogue-thi-app/pages/rooms/list.jsx
similarity index 100%
rename from rogue-thi-app/pages/rooms/list.js
rename to rogue-thi-app/pages/rooms/list.jsx
diff --git a/rogue-thi-app/pages/rooms/search.js b/rogue-thi-app/pages/rooms/search.jsx
similarity index 100%
rename from rogue-thi-app/pages/rooms/search.js
rename to rogue-thi-app/pages/rooms/search.jsx
diff --git a/rogue-thi-app/pages/rooms/suggestions.js b/rogue-thi-app/pages/rooms/suggestions.jsx
similarity index 100%
rename from rogue-thi-app/pages/rooms/suggestions.js
rename to rogue-thi-app/pages/rooms/suggestions.jsx
diff --git a/rogue-thi-app/pages/timetable.js b/rogue-thi-app/pages/timetable.jsx
similarity index 100%
rename from rogue-thi-app/pages/timetable.js
rename to rogue-thi-app/pages/timetable.jsx
From 034cf74370f53df9308c086e654ed5d2367fad41 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Fri, 28 Jul 2023 16:15:39 +0200
Subject: [PATCH 27/34] =?UTF-8?q?=F0=9F=93=9D=20update=20docs=20(#317)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CONTRIBUTING.md | 8 +-
README.md | 2 +-
docs/data-security-de.md | 13 +-
docs/data-security.md | 16 +-
docs/mensa-booking-api.md | 439 +++++++++++++++++++-------------------
docs/thi-rest-api.md | 171 ++++++++-------
rogue-thi-app/README.md | 41 ----
7 files changed, 346 insertions(+), 344 deletions(-)
delete mode 100644 rogue-thi-app/README.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e02314fe..a16a0352 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,12 +20,14 @@ You need to install the necessary tools.
Make a personal copy of the source code using the *Fork* button on the top right of this page.
Open a terminal (e.g. PowerShell if you're on Windows) and download the source code to your PC using Git:
+
```bash
git clone https://github.com/your-github-username/THI-App.git
cd THI-App/rogue-thi-app
```
Download everything else that is required to run the app:
+
```bash
npm install
# or
@@ -40,17 +42,19 @@ Now open the `rogue-thi-app` folder in Visual Studio Code.
## Developing
Start the application locally:
+
```bash
npm run dev
# or
yarn dev
```
-Go to https://localhost:3000 in your browser and voilà, you're ready to go. You can change things in your code and your browser will automatically load the new version.
+Go to [https://localhost:3000](https://localhost:3000) in your browser and voilà, you're ready to go. You can change things in your code and your browser will automatically load the new version.
### Structure of the code
A quick overview over the structure of the source code:
+
* `/pages`
The pages of the application that you see in your browser. This is where to look if you want to change the structure of the UI.
* `/components`
@@ -62,4 +66,4 @@ Various reusable utilities such as a client for the THI API and a timestamp form
## Getting help
-If you need help with the code, don't hesitate to contact us at info@neuland-ingolstadt.de.
+If you need help with the code, don't hesitate to contact us at [info@neuland-ingolstadt.de](mailto:info@neuland-ingolstadt.de).
diff --git a/README.md b/README.md
index 4df907fc..73d1a353 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ You can find more information on this in [this document](docs/data-security.md).
Currently the documentation consists of a list of valid requests and examples
of their responses.
You can view [the full list here](docs/thi-rest-api.md). This documentation was
-created manually by using the official app and logging requests with mitmproxy.
+created manually by using the official app and logging requests with `mitmproxy`.
The app also includes a [debugging tool](https://neuland.app/debug) which handles session generation
and API communication.
diff --git a/docs/data-security-de.md b/docs/data-security-de.md
index 28bdc700..b64f8a79 100644
--- a/docs/data-security-de.md
+++ b/docs/data-security-de.md
@@ -1,14 +1,18 @@
# Neuland App Datensicherheit
+
TL;DR: Nur du und die THI Haben Zugriff auf dein Passwort sowie deine
persönlichen Daten.
-### Speichert ihr meine Daten?
+## Speichert ihr meine Daten?
+
Nur lokal auf deinem Gerät.
-### Klaut ihr mein Passwort?
+## Klaut ihr mein Passwort?
+
Wir haben keinen Zugriff auf dein Passwort.
-### Wie funktioniert das?
+## Wie funktioniert das?
+
Normalerweise wenn man der Browser eine REST API benutzt wird `fetch` oder
`XMLHttpRequests` in Javascript benutzt. Der Browser verhindert aber dass
eine Seite A (z.B. neuland.app) auf eine Seite B (z.B. thi.de) zugreift.
@@ -21,6 +25,7 @@ Anfragen an thi.de werden also im Browser verschlüsselt und nur von unserem
Proxy weitergegeben. So hat nur der THI Server und der Browser Zugriff auf
Passwörter und Nutzerdaten.
-### Ich vertraue euch trotzdem nicht!
+## Ich vertraue euch trotzdem nicht
+
Gerne kannst du hier auf Github den Source Code der Neuland App lesen und
diese wenn du möchtest auch selbst auf deinem Server installieren.
diff --git a/docs/data-security.md b/docs/data-security.md
index 7f83a686..f0a4d434 100644
--- a/docs/data-security.md
+++ b/docs/data-security.md
@@ -1,19 +1,24 @@
# Neuland App Data Safety
+
TL;DR: Only you and the THI will have access to your password and other
sensitive information.
-### Will my data be stored?
+## Will my data be stored?
+
Only on your own device.
-### Will you steal my password?
+## Will you steal my password?
+
We can't.
-### Will my data be accesible to you?
+## Will my data be accesible to you?
+
No. It is transmitted end to end encrypted between your browser and the THI
Server. Even though our server is used as a proxy we can only see encrypted
data.
-### How does this work?
+## How does this work?
+
Normally when accessing a REST API your browser makes requests to it using
`fetch` or `XMLHttpRequests`. Because of CORS policies the App is not able to
directly contact the THI API. Instead it connects via a Proxy which is hosted
@@ -24,6 +29,7 @@ a full javascript implementation of TLS and HTTP. This way packets are
encrypted in your browser and simply relayed by our server. Only the THI server
and your browser have access to plain text packet contents.
-### What if i still do not trust you?
+## What if i still do not trust you?
+
You can read the source code of the neuland app here on github.
If you want, you can even install it on your own server.
diff --git a/docs/mensa-booking-api.md b/docs/mensa-booking-api.md
index 8b89cb53..99b2468b 100644
--- a/docs/mensa-booking-api.md
+++ b/docs/mensa-booking-api.md
@@ -1,45 +1,54 @@
# Mensa Booking API
+
The Studentenwerk Erlangen Nürnberg added a mensa seat reservation system during the COVID-19 pandemic.
-It is embedded on https://www.werkswelt.de/mensareservierung using an iframe.
-The real URL is https://togo.my-mensa.de/5ecb878c-9f58-4aa0-bb1b/erl77vB3r/reservations/formular/
+It is embedded on using an iframe.
+The real URL is
Below is a short reference of the API requests used by the reservation system.
## Request Verification E-Mail
-POST https://togo.my-mensa.de/5ecb878c-9f58-4aa0-bb1b/erl77vB3r/reservations/request_access_code
+
+POST
Request:
-```
+
+```http
email=REDACTED
```
Response:
+
```json
{
- "email": "REDACTED",
- "data": "versendet"
+ "email": "REDACTED",
+ "data": "versendet"
}
```
## Check Verification Code
-POST https://togo.my-mensa.de/5ecb878c-9f58-4aa0-bb1b/erl77vB3r/reservations/check_access_token
+
+POST
Request:
-```
+
+```http
email=REDACTED&accesscode=REDACTED
```
Response is a number `0` for failure and `3` for success.
## Cancel Reservation
-GET https://togo.my-mensa.de/5ecb878c-9f58-4aa0-bb1b/erl77vB3r/reservations/cancelcommit?k=REDACTED&i=REDACTED
+
+GET
With `i=${reservation.id}` and `k=${reservation.krz}`
## Reserve Seat
-POST https://togo.my-mensa.de/5ecb878c-9f58-4aa0-bb1b/erl77vB3r/reservations/add_by_client
+
+POST
Request (with additional newlines for readability)
-```
+
+```http
dauer=30
&gruppe=1
&vorname=REDACTED
@@ -63,216 +72,218 @@ dauer=30
```
Response on success
+
```json
{
- "message": {
- "status": "success",
- "text": "Die Reservierung wurde erfolgreich vorgenommen.",
- "info": "Die Reservierungsbest\u00e4tigung wurde an REDACTED gesendet"
- },
- "tk": {
- "Tischkonfiguration": {
- "id": "664",
- "location_id": "7",
- "stand_id": "84",
- "bemerkung": "Aktuell u50",
- "standdate": "2021-06-14",
- "mo_start": "11:00:00",
- "mo_ende": "13:50:00",
- "di_start": "11:00:00",
- "di_ende": "13:50:00",
- "mi_start": "11:00:00",
- "mi_ende": "13:50:00",
- "do_start": "11:00:00",
- "do_ende": "13:50:00",
- "fr_start": "11:00:00",
- "fr_ende": "13:50:00",
- "sa_start": "00:00:00",
- "sa_ende": "00:00:00",
- "so_start": "00:00:00",
- "so_ende": "00:00:00",
- "plaetze": "58",
- "gruppen": "3",
- "dauer_in_min": "30",
- "sperrzeit_in_min": "0",
- "karenz_vor_start_in_min": "10",
- "karenz_nach_start_in_min": "10",
- "sitznamen": "",
- "open": true,
- "bam": true,
- "cas": false,
- "modified": "2021-06-14 12:30:17",
- "created": "2021-06-14 12:29:58"
- },
- "Stand": {
- "id": "84",
- "date": "2020-10-03",
- "label": "Mehr Pl\u00e4tze ING"
- },
- "Location": {
- "loc_id": "7",
- "customer_id": "1",
- "daily_first_login": "0000-00-00 00:00:00",
- "einrichtung": "Mensa Ingolstadt",
- "einrichtung_lang": "",
- "rang": "70",
- "dir": "mensa-ingolstadt",
- "anschrift": "",
- "plz": "",
- "ort": "",
- "arr_map_cont_locs": "",
- "ctv_pl_dir": "",
- "ctv_content_subdirs": "",
- "ctv_news_subdir": "",
- "url_original_ctvplaylist": "",
- "susti": "",
- "prodgrpLocID": "7",
- "slsys_englisch": "1",
- "slsys_fv": "",
- "slsys_ap": "",
- "slsys_tp": "",
- "aktiver_tl1import": false,
- "print_config": "erlangen_nuernberg_07_mensa_ingolstadt",
- "globale_artikel": false,
- "timestamp": "2017-10-31 11:22:36"
- }
- },
- "client": {
- "Client": {
- "id": "REDACTED (some number)",
- "id_string": "REDACTED (email verification code)",
- "anrede_id": "0",
- "vorname": "REDACTED",
- "name": "REDACTED",
- "email": "REDACTED",
- "language": "de",
- "mensacard_nr": "",
- "strasse_nr": "REDACTED",
- "plz": "REDACTED",
- "ort": "REDACTED",
- "bezirk": "",
- "telefon": "REDACTED",
- "typ": "3",
- "guthaben": "0.00",
- "blocked_until": null,
- "created": "2021-09-04 13:20:01",
- "modified": "2021-09-04 13:43:46"
- },
- "ClientTyp": {
- "id": "3",
- "name": "Gast",
- "preis_feld": "Preis3"
- },
- "Anrede": {
- "id": null,
- "name": null
- }
- },
- "reservation": {
- "Reservation": {
- "client_id": "REDACTED (clent.Client.id from above)",
- "location_id": "7",
- "tag": "2021-09-07",
- "uhrzeit_start": "11:00",
- "dauer": "30",
- "tischgruppe": "1",
- "tischkonfiguration_id": "664",
- "modified": "2021-09-04 13:49:19",
- "created": "2021-09-04 13:49:19",
- "uhrzeit_ende": "11:30:00",
- "tischnr": 1,
- "id": "REDACTED (some id number)",
- "krz": "REDACTED (reservation code)",
- "sitzplatz": false
- }
- },
- "data": {
- "dauer": "30",
- "gruppe": "1",
- "vorname": "REDACTED",
- "name": "REDACTED",
- "strasse_nr": "REDACTED",
- "plz": "REDACTED",
- "ort": "REDACTED",
- "telefon": "REDACTED",
- "email": "REDACTED",
- "accesscode": "REDACTED (email verification code)",
- "save_allowed": "on",
- "nutzungsbedingungen": "on",
- "drei_g": "on",
- "date_val": "0",
- "date": "Bitte Datum w\u00e4hlen",
- "einrichtung_val": "7",
- "einrichtung": "Mensa Ingolstadt",
- "zeitpunkt": "11:00",
- "tkid": "664",
- "date_iso": "2021-09-07"
- },
- "intern_error": ""
+ "message": {
+ "status": "success",
+ "text": "Die Reservierung wurde erfolgreich vorgenommen.",
+ "info": "Die Reservierungsbest\u00e4tigung wurde an REDACTED gesendet"
+ },
+ "tk": {
+ "Tischkonfiguration": {
+ "id": "664",
+ "location_id": "7",
+ "stand_id": "84",
+ "bemerkung": "Aktuell u50",
+ "standdate": "2021-06-14",
+ "mo_start": "11:00:00",
+ "mo_ende": "13:50:00",
+ "di_start": "11:00:00",
+ "di_ende": "13:50:00",
+ "mi_start": "11:00:00",
+ "mi_ende": "13:50:00",
+ "do_start": "11:00:00",
+ "do_ende": "13:50:00",
+ "fr_start": "11:00:00",
+ "fr_ende": "13:50:00",
+ "sa_start": "00:00:00",
+ "sa_ende": "00:00:00",
+ "so_start": "00:00:00",
+ "so_ende": "00:00:00",
+ "plaetze": "58",
+ "gruppen": "3",
+ "dauer_in_min": "30",
+ "sperrzeit_in_min": "0",
+ "karenz_vor_start_in_min": "10",
+ "karenz_nach_start_in_min": "10",
+ "sitznamen": "",
+ "open": true,
+ "bam": true,
+ "cas": false,
+ "modified": "2021-06-14 12:30:17",
+ "created": "2021-06-14 12:29:58"
+ },
+ "Stand": {
+ "id": "84",
+ "date": "2020-10-03",
+ "label": "Mehr Pl\u00e4tze ING"
+ },
+ "Location": {
+ "loc_id": "7",
+ "customer_id": "1",
+ "daily_first_login": "0000-00-00 00:00:00",
+ "einrichtung": "Mensa Ingolstadt",
+ "einrichtung_lang": "",
+ "rang": "70",
+ "dir": "mensa-ingolstadt",
+ "anschrift": "",
+ "plz": "",
+ "ort": "",
+ "arr_map_cont_locs": "",
+ "ctv_pl_dir": "",
+ "ctv_content_subdirs": "",
+ "ctv_news_subdir": "",
+ "url_original_ctvplaylist": "",
+ "susti": "",
+ "prodgrpLocID": "7",
+ "slsys_englisch": "1",
+ "slsys_fv": "",
+ "slsys_ap": "",
+ "slsys_tp": "",
+ "aktiver_tl1import": false,
+ "print_config": "erlangen_nuernberg_07_mensa_ingolstadt",
+ "globale_artikel": false,
+ "timestamp": "2017-10-31 11:22:36"
+ }
+ },
+ "client": {
+ "Client": {
+ "id": "REDACTED (some number)",
+ "id_string": "REDACTED (email verification code)",
+ "anrede_id": "0",
+ "vorname": "REDACTED",
+ "name": "REDACTED",
+ "email": "REDACTED",
+ "language": "de",
+ "mensacard_nr": "",
+ "strasse_nr": "REDACTED",
+ "plz": "REDACTED",
+ "ort": "REDACTED",
+ "bezirk": "",
+ "telefon": "REDACTED",
+ "typ": "3",
+ "guthaben": "0.00",
+ "blocked_until": null,
+ "created": "2021-09-04 13:20:01",
+ "modified": "2021-09-04 13:43:46"
+ },
+ "ClientTyp": {
+ "id": "3",
+ "name": "Gast",
+ "preis_feld": "Preis3"
+ },
+ "Anrede": {
+ "id": null,
+ "name": null
+ }
+ },
+ "reservation": {
+ "Reservation": {
+ "client_id": "REDACTED (clent.Client.id from above)",
+ "location_id": "7",
+ "tag": "2021-09-07",
+ "uhrzeit_start": "11:00",
+ "dauer": "30",
+ "tischgruppe": "1",
+ "tischkonfiguration_id": "664",
+ "modified": "2021-09-04 13:49:19",
+ "created": "2021-09-04 13:49:19",
+ "uhrzeit_ende": "11:30:00",
+ "tischnr": 1,
+ "id": "REDACTED (some id number)",
+ "krz": "REDACTED (reservation code)",
+ "sitzplatz": false
+ }
+ },
+ "data": {
+ "dauer": "30",
+ "gruppe": "1",
+ "vorname": "REDACTED",
+ "name": "REDACTED",
+ "strasse_nr": "REDACTED",
+ "plz": "REDACTED",
+ "ort": "REDACTED",
+ "telefon": "REDACTED",
+ "email": "REDACTED",
+ "accesscode": "REDACTED (email verification code)",
+ "save_allowed": "on",
+ "nutzungsbedingungen": "on",
+ "drei_g": "on",
+ "date_val": "0",
+ "date": "Bitte Datum w\u00e4hlen",
+ "einrichtung_val": "7",
+ "einrichtung": "Mensa Ingolstadt",
+ "zeitpunkt": "11:00",
+ "tkid": "664",
+ "date_iso": "2021-09-07"
+ },
+ "intern_error": ""
}
```
Response on error
+
```json
{
- "message": {
- "status": "error",
- "code": "E_ABC_4",
- "text": "Kein Tisch mehr frei!",
- "info": "Leider wurde zwischenzeitlich vom Buchungssystem eine andere Reservierung angenommen."
- },
- "tk": [],
- "client": {
- "Client": {
- "id": "REDACTED",
- "id_string": "REDACTED",
- "anrede_id": "0",
- "vorname": "REDACTED",
- "name": "REDACTED",
- "email": "REDACTED",
- "language": "de",
- "mensacard_nr": "REDACTED",
- "strasse_nr": "REDACTED",
- "plz": "REDACTED",
- "ort": "REDACTED",
- "bezirk": "",
- "telefon": "",
- "typ": "3",
- "guthaben": "0.00",
- "blocked_until": null,
- "created": "2021-09-04 13:20:01",
- "modified": "2021-09-04 13:20:01"
- },
- "ClientTyp": {
- "id": "3",
- "name": "Gast",
- "preis_feld": "Preis3"
- },
- "Anrede": {
- "id": null,
- "name": null
- }
- },
- "reservation": [],
- "data": {
- "dauer": "30",
- "vorname": "REDACTED",
- "name": "REDACTED",
- "strasse_nr": "REDACTED",
- "plz": "REDACTED",
- "ort": "REDACTED",
- "telefon": "",
- "email": "REDACTED",
- "accesscode": "REDACTED",
- "save_allowed": "on",
- "nutzungsbedingungen": "on",
- "drei_g": "on",
- "date_val": "2021-09-04",
- "date": "Sa, 04. Sep 2021",
- "einrichtung_val": "7",
- "einrichtung": "Mensa Ingolstadt",
- "date_iso": "2021-09-04"
- },
- "intern_error": ""
+ "message": {
+ "status": "error",
+ "code": "E_ABC_4",
+ "text": "Kein Tisch mehr frei!",
+ "info": "Leider wurde zwischenzeitlich vom Buchungssystem eine andere Reservierung angenommen."
+ },
+ "tk": [],
+ "client": {
+ "Client": {
+ "id": "REDACTED",
+ "id_string": "REDACTED",
+ "anrede_id": "0",
+ "vorname": "REDACTED",
+ "name": "REDACTED",
+ "email": "REDACTED",
+ "language": "de",
+ "mensacard_nr": "REDACTED",
+ "strasse_nr": "REDACTED",
+ "plz": "REDACTED",
+ "ort": "REDACTED",
+ "bezirk": "",
+ "telefon": "",
+ "typ": "3",
+ "guthaben": "0.00",
+ "blocked_until": null,
+ "created": "2021-09-04 13:20:01",
+ "modified": "2021-09-04 13:20:01"
+ },
+ "ClientTyp": {
+ "id": "3",
+ "name": "Gast",
+ "preis_feld": "Preis3"
+ },
+ "Anrede": {
+ "id": null,
+ "name": null
+ }
+ },
+ "reservation": [],
+ "data": {
+ "dauer": "30",
+ "vorname": "REDACTED",
+ "name": "REDACTED",
+ "strasse_nr": "REDACTED",
+ "plz": "REDACTED",
+ "ort": "REDACTED",
+ "telefon": "",
+ "email": "REDACTED",
+ "accesscode": "REDACTED",
+ "save_allowed": "on",
+ "nutzungsbedingungen": "on",
+ "drei_g": "on",
+ "date_val": "2021-09-04",
+ "date": "Sa, 04. Sep 2021",
+ "einrichtung_val": "7",
+ "einrichtung": "Mensa Ingolstadt",
+ "date_iso": "2021-09-04"
+ },
+ "intern_error": ""
}
```
diff --git a/docs/thi-rest-api.md b/docs/thi-rest-api.md
index 8967e994..2b9a2735 100644
--- a/docs/thi-rest-api.md
+++ b/docs/thi-rest-api.md
@@ -1,13 +1,12 @@
# Endpoint
-- https://hiplan.thi.de/webservice/production2/index.php
+-
- POST request
- `Content-Type: application/x-www-form-urlencoded`
- `User-Agent:Embarcadero RESTClient/1.0`
Only the House plan uses:
-https://hiplan.thi.de/webservice/raumplan.html
-
+
# Services
@@ -15,7 +14,7 @@ https://hiplan.thi.de/webservice/raumplan.html
### open
-```
+```http
username:
passwd:
service: session
@@ -40,7 +39,7 @@ Note the third value in the `data` list seems to be related to the account type.
### close
-```
+```http
service: session
method: close
format: json
@@ -58,7 +57,7 @@ session:
### isalive
-```
+```http
service: session
method: isalive
format: json
@@ -75,7 +74,8 @@ session:
```
## Personal Data
-```
+
+```http
service: thiapp
method: persdata
format: json
@@ -120,7 +120,8 @@ session:
```
## Exams
-```
+
+```http
service: thiapp
method: exams
format: json
@@ -163,7 +164,8 @@ session:
```
## Grades
-```
+
+```http
service: thiapp
method: grades
format: json
@@ -207,9 +209,10 @@ session:
```
## Personal Timetable
+
Setting `details` to `1` results in the fields `inhalt`, `literatur` and `ziel` to be filled with HTML code with further information.
-```
+```http
service: thiapp
method: stpl
format: json
@@ -285,7 +288,8 @@ details: 0
```
## Rooms
-```
+
+```http
service: thiapp
method: rooms
format: json
@@ -346,7 +350,8 @@ year: 2020
```
## Mensa
-```
+
+```http
service: thiapp
method: mensa
format: json
@@ -390,11 +395,12 @@ session:
```
## Parking
+
In the official app the UI using this request is hidden from all users except employees.
The area `TG Gießerei Hochschule` refers to the area of the THI itself, guarded by
barries and only accesible to employees and some mebers of the students council.
-```
+```http
service: thiapp
method: parking
format: json
@@ -427,7 +433,8 @@ session:
```
## News
-```
+
+```http
service: thiapp
method: thiwebinfo
format: json
@@ -447,7 +454,8 @@ session:
```
## Imprint
-```
+
+```http
service: thiapp
method: impressum
format: json
@@ -469,7 +477,8 @@ session:
## Jobs
### categories
-```
+
+```http
service: thiapp
method: jobs
query: categories
@@ -509,7 +518,8 @@ session:
```
### offers
-```
+
+```http
service: thiapp
method: jobs
query: offers
@@ -530,14 +540,15 @@ session:
## Lecturers
### Personal Lecturers
-```
+
+```http
service: thiapp
format: json
session:
method: stpllecturers
```
-```
+```json
{
"data": [
0,
@@ -566,7 +577,8 @@ method: stpllecturers
```
### All lecturers (from A to Z)
-```
+
+```http
service: thiapp
method: lecturers
format: json
@@ -575,7 +587,6 @@ from: a
to: c
```
-
```json
{
"date": "30.03.2021",
@@ -608,7 +619,7 @@ to: c
### getreservation
-```
+```http
service: thiapp
method: reservations
format: json
@@ -620,6 +631,7 @@ data:
```
As of 2021-06 the API returns "Service not available" when no reservations are available
+
```json
{
"data": "Service not available",
@@ -630,6 +642,7 @@ As of 2021-06 the API returns "Service not available" when no reservations are a
```
Old response:
+
```json
{
"data": "No reservation data",
@@ -664,7 +677,8 @@ Old response:
```
### addreservation
-```
+
+```http
service: thiapp
method: reservations
format: json
@@ -690,7 +704,8 @@ data: {"resource":"3","from":"06:00","to":"10:00","at":"2020-11-03","place":"
```
### delreservation
-```
+
+```http
service: thiapp
method: reservations
format: json
@@ -711,7 +726,8 @@ data: 6678
```
### getavailabilities
-```
+
+```http
service: thiapp
method: reservations
format: json
@@ -723,60 +739,61 @@ data:
```
parts redacted, see getavailabilities-full.json
+
```json
{
- "date": "02.11.2020",
- "time": "20:37:54",
- "data": [
- 0,
- [
- {
- "date": "2020-11-02",
- "hasReservation": false,
- "resource": [
- {
- "from": "18:00",
- "to": "24:00",
- "resources": {
- "1": {
- "room_name": "Lesesaal Nord (alte Bibliothek)",
- "seats": [
- "1",
- "4",
- "5",
- "8",
- "9",
- "12",
- "14",
- "16",
- "18",
- "20",
- "21",
- "24",
- "25",
- "28",
- "29",
- "32",
- "33",
- "36",
- "37",
- "40",
- "41",
- "44",
- "45",
- "48",
- "49",
- "50",
- "51",
- "52",
- "53",
- "54",
- "56",
- "57"
- ],
- "num_seats": 32,
- "maxnum_seats": 32
- },
+ "date": "02.11.2020",
+ "time": "20:37:54",
+ "data": [
+ 0,
+ [
+ {
+ "date": "2020-11-02",
+ "hasReservation": false,
+ "resource": [
+ {
+ "from": "18:00",
+ "to": "24:00",
+ "resources": {
+ "1": {
+ "room_name": "Lesesaal Nord (alte Bibliothek)",
+ "seats": [
+ "1",
+ "4",
+ "5",
+ "8",
+ "9",
+ "12",
+ "14",
+ "16",
+ "18",
+ "20",
+ "21",
+ "24",
+ "25",
+ "28",
+ "29",
+ "32",
+ "33",
+ "36",
+ "37",
+ "40",
+ "41",
+ "44",
+ "45",
+ "48",
+ "49",
+ "50",
+ "51",
+ "52",
+ "53",
+ "54",
+ "56",
+ "57"
+ ],
+ "num_seats": 32,
+ "maxnum_seats": 32
+ },
// more rooms ...
}
}
diff --git a/rogue-thi-app/README.md b/rogue-thi-app/README.md
deleted file mode 100644
index 1838c758..00000000
--- a/rogue-thi-app/README.md
+++ /dev/null
@@ -1,41 +0,0 @@
-This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
-
-## Getting Started
-
-First, install the dependencies:
-
-```bash
-npm install
-# or
-yarn
-```
-
-> **Note**
-> For node.js versions 17 and above, you need to set the environment variable `NODE_OPTIONS=--openssl-legacy-provider` to avoid an error when installing the dependencies.
-
-Run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
From 2801b9e90cd7311138b6155930eb069e9b05c1f1 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Fri, 28 Jul 2023 16:16:16 +0200
Subject: [PATCH 28/34] =?UTF-8?q?=F0=9F=8D=B1=20add=20rounded=20corners=20?=
=?UTF-8?q?to=20favicon=20(#318)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/public/favicon.ico | Bin 67646 -> 67646 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/rogue-thi-app/public/favicon.ico b/rogue-thi-app/public/favicon.ico
index d6a8fedf98f9d47705338fdc40e207d37ccf7435..00b742c367c2873f7a8ec83553e8a0f6f2a4a3f0 100644
GIT binary patch
literal 67646
zcmeHQ349b~mhXrwx-dHGj3YX`Afhk`3?4Y>u7ZHLBRJwhW@lFsBPw1yT)GQ7N&rK+
zPaz36K^8AW6o*U50RiL=gd+$cAqgRbgwB2E?fX}Eb*d`eov!Yx?y63I`F*MSs=njB
z_kZ;r?>pLJ3FE)WNDKe(VCiyWnB@+O#nOd6Kf*)dIc|l3?Ivjd6Hy!Xz1WXpAJ0CO
zeJ=YF_Lb~w*w?dfU@tn{;~Cx|4&ovW(jpJ!g*@8?t>8?`7-RA%`|<3P*cYeF9%g*$3Kzh6MH@eb7m(Shn5SPhfw9z0n3mgRRh
z`_b%^*_+jYcJOd_ivUdnCRtXU*iU1Bm3^={@B*GXX&|DY%z|ZuI({DeGWNmhz#DiB
zwGDPHW?6J)AIrXueM@oR6+Cwh%8%ItrkMAe*+;U+Si7Y<@D3T=?19LFCJOUAl6@Nc
zkh4huvKSdO0J~s>d3>DxRvXKq|4t+T**xw-e5QWPe7(uu2=*E$VmH(y$mmT|gDoMR
zd1=c!wPA2QiRYTZt%5cgysnzE3fSxD81qWM|6ufawlrAFA(tS1ZUc+)N+89edw7
z59J`0fGKn!guN6p&F>AZ1I%rN<{XDIR7xPih;0EAIY~)qD6E?VB8{X2%q47?A?IHS
z7(xe_b2)Cg^ok?K0KXrf!fL$+S+i`}3
zm(S`dFO&7;Cv+w8X}Y}QQOZx~M^};uQ9;TuD*Wjcx_0zED$Q6)HP%zkN&psqu@iDR
zu;+(ty3SDW;#plq9$nr4GF|*KjIutrc;s_7-A;uEUZcwVUA&9Iv1|sI=ssV-W%y+-
zr>-)e$}&GAYuZ#QI{a6@CCdaQtuRg9Qty
z$CP5amq%1e-+6K!#l^lROC#!laAkQF?0%lA3ijwScg+0j*5+4VJI3Gla8-4njCXsz
ziF&c*>>_HYuhtSS?egNfy7=@=y7*ONU61cQ>eI>ez?6z6m@9SQ{>XaTzVZ>*I$)DS
zX@1f`&HDl@1kKm&pf6LfXQ)el4#)qB*2MjVK3X`NUY)a%{xtm{4W53OUYN0$-kAFZ
zOdC-fqBa|f!rx=%aYG{~vOdXg@0e^|!rSIY;{6H%GX;x5&Lh}oND
zc{uwH_}Nx1+W}@D>-98q^;K87|MI%iIzDAhM~Ye$Ndu;x@>nhpO)V1I%R}*ZHp+(U
zLu={>0j~=h>Z^o)y|jOaZ~p!HOQ=(1gU39IITW9BZ^@(|^MYxgN7v^eH1V%{D3=WTg_
zYKl*5rn_n~s>s_$mp0$ypatLRzWDYuFgndEKA+>BcO7;q+g-KwI29cnMHjzrB`d>T
zC#3!SnZ@UI#aZhuk@>s&a{n@0EY_=QRp9~d*F7ks^8(9b?b2b?Z(6!n@{nRLy&pPb
z5A9vkDEEtBU(aKSbzaf!O`_^+M|tf2oQzjs^23V9>5)$gyyCSJ>-YW(&Z()dv2q;~
z)nqII?0jhlvc9x*Wx7j1d-gXs@^~eV``8i%bWV=!X?^uIDm?f`le&JxvS+COv@>4G
zLW;Gk`v<0!)BGi)2|8x~9;k5t2*MacAJ@R+@dUgogW>r`BH%|=5D9c(;Ip5wQlgFX8zorqhHhV1x
zS9rU6j=tjFH60u@pzYDJE~vJq3O0${&Fvj!h_b?U`^tytndlQf@%!XlB5Q?5A{Qdn
zSkt6+ZLBx=XMbtw&h*zwd6C}3E{SoQGgn)d$#%?Vn#f~WYy|b3a?K}MxWd`p^KMhh
zC?;lcq>lf1uR0jrCFX#x^$F8SASv2fWL3zNKqN#tAlgX&>H|Jkr;-qFIY462>IT^1TT!
zOmIe-=g&$rR-^5mrv`t{9ec3LYD>>k{>~?w)d5$sKtFtG?5osss#OzNcq2b$8uY)u
zSoSA5Pdrwm+NYD~(N8XG#_OBw`A{P1DMg;c
zE{j=qA-;_30FU>cb7JgOx4Q;gJI=N>cl;~g9f-e6uwN_XZ_(cja5{CJT&bx%^wJxQL83qXh@h{o@P>T?@4s8oR~S?Lb>1p^I{Kt;{5`vv
z53<5Z0UEl7+S02P=`qcoL&8Pkyb_mAn4>xVH}3n~)Am@}f<3BFR0e
zJ|wN{O;1hF@|&!bvNL=5Nj@1Q*QyWj_FR+Q;m>h!28&M)ZPx#6Y&w5wc5L*fe+Zk_
zF6)cEh^x!7AV-)WM)LKFG3&rZAwBR!Z$@!HU(3E;GuTaVRR?f7oL#4xtKez)1-wM5
z2ahH^kwZ(11DihN)1bi^PX%a4oZ-*uajvE=?pis~oF9q0MTPW0)bV}`i1uaZB5qGI
zZE|icCmQ2PLSN!sE+&fF(z5G=&QpkHZs$X$YkZNU&uPaf>j2Jio6os=m_bP;(Q00v
zGiXW6st?f4pU(48Ncu+G<^+HKI{oj`E)hjJvQ2>{9x1P
zc%C9ZlqFxU<2ONiR!~n-2RQn>o+a^LEUa@v-LQd2Bkx<4>}
z0~XZNzz_52xd~bH!nkxA_D(8=zkYy*y}X--hW|*<5B`Dv_|!Mluh)9&-DNd(Zx=^v
zmLB&XwX&=J0n@oXTW}xo&^{!@9a#gs&5~Cg$Zx(
zFd7?L(F5$`waUy4`6TVgRE_^fV@=aNRto{JPgR@9%<-2mI-(l?`BD4K%(6S$fqkyK
z2*LL5?U#N~jsNGXP6V46M1Fw1eyfedQVewMc~_ij{P!iZF(Eh|u;Yqy
z^#2MA%7axvz}_!2kqxq++YO93XHQO1jenP07YBw||C9>s)otihNf&UxXU{$K4?C}?
zkI#LqGPSnRAG>dbYWyQ!_*PpYjFlU(XN#DAuCS(%EZZy&f3EzTeucTo`Sghb)!3u%
zkDYtaO|GVYr7QN3&grt#4(y=^J@@^difW4dF1VaMsijw@o_R7WT
zZ9b^`gV^pT${pD2+W)KLGtaL7@p`8^VA*b=$a9gr$5lTPFyH$~`(>)Xec*rMsJin3
zM4_7K3c>3>{_A_NuXFtE{lcdYKTWaOu@sdNMd&YT={9dj_X_)`y06ia45Q`+>~**Q
z%*mYN7JupZpv|r}^m*QTP2^QuTTi|3RX^s#x4&x1QB7ztcqYJJcl$5C>Fa*H&SM>b
zK8!p$lFI6Q+XNjScg(AAzhj$4R9>cgwP*y@0QP>F|9Lg-RT=*6lG{+4$d3`07a9baF
zeloWStz_?&dT>|rU9>WHCDpn(>mBPmpMPS#SJ?ld<8rF0(QhoO6utq%z}~OxKWiFl
z=$XUM$S{85;FDynEfx$BIaQbF^^^!ayg$uIyU
zekB3e`+fa)6`!m3!m$@*`1aV>gU(fC2s(f-@G$rLo<2NK7DtM|cz?sr8)$s`c*@|k
znBQ;LYMxi${+Ng@eighy3TtEE1E8(Z#RKMG7o2!@q72{GJ6qG5OYv04QXOQl@v=h33)(&5JVC7f7tQ&k0IWP(7wO&
z9o?FEs|??NJ@7A-crDqX1JHvbCC6xJ>ab?p1wkiR7kX@elm^UwhdTc*+O00!#(P2b
zCI#*KE+-OqJW2xgaQtMv<7ohHFdsbP#0Z%V0LN~7yU~mbGwA})A&K=tZdD#dUzko9
zJIKC&yDiz-v;OnncBuoO&ip9=92=s@@iz1Y{B=9~MGdEiiw{%ppL#pR6?hLz4W}vR
zr&3(bQu^}pmo)su%XHn2FsJdQdT`&)u1@iPZP+G%pS`0-9D2us9l-n!)&;asS1Nw{;+5r)ptUS2d0hYE@fRtKztN@q
z&8l%2C$7D;mb#>L(L@jW$GqXdzgLH4l%J=Yx~pL`P;nDP+{G&sqIn-6E<4VF9c=v@
z^7+6Fl@9emVo@T!!8U(e_L5*Srs~AZtMr
zyoMYbLMhf1Rck&lf4IGHJ4I(i)5H58Zjz^`x-ekK0=js?8gyWK!H}%&m*{||=RXQ-
zKV8`Iu>NZx+jqvD87Hj!m~mkSP0N@@6V6VcF=s|oIQR7)-1i{ewChH%@mA_ZO5t8F
zL=|*NoSy#(9bis1JO5Rb+J7DYPtEXC;;A?m{bZ*aox8pxr54m7|
zrvIV-hS7n4+;tOSe1dt17UVqn{RgMN|Du(QHeLCa1{@mTnH}GaO;=hspnXx!AxPhg
z_b;6o-bDYp?(Isuigszmo{{sxMy6(e|HWPh@adcSy6nnqis!jLtn%Xt0K=7P;m`y+#SjuCs6g4lWV`%iZKJFy&mTYT$YstAu?+1=R(P5@0Kot`~uwsGpOzrqiBIRL!>N
zY+j4l_s~;K^aDC_ckt4Dq^i`wt(wD?*Okk##QwrLnR8{Jb?E({DT07kqX3E3fQHz+5`-gu14f
z*Do2^srvm#JN_68nx=Dv^94kVG1j=q<4$RvAil#jg4d#B&7nA_jJVK+S(&qWEzx^4
zBJCAn%?b2Kn$F!l+B1VXVCZ(t2fUp*PG_18u>B78lVP>1D1rMHugRWC@k75a>Dq3G
z4v4ydE7ll%@bia+^+?h*uI{n7#npOL1L%ZTzyAzLP)mFOK=TnSGi;`IC{AU!5=X$F;=PU0zs|G@JVj
zSkG)g2h48v1$!|q=QGlP{5d5F<3j8yafh4u&KsKp_9y>2_0PgM(BSh3BSrkQxU#Uf
zIu*D9AL4r)duB|tI`AR)@o+u~Yzl9kdW-&eu0d2ukBCH#atiS1?WO%MW*t#*}}TD`EGO|+-yP$VtT_}
z;iTttmS;!@0kPZY?0RoD>L&-ALh(4(O@_hQ?mhLzci;U8T@6GaoC;*9=#NXgGUQ%@-L|tsukUX24uY
zBhw8#p=oW!2G9q{PM5hnC02TcnSRU{m~zf2!1zOEezE84g}pRi<^^-dq4uD8rGkuf
zox_#l;?H~L3+;hv))6%imF6)Z8&lW!p6C22A}JNB5wvNz3MGp5y72VTLmPkS@GmAMhqu`ICP
z5PMF`*axcvZ{X3Z{dh*iO>3H1HrRWP^9Qc74;BYrz>}&y*jmfhO!={_a1IF;=4pN&
z5$EYI2in1dN9S&s2_Jvb%JM_oFoC_M=M!>%dN|MsT0?cb4obpu#W|&8*l%NRghLlV
z184zFPS4fUK@6cR?Gj*__hkPrdmQ3a$X;)UGEf%E>=^>WSQ21N@Owu1T_gOq6*gw4
zvd?9Yv34c<8umEmp@F^VaF1trhd79fG)Rj)#NWvhelN#Z9Q#HM^Ty6JWi1Q}#DrWXoKYzzwpMmB~85i|w`0wl#-ER$r#
z3j|1t7YL9PFA$&%XG-&Df)b@+CP*2Ml)h&Ir9lv=2+PQ2f|X&3^pLWTGAv_<8B&I2
z>@b7Pg)`;!wQeq)D39MtZ8%b%K1^*`o;(!tw*=JUrHr2_Cs?)qWXjXGZZ6!M{wv=r
zzb9o_`8_Jb%I{ejR@Mhi!jV!2xjsphhMm+unbP-MU&$Ea3RW4GF~k+JjDbGfN=x(O
z`cy_B2Pv{Kf>0kPHim`zI)aMccg!*1WKzrmTA)pV!5&|Y6EFnM$HcCV&j>u7<
zP#lq?fcCcdjsp7F!j25>K`}#nb`Fs_T4;L)Vsg6(1m{p^bc1$RN}B7%yFYeA}l3JbWd0%Cy`cNNrKkyVpG5+Jlt6G9DQ
z6cIsTg#ZZ&B-8+DkRS=Er18>w{ddmI-p9SK<=yvQUP5w)@8#B+JLmt;%$zxM?#0EW
zo%rYG<|6)fap`e=JD1z$R;@o<2elP^a#DNuSmB08+{=wPH`Ie(u5}1nKTw}n#lHfd
zoY0q0RS;!$wMr8GMVamNrKY4Zm7I`q^YZ~sr>4~IAjGqp~sly+`4uY
zp7Tg7RfZBT)xPLI$*!oYylkQk`_i
z-#7W(@J-#}vD_8IJkMH<%j0wMr1A1(z=K!h5Hb}+ebMZFL494NWY;VEHfr;qEnS74
zuJu;SDC@wsqnA+>zX+Lo`d^V%(e_n3AkL@%)KQF$KbobA#!kaL@-fx9D1NaRSL@8?
z*K|R$dHj&};~lDJCVkrhfosO#uM2`T<00ch+eh7tv2yhXNdERpUzYjnBd9Jqr>UBb
zx|!^$OS>Lc$$BK9D@J(5+9c;ItASbt6;%b%$U89&sXyGLdWOuOa!c*a
z#r)a9gN1#WW*01#y5qvGdrbA5i-GO2dChnX@l3D@4<+}?{c-aTBVyAP+*5zJS?qC^f4Yra|tE?w4Z(8Suq~y
zOo~#HxK3r480Jm;@pds^@zdNF)K%p}cVeoMJ-^L=B!+s%+k}OZYjgXLxR+t+s*gcB
zCd;BbI=Kn$ntCxG4-veN7SFrV|Io7&zr9wn@p`+}*evgVcABJ3b&|NHwrt6lkh$kk
z5GSTmRo+Qt{M=cOmyDZoOLMcz%STORH#%3E5-5#7%KRrccfw@vT{h!jj&F14$ySWm
z)J-oFjXjR+3&_qUA4SHluKH&v7&eSIfL%Pn##1dbt6|%=G&f>n?mD`DgxZ
zwtnPOvy{lkM&qU|kGU<)X^PHy3%|o*$v0hecD|84LT!HH!e5=vyP-}@TDTign>*@d
zO51*CgYQh}tEohMm!HB}p4i
z&a=qB)>uDYQ<5lbhUfHnnUmSvd9uUT_`ZVya|#`Tg)R6Q<{|s*JO5YJG6^hl4vrA>
zq-`L7JdK0oYc`yxn3I#vlmE_h#n*UhZkEGj{rk5-UA^^Oo4d30Vt|lWZIe))X?>~j
zR1aw!7rr%1@*7?|9?(VdUmN1zQjR}dvKjj}sOKYwGBkEf{;9QoZ)2kOe?>oE>oDF4
zqt|?a64OI;2`-b?c4#a!lb9_s#A2df7bm
zb*peF9!;e;Gzalo@g+!KP02&&$Ui~Xcdi>O%_%-wvH`=r;+jmpmsbM1QY(qE^0xY#
z5^4TcUHqGsIZWS1e&K@X{|MXXUL(BbE_auFFipi(QX#A$_{kSMo=_
z;E^QSc6F0+Z79>QWk61`Y3y6nSz%w6XYLWU(G1D2P5KbJWe{GRm*O<646(b~kDp%x
zWE8Y0mNi#~#;kvdrP!;T3uo7VjBu@G6>5gN#b%&6>xkv72iEA(G+~B0($;G!N>MJt
z2z*3{Qh!g@15jMCBSPBFnDR})P*PJ
z1FpM(_^h_Yt1Z$}+wcto-rX!>d0Lpw*mo{4$h%=qGr_;1jx&D%zW$}bw(&pQPnAAR(}KTYK6Mx|I}@*b
zo`SJ66EOb6C`@?&IL1#pgmDx0>cR8m}p-@7c(^qc>cXxyAy1p;+K
zfjYL+#{aAj#pIP!t3ZH!E%&td*3|z0Ir+b>LUtBH_*re}|37(uFGJT#5J*cd)YSIL
zR``7SzE&bVixIjnSu_7%&Ja#|POSogwaZUw=6|V2%X*ivRS?lkLQH>aw`TqWH^ekk
zZTYc|Mqoe}Kh69f2@}o)PFFnwzs2h`b5Hi4E>|pIbFvG8dNhBK!$P-k&HVSdM+D
zvKay5{o%t80jNRdgN4MEBbk#bz>~+!qE|gtp7A(%i
zvNTO3&%JCp{Q&WB~H2^PDc8$E+}$_>YgZ{QVTC3v!h`Fse98
z#Xspl_ru+hSeEECNy^K_`x|#H*UW$a&f+C3PIe)XQl4VOKk2~TA$KD*FVwEGtXvm$fK)kwyMx
zJ)rHK!FM7sGteP;)zs8sP!H{MK5F|LR-JZ89k>wjF0*LUH|)Q`Y8@awcsK4{l-An!
z5_Evy8JpUE_dA!Pth5aW=r@7Vz7zY7ivP}GopIy-8&&6&{Q&LvINSrLi)?F$73DSP
z*J-s){PTLB;}syIB12^dUK4Z;wrBr@znmFoQ3vS0PZPwq?`ummRmyX=ZS(zZxA~y3
zK)l4v$>9jBO<$|xy4#^{$Q5J5j+~u%F!VvyHN(E(zJvE+Rq85KS@CV^#PfORbloDG
z+CKaH98SBcz$vj)R6IX({27$hRiIdm^{$C-xM}}Qs_TY&&?T%3)@Q6ojhSz~Q+(%Z
z&js4V{nOpoqPn{2=AtSf+UP-beKkf#j#P2{+==H=P*Vg#KE3i1-jACK@(mbruRQL0
zsH@Z;7>*0;R@mn2A)EieuSso`73YdwDZZ;W_RLrnzx|K&$A$7FNe4&|B8A;IJaUBU
z9wq_FJn9Rv(2#{ZJ*7;dzx3`_1Ww96&ER95^@>}{Ggw6
ze#Siq@6qc3ar*4>XK^q;Os@l^2d9e9U|iIA{4VHss(Y0>An()I(Ld-Z3|ssmdjHAG
zqAuLq!5a~w9N*QPnn>%xlVc_uwefxt{V@OH0$dbpNOFIWQJD#^i}OHp2bph6Tk2ni
zE}d>x2UaXN)|{gAvlR$L7DZyvu|Y<>5{Jb1_^1hRPn-k4^fmZCYbz$kOv3g1t~a{h
zP#=07=xdVy_r~vR_21r6z}sxQG*`1W{*Uh4lDP#m&okuFp5sTJ^i=5pt?y7=0H>xy
zphC>+-;90}*Y3L(YebvXl4zc|`O;?e3Gd?&Js7%rs-FKRyZIwKQ=30gOWpb21_b=l
z{Pf(BuYX(iHqFpbe{eAGAf^hRK*x}d`tr2)1w9Y^N#dVkdeT$GM*%oBIRZW@J}SOw
z93OLL48jY-HN||;`eBeR2wq8Ec;e_2sxr0c!LTttNWIvKKC;>54BGbqiv6T{xhak)
z{%eHr@63#!DaC!vzc?SBNuK!p{O9;M{$orKw%%h$9>eXyx0}pws?5gDh3g{h6Nqzb
zM&NJJe{&drjq%&q_0JMM(_DYfU>^eM73sJu12q0nyrH#o5&I!VJMLZQ__l@X_;Jjex0l-g^5J;;?Lx5Q8qc~5R)7fI@)B3<1;SVmXDFnq6
zDmf>glk%AM{YCuIFS)x++9RXI8Y8`{#HslZAb;T{5&uDBMQlkdXl;;u!Srqb%@3EQ
zEW?J34bpo6nx2PyqDuIp(?l%d;FB*J=?B?|4~0F1KZN~3
z`ceETy{{<$=skvgx`uB-MHED!S9mY{@z5W!E`6Q8F1+J>1j=g5R9w>gg^Q9GtIo-N
z!3XE2>*EWyX8mB3KZ!V3t~*_~Py9dWB7TWe^C3Xr7mzuoxkjp(cPje?`5JAfh*)%r
zIh5Cw>0Z(W@8pFdmgrMVj(uB-IUzk#=Cia<;~A&sLtx0sAu6td^FoAe@t*3Ol0UNh
zLh}#lbwJhy+ER?c7r)I0#Um;6nA@j!Tg=6yHXkBw>&diMsN{#%4Woq(VQ3dB@1gz2
zk3KH^&R$YX!K~lDkj4XZK5N6g@-f+toSGK_n#T`3I#8tp4}?A-#S$yWYMC=fZfPFF
zsreDeuF96|aYu4%sVs^~S}1G=if883G7zA7A-z*#i8uKg`5b7@Cm&n0rT3Fxk9u92
z2l6^X%i!Fw7StYR#LsAy+u6d#qwkZDPvL{JAK;}^FQIpMZ~gqAY>1any^LvRr%Aqt
z=;CNiaSMj^;$!C`KrthoLOWS(-(>&I6Y<2HS|7y(9r5nu!u0Y-okU<4QeMt~7u1Q-EEfDvE>7y(9r5nu!u0Y-ok
gU<4QeMt~7u1g<&+TsScTi~u9R2rvSSK$}D0f5xo(nE(I)
From 27f9aeb8feddc05893f6c72a945b4c57f66518fd Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Wed, 2 Aug 2023 23:02:58 +0200
Subject: [PATCH 29/34] =?UTF-8?q?=F0=9F=90=9B=20Fix=20timetable=20errors?=
=?UTF-8?q?=20during=20semester=20break=20(#320)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/components/cards/TimetableCard.jsx | 8 +++++---
rogue-thi-app/lib/backend-utils/timetable-utils.js | 9 +++++++++
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/rogue-thi-app/components/cards/TimetableCard.jsx b/rogue-thi-app/components/cards/TimetableCard.jsx
index 77203e11..7bc27e9e 100644
--- a/rogue-thi-app/components/cards/TimetableCard.jsx
+++ b/rogue-thi-app/components/cards/TimetableCard.jsx
@@ -82,11 +82,13 @@ export default function TimetableCard () {
)
}))}
{(timetable && timetable.length === 0) &&
- {t('timetable.text.noLectures')}
+
+ {t('timetable.text.noLectures')}
+
}
{(timetableError &&
- {t('timetable.text.error')} )
- }
+ {t('timetable.text.error')}
+ )}
diff --git a/rogue-thi-app/lib/backend-utils/timetable-utils.js b/rogue-thi-app/lib/backend-utils/timetable-utils.js
index e30fa933..ca13639d 100644
--- a/rogue-thi-app/lib/backend-utils/timetable-utils.js
+++ b/rogue-thi-app/lib/backend-utils/timetable-utils.js
@@ -76,6 +76,15 @@ export function getTimetableGaps (timetable) {
export async function getFriendlyTimetable (date, detailed) {
const { timetable } = await API.getTimetable(date, detailed)
+ /**
+ * During the semester break, the API returns an null array ('[null]').
+ * To prevent errors for the room suggestions or the timetable view, we return an empty array.
+ */
+ if (timetable.every(x => x === null)) {
+ console.error('API returned null array for timetable!')
+ return []
+ }
+
return timetable
.flatMap(day =>
Object.values(day.hours)
From 04a63495017b2c0762adb76378421ec9fbbf2fa8 Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Mon, 7 Aug 2023 11:03:58 +0200
Subject: [PATCH 30/34] =?UTF-8?q?=F0=9F=90=9B=F0=9F=92=84=20Fix=20spo=20we?=
=?UTF-8?q?ights=20and=20rework=20grade=20page=20(#319)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* 🐛💄 Fix spo weights and rework grade page
* 🧑💻 redact grades option for devs
---
.../lib/backend-utils/grades-utils.js | 17 +++-
rogue-thi-app/pages/grades.jsx | 99 +++++++++++++------
rogue-thi-app/pages/personal.jsx | 46 ++++++---
rogue-thi-app/public/locales/de/grades.json | 3 +
rogue-thi-app/public/locales/de/personal.json | 5 -
rogue-thi-app/public/locales/en/grades.json | 3 +
rogue-thi-app/public/locales/en/personal.json | 5 -
rogue-thi-app/styles/Grades.module.css | 44 ++++++++-
rogue-thi-app/styles/Personal.module.css | 7 ++
spo-parser/combine_jsons.py | 15 ++-
10 files changed, 180 insertions(+), 64 deletions(-)
diff --git a/rogue-thi-app/lib/backend-utils/grades-utils.js b/rogue-thi-app/lib/backend-utils/grades-utils.js
index b849c6f2..afcae9f8 100644
--- a/rogue-thi-app/lib/backend-utils/grades-utils.js
+++ b/rogue-thi-app/lib/backend-utils/grades-utils.js
@@ -1,6 +1,8 @@
import API from '../backend/authenticated-api'
import courseSPOs from '../../data/spo-grade-weights.json'
+const redactGrades = process.env.NEXT_PUBLIC_REDACT_GRADES === 'true' || false
+
function simplifyName (x) {
return x.replace(/\W|und|u\./g, '').toLowerCase()
}
@@ -20,6 +22,18 @@ async function getGradeList () {
x.note = 'E'
}
})
+
+ /**
+ * Set NEXT_PUBLIC_REDACT_GRADES to true to redact your grades (e.g. for screenshots)
+ */
+ if (redactGrades) {
+ gradeList.forEach(x => {
+ if (parseFloat(x.note)) {
+ x.note = '1.0'
+ }
+ })
+ }
+
return gradeList
}
@@ -96,7 +110,8 @@ export async function loadGradeAverage () {
if (grade && spoName && courseSPOs[spoName]) {
const spo = courseSPOs[spoName]
const name = simplifyName(x.titel)
- const entry = spo.find(y => simplifyName(y.name) === name)
+ const spoEntries = spo.filter(y => simplifyName(y.name) === name)
+ const entry = spoEntries.find(y => !!y.weight) || spoEntries[0]
const other = average.entries.find(y => y.simpleName === name)
if (other) {
diff --git a/rogue-thi-app/pages/grades.jsx b/rogue-thi-app/pages/grades.jsx
index b3e343df..63fb8678 100644
--- a/rogue-thi-app/pages/grades.jsx
+++ b/rogue-thi-app/pages/grades.jsx
@@ -107,6 +107,36 @@ export default function Grades () {
alert(t('grades.alerts.notImplemented'))
}
+ /**
+ * A spoiler component that hides its children until clicked.
+ * @param {Object} props
+ * @param {string} props.className
+ * @param {React.ReactNode} props.children
+ * @returns {React.ReactElement} The spoiler component.
+ */
+ function Spoiler ({ className, children }) {
+ const [show, setShow] = useState(false)
+
+ function getStyle (visible) {
+ return visible ? styles.spoilerVisible : styles.spoilerHidden
+ }
+
+ return (
+ setShow(!show)}
+ >
+
+ {children}
+
+
+
+ {t('grades.spoiler.reveal')}
+
+
+ )
+ }
+
return (
@@ -123,55 +153,60 @@ export default function Grades () {
{gradeAverage && gradeAverage.entries.length > 0 && (
-
+ <>
- {t('grades.summary.title')}
+ {t('grades.summary.title')}
-
-
-
- {gradeAverage.resultMin !== gradeAverage.resultMax && '~'}
- {formatNum(gradeAverage.result)}
-
- {gradeAverage.resultMin !== gradeAverage.resultMax && (
-
- {t('grades.summary.disclaimer', {
- minAverage: formatNum(gradeAverage.resultMin),
- maxAverage: formatNum(gradeAverage.resultMax)
- })}
+
+
+
+ {gradeAverage.resultMin !== gradeAverage.resultMax && '~'}
+ {formatNum(gradeAverage.result)}
- )}
-
-
+
+ {gradeAverage.resultMin !== gradeAverage.resultMax && (
+
+ {t('grades.summary.disclaimer', {
+ minAverage: formatNum(gradeAverage.resultMin),
+ maxAverage: formatNum(gradeAverage.resultMax)
+ })}
+
+ )}
+
+
+ >
)}
+
+ {t('grades.gradesList.title')}
+
-
- {t('grades.gradesList.title')}
-
-
{grades && grades.map((item, idx) =>
{item.titel}
-
-
- {t('grades.grade')}: {getLocalizedGrade(item.note)}
+
{t('grades.ects')}: {item.ects || t('grades.none')}
-
+
+
+
+
+ {getLocalizedGrade(item.note)}
+
+ {t('grades.grade')}
+
)}
+
+ {t('grades.pendingList.title')}
+
-
- {t('grades.pendingList.title')}
-
-
{missingGrades && missingGrades.map((item, idx) =>
@@ -179,8 +214,10 @@ export default function Grades () {
{item.titel} ({item.stg})
- {t('grades.deadline')}: {item.frist || t('grades.none')}
- {t('grades.ects')}: {item.ects || t('grades.none')}
+
+ {t('grades.deadline')}: {item.frist || t('grades.none')}
+ {t('grades.ects')}: {item.ects || t('grades.none')}
+
diff --git a/rogue-thi-app/pages/personal.jsx b/rogue-thi-app/pages/personal.jsx
index 1171064a..b41376a6 100644
--- a/rogue-thi-app/pages/personal.jsx
+++ b/rogue-thi-app/pages/personal.jsx
@@ -29,7 +29,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { FoodFilterContext, ShowDashboardModal, ShowLanguageModal, ShowPersonalDataModal, ShowThemeModal, ThemeContext } from './_app'
import { NoSessionError, UnavailableSessionError, forgetSession } from '../lib/backend/thi-session-handler'
import { USER_EMPLOYEE, USER_GUEST, USER_STUDENT, useUserKind } from '../lib/hooks/user-kind'
-import { calculateECTS, loadGradeAverage, loadGrades } from '../lib/backend-utils/grades-utils'
+import { calculateECTS, loadGrades } from '../lib/backend-utils/grades-utils'
import API from '../lib/backend/authenticated-api'
import styles from '../styles/Personal.module.css'
@@ -42,7 +42,6 @@ const PRIVACY_URL = process.env.NEXT_PUBLIC_PRIVACY_URL
export default function Personal () {
const [userdata, setUserdata] = useState(null)
- const [average, setAverage] = useState(null)
const [ects, setEcts] = useState(null)
const [grades, setGrades] = useState(null)
const [missingGrades, setMissingGrades] = useState(null)
@@ -96,9 +95,6 @@ export default function Personal () {
data.pcounter = response.pcounter
setUserdata(data)
- const average = await loadGradeAverage()
- setAverage(average)
-
const { finished, missing } = await loadGrades()
setGrades(finished)
setMissingGrades(missing)
@@ -164,10 +160,6 @@ export default function Personal () {
{ects !== null && `${ects} ${t('personal.overview.ects')} `}
- {!isNaN(average?.result) && ' · '}
- {!isNaN(average?.result) && '∅ ' + average.result.toFixed(2).toString().replace('.', ',')}
- {average?.missingWeight === 1 && ` (${average.missingWeight} ${t('personal.grades.missingWeightSingle')})`}
- {average?.missingWeight > 1 && ` (${average.missingWeight} ${t('personal.grades.missingWeightMultiple')})`}
@@ -225,18 +217,28 @@ export default function Personal () {
- window.open('https://www3.primuss.de/cgi-bin/login/index.pl?FH=fhin', '_blank')}>
+ window.open('https://www3.primuss.de/cgi-bin/login/index.pl?FH=fhin', '_blank')}
+ >
Primuss
- window.open('https://moodle.thi.de/moodle', '_blank')}>
+ window.open('https://moodle.thi.de/moodle', '_blank')}
+ >
Moodle
- window.open('https://outlook.thi.de/', '_blank')}>
+ window.open('https://outlook.thi.de/', '_blank')}
+ >
E-Mail
@@ -254,18 +256,30 @@ export default function Personal () {
{showDebug && (
- router.push('/debug')}>
+ router.push('/debug')}
+ >
{t('personal.debug')}
)}
- window.open(PRIVACY_URL, '_blank')}>
+ window.open(PRIVACY_URL, '_blank')}
+ >
{t('personal.privacy')}
- router.push('/imprint')}>
+ router.push('/imprint')}
+ >
{t('personal.imprint')}
diff --git a/rogue-thi-app/public/locales/de/grades.json b/rogue-thi-app/public/locales/de/grades.json
index a6985520..c73f9e4d 100644
--- a/rogue-thi-app/public/locales/de/grades.json
+++ b/rogue-thi-app/public/locales/de/grades.json
@@ -16,6 +16,9 @@
"title": "Notenschnitt",
"disclaimer": "Der genaue Notenschnitt kann nicht ermittelt werden und liegt zwischen {{minAverage} und {{maxAverage}}"
},
+ "spoiler": {
+ "reveal": "Klicken um anzuzeigen"
+ },
"gradesList": {
"title": "Noten"
},
diff --git a/rogue-thi-app/public/locales/de/personal.json b/rogue-thi-app/public/locales/de/personal.json
index bd615fed..3b0296f8 100644
--- a/rogue-thi-app/public/locales/de/personal.json
+++ b/rogue-thi-app/public/locales/de/personal.json
@@ -1,11 +1,6 @@
{
"personal": {
"title": "Profil",
- "grades": {
- "title": "Noten",
- "missingWeightSingle": " Gewichtung fehlt",
- "missingWeightMultiple": " Gewichtungen fehlen"
- },
"overview": {
"grades": "Noten",
"ects": "ECTS",
diff --git a/rogue-thi-app/public/locales/en/grades.json b/rogue-thi-app/public/locales/en/grades.json
index f130a261..6f530da5 100644
--- a/rogue-thi-app/public/locales/en/grades.json
+++ b/rogue-thi-app/public/locales/en/grades.json
@@ -16,6 +16,9 @@
"title": "Grade Average",
"disclaimer": "The exact grade average cannot be determined and ranges between {{minAverage}} and {{maxAverage}}"
},
+ "spoiler": {
+ "reveal": "Click to reveal"
+ },
"gradesList": {
"title": "Grades"
},
diff --git a/rogue-thi-app/public/locales/en/personal.json b/rogue-thi-app/public/locales/en/personal.json
index 118f60b6..07f912a4 100644
--- a/rogue-thi-app/public/locales/en/personal.json
+++ b/rogue-thi-app/public/locales/en/personal.json
@@ -1,11 +1,6 @@
{
"personal": {
"title": "Profile",
- "grades": {
- "title": "Grades",
- "missingWeightSingle": " weight missing",
- "missingWeightMultiple": " weights missing"
- },
"overview": {
"grades": "grades",
"ects": "ECTS",
diff --git a/rogue-thi-app/styles/Grades.module.css b/rogue-thi-app/styles/Grades.module.css
index e73750b3..d1f81278 100644
--- a/rogue-thi-app/styles/Grades.module.css
+++ b/rogue-thi-app/styles/Grades.module.css
@@ -11,11 +11,17 @@
}
.details {
- color: gray;
+ color: var(--gray);
+}
+
+.grade {
+ margin-left: 5px;
+ text-align: center;
+ align-self: center;
}
.right {
- color: gray;
+ color: var(--gray);
text-align: right;
}
@@ -23,15 +29,19 @@
display: flex;
flex-direction: row;
align-items: center;
+ padding: 15px 20px;
}
+
.gradeAverage {
font-weight: bold;
font-size: 3rem;
float: left;
margin-right: 20px;
+ padding-bottom: 5px;
}
+
.gradeAverageDisclaimer {
- color: gray;
+ color: var(--gray);
}
.spacer {
@@ -39,3 +49,31 @@
min-width: 10px;
height: 1px;
}
+
+.spoilerHidden {
+ opacity: 0;
+ transition: opacity 0.5s ease-in-out;
+}
+
+.spoilerVisible {
+ opacity: 1;
+ transition: opacity 0.5s ease-in-out;
+}
+
+.spoilerPlaceholder {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 3px;
+ color: var(--light);
+ font-size: 1rem;
+ font-weight: bold;
+ border: inherit;
+ background-color: color-mix(in srgb, var(--primary), #ffffff00);
+ box-shadow: inset 0 0 25px 0px var(--primary);
+}
diff --git a/rogue-thi-app/styles/Personal.module.css b/rogue-thi-app/styles/Personal.module.css
index 845045f5..0559db34 100644
--- a/rogue-thi-app/styles/Personal.module.css
+++ b/rogue-thi-app/styles/Personal.module.css
@@ -21,6 +21,13 @@
float: right;
}
+.interaction_row {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: space-between;
+ align-items: center;
+}
+
.logout_button {
display: flex;
justify-content: center;
diff --git a/spo-parser/combine_jsons.py b/spo-parser/combine_jsons.py
index 7c7ed7bb..c108c4ce 100644
--- a/spo-parser/combine_jsons.py
+++ b/spo-parser/combine_jsons.py
@@ -1,12 +1,21 @@
import os
import json
+import re
+
+simplifyName = re.compile(r'\W|und|u\./g')
+def simplify(name):
+ return simplifyName.sub("", name).lower()
result = {}
for filename in os.listdir("./weightings/"):
name = filename.replace(".json", "")
with open("./weightings/" + filename) as fd:
- result[name] = json.load(fd)
+ content = json.load(fd)
+ for entry in content:
+ entry["name"] = simplify(entry["name"])
+
+ result[name] = content
ects_sums = []
for name in result:
@@ -21,5 +30,5 @@
for name, ects in ects_sums:
print("{:3d} {}".format(ects, name))
-with open("spo-grade-weights.json", "w+") as fd:
- json.dump(result, fd)
\ No newline at end of file
+with open("spo-grade-weights.json", "w+", encoding="utf-8") as fd:
+ json.dump(result, fd, ensure_ascii=False)
From c398b70b0d9265ee9440d00edd519f25c7fa0ab7 Mon Sep 17 00:00:00 2001
From: Robert Eggl
Date: Tue, 15 Aug 2023 02:59:50 +0200
Subject: [PATCH 31/34] updates calendar entries (#321)
---
rogue-thi-app/data/calendar.json | 262 +++++++++++++++----------------
1 file changed, 131 insertions(+), 131 deletions(-)
diff --git a/rogue-thi-app/data/calendar.json b/rogue-thi-app/data/calendar.json
index d1365818..e0ab092a 100644
--- a/rogue-thi-app/data/calendar.json
+++ b/rogue-thi-app/data/calendar.json
@@ -9,204 +9,204 @@
]
},
{
- "name": "Vorlesungsbeginn und Semesterstart",
- "begin": "2022-03-15"
- },
- {
- "name": "Vorlesungsfrei (Ostern)",
- "begin": "2022-04-14",
- "end": "2022-04-19"
- },
- {
- "name": "Prüfungsanmeldung",
- "begin": "2022-05-02",
- "end": "2022-05-12"
- },
- {
- "name": "Prüfungsabmeldung",
- "begin": "2022-05-02",
- "end": "2022-06-24"
- },
- {
- "name": "Vorlesungsfrei (Christi Himmelfahrt, Brückentag)",
- "begin": "2022-05-26",
- "end": "2022-05-27"
- },
- {
- "name": "Vorlesungsfrei (Pfingsten)",
- "begin": "2022-06-03",
- "end": "2022-06-07"
- },
- {
- "name": "Vorlesungsfrei (Fronleichnam)",
- "begin": "2022-06-16"
- },
- {
- "name": "Rückmeldung",
- "begin": "2022-07-02",
- "end": "2022-08-07"
- },
- {
- "name": "Erweiterter Prüfungszeitraum",
- "begin": "2022-07-02",
- "end": "2022-07-08"
- },
- {
- "name": "Prüfungszeitraum",
- "begin": "2022-07-09",
- "end": "2022-07-19"
- },
- {
- "name": "Notenbekanntgabe",
- "begin": "2022-07-29T13:00",
- "hasHours": true
+ "name": {
+ "de": "Semesterferien",
+ "en": "Semester break"
+ },
+ "begin": "2023-08-01",
+ "end": "2023-09-30"
},
{
- "name": "Semesterferien",
- "begin": "2022-07-30",
- "end": "2022-09-30"
+ "name": {
+ "de": "Vorlesungsbeginn und Semesterstart",
+ "en": "Start of lectures and semester"
+ },
+ "begin": "2023-10-02"
},
{
- "name": "Vorlesungsbeginn und Semesterstart",
- "begin": "2022-10-04"
+ "name": {
+ "de": "Vorlesungsfrei",
+ "en": "No lectures"
+ },
+ "begin": "2023-10-03"
},
{
- "name": "Vorlesungsfrei",
- "begin": "2022-10-31"
+ "name": {
+ "de": "Vorlesungsfrei",
+ "en": "No lectures"
+ },
+ "begin": "2023-10-31"
},
{
- "name": "Prüfungsanmeldung",
- "begin": "2022-11-04",
- "end": "2022-11-14"
+ "name": {
+ "de": "Prüfungsanmeldung",
+ "en": "Registration for exams"
+ },
+ "begin": "2023-11-06",
+ "end": "2023-11-16"
},
{
- "name": "Prüfungsabmeldung",
- "begin": "2022-11-04",
- "end": "2023-01-11"
+ "name": {
+ "de": "Prüfungsabmeldung",
+ "en": "Deregistration for exams"
+ },
+ "begin": "2023-11-06",
+ "end": "2024-01-11"
},
{
- "name": "Vorlesungsfrei (Weihnachten)",
- "begin": "2022-12-24",
- "end": "2023-01-08"
+ "name": {
+ "de": "Vorlesungsfrei (Weihnachten)",
+ "en": "No lectures (Christmas)"
+ },
+ "begin": "2023-12-24",
+ "end": "2024-01-07"
},
{
- "name": "Notenmeldung Prädikate (ZV)",
- "begin": "2023-01-09T09:00",
+ "name": {
+ "de": "Notenmeldung Prädikate (ZV)",
+ "en": "Recording grades predicates (admission requirements)"
+ },
+ "begin": "2024-01-09T9:00",
"hasHours": true
},
{
- "name": "Erweiterter Prüfungszeitraum",
- "begin": "2023-01-19",
- "end": "2023-01-25"
+ "name": {
+ "de": "Erweiterter Prüfungszeitraum",
+ "en": "Additional exam period"
+ },
+ "begin": "2024-01-19",
+ "end": "2024-01-25"
},
{
- "name": "Prüfungszeitraum",
- "begin": "2023-01-26",
- "end": "2023-02-04"
+ "name": {
+ "de": "Ende der Vorlesungszeit",
+ "en": "End of lecture period"
+ },
+ "begin": "2024-01-25"
},
{
- "name": "Ende der Vorlesungszeit",
- "begin": "2023-01-25"
+ "name": {
+ "de": "Prüfungszeitraum",
+ "en": "Exam period"
+ },
+ "begin": "2024-01-26",
+ "end": "2024-02-05"
},
{
- "name": "Notenmeldung",
- "begin": "2023-02-09T09:00",
+ "name": {
+ "de": "Notenmeldung",
+ "en": "Recording grades"
+ },
+ "begin": "2024-02-09T9:00",
"hasHours": true
},
{
- "name": "Notenbekanntgabe",
- "begin": "2023-02-14T13:00",
+ "name": {
+ "de": "Notenbekanntgabe",
+ "en": "Announcement of grades"
+ },
+ "begin": "2024-02-14T13:00",
"hasHours": true
},
{
- "name": "Rückmeldung zum Sommersemester 2023",
- "begin": "2023-01-14",
- "end": "2023-01-31"
- },
- {
- "name": "Semesterferien",
- "begin": "2022-02-15",
- "end": "2022-03-14"
+ "name": {
+ "de": "Semesterferien",
+ "en": "Semester break"
+ },
+ "begin": "2024-02-15",
+ "end": "2024-03-14"
},
{
- "name": "Vorlesungsbeginn und Semesterstart",
- "begin": "2023-03-15"
+ "name": {
+ "de": "Vorlesungsbeginn und Semesterstart",
+ "en": "Start of lectures and semester"
+ },
+ "begin": "2024-03-18"
},
{
- "name": "Vorlesungsfrei (Ostern)",
- "begin": "2023-04-06",
- "end": "2023-04-11"
+ "name": {
+ "de": "Vorlesungsfrei (Ostern)",
+ "en": "No lectures (Easter)"
+ },
+ "begin": "2024-03-28",
+ "end": "2024-04-02"
},
{
- "name": "Prüfungsanmeldung",
- "begin": "2023-04-25",
- "end": "2023-05-04"
+ "name": {
+ "de": "Prüfungsanmeldung",
+ "en": "Registration for exams"
+ },
+ "begin": "2024-04-23",
+ "end": "2024-05-02"
},
{
"name": {
"de": "Prüfungsabmeldung",
- "en": "Exam withdrawal"
+ "en": "Deregistration for exams"
},
- "begin": "2023-04-25",
- "end": "2023-06-26"
+ "begin": "2024-04-23",
+ "end": "2024-06-26"
},
{
- "name": "Vorlesungsfrei (Christi Himmelfahrt, Brückentag)",
- "begin": "2023-05-18",
- "end": "2023-05-19"
- },
- {
- "name": "Vorlesungsfrei (Pfingsten)",
- "begin": "2023-05-26",
- "end": "2023-05-30"
+ "name": {
+ "de": "Vorlesungsfrei (Christi Himmelfahrt, Brückentag)",
+ "en": "No lectures (Ascension Day, Bridge Day)"
+ },
+ "begin": "2024-05-09",
+ "end": "2024-05-10"
},
{
- "name": "Vorlesungsfrei (Fronleichnam)",
- "begin": "2023-06-08"
+ "name": {
+ "de": "Vorlesungsfrei (Pfingsten)",
+ "en": "No lectures (Whitsun)"
+ },
+ "begin": "2024-05-17",
+ "end": "2024-05-21"
},
{
- "name": "Notenmeldung Prädikate (ZV)",
- "begin": "2023-06-19T09:00",
- "hasHours": true
+ "name": {
+ "de": "Vorlesungsfrei (Fronleichnam)",
+ "en": "No lectures (Corpus Christi)"
+ },
+ "begin": "2024-05-30"
},
{
"name": {
- "de": "Rückmeldung zum Wintersemester 2023/2024",
- "en": "Re-registration for winter semester 2023/2024"
+ "de": "Notenmeldung Prädikate (ZV)",
+ "en": "Recording grades predicates (admission requirements)"
},
-
- "begin": "2023-07-02",
- "end": "2023-08-07"
+ "begin": "2024-06-19T9:00",
+ "hasHours": true
},
{
"name": {
"de": "Erweiterter Prüfungszeitraum",
"en": "Additional exam period"
},
- "begin": "2023-07-04",
- "end": "2023-07-07"
+ "begin": "2024-07-04",
+ "end": "2024-07-10"
},
{
"name": {
- "de": "Prüfungszeitraum",
- "en": "Exam period"
+ "de": "Ende der Vorlesungszeit",
+ "en": "End of lecture period"
},
- "begin": "2023-07-08",
- "end": "2023-07-20"
+ "begin": "2024-07-10"
},
{
"name": {
- "de": "Ende der Vorlesungszeit",
- "en": "End of lecture period"
+ "de": "Prüfungszeitraum",
+ "en": "Exam period"
},
- "begin": "2023-07-07"
+ "begin": "2024-07-11",
+ "end": "2024-07-23"
},
{
"name": {
"de": "Notenmeldung",
"en": "Recording grades"
},
- "begin": "2023-07-25T09:00",
+ "begin": "2024-07-26T9:00",
"hasHours": true
},
{
@@ -214,7 +214,7 @@
"de": "Notenbekanntgabe",
"en": "Announcement of grades"
},
- "begin": "2023-07-31T13:00",
+ "begin": "2024-07-31T13:00",
"hasHours": true
},
{
@@ -222,7 +222,7 @@
"de": "Semesterferien",
"en": "Semester break"
},
- "begin": "2023-08-01",
- "end": "2023-09-30"
+ "begin": "2024-08-01",
+ "end": "2024-09-30"
}
]
From 8c132194edf3fa628bcb7f98647520c511e1447d Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Mon, 18 Sep 2023 11:48:01 +0200
Subject: [PATCH 32/34] =?UTF-8?q?=F0=9F=90=9B=20fix=20german=20grade=20sum?=
=?UTF-8?q?mary=20disclaimer=20(#322)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/public/locales/de/grades.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rogue-thi-app/public/locales/de/grades.json b/rogue-thi-app/public/locales/de/grades.json
index c73f9e4d..d065c06a 100644
--- a/rogue-thi-app/public/locales/de/grades.json
+++ b/rogue-thi-app/public/locales/de/grades.json
@@ -14,7 +14,7 @@
},
"summary": {
"title": "Notenschnitt",
- "disclaimer": "Der genaue Notenschnitt kann nicht ermittelt werden und liegt zwischen {{minAverage} und {{maxAverage}}"
+ "disclaimer": "Der genaue Notenschnitt kann nicht ermittelt werden und liegt zwischen {{minAverage}} und {{maxAverage}}"
},
"spoiler": {
"reveal": "Klicken um anzuzeigen"
From 4f4cfd58f583eb856be0caba7fea95b5abe72cac Mon Sep 17 00:00:00 2001
From: Alexander Horn
Date: Mon, 25 Sep 2023 20:20:07 +0200
Subject: [PATCH 33/34] Fix source URL for Hochschule bus stop
---
rogue-thi-app/data/mobility.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rogue-thi-app/data/mobility.json b/rogue-thi-app/data/mobility.json
index 1f9fc5b6..ed414b8c 100644
--- a/rogue-thi-app/data/mobility.json
+++ b/rogue-thi-app/data/mobility.json
@@ -20,7 +20,7 @@
{
"id": "hochschule",
"name": "Hochschule",
- "url": "https://www.invg.de/rt/getRealtimeData.action?stopPoint=2&station=IN-THoScu&sid=413"
+ "url": "https://www.invg.de/rt/getRealtimeData.action?stopPoint=2&station=IN-THoSc&sid=413"
},
{
"id": "rathausplatz",
From ac164b5c13faf37fe4c9ed5d3988147b67e76a4e Mon Sep 17 00:00:00 2001
From: Philipp Opheys
Date: Thu, 28 Sep 2023 19:40:19 +0200
Subject: [PATCH 34/34] =?UTF-8?q?=F0=9F=90=9B=20fix=20food=20disclaimer=20?=
=?UTF-8?q?for=20non-translated=20meals?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
rogue-thi-app/pages/food.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rogue-thi-app/pages/food.jsx b/rogue-thi-app/pages/food.jsx
index 7015f01f..6c4006fd 100644
--- a/rogue-thi-app/pages/food.jsx
+++ b/rogue-thi-app/pages/food.jsx
@@ -532,7 +532,7 @@ export default function Mensa () {
{t('foodModal.warning.title')}
- {`${isTranslated(showMealDetails) && `${t('foodModal.translation.warning')} `}${t('foodModal.warning.text')}`}
+ {`${isTranslated(showMealDetails) ? `${t('foodModal.translation.warning')} ` : ''}${t('foodModal.warning.text')}`}
{isTranslated(showMealDetails) && (