Merge branch '15-lint-test-and-build-for-frontend-image' into 'main'

Resolve "lint, test and build for frontend image"

Closes #15

See merge request ekresse/htwkalender!3
This commit is contained in:
ekresse
2024-01-12 13:58:52 +00:00
16 changed files with 1970 additions and 150 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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" />

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">
<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>

View File

@ -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'),

View File

@ -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

View File

@ -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'),

View File

@ -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>

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">
<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>

View File

@ -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>

View File

@ -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": [