Resolve "lint, test and build for frontend image"

This commit is contained in:
ekresse
2024-01-12 13:58:52 +00:00
parent 718d190a04
commit d4d0c5199c
16 changed files with 1970 additions and 150 deletions

View File

@@ -1,8 +1,20 @@
stages: stages:
- lint
- build - build
- test - test
- docker - docker
lint-frontend:
image: node:lts
stage: lint
rules:
- changes:
- frontend/**/*
script:
- cd frontend
- npm i
- npm run lint-no-fix
build-backend: build-backend:
image: golang:1.21-alpine image: golang:1.21-alpine
stage: build stage: build
@@ -18,7 +30,7 @@ build-backend:
- backend/go.sum - backend/go.sum
- backend/go.mod - backend/go.mod
test: test-backend:
image: golang:1.21-alpine image: golang:1.21-alpine
stage: test stage: test
rules: rules:
@@ -30,6 +42,19 @@ test:
dependencies: dependencies:
- build-backend - 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: build-backend-image:
stage: docker stage: docker
image: docker:20.10.16 image: docker:20.10.16
@@ -53,3 +78,27 @@ build-backend-image:
only: only:
- main - main
- development - 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

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,9 @@
"build": "vue-tsc && vite build", "build": "vue-tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", "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": { "dependencies": {
"@fullcalendar/core": "^6.1.9", "@fullcalendar/core": "^6.1.9",
@@ -38,6 +40,7 @@
"sass-loader": "^13.3.2", "sass-loader": "^13.3.2",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.4.12", "vite": "^4.4.12",
"vitest": "^1.1.3",
"vue-tsc": "^1.8.5" "vue-tsc": "^1.8.5"
} }
} }

View File

@@ -31,15 +31,15 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e =
<template> <template>
<Button <Button
id="dark-mode-switcher"
size="small" size="small"
class="p-button-rounded w-full md:w-auto" class="p-button-rounded w-full md:w-auto"
id="dark-mode-switcher"
style="margin-right: 1rem" style="margin-right: 1rem"
:severity="isDark ? 'warning' : 'info'" :severity="isDark ? 'warning' : 'info'"
@click="toggleTheme" @click="toggleTheme"
> >
<i class="pi pi-sun" v-if="isDark"></i> <i v-if="isDark" class="pi pi-sun"></i>
<i class="pi pi-moon" v-else></i> <i v-else class="pi pi-moon"></i>
</Button> </Button>
</template> </template>

View File

@@ -2,7 +2,7 @@
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import LocaleSwitcher from "./LocaleSwitcher.vue"; import LocaleSwitcher from "./LocaleSwitcher.vue";
import DarkModeSwitcher from "./DarkModeSwitcher.vue" import DarkModeSwitcher from "./DarkModeSwitcher.vue";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const items = computed(() => [ const items = computed(() => [
@@ -43,12 +43,7 @@ const items = computed(() => [
<Menubar :model="items" class="menubar justify-content-between flex-wrap"> <Menubar :model="items" class="menubar justify-content-between flex-wrap">
<template #start> <template #start>
<router-link v-slot="{ navigate }" :to="`/`" custom> <router-link v-slot="{ navigate }" :to="`/`" custom>
<Button <Button severity="secondary" text class="p-0 mx-2" @click="navigate">
severity="secondary"
text
class="p-0 mx-2"
@click="navigate"
>
<img <img
width="50" width="50"
height="50" height="50"
@@ -65,12 +60,12 @@ const items = computed(() => [
:label="String(item.label)" :label="String(item.label)"
:icon="item.icon" :icon="item.icon"
text text
@click="navigate"
severity="secondary" severity="secondary"
@click="navigate"
/> />
</router-link> </router-link>
</template> </template>
<template #end class="align-items-stretch"> <template #end>
<div class="flex align-items-stretch justify-content-center"> <div class="flex align-items-stretch justify-content-center">
<DarkModeSwitcher></DarkModeSwitcher> <DarkModeSwitcher></DarkModeSwitcher>
<LocaleSwitcher></LocaleSwitcher> <LocaleSwitcher></LocaleSwitcher>

View File

@@ -125,9 +125,9 @@ async function finalStep() {
<Button <Button
:disabled="store.isEmpty()" :disabled="store.isEmpty()"
class="col-12 md:col-4 mb-3 align-self-end" class="col-12 md:col-4 mb-3 align-self-end"
@click="finalStep()"
icon="pi pi-save" icon="pi pi-save"
:label="$t('renameModules.nextStep')" :label="$t('renameModules.nextStep')"
@click="finalStep()"
/> />
</div> </div>
</div> </div>

View File

@@ -4,7 +4,7 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
import { computed, ComputedRef, inject, ref, Ref, watch } from "vue"; 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 { fetchEventsByRoomAndDuration } from "../api/fetchRoom.ts";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
@@ -46,111 +46,114 @@ async function getOccupation() {
currentDateFrom.value, currentDateFrom.value,
currentDateTo.value, currentDateTo.value,
) )
.then((events) => { .then((events) => {
occupations.value = events.map((event, index) => { occupations.value = events.map((event, index) => {
return { return {
id: index, id: index,
start: event.start.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"), start: event.start.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"),
end: event.end.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"), end: event.end.replace(/\s\+\d{4}\s\w+$/, "").replace(" ", "T"),
showFree: event.free showFree: event.free,
}; };
}); });
const calendar = fullCalendar.value?.getApi(); const calendar = fullCalendar.value?.getApi();
calendar?.refetchEvents(); calendar?.refetchEvents();
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
}); });
} }
import allLocales from "@fullcalendar/core/locales-all"; import allLocales from "@fullcalendar/core/locales-all";
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
({ locales: allLocales,
locales: allLocales, locale: t("languageCode"),
locale: t("languageCode"), plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin],
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin], // local debugging of mobilePage variable in object creation on ternary expression
// local debugging of mobilePage variable in object creation on ternary expression initialView: mobilePage.value ? "Day" : "week",
initialView: mobilePage.value ? "Day" : "week", dayHeaderFormat: { weekday: "short", omitCommas: true },
dayHeaderFormat: { weekday: "short", omitCommas: true }, slotDuration: "00:15:00",
slotDuration: "00:15:00", eventTimeFormat: {
eventTimeFormat: { hour: "2-digit",
hour: "2-digit", minute: "2-digit",
minute: "2-digit", hour12: false,
hour12: false, },
}, height: "auto",
height: "auto", views: {
views: { week: {
week: { description: "Wochenansicht",
description: "Wochenansicht", type: "timeGrid",
type: "timeGrid", slotLabelFormat: {
slotLabelFormat: { hour: "numeric",
hour: "numeric", minute: "2-digit",
minute: "2-digit", omitZeroMinute: false,
omitZeroMinute: false, meridiem: false,
meridiem: false, hour12: 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],
}, },
Day: { dateAlignment: "week",
type: "timeGrid", titleFormat: { month: "short", day: "numeric" },
slotLabelFormat: { slotMinTime: "06:00:00",
hour: "numeric", slotMaxTime: "22:00:00",
minute: "2-digit", duration: { days: 7 },
omitZeroMinute: false, firstDay: 1,
meridiem: false, allDaySlot: false,
hour12: false, hiddenDays: [0],
}, },
titleFormat: { month: "short", day: "numeric" }, Day: {
slotMinTime: "06:00:00", type: "timeGrid",
slotMaxTime: "22:00:00", slotLabelFormat: {
duration: { days: 1 }, hour: "numeric",
allDaySlot: false, minute: "2-digit",
hiddenDays: [0], 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", headerToolbar: {
center: "title", end: "prev,next today",
start: "week,Day", center: "title",
}, start: "week,Day",
},
datesSet: function (dateInfo: any) { datesSet: function (dateInfo: DatesSetArg) {
const view = dateInfo.view; const view = dateInfo.view;
const offset = new Date().getTimezoneOffset(); const offset = new Date().getTimezoneOffset();
const startDate = new Date( const startDate = new Date(view.activeStart.getTime() - offset * 60 * 1000);
view.activeStart.getTime() - offset * 60 * 1000, const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
); currentDateFrom.value = startDate.toISOString().split("T")[0];
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000); currentDateTo.value = endDate.toISOString().split("T")[0];
currentDateFrom.value = startDate.toISOString().split("T")[0]; getOccupation();
currentDateTo.value = endDate.toISOString().split("T")[0]; },
getOccupation(); events: function (
}, _info: unknown,
events: function (_info: any, successCallback: any, _: any) { successCallback: (events: EventInput[]) => void,
successCallback( ) {
occupations.value.map((event) => { successCallback(
return { occupations.value.map((event) => {
id: event.id.toString(), return {
start: event.start, id: event.id.toString(),
end: event.end, start: event.start,
color: event.showFree ? "var(--green-800)" : "var(--primary-color)", end: event.end,
textColor: event.showFree ? "var(--green-50)" : "var(--primary-color-text)", color: event.showFree ? "var(--green-800)" : "var(--primary-color)",
title: event.showFree ? t("roomFinderPage.available") : t("roomFinderPage.occupied"), textColor: event.showFree
} as EventInput; ? "var(--green-50)"
}), : "var(--primary-color-text)",
); title: event.showFree
}, ? t("roomFinderPage.available")
}) : t("roomFinderPage.occupied"),
); } as EventInput;
}),
);
},
}));
</script> </script>
<template> <template>
<FullCalendar ref="fullCalendar" :options="calendarOptions" /> <FullCalendar ref="fullCalendar" :options="calendarOptions" />

View 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);
});

View File

@@ -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"> <div class="flex align-items-center justify-content-end h-4rem m-2 w-full lg:w-10">
<Button <Button
:disabled="store.isEmpty()" :disabled="store.isEmpty()"
@click="nextStep()"
class="col-12 md:col-4 mb-3 align-self-end" class="col-12 md:col-4 mb-3 align-self-end"
icon="pi pi-arrow-right" icon="pi pi-arrow-right"
:label="$t('additionalModules.nextStep')" :label="$t('additionalModules.nextStep')"
@click="nextStep()"
/> />
</div> </div>
</div> </div>

View File

@@ -58,9 +58,9 @@ async function getModules() {
<template> <template>
<DynamicPage <DynamicPage
:hideContent="selectedCourse.name === ''" :hide-content="selectedCourse.name === ''"
:headline="$t('courseSelection.headline')" :headline="$t('courseSelection.headline')"
:subTitle="$t('courseSelection.subTitle')" :sub-title="$t('courseSelection.subTitle')"
icon="pi pi-calendar" icon="pi pi-calendar"
:button="{ :button="{
label: $t('courseSelection.nextStep'), label: $t('courseSelection.nextStep'),

View File

@@ -49,7 +49,7 @@ const hasContent = computed(() => {
> >
<slot <slot
name="selection" name="selection"
flexSpecs="flex-1 m-0" flex-specs="flex-1 m-0"
></slot> ></slot>
</div> </div>
<div <div
@@ -59,9 +59,9 @@ const hasContent = computed(() => {
<Button <Button
:disabled="button.disabled" :disabled="button.disabled"
class="col-12 md:col-4" class="col-12 md:col-4"
@click="button.onClick()"
:icon="button.icon" :icon="button.icon"
:label="button.label" :label="button.label"
@click="button.onClick()"
/> />
</div> </div>
<div <div

View File

@@ -72,9 +72,9 @@ function loadCalendar(): void {
<template> <template>
<DynamicPage <DynamicPage
hideContent hide-content
:headline="$t('editCalendarView.headline')" :headline="$t('editCalendarView.headline')"
:subTitle="$t('editCalendarView.subTitle')" :sub-title="$t('editCalendarView.subTitle')"
icon="pi pi-pencil" icon="pi pi-pencil"
:button="{ :button="{
label: $t('editCalendarView.loadCalendar'), label: $t('editCalendarView.loadCalendar'),

View File

@@ -21,9 +21,9 @@ rooms().then(
<template> <template>
<DynamicPage <DynamicPage
:hideContent="selectedRoom.name === ''" :hide-content="selectedRoom.name === ''"
:headline="$t('roomFinderPage.headline')" :headline="$t('roomFinderPage.headline')"
:subTitle="$t('roomFinderPage.detail')" :sub-title="$t('roomFinderPage.detail')"
icon="pi pi-search" icon="pi pi-search"
> >
<template #selection> <template #selection>

View File

@@ -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"> <div class="flex align-items-center justify-content-end h-4rem m-2 w-full lg:w-10">
<Button <Button
:disabled="store.isEmpty()" :disabled="store.isEmpty()"
@click="nextStep()"
class="col-12 md:col-4 mb-3 align-self-end" class="col-12 md:col-4 mb-3 align-self-end"
icon="pi pi-arrow-right" icon="pi pi-arrow-right"
:label="$t('additionalModules.nextStep')" :label="$t('additionalModules.nextStep')"
@click="nextStep()"
/> />
</div> </div>
</div> </div>

View File

@@ -188,9 +188,9 @@ async function deleteFeed() {
<div <div
class="flex flex-column sm:flex-row flex-wrap justify-content-between gap-2 w-full" 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="danger" outlined icon="pi pi-trash" :label="$t('editCalendarView.delete')" @click="visible = true"/>
<Button type="button" severity="info" outlined @click="router.push('edit-additional-modules')" icon="pi pi-plus" :label="$t('editCalendarView.addModules')"/> <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 @click="finalStep()" icon="pi pi-save" :label="$t('editCalendarView.save')"/> <Button type="button" severity="success" outlined icon="pi pi-save" :label="$t('editCalendarView.save')" @click="finalStep()"/>
</div> </div>
</template> </template>
</DataTable> </DataTable>
@@ -206,7 +206,7 @@ async function deleteFeed() {
</template> </template>
<p class="m-0">{{ $t('editCalendarView.dialog.subTitle') }}</p> <p class="m-0">{{ $t('editCalendarView.dialog.subTitle') }}</p>
<template #footer> <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> </template>
</Dialog> </Dialog>
</div> </div>

View File

@@ -18,10 +18,10 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
}, "paths": {
"paths": { "@/*": ["./src/*"]
"@/*": ["./src/*"] }
}, },
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [ "references": [