mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 09:38:51 +02:00
Resolve "lint, test and build for frontend image"
This commit is contained in:
@ -1,8 +1,20 @@
|
||||
stages:
|
||||
- lint
|
||||
- build
|
||||
- test
|
||||
- docker
|
||||
|
||||
lint-frontend:
|
||||
image: node:lts
|
||||
stage: lint
|
||||
rules:
|
||||
- changes:
|
||||
- frontend/**/*
|
||||
script:
|
||||
- cd frontend
|
||||
- npm i
|
||||
- npm run lint-no-fix
|
||||
|
||||
build-backend:
|
||||
image: golang:1.21-alpine
|
||||
stage: build
|
||||
@ -18,7 +30,7 @@ build-backend:
|
||||
- backend/go.sum
|
||||
- backend/go.mod
|
||||
|
||||
test:
|
||||
test-backend:
|
||||
image: golang:1.21-alpine
|
||||
stage: test
|
||||
rules:
|
||||
@ -30,6 +42,19 @@ test:
|
||||
dependencies:
|
||||
- build-backend
|
||||
|
||||
test-frontend:
|
||||
image: node:lts
|
||||
stage: test
|
||||
rules:
|
||||
- changes:
|
||||
- frontend/**/*
|
||||
script:
|
||||
- cd frontend
|
||||
- npm i
|
||||
- npm run test
|
||||
dependencies:
|
||||
- lint-frontend
|
||||
|
||||
build-backend-image:
|
||||
stage: docker
|
||||
image: docker:20.10.16
|
||||
@ -53,3 +78,27 @@ build-backend-image:
|
||||
only:
|
||||
- main
|
||||
- development
|
||||
|
||||
build-frontend-image:
|
||||
stage: docker
|
||||
image: docker:20.10.16
|
||||
services:
|
||||
- name: docker:20.10.16-dind
|
||||
alias: docker
|
||||
tags:
|
||||
- image
|
||||
variables:
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||
DOCKER_HOST: tcp://docker:2376
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
DOCKER_TLS_VERIFY: 1
|
||||
DOCKER_CERT_PATH: "/certs/client"
|
||||
script:
|
||||
- cd frontend
|
||||
- docker login -u $CI_DOCKER_REGISTRY_USER -p $CI_DOCKER_REGISTRY_PASSWORD $CI_DOCKER_REGISTRY
|
||||
- docker build -f Dockerfile_prod -t htwkalender-frontend$IMAGE_TAG .
|
||||
- docker tag htwkalender-frontend$IMAGE_TAG $CI_DOCKER_REGISTRY_USER/htwkalender:frontend
|
||||
- docker push $CI_DOCKER_REGISTRY_USER/htwkalender:frontend
|
||||
only:
|
||||
- main
|
||||
- development
|
1796
frontend/package-lock.json
generated
1796
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,9 @@
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
|
||||
"format": "prettier . --write"
|
||||
"lint-no-fix": "eslint --ext .js,.vue --ignore-path .gitignore src",
|
||||
"format": "prettier . --write",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.9",
|
||||
@ -38,6 +40,7 @@
|
||||
"sass-loader": "^13.3.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.12",
|
||||
"vitest": "^1.1.3",
|
||||
"vue-tsc": "^1.8.5"
|
||||
}
|
||||
}
|
||||
|
@ -31,15 +31,15 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e =
|
||||
<template>
|
||||
|
||||
<Button
|
||||
id="dark-mode-switcher"
|
||||
size="small"
|
||||
class="p-button-rounded w-full md:w-auto"
|
||||
id="dark-mode-switcher"
|
||||
style="margin-right: 1rem"
|
||||
:severity="isDark ? 'warning' : 'info'"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<i class="pi pi-sun" v-if="isDark"></i>
|
||||
<i class="pi pi-moon" v-else></i>
|
||||
<i v-if="isDark" class="pi pi-sun"></i>
|
||||
<i v-else class="pi pi-moon"></i>
|
||||
</Button>
|
||||
|
||||
</template>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import LocaleSwitcher from "./LocaleSwitcher.vue";
|
||||
import DarkModeSwitcher from "./DarkModeSwitcher.vue"
|
||||
import DarkModeSwitcher from "./DarkModeSwitcher.vue";
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const items = computed(() => [
|
||||
@ -43,12 +43,7 @@ const items = computed(() => [
|
||||
<Menubar :model="items" class="menubar justify-content-between flex-wrap">
|
||||
<template #start>
|
||||
<router-link v-slot="{ navigate }" :to="`/`" custom>
|
||||
<Button
|
||||
severity="secondary"
|
||||
text
|
||||
class="p-0 mx-2"
|
||||
@click="navigate"
|
||||
>
|
||||
<Button severity="secondary" text class="p-0 mx-2" @click="navigate">
|
||||
<img
|
||||
width="50"
|
||||
height="50"
|
||||
@ -65,12 +60,12 @@ const items = computed(() => [
|
||||
:label="String(item.label)"
|
||||
:icon="item.icon"
|
||||
text
|
||||
@click="navigate"
|
||||
severity="secondary"
|
||||
@click="navigate"
|
||||
/>
|
||||
</router-link>
|
||||
</template>
|
||||
<template #end class="align-items-stretch">
|
||||
<template #end>
|
||||
<div class="flex align-items-stretch justify-content-center">
|
||||
<DarkModeSwitcher></DarkModeSwitcher>
|
||||
<LocaleSwitcher></LocaleSwitcher>
|
||||
|
@ -125,9 +125,9 @@ async function finalStep() {
|
||||
<Button
|
||||
:disabled="store.isEmpty()"
|
||||
class="col-12 md:col-4 mb-3 align-self-end"
|
||||
@click="finalStep()"
|
||||
icon="pi pi-save"
|
||||
:label="$t('renameModules.nextStep')"
|
||||
@click="finalStep()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import { computed, ComputedRef, inject, ref, Ref, watch } from "vue";
|
||||
import { CalendarOptions, EventInput } from "@fullcalendar/core";
|
||||
import { CalendarOptions, DatesSetArg, EventInput } from "@fullcalendar/core";
|
||||
import { fetchEventsByRoomAndDuration } from "../api/fetchRoom.ts";
|
||||
import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
@ -46,111 +46,114 @@ async function getOccupation() {
|
||||
currentDateFrom.value,
|
||||
currentDateTo.value,
|
||||
)
|
||||
.then((events) => {
|
||||
occupations.value = events.map((event, index) => {
|
||||
return {
|
||||
id: index,
|
||||
start: event.start.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"),
|
||||
end: event.end.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"),
|
||||
showFree: event.free
|
||||
};
|
||||
});
|
||||
.then((events) => {
|
||||
occupations.value = events.map((event, index) => {
|
||||
return {
|
||||
id: index,
|
||||
start: event.start.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"),
|
||||
end: event.end.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"),
|
||||
showFree: event.free,
|
||||
};
|
||||
});
|
||||
|
||||
const calendar = fullCalendar.value?.getApi();
|
||||
calendar?.refetchEvents();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
const calendar = fullCalendar.value?.getApi();
|
||||
calendar?.refetchEvents();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
import allLocales from "@fullcalendar/core/locales-all";
|
||||
|
||||
const calendarOptions: ComputedRef<CalendarOptions> = computed(() =>
|
||||
({
|
||||
locales: allLocales,
|
||||
locale: t("languageCode"),
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
|
||||
// local debugging of mobilePage variable in object creation on ternary expression
|
||||
initialView: mobilePage.value ? "Day" : "week",
|
||||
dayHeaderFormat: { weekday: "short", omitCommas: true },
|
||||
slotDuration: "00:15:00",
|
||||
eventTimeFormat: {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
},
|
||||
height: "auto",
|
||||
views: {
|
||||
week: {
|
||||
description: "Wochenansicht",
|
||||
type: "timeGrid",
|
||||
slotLabelFormat: {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
omitZeroMinute: false,
|
||||
meridiem: false,
|
||||
hour12: false,
|
||||
},
|
||||
dateAlignment: "week",
|
||||
titleFormat: { month: "short", day: "numeric" },
|
||||
slotMinTime: "06:00:00",
|
||||
slotMaxTime: "22:00:00",
|
||||
duration: { days: 7 },
|
||||
firstDay: 1,
|
||||
allDaySlot: false,
|
||||
hiddenDays: [0],
|
||||
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
|
||||
locales: allLocales,
|
||||
locale: t("languageCode"),
|
||||
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
|
||||
// local debugging of mobilePage variable in object creation on ternary expression
|
||||
initialView: mobilePage.value ? "Day" : "week",
|
||||
dayHeaderFormat: { weekday: "short", omitCommas: true },
|
||||
slotDuration: "00:15:00",
|
||||
eventTimeFormat: {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
},
|
||||
height: "auto",
|
||||
views: {
|
||||
week: {
|
||||
description: "Wochenansicht",
|
||||
type: "timeGrid",
|
||||
slotLabelFormat: {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
omitZeroMinute: false,
|
||||
meridiem: false,
|
||||
hour12: false,
|
||||
},
|
||||
Day: {
|
||||
type: "timeGrid",
|
||||
slotLabelFormat: {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
omitZeroMinute: false,
|
||||
meridiem: false,
|
||||
hour12: false,
|
||||
},
|
||||
titleFormat: { month: "short", day: "numeric" },
|
||||
slotMinTime: "06:00:00",
|
||||
slotMaxTime: "22:00:00",
|
||||
duration: { days: 1 },
|
||||
allDaySlot: false,
|
||||
hiddenDays: [0],
|
||||
dateAlignment: "week",
|
||||
titleFormat: { month: "short", day: "numeric" },
|
||||
slotMinTime: "06:00:00",
|
||||
slotMaxTime: "22:00:00",
|
||||
duration: { days: 7 },
|
||||
firstDay: 1,
|
||||
allDaySlot: false,
|
||||
hiddenDays: [0],
|
||||
},
|
||||
Day: {
|
||||
type: "timeGrid",
|
||||
slotLabelFormat: {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
omitZeroMinute: false,
|
||||
meridiem: false,
|
||||
hour12: false,
|
||||
},
|
||||
titleFormat: { month: "short", day: "numeric" },
|
||||
slotMinTime: "06:00:00",
|
||||
slotMaxTime: "22:00:00",
|
||||
duration: { days: 1 },
|
||||
allDaySlot: false,
|
||||
hiddenDays: [0],
|
||||
},
|
||||
headerToolbar: {
|
||||
end: "prev,next today",
|
||||
center: "title",
|
||||
start: "week,Day",
|
||||
},
|
||||
},
|
||||
headerToolbar: {
|
||||
end: "prev,next today",
|
||||
center: "title",
|
||||
start: "week,Day",
|
||||
},
|
||||
|
||||
datesSet: function (dateInfo: any) {
|
||||
const view = dateInfo.view;
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
const startDate = new Date(
|
||||
view.activeStart.getTime() - offset * 60 * 1000,
|
||||
);
|
||||
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
|
||||
currentDateFrom.value = startDate.toISOString().split("T")[0];
|
||||
currentDateTo.value = endDate.toISOString().split("T")[0];
|
||||
getOccupation();
|
||||
},
|
||||
events: function (_info: any, successCallback: any, _: any) {
|
||||
successCallback(
|
||||
occupations.value.map((event) => {
|
||||
return {
|
||||
id: event.id.toString(),
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
color: event.showFree ? "var(--green-800)" : "var(--primary-color)",
|
||||
textColor: event.showFree ? "var(--green-50)" : "var(--primary-color-text)",
|
||||
title: event.showFree ? t("roomFinderPage.available") : t("roomFinderPage.occupied"),
|
||||
} as EventInput;
|
||||
}),
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
datesSet: function (dateInfo: DatesSetArg) {
|
||||
const view = dateInfo.view;
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
const startDate = new Date(view.activeStart.getTime() - offset * 60 * 1000);
|
||||
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
|
||||
currentDateFrom.value = startDate.toISOString().split("T")[0];
|
||||
currentDateTo.value = endDate.toISOString().split("T")[0];
|
||||
getOccupation();
|
||||
},
|
||||
events: function (
|
||||
_info: unknown,
|
||||
successCallback: (events: EventInput[]) => void,
|
||||
) {
|
||||
successCallback(
|
||||
occupations.value.map((event) => {
|
||||
return {
|
||||
id: event.id.toString(),
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
color: event.showFree ? "var(--green-800)" : "var(--primary-color)",
|
||||
textColor: event.showFree
|
||||
? "var(--green-50)"
|
||||
: "var(--primary-color-text)",
|
||||
title: event.showFree
|
||||
? t("roomFinderPage.available")
|
||||
: t("roomFinderPage.occupied"),
|
||||
} as EventInput;
|
||||
}),
|
||||
);
|
||||
},
|
||||
}));
|
||||
</script>
|
||||
<template>
|
||||
<FullCalendar ref="fullCalendar" :options="calendarOptions" />
|
||||
|
14
frontend/src/helpers/strings.test.ts
Normal file
14
frontend/src/helpers/strings.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { onlyWhitespace } from "@/helpers/strings.ts";
|
||||
|
||||
test("contains No whitespace", () => {
|
||||
expect(onlyWhitespace("awdawdawd")).toBe(false);
|
||||
});
|
||||
|
||||
test("contains whitespace", () => {
|
||||
expect(onlyWhitespace("aw daw dawd")).toBe(false);
|
||||
});
|
||||
|
||||
test("contains only whitespace", () => {
|
||||
expect(onlyWhitespace(" ")).toBe(true);
|
||||
});
|
@ -24,10 +24,10 @@ async function nextStep() {
|
||||
<div class="flex align-items-center justify-content-end h-4rem m-2 w-full lg:w-10">
|
||||
<Button
|
||||
:disabled="store.isEmpty()"
|
||||
@click="nextStep()"
|
||||
class="col-12 md:col-4 mb-3 align-self-end"
|
||||
icon="pi pi-arrow-right"
|
||||
:label="$t('additionalModules.nextStep')"
|
||||
@click="nextStep()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,9 +58,9 @@ async function getModules() {
|
||||
|
||||
<template>
|
||||
<DynamicPage
|
||||
:hideContent="selectedCourse.name === ''"
|
||||
:hide-content="selectedCourse.name === ''"
|
||||
:headline="$t('courseSelection.headline')"
|
||||
:subTitle="$t('courseSelection.subTitle')"
|
||||
:sub-title="$t('courseSelection.subTitle')"
|
||||
icon="pi pi-calendar"
|
||||
:button="{
|
||||
label: $t('courseSelection.nextStep'),
|
||||
|
@ -49,7 +49,7 @@ const hasContent = computed(() => {
|
||||
>
|
||||
<slot
|
||||
name="selection"
|
||||
flexSpecs="flex-1 m-0"
|
||||
flex-specs="flex-1 m-0"
|
||||
></slot>
|
||||
</div>
|
||||
<div
|
||||
@ -59,9 +59,9 @@ const hasContent = computed(() => {
|
||||
<Button
|
||||
:disabled="button.disabled"
|
||||
class="col-12 md:col-4"
|
||||
@click="button.onClick()"
|
||||
:icon="button.icon"
|
||||
:label="button.label"
|
||||
@click="button.onClick()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
@ -72,9 +72,9 @@ function loadCalendar(): void {
|
||||
|
||||
<template>
|
||||
<DynamicPage
|
||||
hideContent
|
||||
hide-content
|
||||
:headline="$t('editCalendarView.headline')"
|
||||
:subTitle="$t('editCalendarView.subTitle')"
|
||||
:sub-title="$t('editCalendarView.subTitle')"
|
||||
icon="pi pi-pencil"
|
||||
:button="{
|
||||
label: $t('editCalendarView.loadCalendar'),
|
||||
|
@ -21,9 +21,9 @@ rooms().then(
|
||||
|
||||
<template>
|
||||
<DynamicPage
|
||||
:hideContent="selectedRoom.name === ''"
|
||||
:hide-content="selectedRoom.name === ''"
|
||||
:headline="$t('roomFinderPage.headline')"
|
||||
:subTitle="$t('roomFinderPage.detail')"
|
||||
:sub-title="$t('roomFinderPage.detail')"
|
||||
icon="pi pi-search"
|
||||
>
|
||||
<template #selection>
|
||||
|
@ -24,10 +24,10 @@ async function nextStep() {
|
||||
<div class="flex align-items-center justify-content-end h-4rem m-2 w-full lg:w-10">
|
||||
<Button
|
||||
:disabled="store.isEmpty()"
|
||||
@click="nextStep()"
|
||||
class="col-12 md:col-4 mb-3 align-self-end"
|
||||
icon="pi pi-arrow-right"
|
||||
:label="$t('additionalModules.nextStep')"
|
||||
@click="nextStep()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -188,9 +188,9 @@ async function deleteFeed() {
|
||||
<div
|
||||
class="flex flex-column sm:flex-row flex-wrap justify-content-between gap-2 w-full"
|
||||
>
|
||||
<Button type="button" severity="danger" outlined @click="visible = true" icon="pi pi-trash" :label="$t('editCalendarView.delete')"/>
|
||||
<Button type="button" severity="info" outlined @click="router.push('edit-additional-modules')" icon="pi pi-plus" :label="$t('editCalendarView.addModules')"/>
|
||||
<Button type="button" severity="success" outlined @click="finalStep()" icon="pi pi-save" :label="$t('editCalendarView.save')"/>
|
||||
<Button type="button" severity="danger" outlined icon="pi pi-trash" :label="$t('editCalendarView.delete')" @click="visible = true"/>
|
||||
<Button type="button" severity="info" outlined icon="pi pi-plus" :label="$t('editCalendarView.addModules')" @click="router.push('edit-additional-modules')"/>
|
||||
<Button type="button" severity="success" outlined icon="pi pi-save" :label="$t('editCalendarView.save')" @click="finalStep()"/>
|
||||
</div>
|
||||
</template>
|
||||
</DataTable>
|
||||
@ -206,7 +206,7 @@ async function deleteFeed() {
|
||||
</template>
|
||||
<p class="m-0">{{ $t('editCalendarView.dialog.subTitle') }}</p>
|
||||
<template #footer>
|
||||
<Button :label="$t('editCalendarView.dialog.delete')" severity="danger" icon="pi pi-trash" @click="deleteFeed()" autofocus />
|
||||
<Button :label="$t('editCalendarView.dialog.delete')" severity="danger" icon="pi pi-trash" autofocus @click="deleteFeed()" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
@ -18,10 +18,10 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [
|
||||
|
Reference in New Issue
Block a user