mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 09:03:02 +00:00
feat: migrate backend to cosmiconfig
This commit is contained in:
27
backend/backend/config/default/app/index.js
Normal file
27
backend/backend/config/default/app/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import userGroupSetting from './user-group-setting.js';
|
||||
import languageSetting from './language-setting.js';
|
||||
import menus from './menu.js';
|
||||
|
||||
/** @type {import('@openstapps/core').SCAppConfiguration} */
|
||||
const app = {
|
||||
aboutPages: {},
|
||||
campusPolygon: {
|
||||
coordinates: [
|
||||
[
|
||||
[8.660_432_999_690_723, 50.123_027_017_044_436],
|
||||
[8.675_496_285_518_358, 50.123_027_017_044_436],
|
||||
[8.675_496_285_518_358, 50.130_661_764_486_42],
|
||||
[8.660_432_999_690_723, 50.130_661_764_486_42],
|
||||
[8.660_432_999_690_723, 50.123_027_017_044_436],
|
||||
],
|
||||
],
|
||||
type: 'Polygon',
|
||||
},
|
||||
features: {},
|
||||
menus,
|
||||
name: 'Goethe-Uni',
|
||||
privacyPolicyUrl: 'https://mobile.server.uni-frankfurt.de/_static/privacy.md',
|
||||
settings: [userGroupSetting, languageSetting],
|
||||
};
|
||||
|
||||
export default app;
|
||||
34
backend/backend/config/default/app/language-setting.js
Normal file
34
backend/backend/config/default/app/language-setting.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCLanguageSetting} */
|
||||
const languageSetting = {
|
||||
categories: ['profile'],
|
||||
defaultValue: 'en',
|
||||
description: 'The language this app is going to use.',
|
||||
inputType: SCSettingInputType.SingleChoice,
|
||||
name: 'language',
|
||||
order: 0,
|
||||
origin: {
|
||||
indexed: '2018-09-11T12:30:00Z',
|
||||
name: 'SCConfigFile Default Values',
|
||||
type: SCThingOriginType.Remote,
|
||||
},
|
||||
translations: {
|
||||
de: {
|
||||
description: 'Die Sprache in der die App angezeigt wird.',
|
||||
name: 'Sprache',
|
||||
values: ['Deutsch', 'English'],
|
||||
},
|
||||
en: {
|
||||
description: 'The language this app is going to use.',
|
||||
name: 'Language',
|
||||
values: ['Deutsch', 'English'],
|
||||
},
|
||||
},
|
||||
type: SCThingType.Setting,
|
||||
uid: 'dc9d6dec-6576-45ef-9e35-3598c0d6a662',
|
||||
values: ['de', 'en'],
|
||||
};
|
||||
|
||||
export default languageSetting;
|
||||
195
backend/backend/config/default/app/menu.js
Normal file
195
backend/backend/config/default/app/menu.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// @ts-check
|
||||
/** @type {import('@openstapps/core').SCAppConfigurationMenuCategory[]} */
|
||||
const menus = [
|
||||
{
|
||||
icon: 'home',
|
||||
items: [
|
||||
{
|
||||
icon: 'newspaper',
|
||||
route: '/news',
|
||||
title: 'news',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Aktuelles',
|
||||
},
|
||||
en: {
|
||||
title: 'news',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'search',
|
||||
route: '/search',
|
||||
title: 'search',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Suche',
|
||||
},
|
||||
en: {
|
||||
title: 'search',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'local_library',
|
||||
route: '/hebis-search',
|
||||
title: 'library catalog',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Bibliothekskatalog',
|
||||
},
|
||||
en: {
|
||||
title: 'library catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'inventory_2',
|
||||
route: '/catalog',
|
||||
title: 'course catalog',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Vorlesungsverzeichnis',
|
||||
},
|
||||
en: {
|
||||
title: 'course catalog',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
title: 'overview',
|
||||
route: '/overview',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Übersicht',
|
||||
},
|
||||
en: {
|
||||
title: 'overview',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'local_cafe',
|
||||
items: [],
|
||||
route: '/canteen',
|
||||
title: 'canteen',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Mensa',
|
||||
},
|
||||
en: {
|
||||
title: 'canteen',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'map',
|
||||
items: [],
|
||||
route: '/map',
|
||||
title: 'campus map',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Campus Karte',
|
||||
},
|
||||
en: {
|
||||
title: 'campus map',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'school',
|
||||
items: [
|
||||
{
|
||||
icon: 'grade',
|
||||
route: '/favorites',
|
||||
title: 'favorites',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Favoriten',
|
||||
},
|
||||
en: {
|
||||
title: 'favorites',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'calendar_today',
|
||||
route: '/schedule',
|
||||
title: 'schedule',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Stundenplan',
|
||||
},
|
||||
en: {
|
||||
title: 'schedule',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
authProvider: 'paia',
|
||||
icon: 'badge',
|
||||
route: '/library-account',
|
||||
title: 'library account',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Bibliothekskonto',
|
||||
},
|
||||
en: {
|
||||
title: 'library account',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'settings',
|
||||
route: '/settings',
|
||||
title: 'settings',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Einstellungen',
|
||||
},
|
||||
en: {
|
||||
title: 'settings',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'rate_review',
|
||||
route: '/feedback',
|
||||
title: 'feedback',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Feedback',
|
||||
},
|
||||
en: {
|
||||
title: 'feedback',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'info',
|
||||
route: '/about',
|
||||
title: 'about',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Über die App',
|
||||
},
|
||||
en: {
|
||||
title: 'About the App',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
title: 'my app',
|
||||
route: '/profile',
|
||||
translations: {
|
||||
de: {
|
||||
title: 'Meine App',
|
||||
},
|
||||
en: {
|
||||
title: 'my app',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default menus;
|
||||
40
backend/backend/config/default/app/user-group-setting.js
Normal file
40
backend/backend/config/default/app/user-group-setting.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @ts-check
|
||||
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCUserGroupSetting} */
|
||||
const userGroupSetting = {
|
||||
categories: ['profile'],
|
||||
defaultValue: 'students',
|
||||
description:
|
||||
'The user group the app is going to be used.' +
|
||||
'This settings for example is getting used for the predefined price category of mensa meals.',
|
||||
inputType: SCSettingInputType.SingleChoice,
|
||||
name: 'group',
|
||||
order: 1,
|
||||
origin: {
|
||||
indexed: '2018-09-11T12:30:00Z',
|
||||
name: 'SCConfigFile Default Values',
|
||||
type: SCThingOriginType.Remote,
|
||||
},
|
||||
translations: {
|
||||
de: {
|
||||
description:
|
||||
'Mit welcher Benutzergruppe soll die App verwendet werden?' +
|
||||
' Die Einstellung wird beispielsweise für die Vorauswahl der Preiskategorie der Mensa verwendet.',
|
||||
name: 'Gruppe',
|
||||
values: ['Studierende', 'Angestellte', 'Gäste'],
|
||||
},
|
||||
en: {
|
||||
description:
|
||||
'The user group the app is going to be used.' +
|
||||
' This settings for example is getting used for the predefined price category of mensa meals.',
|
||||
name: 'Group',
|
||||
values: ['students', 'employees', 'guests'],
|
||||
},
|
||||
},
|
||||
type: SCThingType.Setting,
|
||||
uid: '2c97aa36-4aa2-43de-bc5d-a2b2cb3a530e',
|
||||
values: ['students', 'employees', 'guests'],
|
||||
};
|
||||
|
||||
export default userGroupSetting;
|
||||
51
backend/backend/config/default/backend/aggregations.js
Normal file
51
backend/backend/config/default/backend/aggregations.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @ts-check
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCBackendAggregationConfiguration[]} */
|
||||
const aggregations = [
|
||||
{
|
||||
fieldName: 'categories',
|
||||
onlyOnTypes: [
|
||||
SCThingType.AcademicEvent,
|
||||
SCThingType.Article,
|
||||
SCThingType.Building,
|
||||
SCThingType.Catalog,
|
||||
SCThingType.Dish,
|
||||
SCThingType.PointOfInterest,
|
||||
SCThingType.Room,
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'inPlace.name',
|
||||
onlyOnTypes: [
|
||||
SCThingType.DateSeries,
|
||||
SCThingType.Dish,
|
||||
SCThingType.Floor,
|
||||
SCThingType.Organization,
|
||||
SCThingType.PointOfInterest,
|
||||
SCThingType.Room,
|
||||
SCThingType.Ticket,
|
||||
],
|
||||
},
|
||||
{
|
||||
fieldName: 'academicTerms.acronym',
|
||||
onlyOnTypes: [SCThingType.AcademicEvent, SCThingType.SportCourse],
|
||||
},
|
||||
{
|
||||
fieldName: 'academicTerm.acronym',
|
||||
onlyOnTypes: [SCThingType.Catalog],
|
||||
},
|
||||
{
|
||||
fieldName: 'majors',
|
||||
onlyOnTypes: [SCThingType.AcademicEvent],
|
||||
},
|
||||
{
|
||||
fieldName: 'keywords',
|
||||
onlyOnTypes: [SCThingType.Article, SCThingType.Book, SCThingType.Message, SCThingType.Video],
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
},
|
||||
];
|
||||
|
||||
export default aggregations;
|
||||
103
backend/backend/config/default/backend/boostings.js
Normal file
103
backend/backend/config/default/backend/boostings.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// @ts-check
|
||||
import {
|
||||
month,
|
||||
sommerRange,
|
||||
ssAcronymLong,
|
||||
ssAcronymShort,
|
||||
winterRange,
|
||||
wsAcronymLong,
|
||||
wsAcronymShort,
|
||||
} from '../tools/semester-acronym.js';
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
|
||||
/** @type {import('@openstapps/core').SCBackendConfigurationSearchBoostingContext} */
|
||||
const boostings = {
|
||||
default: [
|
||||
{
|
||||
factor: 1,
|
||||
fields: {
|
||||
'academicTerms.acronym': {
|
||||
[ssAcronymShort]: sommerRange.includes(month) ? 1.1 : 1.05,
|
||||
[wsAcronymShort]: winterRange.includes(month) ? 1.1 : 1.05,
|
||||
[ssAcronymLong]: sommerRange.includes(month) ? 1.1 : 1.05,
|
||||
[wsAcronymLong]: winterRange.includes(month) ? 1.1 : 1.05,
|
||||
},
|
||||
},
|
||||
type: SCThingType.AcademicEvent,
|
||||
},
|
||||
{
|
||||
factor: 1,
|
||||
fields: {
|
||||
categories: {
|
||||
'course': 1.08,
|
||||
'integrated course': 1.08,
|
||||
'introductory class': 1.05,
|
||||
'lecture': 1.1,
|
||||
'seminar': 1.01,
|
||||
'tutorial': 1.05,
|
||||
},
|
||||
},
|
||||
type: SCThingType.AcademicEvent,
|
||||
},
|
||||
{
|
||||
factor: 1.6,
|
||||
type: SCThingType.Building,
|
||||
},
|
||||
{
|
||||
factor: 1,
|
||||
fields: {
|
||||
categories: {
|
||||
cafe: 1.1,
|
||||
learn: 1.1,
|
||||
library: 1.2,
|
||||
restaurant: 1.1,
|
||||
},
|
||||
},
|
||||
type: SCThingType.PointOfInterest,
|
||||
},
|
||||
{
|
||||
factor: 1,
|
||||
fields: {
|
||||
categories: {
|
||||
'main dish': 2,
|
||||
},
|
||||
},
|
||||
type: SCThingType.Dish,
|
||||
},
|
||||
],
|
||||
dining: [
|
||||
{
|
||||
factor: 1,
|
||||
fields: {
|
||||
categories: {
|
||||
'cafe': 2,
|
||||
'canteen': 2,
|
||||
'restaurant': 2,
|
||||
'restroom': 1.2,
|
||||
'student canteen': 2,
|
||||
},
|
||||
},
|
||||
type: SCThingType.Building,
|
||||
},
|
||||
{
|
||||
factor: 2,
|
||||
type: SCThingType.Dish,
|
||||
},
|
||||
],
|
||||
place: [
|
||||
{
|
||||
factor: 2,
|
||||
type: SCThingType.Building,
|
||||
},
|
||||
{
|
||||
factor: 2,
|
||||
type: SCThingType.PointOfInterest,
|
||||
},
|
||||
{
|
||||
factor: 2,
|
||||
type: SCThingType.Room,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default boostings;
|
||||
73
backend/backend/config/default/backend/index.js
Normal file
73
backend/backend/config/default/backend/index.js
Normal file
@@ -0,0 +1,73 @@
|
||||
// @ts-check
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
import aggregations from './aggregations.js';
|
||||
import boostings from './boostings.js';
|
||||
import {readFile} from 'fs/promises';
|
||||
|
||||
/** @type {import('@openstapps/core').SCBackendInternalConfiguration} */
|
||||
export const internal = {
|
||||
aggregations,
|
||||
boostings,
|
||||
};
|
||||
|
||||
/** @type {import('@openstapps/core').SCBackendConfiguration} */
|
||||
export const backend = {
|
||||
SCVersion: JSON.parse(await readFile('package.json', 'utf8')).version,
|
||||
externalRequestTimeout: 5000,
|
||||
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
||||
mappingIgnoredTags: ['minlength', 'pattern', 'see', 'tjs-format'],
|
||||
maxMultiSearchRouteQueries: 5,
|
||||
maxRequestBodySize: 512 * 1024,
|
||||
name: 'Goethe-Universität Frankfurt am Main',
|
||||
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
|
||||
sortableFields: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
sortTypes: ['ducet'],
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
sortTypes: ['ducet'],
|
||||
},
|
||||
{
|
||||
fieldName: 'categories',
|
||||
onlyOnTypes: [
|
||||
SCThingType.AcademicEvent,
|
||||
SCThingType.Building,
|
||||
SCThingType.Catalog,
|
||||
SCThingType.Dish,
|
||||
SCThingType.PointOfInterest,
|
||||
SCThingType.Room,
|
||||
],
|
||||
sortTypes: ['ducet'],
|
||||
},
|
||||
{
|
||||
fieldName: 'geo',
|
||||
onlyOnTypes: [SCThingType.Building, SCThingType.PointOfInterest, SCThingType.Room],
|
||||
sortTypes: ['distance'],
|
||||
},
|
||||
{
|
||||
fieldName: 'geo',
|
||||
onlyOnTypes: [SCThingType.Building, SCThingType.PointOfInterest, SCThingType.Room],
|
||||
sortTypes: ['distance'],
|
||||
},
|
||||
{
|
||||
fieldName: 'inPlace.geo',
|
||||
onlyOnTypes: [
|
||||
SCThingType.DateSeries,
|
||||
SCThingType.Dish,
|
||||
SCThingType.Floor,
|
||||
SCThingType.Organization,
|
||||
SCThingType.PointOfInterest,
|
||||
SCThingType.Room,
|
||||
SCThingType.Ticket,
|
||||
],
|
||||
sortTypes: ['distance'],
|
||||
},
|
||||
{
|
||||
fieldName: 'offers',
|
||||
onlyOnTypes: [SCThingType.Dish],
|
||||
sortTypes: ['price'],
|
||||
},
|
||||
],
|
||||
};
|
||||
26
backend/backend/config/default/backendrc.js
Normal file
26
backend/backend/config/default/backendrc.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @ts-check
|
||||
import app from './app/index.js';
|
||||
import {backend, internal} from './backend/index.js';
|
||||
|
||||
/**
|
||||
* This is the default configuration for app and backend
|
||||
*
|
||||
* University-specific files can be created with the following naming scheme: default-<university license plate>.ts
|
||||
*
|
||||
* To select your university-specific configuration which is merged from this default file and your university-specific
|
||||
* file, you have to supply the `NODE_APP_INSTANCE` environment variable with your license plate
|
||||
*
|
||||
* To get more information about the meaning of specific fields, please have a look at `@openstapps/core` or use your
|
||||
* IDE to read the TSDoc documentation.
|
||||
*
|
||||
* @type {import('@openstapps/core').SCConfigFile}
|
||||
*/
|
||||
const config = {
|
||||
app,
|
||||
auth: {},
|
||||
backend,
|
||||
internal,
|
||||
uid: 'f-u',
|
||||
};
|
||||
|
||||
export default config;
|
||||
33
backend/backend/config/default/elasticsearchrc.js
Normal file
33
backend/backend/config/default/elasticsearchrc.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* This is the default configuration for elasticsearch (a database)
|
||||
*
|
||||
* To select your university-specific configuration which is merged from this default file and your university-specific
|
||||
* file, you have to supply the `NODE_APP_INSTANCE` environment variable with your license plate
|
||||
*
|
||||
* To select a different database, you have to supply the `NODE_CONFIG_ENV` environment variable with a database name
|
||||
* that is implemented in the backend
|
||||
*
|
||||
* To get more information about the meaning of specific fields, please use your IDE to read the TSDoc documentation.
|
||||
*
|
||||
* @type {import('../../src/storage/elasticsearch/types/elasticsearch.js')}
|
||||
*/
|
||||
const config = {
|
||||
internal: {
|
||||
database: {
|
||||
name: 'elasticsearch',
|
||||
version: '5.6',
|
||||
query: {
|
||||
minMatch: '75%',
|
||||
queryType: 'dis_max',
|
||||
matchBoosting: 1.3,
|
||||
fuzziness: 'AUTO',
|
||||
cutoffFrequency: 0,
|
||||
tieBreaker: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
9
backend/backend/config/default/prometheusrc.json
Normal file
9
backend/backend/config/default/prometheusrc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"metricsPath": "/metrics",
|
||||
"includeMethod": true,
|
||||
"includePath": true,
|
||||
"promClient": {
|
||||
"collectDefaultMetrics": {}
|
||||
},
|
||||
"for-more-options-see": "https://github.com/jochen-schweizer/express-prom-bundle#options"
|
||||
}
|
||||
29
backend/backend/config/default/tools/markdown.js
Normal file
29
backend/backend/config/default/tools/markdown.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// @ts-check
|
||||
import {readFile} from 'fs/promises';
|
||||
import {SCAboutPageContentType} from '@openstapps/core';
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
*
|
||||
* ```js
|
||||
* await markdown('./page.md', import.meta.url)
|
||||
* ```
|
||||
*
|
||||
* @param {string} path relative path to the file, omitting the language marker
|
||||
* @param {string | URL} base base path, usually import.meta.url
|
||||
* @returns {Promise<import('@openstapps/core').SCAboutPageMarkdown>}
|
||||
*/
|
||||
export async function markdown(path, base) {
|
||||
const de = await readFile(new URL(path.replace(/\.md$/, '.de.md'), base), 'utf8');
|
||||
const en = await readFile(new URL(path.replace(/\.md$/, '.en.md'), base), 'utf8');
|
||||
|
||||
return {
|
||||
value: de,
|
||||
translations: {
|
||||
en: {
|
||||
value: en,
|
||||
},
|
||||
},
|
||||
type: SCAboutPageContentType.MARKDOWN,
|
||||
};
|
||||
}
|
||||
34
backend/backend/config/default/tools/semester-acronym.js
Normal file
34
backend/backend/config/default/tools/semester-acronym.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Generates a range of numbers that represent consecutive calendar months
|
||||
*
|
||||
* @param {number} startMonth The month to start with (inclusive)
|
||||
* @param {number} endMonth The month to end with (inclusive)
|
||||
* @returns {number[]}
|
||||
*/
|
||||
export function yearSlice(startMonth, endMonth) {
|
||||
let months = [...Array.from({length: 13}).keys()].slice(1);
|
||||
months = [...months, ...months];
|
||||
if (!months.includes(startMonth) || !months.includes(endMonth)) {
|
||||
throw new Error(`Given months not part of a year! Check ${startMonth} or ${endMonth}!`);
|
||||
}
|
||||
|
||||
const startIndex = months.indexOf(startMonth);
|
||||
const endIndex =
|
||||
months.indexOf(endMonth) <= startIndex ? months.lastIndexOf(endMonth) : months.indexOf(endMonth);
|
||||
|
||||
return months.slice(startIndex, endIndex + 1);
|
||||
}
|
||||
|
||||
export const sommerRange = yearSlice(3, 8);
|
||||
export const winterRange = yearSlice(9, 2);
|
||||
export const month = new Date().getMonth();
|
||||
export const year = new Date().getFullYear();
|
||||
export const winterYearOffset = month < winterRange[0] ? -1 : 0;
|
||||
export const sommerYear = year + (month <= winterRange[winterRange.length] ? -1 : 0);
|
||||
export const winterYear = `${year + winterYearOffset}/${(year + 1 + winterYearOffset).toString().slice(-2)}`;
|
||||
|
||||
export const wsAcronymShort = `WS ${winterYear}`;
|
||||
export const ssAcronymShort = `SS ${sommerYear}`;
|
||||
export const wsAcronymLong = `WiSe ${winterYear}`;
|
||||
export const ssAcronymLong = `SoSe ${sommerYear}`;
|
||||
Reference in New Issue
Block a user