Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

59 changed files with 7435 additions and 10377 deletions

View File

@ -1,11 +1,2 @@
.browserslistrc
.dockerignore
.drone.yml
.editorconfig
.git
.gitignore
.reuse
Dockerfile
LICENSES
README.md
node_modules

View File

@ -4,110 +4,17 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
kind: pipeline
type: docker
name: qa
trigger:
event:
- push
- pull_request
branch:
- main
name: default
steps:
- name: reuse
image: fsfe/reuse
- name: lint
image: node:20
commands:
- npm ci
- npm run lint
- name: docker-build
image: plugins/docker
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/frontend
target: ki-frontend
dry_run: true
---
kind: pipeline
type: docker
name: build
trigger:
event:
- push
branch:
- main
depends_on:
- qa
steps:
- name: docker-publish
image: plugins/docker
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/frontend
target: ki-frontend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
---
kind: pipeline
type: docker
name: deploy
trigger:
event:
- push
branch:
- main
depends_on:
- build
steps:
- name: deploy-dev
image: appleboy/drone-ssh
settings:
host:
- dev01.wtf-eg.net
username: drone_deployment
key:
from_secret: "dev01_deployment_key"
command_timeout: 2m
script:
- echo "Executing forced command..."
---
kind: pipeline
type: docker
name: tag-release
trigger:
event:
- tag
steps:
- name: reuse
image: fsfe/reuse
- name: lint
image: node:20
commands:
- npm ci
- npm run lint
- name: docker-publish
image: plugins/docker
settings:
registry: git.wtf-eg.de
repo: git.wtf-eg.de/kompetenzinventar/frontend
target: ki-frontend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"
- name: docker-publish
image: plugins/docker
settings:
registry: registry.wtf-eg.net
repo: registry.wtf-eg.net/ki-frontend
target: ki-frontend
auto_tag: true
username:
from_secret: "docker_username"
password:
from_secret: "docker_password"

1
.gitignore vendored
View File

@ -23,4 +23,3 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
jsconfig.json

View File

@ -3,26 +3,8 @@ Upstream-Name: ki-frontend
Upstream-Contact: Scammo <kontakt@samuelbrinkmann.de>
Source: https://git.wtf-eg.de/kompetenzinventar/ki-frontend
Files: package.json package-lock.json
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
License: AGPL-3.0-or-later
Files: .browserslistrc .dockerignore .eslintrc.js .gitignore
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
License: AGPL-3.0-or-later
Files: src/assets/img/wtf*
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
License: LicenseRef-WTF
Files: src/assets/language_level.json src/assets/skill_level.json
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
License: AGPL-3.0-or-later
Files: public/img/bootstrap-icons-1.5.0/*
Copyright: Copyright (c) 2019-2020 The Bootstrap Authors
License: MIT
Files: public/fonts/Lato*
Copyright: 2010-2015, Łukasz Dziedzic (dziedzic@typoland.com)
License: OFL-1.1-RFN
# Sample paragraph, commented out:
#
# Files: src/*
# Copyright: $YEAR $NAME <$CONTACT>
# License: ...

View File

@ -1,28 +1,10 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM node:14-alpine as builder
COPY package.json package-lock.json ./
RUN npm install
COPY .eslintrc.js .
COPY babel.config.js .
COPY public public
COPY src src
COPY . ./
RUN npm ci && npm run build
FROM nginx as ki-frontend
LABEL org.opencontainers.image.source=https://git.wtf-eg.de/kompetenzinventar/ki-frontend.git
LABEL org.opencontainers.image.url=https://git.wtf-eg.de/kompetenzinventar/ki-frontend
LABEL org.opencontainers.image.documentation=https://git.wtf-eg.de/kompetenzinventar/ki-frontend#docker
LABEL org.opencontainers.image.vendor="WTF Kooperative eG"
WORKDIR /usr/share/nginx/html
COPY etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /dist .
COPY --from=builder /dist/ /usr/share/nginx/html/

View File

@ -1 +0,0 @@
Alle Rechte vorbehalten

9
LICENSES/MIT.txt Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,43 +0,0 @@
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -1,40 +1,8 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
# ki-frontend
[![Build Status](https://drone.wtf-eg.de/api/badges/kompetenzinventar/ki-frontend/status.svg)](https://drone.wtf-eg.de/kompetenzinventar/ki-frontend)
[![REUSE status](https://api.reuse.software/badge/git.wtf-eg.de/kompetenzinventar/ki-frontend)](https://api.reuse.software/info/git.wtf-eg.de/kompetenzinventar/ki-frontend)
## Über
Dieses Repo enthält das Frontend des Projekts Kompentenzinventar - einer Webapplikation zur Erfassung von Userprofilen für die WTF eG.
Implementiert ist das Frontend mit Vue3.
### Mitmachen
Du kannst gerne bei der Entwicklung des Kompetenzinventars mitmachen.
- Fehler oder fehlende Funktionen erfassen. Bitte direkt über die [Issues](https://git.wtf-eg.de/kompetenzinventar/ki-frontend/issues) in Gitea.
- Dokumentation oder Implementierung verbessern. Bitte forke hierzu das Projekt, branche von `main` ab und erstelle dann einen [Pull Request](https://git.wtf-eg.de/kompetenzinventar/ki-frontend/pulls).
### Kommunikation
Folgende Kanäle gibt es für die Kommunikation über das Kompetenzinventar:
- Die [Issues](https://git.wtf-eg.de/kompetenzinventar/ki-frontend/issues) im WTF Gitea.
- Den Bereich [AG Entwicklung](https://forum.wtf-eg.de/c/interna/ag-entwicklung/21) im WTF Forum.
- Einen Raum in Matrix. Zutritt per Einladung, frlan lädt ein, eine einfache PN im Forum reicht.
### Repos
* [ki-backend](https://git.wtf-eg.de/kompetenzinventar/ki-backend) enthält das Backend
* **[ki-frontend](https://git.wtf-eg.de/kompetenzinventar/ki-frontend)** (dieses Repo) enthält das Frontend
* Weitere Repositories befinden sich in der Gitea Organisation [Kompetenzinventar](https://git.wtf-eg.de/kompetenzinventar).
Vue3 Projekt:
## Project setup
```

View File

@ -1,7 +1,3 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'

View File

@ -1,63 +0,0 @@
# SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
server {
listen 80;
listen [::]:80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
root /usr/share/nginx/html;
# routes without dots serve the index.html without caching
location / {
add_header Cache-Control "no-cache";
try_files $uri $uri/index.html /index.html;
}
# static js and css files that get replaced instead of updated
location ~ \.(js|css) {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# cache other static files for 30 days
location ~ \.(?!html) {
add_header Cache-Control "public, max-age=2592000";
try_files $uri =404;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}

14804
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,22 +7,20 @@
"lint": "vue-cli-service lint"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.0",
"@vue/cli-plugin-eslint": "^4.5.0",
"@vue/cli-plugin-router": "^4.5.0",
"@vue/cli-service": "^4.5.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"axios": "^0.21.1",
"babel-eslint": "^10.1.0",
"bootstrap": "^5.0.1",
"bootstrap-icons": "^1.5.0",
"core-js": "^3.6.5",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0",
"sass": "^1.37.5",
"sass-loader": "^10.2.0",
"v-tooltip": "^4.0.0-alpha.1",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.2"
"vue-router": "^4.0.0-0"
}
}

BIN
public/apple-touch-icon.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1,7 +1,4 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
window.ki = {
apiUrl: 'http://localhost:5000'
}

View File

@ -1,7 +1,4 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
window.ki = {
apiUrl: 'http://localhost:13338'
}

BIN
public/favicon-96x96.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
SPDX-License-Identifier: MIT

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
<path d="M8 0a1 1 0 0 1 1 1v6h6a1 1 0 1 1 0 2H9v6a1 1 0 1 1-2 0V9H1a1 1 0 0 1 0-2h6V1a1 1 0 0 1 1-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@ -0,0 +1 @@
SPDX-License-Identifier: MIT

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@ -0,0 +1 @@
SPDX-License-Identifier: MIT

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

View File

@ -0,0 +1 @@
SPDX-License-Identifier: MIT

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,9 +1,3 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!DOCTYPE html>
<html lang="de">
<head>

View File

@ -1,25 +1,115 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<div class="alert alert-success text-center mb-0" role="alert">
Willkommen beim Alpha-Test!
<a href="https://git.wtf-eg.de/kompetenzinventar/ki-frontend/issues/new">Problem gefunden? Bitte hier ein Issue anlegen.</a>
</div>
<header v-if="this.$route.path.includes('/s/')">
<Navbar />
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">KI</a>
<button
@click="showMobileNavbar = !showMobileNavbar"
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
:class="{
show: showMobileNavbar,
}"
id="navbarSupportedContent"
>
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link
class="nav-link"
:to="{ path: `/s/search` }"
active-class="active"
>Suche</router-link
>
</li>
<li class="nav-item">
<router-link
class="nav-link"
:to="{ path: `/s/profile/${memberId}` }"
active-class="active"
>Mein Profil</router-link
>
</li>
<li class="nav-item">
<router-link
class="nav-link"
:to="{ path: `/s/profile-edit` }"
active-class="active"
>Bearbeiten</router-link
>
</li>
<li class="nav-item">
<button class="btn btn-outline-danger" @click="logout()">
Logout
</button>
<a class="nav-link active" aria-current="page" href="#"></a>
</li>
</ul>
<form class="d-flex" @submit.prevent="searchRedirect()">
<input
class="form-control me-2"
v-model="searchText"
type="search"
placeholder="Profile durchsuchen"
aria-label="Search"
/>
<button
class="btn btn-outline-primary"
type="submit"
>
<img
src="/img/bootstrap-icons-1.5.0/search.svg"
alt="Suche Icon"
/>
</button>
</form>
</div>
</div>
</nav>
</header>
<router-view :key="$route.fullPath" />
<Footer />
<router-view />
<footer>
<a href="https://wtf-eg.de/impressum/" class="m-4">Impressum</a>
<a href="https://wtf-eg.de/datenschutz/" class="m-4">Datenschutz</a>
<a href="https://git.wtf-eg.de/kompetenzinventar" class="m-4">git(ea)</a>
<a href="https://git.wtf-eg.de/kompetenzinventar/ki-frontend/issues/new" class="m-4">Problem melden</a>
</footer>
</template>
<script>
import Footer from '@/components/Footer'
import Navbar from '@/components/Navbar'
export default {
name: 'App',
components: {
Footer,
Navbar,
}
name: "App",
data() {
return {
showMobileNavbar: false,
memberId: null,
searchText: "",
};
},
created() {
this.memberId = localStorage.getItem("user_id");
},
methods: {
logout() {
localStorage.clear();
this.$router.push({ path: "/" });
},
searchRedirect() {
this.$router.push({ path: `/s/search?text`, query: { query: this.searchText } } );
},
},
};
</script>

View File

@ -1,23 +1,10 @@
/*
* SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
@import "variables";
@import "bootstrap/scss/bootstrap";
.bg-wtf {
background-image: url(../assets/img/wtf-header-bg.jpg);
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
.btn-primary {
color: #fff !important;
}
.search-card{
height: 100%;
.container{
min-height: calc(100vh - 70px - 24px);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@ -1,100 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1476 617" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Background" transform="matrix(1,0,0,1,-184.291,-861.078)">
<rect x="184.291" y="861.078" width="1475.62" height="616.639" style="fill:none;"/>
</g>
<g id="Logo" transform="matrix(1,0,0,1,-184.236,-860.997)">
<g id="WTF-Koorperative-eG" serif:id="WTF Koorperative eG">
<g id="eG" transform="matrix(4.16667,0,0,4.16667,-389.557,-181.762)">
<g transform="matrix(15.962,0,0,15.962,431.653,372.57)">
<path d="M0.288,-0.434C0.271,-0.434 0.255,-0.431 0.241,-0.424C0.227,-0.417 0.214,-0.407 0.202,-0.395C0.191,-0.383 0.181,-0.368 0.172,-0.351C0.164,-0.334 0.157,-0.315 0.152,-0.295C0.193,-0.3 0.227,-0.306 0.252,-0.313C0.277,-0.32 0.296,-0.327 0.31,-0.335C0.324,-0.343 0.333,-0.352 0.337,-0.361C0.342,-0.37 0.344,-0.38 0.344,-0.39C0.344,-0.395 0.343,-0.4 0.341,-0.405C0.339,-0.41 0.335,-0.415 0.331,-0.419C0.326,-0.423 0.32,-0.427 0.313,-0.43C0.306,-0.433 0.298,-0.434 0.288,-0.434ZM0.142,-0.209C0.142,-0.128 0.176,-0.088 0.244,-0.088C0.259,-0.088 0.272,-0.089 0.283,-0.092C0.294,-0.095 0.304,-0.098 0.313,-0.103C0.321,-0.107 0.329,-0.111 0.336,-0.116C0.343,-0.121 0.349,-0.126 0.355,-0.13C0.361,-0.134 0.366,-0.137 0.372,-0.14C0.378,-0.143 0.384,-0.144 0.391,-0.144C0.395,-0.144 0.398,-0.143 0.402,-0.141C0.406,-0.139 0.41,-0.136 0.413,-0.133L0.444,-0.095C0.426,-0.077 0.409,-0.062 0.392,-0.049C0.375,-0.036 0.358,-0.025 0.34,-0.017C0.322,-0.009 0.304,-0.003 0.284,0.002C0.264,0.006 0.243,0.008 0.22,0.008C0.19,0.008 0.163,0.002 0.139,-0.008C0.114,-0.018 0.093,-0.032 0.076,-0.051C0.059,-0.069 0.045,-0.092 0.036,-0.118C0.026,-0.144 0.022,-0.173 0.022,-0.206C0.022,-0.233 0.024,-0.259 0.03,-0.285C0.036,-0.311 0.045,-0.336 0.056,-0.359C0.067,-0.382 0.08,-0.404 0.096,-0.424C0.112,-0.443 0.13,-0.46 0.15,-0.475C0.17,-0.489 0.193,-0.5 0.217,-0.508C0.241,-0.516 0.267,-0.52 0.295,-0.52C0.321,-0.52 0.345,-0.516 0.365,-0.509C0.385,-0.501 0.402,-0.491 0.415,-0.479C0.429,-0.467 0.439,-0.453 0.446,-0.438C0.452,-0.423 0.456,-0.408 0.456,-0.394C0.456,-0.371 0.451,-0.35 0.442,-0.331C0.433,-0.312 0.416,-0.295 0.393,-0.28C0.369,-0.265 0.337,-0.252 0.297,-0.241C0.256,-0.23 0.204,-0.222 0.142,-0.216L0.142,-0.209Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(15.962,0,0,15.962,440.065,372.57)">
<path d="M0.41,-0.361L0.635,-0.361L0.599,-0.07C0.562,-0.043 0.522,-0.024 0.48,-0.011C0.438,0.002 0.389,0.008 0.335,0.008C0.291,0.008 0.25,0 0.214,-0.016C0.178,-0.032 0.147,-0.054 0.121,-0.083C0.095,-0.111 0.076,-0.145 0.062,-0.184C0.048,-0.223 0.041,-0.266 0.041,-0.313C0.041,-0.353 0.045,-0.392 0.054,-0.428C0.063,-0.465 0.076,-0.498 0.092,-0.53C0.109,-0.561 0.129,-0.588 0.152,-0.613C0.175,-0.638 0.201,-0.659 0.23,-0.677C0.259,-0.694 0.29,-0.708 0.323,-0.717C0.357,-0.726 0.392,-0.731 0.429,-0.731C0.458,-0.731 0.484,-0.728 0.508,-0.723C0.532,-0.718 0.554,-0.711 0.573,-0.702C0.592,-0.693 0.61,-0.683 0.625,-0.671C0.64,-0.659 0.654,-0.646 0.666,-0.633L0.624,-0.579C0.617,-0.569 0.609,-0.563 0.6,-0.561C0.59,-0.559 0.58,-0.561 0.57,-0.568C0.56,-0.574 0.551,-0.581 0.541,-0.587C0.531,-0.593 0.521,-0.599 0.509,-0.604C0.498,-0.608 0.485,-0.612 0.47,-0.616C0.455,-0.619 0.438,-0.62 0.419,-0.62C0.383,-0.62 0.349,-0.613 0.32,-0.598C0.29,-0.584 0.264,-0.563 0.242,-0.537C0.22,-0.51 0.204,-0.478 0.192,-0.441C0.18,-0.404 0.174,-0.363 0.174,-0.317C0.174,-0.282 0.178,-0.251 0.187,-0.224C0.196,-0.197 0.208,-0.173 0.224,-0.155C0.24,-0.135 0.259,-0.121 0.282,-0.111C0.304,-0.101 0.329,-0.095 0.357,-0.096C0.384,-0.095 0.408,-0.098 0.429,-0.103C0.45,-0.108 0.47,-0.116 0.489,-0.126L0.505,-0.263L0.425,-0.263C0.417,-0.263 0.41,-0.265 0.406,-0.27C0.402,-0.274 0.4,-0.28 0.401,-0.287L0.41,-0.361Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
</g>
<g id="Kooperative" transform="matrix(4.16667,0,0,4.16667,896.411,1370.07)">
<g transform="matrix(18.037,0,0,18.037,0,0)">
<path d="M0.786,-0.365C0.786,-0.312 0.776,-0.263 0.758,-0.218C0.74,-0.172 0.715,-0.133 0.682,-0.099C0.649,-0.066 0.609,-0.04 0.562,-0.021C0.516,-0.002 0.464,0.008 0.408,0.008C0.351,0.008 0.299,-0.002 0.253,-0.021C0.206,-0.04 0.166,-0.066 0.133,-0.099C0.1,-0.133 0.074,-0.172 0.056,-0.218C0.038,-0.263 0.029,-0.312 0.029,-0.365C0.029,-0.417 0.038,-0.466 0.056,-0.512C0.074,-0.557 0.1,-0.596 0.133,-0.63C0.166,-0.663 0.206,-0.689 0.253,-0.708C0.299,-0.727 0.351,-0.737 0.408,-0.737C0.464,-0.737 0.516,-0.727 0.562,-0.708C0.609,-0.689 0.649,-0.662 0.682,-0.629C0.715,-0.596 0.74,-0.556 0.758,-0.511C0.776,-0.466 0.786,-0.417 0.786,-0.365ZM0.613,-0.365C0.613,-0.401 0.608,-0.433 0.599,-0.462C0.589,-0.491 0.576,-0.515 0.558,-0.535C0.541,-0.555 0.519,-0.57 0.494,-0.581C0.469,-0.592 0.44,-0.597 0.408,-0.597C0.375,-0.597 0.346,-0.592 0.32,-0.581C0.295,-0.57 0.273,-0.555 0.256,-0.535C0.238,-0.515 0.225,-0.491 0.216,-0.462C0.206,-0.433 0.202,-0.401 0.202,-0.365C0.202,-0.328 0.206,-0.296 0.216,-0.267C0.225,-0.238 0.238,-0.214 0.256,-0.194C0.273,-0.173 0.295,-0.158 0.32,-0.148C0.346,-0.137 0.375,-0.132 0.408,-0.132C0.44,-0.132 0.469,-0.137 0.494,-0.148C0.519,-0.158 0.541,-0.173 0.558,-0.194C0.576,-0.214 0.589,-0.238 0.599,-0.267C0.608,-0.296 0.613,-0.328 0.613,-0.365Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,14.6911,0)">
<path d="M0.786,-0.365C0.786,-0.312 0.776,-0.263 0.758,-0.218C0.74,-0.172 0.715,-0.133 0.682,-0.099C0.649,-0.066 0.609,-0.04 0.562,-0.021C0.516,-0.002 0.464,0.008 0.408,0.008C0.351,0.008 0.299,-0.002 0.253,-0.021C0.206,-0.04 0.166,-0.066 0.133,-0.099C0.1,-0.133 0.074,-0.172 0.056,-0.218C0.038,-0.263 0.029,-0.312 0.029,-0.365C0.029,-0.417 0.038,-0.466 0.056,-0.512C0.074,-0.557 0.1,-0.596 0.133,-0.63C0.166,-0.663 0.206,-0.689 0.253,-0.708C0.299,-0.727 0.351,-0.737 0.408,-0.737C0.464,-0.737 0.516,-0.727 0.562,-0.708C0.609,-0.689 0.649,-0.662 0.682,-0.629C0.715,-0.596 0.74,-0.556 0.758,-0.511C0.776,-0.466 0.786,-0.417 0.786,-0.365ZM0.613,-0.365C0.613,-0.401 0.608,-0.433 0.599,-0.462C0.589,-0.491 0.576,-0.515 0.558,-0.535C0.541,-0.555 0.519,-0.57 0.494,-0.581C0.469,-0.592 0.44,-0.597 0.408,-0.597C0.375,-0.597 0.346,-0.592 0.32,-0.581C0.295,-0.57 0.273,-0.555 0.256,-0.535C0.238,-0.515 0.225,-0.491 0.216,-0.462C0.206,-0.433 0.202,-0.401 0.202,-0.365C0.202,-0.328 0.206,-0.296 0.216,-0.267C0.225,-0.238 0.238,-0.214 0.256,-0.194C0.273,-0.173 0.295,-0.158 0.32,-0.148C0.346,-0.137 0.375,-0.132 0.408,-0.132C0.44,-0.132 0.469,-0.137 0.494,-0.148C0.519,-0.158 0.541,-0.173 0.558,-0.194C0.576,-0.214 0.589,-0.238 0.599,-0.267C0.608,-0.296 0.613,-0.328 0.613,-0.365Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,29.3823,0)">
<path d="M0.328,-0.371C0.372,-0.371 0.403,-0.382 0.423,-0.403C0.442,-0.424 0.452,-0.453 0.452,-0.491C0.452,-0.507 0.449,-0.522 0.444,-0.536C0.439,-0.55 0.431,-0.561 0.421,-0.571C0.411,-0.581 0.398,-0.589 0.383,-0.594C0.367,-0.599 0.349,-0.602 0.328,-0.602L0.24,-0.602L0.24,-0.371L0.328,-0.371ZM0.328,-0.729C0.379,-0.729 0.423,-0.722 0.46,-0.71C0.497,-0.698 0.527,-0.681 0.551,-0.66C0.575,-0.639 0.592,-0.614 0.604,-0.585C0.615,-0.556 0.621,-0.524 0.621,-0.491C0.621,-0.454 0.615,-0.421 0.603,-0.391C0.591,-0.36 0.574,-0.334 0.55,-0.313C0.526,-0.291 0.495,-0.274 0.458,-0.262C0.421,-0.25 0.378,-0.243 0.328,-0.244L0.24,-0.244L0.24,-0L0.071,-0L0.071,-0.729L0.328,-0.729Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,40.8538,0)">
<path d="M0.241,-0.599L0.241,-0.429L0.47,-0.429L0.47,-0.304L0.241,-0.304L0.241,-0.13L0.54,-0.13L0.54,-0L0.071,-0L0.071,-0.729L0.54,-0.729L0.54,-0.599L0.241,-0.599Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,51.189,0)">
<path d="M0.308,-0.386C0.331,-0.386 0.35,-0.389 0.366,-0.395C0.382,-0.401 0.396,-0.409 0.406,-0.419C0.416,-0.429 0.424,-0.441 0.429,-0.455C0.433,-0.469 0.436,-0.484 0.436,-0.5C0.436,-0.532 0.425,-0.557 0.404,-0.575C0.383,-0.593 0.351,-0.602 0.308,-0.602L0.24,-0.602L0.24,-0.386L0.308,-0.386ZM0.667,-0L0.514,-0C0.485,-0 0.465,-0.011 0.453,-0.032L0.332,-0.244C0.326,-0.253 0.32,-0.259 0.313,-0.263C0.306,-0.267 0.296,-0.27 0.283,-0.27L0.24,-0.27L0.24,-0L0.071,-0L0.071,-0.729L0.308,-0.729C0.36,-0.729 0.405,-0.723 0.442,-0.712C0.479,-0.701 0.51,-0.686 0.533,-0.667C0.557,-0.648 0.574,-0.625 0.585,-0.599C0.595,-0.572 0.601,-0.544 0.601,-0.513C0.601,-0.489 0.597,-0.467 0.591,-0.446C0.585,-0.425 0.575,-0.405 0.563,-0.387C0.551,-0.369 0.535,-0.354 0.517,-0.34C0.499,-0.326 0.478,-0.314 0.455,-0.305C0.466,-0.299 0.476,-0.292 0.486,-0.284C0.495,-0.275 0.504,-0.265 0.512,-0.254L0.667,-0Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,63.0483,0)">
<path d="M0.456,-0.279L0.388,-0.478C0.383,-0.491 0.378,-0.506 0.372,-0.523C0.366,-0.54 0.361,-0.559 0.355,-0.579C0.35,-0.558 0.345,-0.539 0.339,-0.522C0.333,-0.505 0.328,-0.49 0.323,-0.477L0.256,-0.279L0.456,-0.279ZM0.724,-0L0.593,-0C0.578,-0 0.567,-0.003 0.558,-0.01C0.549,-0.017 0.542,-0.026 0.537,-0.037L0.494,-0.164L0.217,-0.164L0.174,-0.037C0.17,-0.027 0.164,-0.019 0.154,-0.011C0.145,-0.004 0.133,-0 0.119,-0L-0.013,-0L0.269,-0.729L0.442,-0.729L0.724,-0Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,74.592,0)">
<path d="M0.594,-0.595L0.389,-0.595L0.389,-0L0.22,-0L0.22,-0.595L0.015,-0.595L0.015,-0.729L0.594,-0.729L0.594,-0.595Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,85.5765,0)">
<rect x="0.071" y="-0.729" width="0.17" height="0.729" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,91.1861,0)">
<path d="M0.724,-0.729L0.432,-0L0.279,-0L-0.013,-0.729L0.123,-0.729C0.138,-0.729 0.15,-0.725 0.159,-0.718C0.168,-0.711 0.174,-0.703 0.179,-0.692L0.32,-0.302C0.327,-0.285 0.333,-0.266 0.34,-0.246C0.346,-0.226 0.352,-0.205 0.358,-0.183C0.362,-0.205 0.367,-0.226 0.373,-0.246C0.379,-0.266 0.385,-0.285 0.392,-0.302L0.532,-0.692C0.536,-0.701 0.542,-0.71 0.552,-0.717C0.561,-0.725 0.573,-0.729 0.587,-0.729L0.724,-0.729Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,104.01,0)">
<path d="M0.241,-0.599L0.241,-0.429L0.47,-0.429L0.47,-0.304L0.241,-0.304L0.241,-0.13L0.54,-0.13L0.54,-0L0.071,-0L0.071,-0.729L0.54,-0.729L0.54,-0.599L0.241,-0.599Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(18.037,0,0,18.037,-13.068,0)">
<path d="M0.24,-0.434L0.267,-0.434C0.292,-0.434 0.31,-0.441 0.321,-0.457L0.485,-0.695C0.494,-0.708 0.505,-0.716 0.516,-0.721C0.527,-0.726 0.541,-0.729 0.558,-0.729L0.705,-0.729L0.489,-0.434C0.473,-0.414 0.457,-0.399 0.439,-0.39C0.452,-0.385 0.463,-0.379 0.474,-0.371C0.484,-0.362 0.494,-0.351 0.503,-0.338L0.723,-0L0.572,-0C0.562,-0 0.554,-0.001 0.547,-0.002C0.54,-0.003 0.534,-0.005 0.529,-0.008C0.524,-0.011 0.52,-0.014 0.516,-0.018C0.512,-0.022 0.509,-0.026 0.506,-0.031L0.341,-0.285C0.335,-0.294 0.327,-0.3 0.318,-0.304C0.309,-0.308 0.296,-0.31 0.281,-0.31L0.24,-0.31L0.24,-0L0.071,-0L0.071,-0.729L0.24,-0.729L0.24,-0.434Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
</g>
<g id="WTF" transform="matrix(4.16667,0,0,4.16667,-363.557,-181.762)">
<g transform="matrix(63.848,0,0,63.848,353.279,347.753)">
<path d="M0.59,-0.605L0.378,-0.605L0.378,-0L0.227,-0L0.227,-0.605L0.015,-0.605L0.015,-0.726L0.59,-0.726L0.59,-0.605Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(63.848,0,0,63.848,391.843,347.753)">
<path d="M0.227,-0.608L0.227,-0.408L0.488,-0.408L0.488,-0.29L0.227,-0.29L0.227,-0L0.076,-0L0.076,-0.726L0.538,-0.726L0.538,-0.608L0.227,-0.608Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
<g transform="matrix(63.848,0,0,63.848,282.658,347.753)">
<path d="M1.052,-0.726L0.827,-0L0.692,-0L0.543,-0.475C0.541,-0.481 0.538,-0.489 0.536,-0.497C0.534,-0.505 0.532,-0.513 0.53,-0.522C0.528,-0.513 0.525,-0.505 0.523,-0.497C0.521,-0.489 0.518,-0.481 0.516,-0.475L0.366,-0L0.23,-0L0.006,-0.726L0.132,-0.726C0.145,-0.725 0.155,-0.723 0.164,-0.717C0.173,-0.711 0.179,-0.703 0.182,-0.693L0.294,-0.284C0.297,-0.272 0.299,-0.26 0.302,-0.247C0.305,-0.234 0.308,-0.22 0.311,-0.205C0.314,-0.22 0.317,-0.234 0.32,-0.247C0.323,-0.26 0.327,-0.272 0.331,-0.284L0.462,-0.693C0.465,-0.701 0.47,-0.708 0.479,-0.715C0.488,-0.722 0.499,-0.725 0.512,-0.726L0.556,-0.726C0.569,-0.725 0.579,-0.722 0.587,-0.716C0.595,-0.71 0.602,-0.702 0.606,-0.693L0.736,-0.284C0.74,-0.273 0.744,-0.261 0.747,-0.249C0.75,-0.236 0.753,-0.223 0.757,-0.209C0.759,-0.223 0.762,-0.236 0.764,-0.249C0.767,-0.261 0.769,-0.273 0.772,-0.284L0.885,-0.693C0.887,-0.701 0.893,-0.709 0.902,-0.716C0.911,-0.722 0.921,-0.725 0.934,-0.726L1.052,-0.726Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
</g>
</g>
<g id="Einhorn" transform="matrix(1,0,0,1,-31.2941,-563.989)">
<path d="M450.529,1662.06L400.757,1697.42L401.385,1712.12C401.385,1712.12 401.392,1712.51 401.318,1712.69C401.189,1713.12 400.944,1713.39 400.944,1713.39L450.529,1662.06Z" style="fill:#fff;"/>
<path d="M736.631,1698.47L756.815,1695.93C756.815,1695.93 757.296,1695.91 757.696,1695.65C758.083,1695.4 758.264,1695.06 758.264,1695.06L750.801,1710.64C750.801,1710.64 750.463,1711.42 749.582,1711.56C748.632,1711.72 748.087,1711.18 748.087,1711.18L743.517,1707.05L745.991,1710.95C745.991,1710.95 746.486,1711.78 746.06,1712.62C745.565,1713.62 744.582,1713.64 744.582,1713.64L734.515,1714.3C734.515,1714.3 735.304,1714.06 735.548,1713.76C735.8,1713.44 736.025,1712.42 736.025,1712.42L736.631,1698.47Z" style="fill:#fff;"/>
<g transform="matrix(-1,0,0,1,1133.78,-0.317371)">
<path d="M736.631,1698.47L756.815,1695.93C756.815,1695.93 757.296,1695.91 757.696,1695.65C758.083,1695.4 758.264,1695.06 758.264,1695.06L750.801,1710.64C750.801,1710.64 750.463,1711.42 749.582,1711.56C748.632,1711.72 748.087,1711.18 748.087,1711.18L743.517,1707.05L745.991,1710.95C745.991,1710.95 746.486,1711.78 746.06,1712.62C745.565,1713.62 744.582,1713.64 744.582,1713.64L734.515,1714.3C734.515,1714.3 735.304,1714.06 735.548,1713.76C735.8,1713.44 736.025,1712.42 736.025,1712.42L736.631,1698.47Z" style="fill:#fff;"/>
</g>
<path d="M392.458,1653.33L445.559,1660.94L398.452,1694.68L377.352,1692.04C377.352,1692.04 376.875,1691.98 376.441,1692.17C375.813,1692.44 375.559,1692.97 375.559,1692.97L392.458,1653.33Z" style="fill:#fff;"/>
<g transform="matrix(-1,0,0,1,1133.82,0.344083)">
<path d="M392.458,1653.33L445.559,1660.94L398.452,1694.68L377.352,1692.04C377.352,1692.04 376.875,1691.98 376.441,1692.17C375.813,1692.44 375.559,1692.97 375.559,1692.97L392.458,1653.33Z" style="fill:#fff;"/>
</g>
<path d="M671.12,1573.37L702.593,1592.58L707.544,1588.92L698.064,1578.83L687.348,1576.59L689.224,1568.43C689.224,1568.43 689.271,1568.29 689.281,1568.09C689.288,1567.93 689.255,1567.77 689.255,1567.77L686.724,1550.74C686.724,1550.74 686.529,1549.58 685.362,1549.35C684.059,1549.08 683.457,1550.14 683.457,1550.14L674.166,1565.5C674.166,1565.5 674.065,1565.68 674.02,1565.84C673.954,1566.06 673.947,1566.24 673.947,1566.24L673.835,1574.13L671.12,1573.37Z" style="fill:#fff;"/>
<g transform="matrix(-1,0,0,1,1133.81,-0.623595)">
<path d="M670.409,1573.25L702.616,1592.94L707.454,1589.14L698.064,1578.83L687.348,1576.59L689.224,1568.43C689.224,1568.43 689.271,1568.29 689.281,1568.09C689.288,1567.93 689.255,1567.77 689.255,1567.77L686.724,1550.74C686.724,1550.74 686.529,1549.58 685.362,1549.35C684.059,1549.08 683.457,1550.14 683.457,1550.14L674.166,1565.5C674.166,1565.5 674.065,1565.68 674.02,1565.84C673.954,1566.06 673.947,1566.24 673.947,1566.24L673.84,1573.91L670.409,1573.25Z" style="fill:#fff;"/>
</g>
<path d="M427.91,1594.32L407.278,1606.88L315.202,1511.83C315.202,1511.83 314.118,1510.66 315.278,1509.41C316.302,1508.31 317.631,1509.24 317.631,1509.24L427.91,1594.32Z" style="fill:#fff;"/>
<g transform="matrix(-1,0,0,1,1133.84,0.346916)">
<path d="M427.91,1594.32L407.278,1606.88L315.202,1511.83C315.202,1511.83 314.118,1510.66 315.278,1509.41C316.302,1508.31 317.631,1509.24 317.631,1509.24L427.91,1594.32Z" style="fill:#fff;"/>
</g>
<path d="M352.629,1973.55L412.821,1957.45L564.149,1813.8L502.768,1776.15L352.629,1973.55Z" style="fill:#fff;"/>
<path d="M724.493,1958.33L791.81,1974.99L636.355,1779.22L570.066,1813.9L724.493,1958.33Z" style="fill:#fff;"/>
<path d="M567.109,1811.42L634.096,1776.37L566.931,1691.79L504.928,1773.31L567.109,1811.42Z" style="fill:#fff;"/>
<path d="M501.388,1771.76L565.007,1688.47L507.888,1603.52L471.991,1672.38L501.388,1771.76Z" style="fill:#fff;"/>
<path d="M568.964,1688.62L636.117,1772.79L662.194,1673.36L625.972,1603.85L568.964,1688.62Z" style="fill:#fff;"/>
<path d="M628.44,1600.9L663.644,1574.69L681.595,1659.26L664.468,1669.99L628.44,1600.9Z" style="fill:#fff;"/>
<path d="M452.254,1658.94L469.378,1669.67L505.406,1600.56L470.221,1574.36L452.254,1658.94Z" style="fill:#fff;"/>
<path d="M409.728,1609.57L466.44,1574.95L449.265,1655.87L409.728,1609.57ZM419.698,1608.4C418.738,1609.21 418.917,1610.78 420.06,1611.35L425.353,1613.99C426.003,1614.31 426.771,1614.21 427.32,1613.75L432.618,1609.17C433.604,1608.31 433.359,1606.71 432.159,1606.19C425.3,1602.76 425.728,1603.15 420.361,1607.83C420.143,1608.01 419.68,1608.37 419.698,1608.4Z" style="fill:#fff;"/>
<g transform="matrix(-1,0,0,1,1133.87,0.365109)">
<path d="M450.529,1662.06L400.757,1697.42L401.402,1712.11C401.402,1712.11 401.412,1712.43 401.327,1712.69C401.198,1713.08 400.954,1713.36 400.954,1713.36L450.529,1662.06Z" style="fill:#fff;"/>
</g>
<path d="M667.407,1575.3L724.122,1609.91L684.599,1656.21L667.407,1575.3ZM707.873,1603.98C707.655,1603.99 707.437,1604.03 707.227,1604.13L701.695,1606.53C700.477,1607.05 700.242,1608.66 701.232,1609.52L706.538,1614.1C707.075,1614.57 707.86,1614.67 708.506,1614.34L713.79,1611.69C714.933,1611.11 715.121,1609.57 714.156,1608.74L709.095,1604.41C708.754,1604.12 708.323,1603.97 707.873,1603.98Z" style="fill:#fff;"/>
<path d="M687.949,1657.75L740.377,1650.89C740.377,1650.89 731.48,1619.35 729.222,1611.36C729.041,1610.72 728.918,1610.57 728.71,1610.53C728.604,1610.5 728.323,1610.47 727.692,1611.21C721.177,1618.84 687.949,1657.75 687.949,1657.75Z" style="fill:#fff;"/>
<g transform="matrix(-1,0,0,1,1133.99,0.137922)">
<path d="M688.136,1657.23L740.413,1650.04C740.413,1650.04 731.949,1620.01 729.672,1611.95C729.314,1610.68 728.761,1610.52 728.548,1610.52C728.308,1610.52 728.093,1610.44 727.596,1611.02C721.135,1618.59 688.136,1657.23 688.136,1657.23Z" style="fill:#fff;"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,6 +1,6 @@
{
"1": "Keine Angabe",
"2": "Grundkenntnisse",
"2": "Grundkentnisse",
"3": "Gut",
"4": "Fließend",
"5": "Muttersprache"

View File

@ -1,22 +1,7 @@
{
"1": {
"short": "≤ 6M",
"long": "bis 6 Monate"
},
"2":{
"short": "≤ 1J",
"long": "bis 1 Jahr"
},
"3": {
"short": "≤ 3J",
"long": "bis 3 Jahre"
},
"4": {
"short": "≤ 5J",
"long": "bis 5 Jahre"
},
"5": {
"short": "> 5J",
"long": "mehr als 5 Jahre"
}
}
"1": "bis 6 Monate",
"2": "bis 1 Jahr",
"3": "bis 3 Jahre",
"4": "bis 5 Jahre",
"5": "mehr als 5 Jahre"
}

View File

@ -1,18 +1 @@
/*
* SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
$gray-100: #edefeb;
$gray-800: #344b5d;
$primary: #0790a9;
$dark: $gray-800;
$body-bg: $gray-100;
$link-decoration: none;
$link-hover-decoration: underline;
$spinner-animation-speed: 1s;

View File

@ -1,50 +1,20 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<profile-list
:values="values"
:type="type"
:editable="true"
:show-secondary="showSecondary"
@remove-value="removeValue($event)"
@update-values="this.$emit('update-values', this.values)"
>
</profile-list>
<div v-bind="$attrs" class="card-body">
<div class="row">
<div class="col-12 col-md-4 col-lg-3 col-xl-2">
<div class="form-control-plaintext form-control-sm">Eintrag hinzufügen:</div>
</div>
<div class="col-12 col-md-6">
<div>
<label for="searchText" class="form-label fw-bold">{{ label }}</label>
<div class="row mb-2">
<div class="col">
<input
autocomplete="off"
type="text"
class="form-control form-control-sm"
class="form-control"
id="searchText"
:maxlength="maxlength"
:placeholder="placeholder"
v-model="searchText"
@input="search()"
@keyup="search()"
@keyup.enter="addResult()"
/>
<div v-if="searchResults">
<ul class="list-group">
<li
class="list-group-item"
v-for="result in searchResults"
:key="result.id"
@click="addResult(result)"
>
{{ result.name }}
</li>
</ul>
</div>
</div>
<div class="col-md-2">
<div class="col">
<button
v-if="searchText != ''"
type="button"
@ -52,21 +22,43 @@ SPDX-License-Identifier: AGPL-3.0-or-later
aria-label="Hinzufügen"
@click="addResult()"
>
<i clas="bi-plus-lg"></i>
<img
src="/img/bootstrap-icons-1.5.0/plus-lg.svg"
alt="Hinzufügen Icon"
/>
Hinzufügen
</button>
</div>
</div>
<div class="" v-if="searchResults">
<ul class="list-group">
<li
class="list-group-item"
v-for="result in searchResults"
:key="result.id"
@click="addResult(result)"
>
{{ result.name }}
</li>
</ul>
</div>
<profile-list
:values="values"
:type="type"
:editable="true"
@remove-value="removeValue($event)"
@update-values="this.$emit('update-values', this.values)"
>
</profile-list>
</div>
</template>
<script>
import { mapState } from 'vuex'
import RequestMixin from "@/mixins/request.mixin"
import RequestMixin from '@/mixins/request.mixin'
import ProfileList from '@/components/ProfileList';
import ProfileList from "@/components/ProfileList";
export default {
name: 'AutoComplete',
name: "AutoComplete",
mixins: [RequestMixin],
components: {
ProfileList,
@ -81,33 +73,18 @@ export default {
values: {
type: Array,
},
showSecondary: {
type: Boolean,
default: true,
},
placeholder: {
type: String,
default: "",
},
},
data() {
return {
iconUrl: this.apiUrl,
searchText: '',
searchText: "",
searchResults: [],
showErrorMessage: false,
};
},
computed: {
...mapState(['currentUserId']),
maxlength() {
return this.type === 'skill' ? 50 : 25
}
},
methods: {
addResult(result = false) {
if (!result) result = this.searchResults[0];
if (
this.values.map((item) => item[this.type].name).includes(result.name)
) {
@ -116,21 +93,18 @@ export default {
let changeValues = Object.assign(this.values);
let newValue = {
profile_id: this.currentUserId,
profile_id: localStorage.getItem("user_id"),
};
if (this.type != 'contacttype') {
if (this.type != "contacttype") {
newValue.level = 1;
} else {
newValue.content = '';
newValue.content = "";
}
newValue[this.type] = result;
changeValues.unshift(newValue);
this.searchText = '';
this.searchText = "";
this.searchResults = [];
this.$emit('update-values', changeValues);
this.$emit("update-values", changeValues);
},
removeValue(valueName) {
const newValues = this.values.filter((value) => {
@ -140,7 +114,7 @@ export default {
return true;
}
});
this.$emit('update-values', newValues);
this.$emit("update-values", newValues);
},
},
};

View File

@ -1,37 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="avatar bg-dark">
{{ avatarLetters }}
</div>
</template>
<script>
export default {
props: {
name: String
},
computed: {
avatarLetters() {
return this.name.substr(0, 2)
}
}
}
</script>
<style scoped>
.avatar {
align-items: center;
border-radius: .25rem;
color: white;
display: flex;
font-weight: bold;
height: 2.5rem;
justify-content: center;
width: 2.5rem;
}
</style>

View File

@ -1,47 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<footer class="bg-dark py-5">
<div class="container">
<div class="row text-center text-sm-start">
<div class="col-sm-6 d-sm-flex justify-content-center mb-3 mb-sm-0">
<div class="d-inline-block">
<div class="fw-bold text-white mb-2">Kompetenzinventar</div>
<ul class="list-unstyled">
<li><a href="https://git.wtf-eg.de/kompetenzinventar">Quellcode</a></li>
<li><a href="https://git.wtf-eg.de/kompetenzinventar/ki-doku/issues/new/choose">Problem melden</a></li>
</ul>
</div>
</div>
<div class="col-sm-6 d-sm-flex justify-content-center">
<div class="d-inline-block">
<ul class="list-unstyled">
<li><a href="https://wtf-eg.de/impressum/">Impressum</a></li>
<li><a href="https://wtf-eg.de/datenschutz/">Datenschutzerklärung</a></li>
</ul>
<img class="wtf-logo wtf-logo--footer" src="@/assets/img/wtf_logo_white.svg">
</div>
</div>
</div>
</div>
</footer>
</template>
<style scoped>
a {
color: #fff;
text-decoration: none;
}
a:hover {
color: #fff;
text-decoration: underline;
}
.wtf-logo--footer {
max-width: 200px;
}
</style>

View File

@ -1,110 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-white">
<div class="container-fluid">
<router-link
class="navbar-brand"
:to="{ path:'/s/search' }"
>
<img class="wtf-logo wtf-logo--navbar" src="@/assets/img/wtf_logo.svg">
<span class="d-none d-sm-inline">Kompetenzinventar</span>
<span class="d-sm-none">KI</span>
</router-link>
<button
@click="toggleMobileNav"
class="navbar-toggler"
type="button"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse d-lg-flex"
:class="{ show: showMobileNav }"
id="navbarSupportedContent"
>
<ul class="navbar-nav mb-2 mb-lg-0 me-auto">
<li class="nav-item">
<router-link
class="nav-link"
:to="{ path: `/s/search` }"
active-class="active"
>Suche</router-link
>
</li>
<li class="nav-item">
<router-link
class="nav-link"
:to="{ path: `/s/profile/${currentUserId}` }"
active-class="active"
>Mein Profil</router-link
>
</li>
<li class="nav-item">
<router-link
class="nav-link"
:to="{ path: `/s/profile-edit` }"
active-class="active"
>Bearbeiten</router-link
>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<button class="btn btn-danger w-100" @click="logout()">
<i class="bi bi-box-arrow-right"></i>
Logout
</button>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script>
import { mapState } from 'vuex'
import RequestMixin from '@/mixins/request.mixin'
export default {
name: 'Navbar',
mixins: [RequestMixin],
data() {
return {
showMobileNav: false
}
},
computed: {
...mapState(['currentUserId'])
},
methods: {
toggleMobileNav() {
this.showMobileNav = !this.showMobileNav
},
logout() {
this.$store.dispatch('clear')
this.$router.push({ path: '/' });
},
}
}
</script>
<style>
.wtf-logo--navbar {
height: 40px;
margin-bottom: -5px;
margin-top: -5px;
}
</style>

View File

@ -1,49 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<ul class="pagination">
<li
class="page-item"
:class="{ active: page === current }"
v-for="page in pages"
:key="page"
>
<span
class="page-link pointer"
@click="onPageClicked(page)"
>
{{ page }}
</span>
</li>
</ul>
</template>
<script>
export default {
name: 'Paginator',
props: {
page: Number,
current: Number,
pages: Number
},
methods: {
onPageClicked(page) {
if (page == this.current) {
return
}
this.$emit('page', page)
}
}
}
</script>
<style>
.pointer {
cursor: pointer;
}
</style>

View File

@ -1,76 +1,86 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<ul class="list-group list-group-flush">
<li
class="list-group-item"
v-for="(value, valueKey) in values"
:key="value.id"
>
<div class="row">
<div class="col-12 col-md-5 d-flex align-items-center">
<div
class="list-icon me-1"
:style="{ backgroundImage: `url('${iconBaseUrl + value[type].icon_url}'` }"
<ul>
<li v-for="(value, valueKey) in values" :key="value.id">
<div class="row m-1">
<div class="col">
<img
style="max-width: 64px"
v-if="value[type].icon_url"
:src="iconUrl + value[type].icon_url"
:alt="`${value[type].name} Logo`"
/>
<div>
{{ value[type].name }}
</div>
{{ value[type].name }}
</div>
<div class="col-10 col-md-5">
<div v-if="type == 'skill' && showSecondary">
<select
class="form-select form-select-sm"
aria-label="Selektiere dein Level"
v-model="editableValues[valueKey].level"
@input="$emit('update-values', editableValues)"
>
<option
v-for="(value, key) in levelSelection"
:value="key"
:key="key"
<div class="col">
<div v-if="type == 'skill'">
<div v-if="editable">
<select
v-if="editableValues[valueKey]"
class="form-select"
aria-label="Selektiere dein Level"
v-model="editableValues[valueKey].level"
@input="$emit('update-values', editableValues)"
>
{{ value.long || value }}
</option>
</select>
<option
v-for="(value, key) in levelSelection"
:value="key"
:key="key"
>
{{ value }}
</option>
</select>
</div>
<div v-else>({{ levelSelection[value.level] }})</div>
</div>
<div v-else-if="type == 'language'">
<select
class="form-select form-select-sm"
aria-label="Selektiere dein Level"
v-model="editableValues[valueKey].level"
@input="$emit('update-values', editableValues)"
>
<option
v-for="(value, key) in languagesSelection"
:value="key"
:key="key"
<div v-if="editable">
<div v-if="editable">
<select
v-if="editableValues[valueKey]"
class="form-select"
aria-label="Selektiere dein Level"
v-model="editableValues[valueKey].level"
@input="$emit('update-values', editableValues)"
>
{{ value }}
</option>
</select>
<option
v-for="(value, key) in languagesSelection"
:value="key"
:key="key"
>
{{ value }}
</option>
</select>
</div>
</div>
<div v-else></div>
</div>
<div v-else-if="type == 'contacttype'">
<input
class="form-control form-control-sm"
maxlength="200"
v-model="editableValues[valueKey].content"
/>
<div v-else-if="value.content">
<div v-if="editable">
<input class="form-control" v-model="editableValues[valueKey].content"/>
</div>
<div v-else>
<span v-if="value[type].name === 'E-Mail'">
<a :href="`mailto:${value.content}`">{{ value.content }}</a>
</span>
<span v-else>
{{ value.content }}
</span>
</div>
</div>
</div>
<div class="col-2 text-end">
<div class="col">
<button
v-if="editable"
type="button"
class="btn btn-sm btn-light"
class="btn btn-outline-danger"
aria-label="Löschen"
@click="$emit('removeValue', value[type].name)"
>
<i class="text-danger bi bi-x-circle"></i>
<img
src="/img/bootstrap-icons-1.5.0/trash.svg"
alt="Löschen Icon"
/>
</button>
</div>
</div>
@ -78,11 +88,11 @@ SPDX-License-Identifier: AGPL-3.0-or-later
</ul>
</template>
<script>
import levelJson from '@/assets/skill_level.json';
import languagesJson from '@/assets/language_level.json';
import levelJson from "@/assets/skill_level.json";
import languagesJson from "@/assets/language_level.json";
export default {
name: 'ProfileList',
name: "ProfileList",
props: {
type: {
type: String,
@ -91,14 +101,14 @@ export default {
type: Array,
required: true,
},
showSecondary: {
editable: {
type: Boolean,
default: true
}
default: false,
},
},
data() {
return {
iconBaseUrl: this.apiUrl,
iconUrl: this.apiUrl,
levelSelection: levelJson,
languagesSelection: languagesJson,
editableValues: this.values,
@ -111,13 +121,3 @@ export default {
},
};
</script>
<style scope>
.list-icon {
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
height: 32px;
width: 32px;
}
</style>

View File

@ -1,54 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<router-link
class="text-decoration-none d-flex"
:to="{ path: `/s/profile/${profile.user_id}` }"
>
<div class="card w-100">
<div class="card-body d-flex">
<div class="d-flex align-items-center justify-content-center me-3">
<Avatar :name="profile.nickname"/>
</div>
<div class="text-body">
<h5 class="card-title mb-1 lh-1">
{{ profile.nickname}}
<span v-if="profile.pronouns"> ({{ profile.pronouns }})</span>
</h5>
<small
class="card-text lh-1 text-dark"
v-if="profile.skills && profile.skills.length > 0"
>
Top-Fähigkeiten: {{ topSkills }}
</small>
</div>
</div>
</div>
</router-link>
</template>
<script>
import Avatar from '@/components/Avatar'
export default {
components: {
Avatar,
},
props: {
profile: Object
},
computed: {
topSkills() {
return this.profile.skills.slice(0, 5).map(s => s.skill.name).join(', ')
}
}
}
</script>
<style scoped>
.card:hover {
background-color: #f8f9fa;
}
</style>

View File

@ -1,81 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="skill rounded me-2">
<div class="skill__left p-2">
<div class="skill__icon" :style="{ backgroundImage: iconUrl }"></div>
</div>
<div class="skill__right d-flex align-items-center rounded-end px-2">
<div>
<div class="skill__name fw-bold me-1">
{{ profileSkill.skill.name }}
</div>
<small class="skill__level" v-if="showLevel" :title="levelTitle">
{{ level }}
</small>
</div>
</div>
</div>
</template>
<script>
import levels from '@/assets/skill_level.json';
export default {
props: {
profileSkill: Object,
showLevel: {
type: Boolean,
default: false
}
},
data() {
return {
levels
}
},
computed: {
iconUrl() {
return `url("${window.ki.apiUrl}/${this.profileSkill.skill.icon_url}")`
},
level() {
return levels[this.profileSkill.level].short
},
levelTitle() {
return levels[this.profileSkill.level].long
}
}
}
</script>
<style>
.skill {
align-items: stretch;
border: 1px solid #acacac;
display: inline-flex;
line-height: 1;
}
.skill__icon {
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
height: 32px;
width: 32px;
}
.skill__right {
background-color: #edefeb;
color: #202020;
}
.skill__name,
.skill__level {
display: inline-block;
white-space: nowrap;
}
</style>

View File

@ -1,9 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="spinner-grow" role="status"></div>
</template>

View File

@ -1,39 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="container text-center py-5">
<template v-if="notFound">
<div v-if="isOwnProfile">
<div class="fs-1 lh-1">nullptr :/</div>
<div class="fs-3 mb-4">Du hast noch kein Profil</div>
<router-link :to="{ name: 'ProfileEdit' }" class="btn btn-primary" >
Jetzt Profil erstellen
</router-link>
</div>
<div v-else>
<div class="fs-1 mb-3">nullptr :/</div>
<div class="mb-3">
Profil nicht gefunden
</div>
</div>
</template>
<template v-else>
<div class="fs-1 mb-3">Kernel panic :/</div>
Das Profil konnte nicht geladen werden
</template>
</div>
</template>
<script>
export default {
props: {
isOwnProfile: Boolean,
notFound: Boolean
}
}
</script>

View File

@ -1,39 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="contact rounded d-inline-flex align-items-center">
<div class="contact__left px-2">
{{ profileContact.contacttype.name }}
</div>
<div class="contact__right d-flex align-items-center rounded-end px-2">
{{ profileContact.content }}
</div>
</div>
</template>
<script>
export default {
props: {
profileContact: Object,
}
}
</script>
<style>
.contact {
align-items: stretch;
border: 1px solid #acacac;
display: inline-flex;
}
.contact__right {
background-color: #edefeb;
color: #202020;
font-weight: bold;
height: 32px;
}
</style>

View File

@ -1,79 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="bg-wtf py-3">
<div class="container">
<div class="d-flex align-items-center mb-3">
<Avatar class="me-3" :name="profile.nickname" />
<div class="text-white fs-3">
<span class="fs-3">{{ profile.nickname }}</span>
<span v-if="profile.pronouns" class="fs-5">
({{ profile.pronouns }})
</span>
</div>
</div>
<div v-if="profile?.address?.name">
<div class="d-flex align-items-center">
<i class="fs-4 bi bi-person-fill text-dark mx-2"></i>
<div class="text-white">
a.k.a. {{ profile.address.name }}
</div>
</div>
</div>
<div v-if="location">
<div class="d-flex align-items-center">
<i class="fs-4 bi bi-geo-alt-fill text-dark mx-2"></i>
<div class="text-white">
{{ location }}
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Avatar from '@/components/Avatar'
export default {
name: 'ProfileHeader',
components: {
Avatar
},
props: {
profile: Object
},
computed: {
location() {
if (!this.profile.address) {
return
}
const parts = []
if (this.profile.address.postcode) {
parts.push(this.profile.address.postcode)
}
if (this.profile.address.city) {
parts.push(this.profile.address.city)
}
if (this.profile.address.country) {
parts.push(this.profile.address.country)
}
return parts.join(', ')
}
}
}
</script>
<style>
.content {
min-height: calc(100vh - 60px);
}
</style>

View File

@ -1,69 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="language rounded me-2">
<div class="language__left px-2">
<div class="language__icon" :style="{ backgroundImage: iconUrl }"></div>
</div>
<div class="language__right d-flex align-items-center rounded-end px-2">
<div>
<div class="language__name me-1">{{ profileLanguage.language.name }}</div>
<small class="language__level">{{ level }}</small>
</div>
</div>
</div>
</template>
<script>
import levels from '@/assets/language_level.json';
export default {
props: {
profileLanguage: Object,
},
data() {
return {
levels
}
},
computed: {
iconUrl() {
return `url("${window.ki.apiUrl}/${this.profileLanguage.language.icon_url}")`
},
level() {
return levels[this.profileLanguage.level]
}
}
}
</script>
<style>
.language {
align-items: stretch;
border: 1px solid #acacac;
display: inline-flex;
}
.language__icon {
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
height: 32px;
width: 32px;
}
.language__right {
background-color: #edefeb;
color: #202020;
}
.language__name {
display: inline-block;
font-weight: bold;
white-space: nowrap;
}
</style>

View File

@ -1,28 +0,0 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="container mb-5">
<h3 class="text-center">
{{ title }}
</h3>
<div class="card w-100">
<slot name="card-body">
<div class="card-body">
<slot></slot>
</div>
</slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: String
}
}
</script>

View File

@ -1,22 +1,13 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import { createApp } from 'vue/dist/vue.esm-bundler'
import App from './App.vue'
import router from './router'
import store from '@/store'
import VTooltipPlugin from 'v-tooltip'
import 'v-tooltip/dist/v-tooltip.css'
import 'bootstrap-icons/font/bootstrap-icons.css'
import './assets/global.scss'
const app = createApp(App)
app.use(VTooltipPlugin)
app.use(router)
app.use(store)
app.config.globalProperties.apiUrl = window.ki.apiUrl

View File

@ -1,126 +1,87 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import store from '@/store'
import axios from "axios";
export default {
methods: {
async submitLogin() {
this.showErrorMessage = false;
try {
const data = JSON.stringify({
username: this.username,
password: this.password,
})
const response = await fetch(`${this.apiUrl}/users/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: data
})
if (!response.ok) {
this.showErrorMessage = true
return
const loginResult = await axios.post(
`${this.apiUrl}/users/login`,
{
username: this.username,
password: this.password,
}
);
if (loginResult.status === 200) {
this.showErrorMessage = false;
//success login
localStorage.setItem("token", loginResult.data.token);
localStorage.setItem("user_id", loginResult.data.user_id);
this.$router.push({ path: "/s/search" });
} else {
this.showErrorMessage = true;
}
const responseData = await response.json()
store.commit('setCurrentUserId', parseInt(responseData.user_id, 10))
store.commit('setToken', responseData.token)
this.$router.push({ path: '/s/search' });
} catch (error) {
console.error(error);
this.showErrorMessage = true;
}
},
async search() {
if (!this.searchText) {
this.searchResults = []
return
}
try {
const response = await fetch(`${this.apiUrl}/${this.type}s?search=${this.searchText}`, {
const request = await axios.get(
`${this.apiUrl}/${this.type}s?search=${this.searchText}`,
{
headers: {
Authorization: `Bearer ${store.state.token}`,
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
}
);
if (!response.ok) {
console.error();
this.showErrorMessage = true;
return
if (request.status === 200) {
this.searchResults = request.data[`${this.type}s`];
if (
!this.values
.map((item) => item[this.type].name.toLowerCase())
.includes(this.searchText.toLowerCase())
) {
this.searchResults.unshift({ name: this.searchText });
}
}
const responseData = await response.json()
const searchResults = responseData[`${this.type}s`];
if (
!searchResults.map((item) => item.name.toLowerCase())
.includes(this.searchText.toLowerCase())
) {
searchResults.unshift({ name: this.searchText });
}
this.searchResults = searchResults
} catch (error) {
console.error();
this.showErrorMessage = true;
}
},
async initEditPage() {
const userId = store.state.currentUserId
const url = `${this.apiUrl}/users/${userId}/profile`
try {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${store.state.token}`
},
const userProfile = await axios.get(
`${this.apiUrl}/users/${localStorage.getItem("user_id")}/profile`,
{
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
}
);
if (!response.ok) {
return
}
const responseData = await response.json()
this.profile = responseData.profile;
this.profile = userProfile.data.profile;
} catch (error) {
console.error(error);
}
},
async submitFormEdit(isProfileVisible) {
this.showErrorMessage = false
this.showSuccessMessage = false
const userId = store.state.currentUserId
this.profile.visible = isProfileVisible;
async submitFormEdit() {
try {
const body = JSON.stringify(this.profile)
const response = await fetch(
`${this.apiUrl}/users/${userId}/profile`,
const formSubmitResult = await axios.post(
`${this.apiUrl}/users/${localStorage.getItem("user_id")}/profile`,
this.profile,
{
method: 'POST',
headers: {
Authorization: `Bearer ${store.state.token}`,
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
body
}
);
if (!response.ok) {
this.showErrorMessage = true
return
if (formSubmitResult.status === 200) {
// success
this.showSuccessMessage = true;
} else {
// failure
this.showErrorMessage = true;
}
this.showSuccessMessage = true
} catch (error) {
console.error(error);
this.showErrorMessage = true;
@ -128,50 +89,35 @@ export default {
},
async initViewPage() {
try {
const response = await fetch(`${this.apiUrl}/users/${this.$route.params.memberId}/profile`, {
headers: { Authorization: `Bearer ${store.state.token}` },
const userProfile = await axios.get(
`${this.apiUrl}/users/${this.$route.params.memberId}/profile`,
{
headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
}
);
if (!response.ok) {
return
}
const responseData = await response.json()
store.commit('setCurrentProfile', responseData.profile)
this.profile = userProfile.data.profile;
} catch (error) {
console.error(error);
}
},
async submitSearch() {
this.showErrorMessage = false;
try {
const url = new URL(`${this.apiUrl}/users/profiles`)
let url = `${this.apiUrl}/users/profiles`;
if (this.searchText != "") {
url.searchParams.append('nickname', this.searchText)
url += `?nickname=${this.searchText}`;
}
const response = await fetch(url, {
const result = await axios.get(url, {
headers: {
Authorization: `Bearer ${store.state.token}`,
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (!response.ok) {
this.showErrorMessage = true
return
}
const responseData = await response.json()
this.searchResults = responseData.profiles;
this.searchTotal = responseData.total;
this.searchResults = result.data.profiles;
this.searchTotal = result.data.total;
} catch (error) {
console.error(error);
this.showErrorMessage = true;
}
},
},
}
}

View File

@ -1,11 +1,5 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import { createRouter, createWebHistory } from 'vue-router'
import store from '@/store'
import Index from '@/views/Index.vue'
import Search from '@/views/Search.vue'
import Edit from '@/views/profile/Edit.vue'
@ -19,9 +13,9 @@ const routes = [
template: "<router-view/>",
},
beforeEnter: (to, from, next) => {
if (store.state.token){
if(localStorage.getItem('token') !== null){
next()
} else {
}else{
next({path: '/', query: {url: to.fullPath, access: false}})
}
},
@ -46,14 +40,7 @@ const routes = [
{
path: '/',
name: 'Index',
component: Index,
beforeEnter: (_to, _from, next) => {
if (store.state.token) {
next({name: 'Search'})
} else {
next()
}
}
component: Index
},
]
@ -62,4 +49,4 @@ const router = createRouter({
routes
})
export default router
export default router

View File

@ -1,53 +0,0 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
import { createStore } from 'vuex'
import profile from './profile'
import search from './search'
const localStorageKeys = {
currentUserId: 'ki_current_user_id',
token: 'ki_token',
}
export default createStore({
modules: {
profile,
search,
},
state() {
return {
currentUserId: JSON.parse(localStorage.getItem(localStorageKeys.currentUserId)),
token: JSON.parse(localStorage.getItem(localStorageKeys.token)),
}
},
mutations: {
clearCurrentUserId(state) {
state.currentUserId = null
localStorage.removeItem(localStorageKeys.currentUserId)
},
setCurrentUserId(state, currentUserId) {
state.currentUserId = currentUserId
localStorage.setItem(
localStorageKeys.currentUserId,
JSON.stringify(currentUserId)
)
},
clearToken(state) {
state.token = null
localStorage.removeItem(localStorageKeys.token)
},
setToken(state, token) {
state.token = token
localStorage.setItem(localStorageKeys.token, JSON.stringify(token))
},
},
actions: {
clear(context) {
context.commit('clearCurrentUserId')
context.commit('clearToken')
}
}
})

View File

@ -1,120 +0,0 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
export default {
namespaced: true,
state() {
return {
loading: false,
showSpinner: false,
profileId: null,
profile: null,
isOwnProfile: false,
error: false,
notFound: false
}
},
mutations: {
setProfileId(state, profileId) {
state.profileId = profileId
},
clearProfileId(state) {
state.profileId = null
},
setProfile(state, profile) {
state.profile = profile
},
clearProfile(state) {
state.profile = null
},
setLoading(state) {
state.loading = true
},
setNotLoading(state) {
state.loading = false
},
setError(state) {
state.error = true
},
clearError(state) {
state.error = false
},
showSpinner(state) {
state.showSpinner = true
},
hideSpinner(state) {
state.showSpinner = false
},
setNotFound(state, notFound) {
state.notFound = notFound
},
setIsOwnProfile(state, isOwnProfile) {
state.isOwnProfile = isOwnProfile
}
},
actions: {
onError({commit, dispatch}) {
commit('setError')
dispatch('clear')
},
onNotFound({commit, dispatch}) {
dispatch('onError')
commit('setNotFound', true)
},
clear({commit}) {
commit('clearProfileId')
commit('clearProfile')
commit('hideSpinner')
commit('setNotLoading')
},
async load({state, commit, dispatch, rootState}, profileId) {
if (state.loading) {
return
}
commit('setProfileId', profileId)
commit('setIsOwnProfile', rootState.currentUserId === profileId)
commit('setLoading')
const timeoutId = setTimeout(() => {
commit('showSpinner')
commit('clearProfile')
}, 0)
commit('clearError')
commit('setNotFound', false)
const url = new URL(`${window.ki.apiUrl}/users/${profileId}/profile`)
const headers = {
Authorization: `Bearer ${rootState.token}`
}
let response
try {
response = await fetch(url, {headers})
} catch {
dispatch('onError')
return
}
clearTimeout(timeoutId)
if (response.status === 404) {
dispatch('onNotFound')
return
}
if (!response.ok) {
dispatch('onError')
return
}
const responseData = await response.json()
commit('setProfile', responseData.profile)
commit('hideSpinner')
commit('setNotLoading')
}
}
}

View File

@ -1,122 +0,0 @@
// SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
export default {
namespaced: true,
state() {
return {
searching: false,
showSpinner: false,
searched: false,
profiles: [],
error: false,
errorMessage: '',
pages: 1,
query: {
search: '',
page: 1
}
}
},
mutations: {
setSearching(state, searching) {
state.searching = searching
},
showSpinner(state) {
state.showSpinner = true
},
hideSpinner(state) {
state.showSpinner = false
},
clearProfiles(state) {
state.profiles = []
},
setProfiles(state, profiles) {
state.profiles = profiles
},
setError(state, error) {
state.error = error
},
setErrorMessage(state, errorMessage) {
state.errorMessage = errorMessage
},
setQuerySearch(state, search) {
state.query = {...state.query, search}
},
setPages(state, pages) {
state.pages = pages
},
setQueryPage(state, page) {
state.query = {...state.query, page}
}
},
actions: {
async search({state, commit, rootState, dispatch}) {
if (state.searching) {
return
}
commit('setSearching', true)
const timeoutId = setTimeout(() => {
commit('showSpinner')
commit('clearProfiles')
}, 100)
commit('setError', false)
commit('setErrorMessage', '')
const url = new URL(`${window.ki.apiUrl}/users/profiles`)
if (state.query.search) {
url.searchParams.append('search', state.query.search)
}
url.searchParams.append('page', state.query.page)
const headers = {
Authorization: `Bearer ${rootState.token}`,
}
let response
try {
response = await fetch(url, {headers})
} catch {
commit('setError', true)
commit('clearProfiles')
commit('setSearching', false)
commit('hideSpinner')
return
}
console.log(response.ok)
console.log(response.status)
console.log(state.query.page)
clearTimeout(timeoutId)
if (!response.ok && response.status === 404 && state.query.page != 1) {
commit('setQueryPage', 1)
commit('setSearching', false)
await dispatch('search')
return
}
if (!response.ok) {
commit('setError', true)
commit('clearProfiles')
commit('setSearching', false)
commit('hideSpinner')
return
}
const responseData = await response.json()
commit('setProfiles', responseData.profiles)
commit('setPages', responseData.pages)
commit('setSearching', false)
commit('hideSpinner')
}
}
}

View File

@ -1,56 +1,47 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<div class="bg-wtf">
<div class="container pt-5">
<div class="text-center mb-5">
<img class="wtf-logo wtf-logo--index" src="@/assets/img/wtf_logo.svg">
<h1 class="text-white">Kompetenzinventar</h1>
<div class="container">
<h1>WTF Kompetenzinventar</h1>
<form @submit.prevent="submitLogin()">
<div class="mb-3">
<label for="exampleInputusername1" class="form-label"
>Benutzername:
</label>
<input
type="username"
class="form-control"
id="exampleInputusername1"
v-model="username"
required
/>
</div>
<form @submit.prevent="submitLogin()" class="card bg-white login-form">
<div class="card-body">
<div class="mb-3">
<label for="exampleInputusername1" class="form-label" >
WTF-Benutzername:
</label>
<input
type="username"
class="form-control"
id="exampleInputusername1"
v-model="username"
required
autofocus
/>
</div>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Passwort:</label>
<input
type="password"
class="form-control"
id="exampleInputPassword1"
v-model="password"
required
/>
</div>
<button type="submit" class="btn btn-primary">Login</button>
<a class="btn btn-link" href="https://resetpw.wtf-eg.de/">Passwort vergessen?</a>
<div
class="alert alert-danger mt-3 mb-0"
role="alert"
v-if="showErrorMessage"
>
Dein Benutzername oder Passwort ist falsch.<br>Versuche es noch einmal.
</div>
</div>
</form>
<div class="mb-3">
<label for="exampleInputPassword1" class="form-label">Passwort:</label>
<input
type="password"
class="form-control"
id="exampleInputPassword1"
v-model="password"
required
/>
</div>
<button type="submit" class="btn btn-primary mb-4">Login</button>
</form>
<a href="https://resetpw.wtf-eg.de/">Globales WTF Passwort zurücksetzen</a>
<div
class="alert alert-danger mb-4 mt-4"
role="alert"
v-if="showErrorMessage"
>
Mit deinen Login Daten ist ein Fehler aufgetreten. Versuch es nochmal oder
<a href="https://resetpw.wtf-eg.de/">hast du dein Passwort vergessen?</a>.
</div>
<p>
Das Login gilt nur für WTF eG Mitglieder. Du kannst dein LDAP Passwort
hier ändern.
</p>
</div>
</template>
<script>
import RequestMixin from "@/mixins/request.mixin"
@ -66,19 +57,3 @@ export default {
},
};
</script>
<style scoped>
.container {
min-height: 100vh;
}
.login-form {
margin: 0 auto;
max-width: 500px;
}
.wtf-logo--index {
max-width: 200px;
}
</style>

View File

@ -1,186 +1,84 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<div class="content">
<div class="bg-wtf text-white pt-3 pb-4">
<div class="container">
<div class="fs-3 text-center lh-1 mb-3">Finde WTF Member</div>
<div class="card mx-auto bg-white">
<div class="card-body">
<form @submit.prevent="handleSubmit">
<fieldset class="d-flex" :disabled="searching">
<div class="flex-grow-1 me-3">
<input
type="text"
class="form-control"
id="searchText"
v-model="searchText"
placeholder="Nick, Name, Fähigkeit, Sprache"
ref="searchTextInput"
/>
</div>
<div class="">
<button type="submit" class="btn btn-primary">
<i class="bi-search"></i>
<span class="d-none d-md-inline"> Suchen</span>
</button>
</div>
</fieldset>
</form>
</div>
<div class="container">
<h1>Suche</h1>
<form @submit.prevent="submitSearch()">
<div class="row">
<div class="col">
<input
type="text"
class="form-control"
id="searchText"
v-model="searchText"
/>
</div>
<div class="col">
<button type="submit" class="btn btn-primary mb-4">
Suche starten
</button>
</div>
</div>
</form>
<div
class="alert alert-danger mb-4 mt-4"
role="alert"
v-if="showErrorMessage"
>
Bei der Suche ist ein Fehler aufgetreten
</div>
<div class="container pt-4 pb-3">
<div class="text-center" v-if="showSpinner">
<Spinner />
</div>
<div
class="fs-2 text-danger text-center"
role="alert"
v-if="error"
>
<div class="fs-1 mb-3">Kernel panic :/</div>
Bei der Suche ist ein Fehler aufgetreten.
</div>
<div v-else-if="showNoResults" class="fs-2 text-black-50 text-center">
<div class="fs-1 mb-3">nullptr :/</div>
Es wurde kein Suchergebnis gefunden.
<p v-if="searchText !== ''">Probiere eine andere Suche.</p>
</div>
<div v-else-if="showResults">
<div class="d-flex justify-content-around">
<Paginator
:pages="pages"
:current="currentPage"
@page="handlePageSelected"
/>
</div>
<SearchResult
v-for="profile in profiles"
:key="profile.user_id"
class="mb-3"
:profile="profile"
/>
<div class="d-flex justify-content-around">
<Paginator
:pages="pages"
:current="currentPage"
@page="handlePageSelected"
/>
<div v-if="searchTotal == 0">
Es wurde kein Suchergebnis gefunden.
<p v-if="searchText !== ''">Probiere eine andere Suche.</p>
</div>
<div v-else>
<div class="row">
<div
class="col-4 p-2"
v-for="result in searchResults"
:key="result.user_id"
>
<router-link
class="text-decoration-none"
:to="{ path: `/s/profile/${result.user_id}` }"
>
<div class="card">
<div class="card-body">
<h5 class="card-title">
{{ result.nickname
}}<span v-if="result.pronouns"> ({{ result.pronouns }})</span>
</h5>
<h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
<p class="card-text" v-if="result.skills">
Fähigkeiten:
<span v-for="skill in result.skills" :key="skill.skill.name"
>{{ skill.skill.name }}
</span>
</p>
</div>
</div>
</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import Paginator from '@/components/Paginator'
import SearchResult from '@/components/SearchResult'
import Spinner from '@/components/Spinner'
import RequestMixin from "@/mixins/request.mixin"
export default {
name: 'Search',
components: {
Paginator,
SearchResult,
Spinner,
},
name: "Search",
mixins: [RequestMixin],
data() {
return {
textChanged: false
}
},
computed: {
...mapState({
searching: state => state.search.searching,
profiles: state => state.search.profiles,
error: state => state.search.error,
showSpinner: state => state.search.showSpinner,
pages: state => state.search.pages,
}),
searchText: {
get() {
return this.$store.state.search.query.search
},
set(text) {
this.$store.commit('search/setQuerySearch', text)
this.textChanged = true
}
},
currentPage: {
get() {
return this.$store.state.search.query.page
},
set(page) {
this.$store.commit('search/setQueryPage', page)
}
},
showNoResults() {
return !this.searching && (!this.profiles || this.profiles.length === 0)
},
showResults() {
return !this.error && this.profiles && this.profiles.length > 0
}
},
watch: {
searching(value) {
if (!value) {
if (this.$refs.searchTextInput) {
this.focusSearchText()
}
}
}
},
methods: {
handleSubmit() {
if (this.textChanged === true) {
this.$store.commit('search/setQueryPage', 1)
}
this.pushState()
this.$store.dispatch('search/search')
},
focusSearchText() {
this.$nextTick(() => {
this.$refs.searchTextInput.focus()
})
},
handlePageSelected(page) {
this.currentPage = page
this.pushState()
this.$store.dispatch('search/search')
},
pushState() {
this.$router.push({ query: { query: this.searchText, page: this.currentPage }})
}
showErrorMessage: false,
searchText: "",
searchResults: null,
searchTotal: 0,
};
},
created() {
if (this.$route.query.query) {
this.searchText = this.$route.query.query
this.$store.commit('search/clearProfiles')
}
if (this.$route.query.page) {
this.currentPage = parseInt(this.$route.query.page, 10)
this.$store.commit('search/clearProfiles')
}
this.$store.dispatch('search/search')
}
if (this.$route.query.query) this.searchText = this.$route.query.query;
this.submitSearch();
},
};
</script>
<style scoped>
.container {
max-width: 768px;
}
.content {
min-height: calc(100vh - 60px);
}
</style>

View File

@ -1,253 +1,189 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<div>
<div class="bg-wtf py-3 mb-4">
<div class="container">
<h3 class="text-white text-center mb-0">Profil bearbeiten</h3>
</div>
</div>
<div class="container">
<form @submit.prevent="submitFormEdit(false)">
<Section title="Grunddaten">
<div class="row mb-4">
<div class="col-12 col-md-4 mb-3 mb-md-0">
<label class="form-label">Nickname</label>
<input
type="text"
class="form-control"
id="nickname"
maxlength="25"
v-model="profile.nickname"
required
/>
</div>
<div class="col-12 col-md-4 mb-3 mb-md-0">
<label class="form-label">Klarname (optional)</label>
<input
type="text"
class="form-control"
id="realname"
maxlength="25"
v-model="profile.address.name"
/>
</div>
<div class="col-12 col-md-4">
<label class="form-label">
Pronomen
<i class="bi bi-info-circle" v-tooltip="pronounsTooltip"></i>
</label>
<input
type="text"
class="form-control"
id="pronouns"
maxlength="25"
v-model="profile.pronouns"
/>
</div>
</div>
<label class="form-label">Anschrift</label>
<div class="row">
<div class="col-12 col-md-4 mb-3 mb-md-0">
<input
type="text"
class="form-control"
id="postcode"
maxlength="10"
placeholder="Postleitzahl"
v-model="profile.address.postcode"
/>
</div>
<div class="col-12 col-md-4 mb-3 mb-md-0">
<input
type="text"
class="form-control"
id="city"
maxlength="25"
placeholder="Ort"
v-model="profile.address.city"
/>
</div>
<div class="col-12 col-md-4">
<input
type="text"
class="form-control"
id="country"
maxlength="25"
placeholder="Land"
v-model="profile.address.country"
/>
</div>
</div>
</Section>
<Section title="Meine Fähigkeiten">
<template v-slot:card-body>
<auto-complete
type="skill"
:values="profile.skills"
placeholder="z.B. Python, JavaScript, Linux"
@update-values="profile.skills = $event"
></auto-complete>
</template>
</Section>
<Section title="Meine Sprachkenntnisse">
<template v-slot:card-body>
<auto-complete
type="language"
:values="profile.languages"
placeholder="z.B. Deutsch, Englisch, Französisch"
@update-values="profile.languages = $event"
></auto-complete>
</template>
</Section>
<Section title="Ich suche für mich Projekte/Aufträge in folgenden Bereichen">
<template v-slot:card-body>
<auto-complete
type="skill"
label="Ich suche für mich Projekte/Aufträge in folgenden Bereichen"
:values="profile.searchtopics"
:showSecondary="false"
placeholder="z.B. Python, JavaScript, Linux"
@update-values="profile.searchtopics = $event"
></auto-complete>
</template>
</Section>
<Section title="Verfügbarkeit">
<div class="form-check mb-3">
<input
v-model="profile.availability_status"
class="form-check-input me-2"
type="checkbox"
id="availability_status">
<label class="form-check-label" for="availability_status">
Ich bin aktuell verfügbar
</label>
</div>
<div class="mb-3" v-if="profile.availability_status">
<label class="form-label">
Stunden pro Woche
</label>
<input
v-model="profile.availability_hours_per_week"
type="number"
min="0"
max="168"
class="form-control">
</div>
<label for="availability" class="form-label">
Anmerkungen
</label>
<textarea
class="form-control"
id="availability"
rows="3"
maxlength="4000"
v-model="profile.availability_text"
></textarea>
</Section>
<Section title="Meine Kontaktmöglichkeiten">
<template v-slot:card-body>
<auto-complete
type="contacttype"
:values="profile.contacts"
placeholder="z.B. E-Mail, Mobiltelefon, Matrix, Web"
@update-values="profile.contacts = $event"
></auto-complete>
</template>
</Section>
<Section title="Sonstiges">
<div class="mb-3">
<label class="form-label">Über mich</label>
<textarea
class="form-control"
rows="3"
maxlength="4000"
v-model="profile.freetext"
/>
</div>
<div>
<label class="form-label">Ehrenamtliche Arbeit</label>
<textarea
class="form-control"
rows="3"
maxlength="4000"
v-model="profile.volunteerwork"
/>
</div>
</Section>
<input type="submit" class="d-none">
</form>
<div class="container">
<h1>Profil bearbeiten</h1>
<form @submit.prevent="submitFormEdit()">
<div class="row">
<div class="col">
<input
type="radio"
id="false"
:value="false"
v-model="profile.visible"
class="mr-2"
/>
<label for="false" class="m-2 fw-bold"> Nicht öffentlich</label>
</div>
<div class="savebar bg-white border-top py-3">
<div class="container d-flex align-items-center justify-content-end">
<div
class="text-danger"
v-if="showErrorMessage"
>
<i class="bi bi-bug"></i>
Beim Speichern ist ein Fehler aufgetreten.
</div>
<div
class="text-success"
v-if="showSuccessMessage"
>
<i class="bi bi-check-lg"></i>
Gespeichert
</div>
<button
class="btn btn-secondary ms-3"
@click="submitFormEdit(false)"
>
Entwurf Speichern
</button>
<button
class="btn btn-primary ms-3"
@click="submitFormEdit(true)"
>
Speichern und Veröffentlichen
</button>
<div class="col">
<input
type="radio"
id="true"
:value="true"
v-model="profile.visible"
/>
<label for="true" class="m-2 fw-bold"> Öffentlich</label>
</div>
</div>
<div id="visibilityHelp" class="form-text">
Erst wenn du dein Profil Öffentlich stellst, können andere Genoss:innen
darauf zugreifen oder es in der Suche finden.
</div>
<div class="row">
<div class="col-6 col-xs-12">
<label for="nickname" class="form-label fw-bold">Nickname:</label>
<input
type="text"
class="form-control"
id="nickname"
v-model="profile.nickname"
required
/>
</div>
<div class="col-6 col-xs-12">
<label for="pronouns" class="form-label fw-bold">Pronomen:</label>
<input
type="text"
class="form-control"
id="pronouns"
v-model="profile.pronouns"
/>
<div for="pronouns" class="form-text">
Z.B.: Er/Ihn, Sie/Ihr, Es etc..
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-xs-12">
<label for="freetext" class="form-label fw-bold">Vorstellung:</label>
<textarea
class="form-control"
id="freetext"
rows="3"
v-model="profile.freetext"
></textarea>
</div>
<div class="col-12 col-xs-12">
<label for="volunteerwork" class="form-label fw-bold"
>Ehrenamtliche Arbeit:</label
>
<textarea
class="form-control"
id="volunteerwork"
rows="3"
v-model="profile.volunteerwork"
></textarea>
</div>
</div>
<auto-complete
type="skill"
label="Deine Fähigkeiten"
:values="profile.skills"
@update-values="profile.skills = $event"
></auto-complete>
<auto-complete
type="language"
label="Deine Sprachen"
:values="profile.languages"
@update-values="profile.languages = $event"
></auto-complete>
<auto-complete
type="skill"
label="Ich suche"
:values="profile.searchtopics"
@update-values="profile.searchtopics = $event"
></auto-complete>
<div class="col-12 col-xs-12">
<label for="availability" class="form-label fw-bold"
>Ich bin für Anfragen verfügbar:</label
>
<textarea
class="form-control"
id="availability"
rows="3"
v-model="profile.availability"
></textarea>
</div>
<auto-complete
type="contacttype"
label="Kontaktmöglichkeiten"
:values="profile.contacts"
@update-values="profile.contacts = $event"
></auto-complete>
<div class="row">
<div class="col">
<label for="pzl" class="form-label fw-bold">PLZ</label>
<input
type="text"
class="form-control"
id="pzl"
v-model="profile.address.postcode"
/>
</div>
<div class="col">
<label for="city" class="form-label fw-bold">Stadt</label>
<input
type="text"
class="form-control"
id="city"
v-model="profile.address.city"
/>
</div>
<div class="col">
<label for="country" class="form-label fw-bold">Land</label>
<input
type="text"
class="form-control"
id="country"
v-model="profile.address.country"
/>
</div>
</div>
<button type="submit" class="btn btn-outline-success mb-4 mt-4 col-12">
Speichern
</button>
<div
class="alert alert-danger mb-4 mt-4"
role="alert"
v-if="showErrorMessage"
>
Es ist Fehler aufgetreten
</div>
<div
class="alert alert-success mb-4 mt-4"
role="alert"
v-if="showSuccessMessage"
>
Deine Änderungen wurden erfolgreich gespeichert
</div>
</form>
</div>
</template>
<script>
import RequestMixin from "@/mixins/request.mixin"
import AutoComplete from "@/components/AutoComplete";
import Section from '@/components/profile/Section'
export default {
name: "profileEdit",
mixins: [RequestMixin],
components: {
AutoComplete,
Section,
},
data() {
return {
showErrorMessage: false,
showSuccessMessage: false,
clearMessagesHandle: null,
profile: {
visible: false,
nickname: "",
pronouns: "",
volunteerwork: "",
freetext: "",
availability_status: false,
availability_hours_per_week: null,
availability_text: "",
availability: "",
address: {
postcode: "",
city: "",
@ -258,49 +194,10 @@ export default {
searchtopics: [],
contacts: [],
},
pronounsTooltip: {
content: 'Wie möchtest du angesprochen werden?<br>Zum Beispiel "er/ihn" oder "sie/ihre".',
html: true
}
};
},
async created() {
await this.initEditPage();
},
unmounted() {
this.cancelClearMessages()
},
methods: {
cancelClearMessages() {
if (this.clearMessagesHandle) {
window.clearTimeout(this.clearMessagesHandle)
}
},
scheduleClearMessages() {
this.cancelClearMessages()
this.clearMessagesHandle = window.setTimeout(() => {
this.showErrorMessage = false
this.showSuccessMessage = false
}, 2500)
}
},
watch: {
showErrorMessage(curr) {
if (curr) {
this.scheduleClearMessages()
}
},
showSuccessMessage(curr) {
if (curr) {
this.scheduleClearMessages()
}
}
}
};
</script>
<style scoped>
.savebar {
bottom: 0;
position: sticky;
}
</style>

View File

@ -1,160 +1,57 @@
<!--
SPDX-FileCopyrightText: WTF Kooperative eG <https://wtf-eg.de/>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<template>
<div>
<template v-if="error">
<ViewError :isOwnProfile="isOwnProfile" :notFound="notFound" />
</template>
<template
v-else-if="profile"
class="container">
<ProfileHeader
class="mb-4"
:profile="profile" />
<Section
v-if="profile.skills && profile.skills.length > 0"
title="Das kann ich">
<div style="margin-bottom: -.5rem;">
<Skill
class="me-2 mb-2"
v-for="skill in profile.skills"
:key="skill.skill.id"
:profileSkill="skill"
:showLevel="true" />
</div>
</Section>
<Section
v-if="profile.searchtopics && profile.searchtopics.length > 0"
title="Ich suche für mich Projekte/Aufträge in folgenden Bereichen">
<div style="margin-bottom: -.5rem;">
<Skill
class="me-2 mb-2"
v-for="skill in profile.searchtopics"
:key="skill.skill.id"
:profileSkill="skill"
:showLevel="false" />
</div>
</Section>
<Section
v-if="profile.languages && profile.languages.length > 0"
title="Ich spreche diese Sprachen">
<div style="margin-bottom: -.5rem;">
<Language
class="me-2 mb-2"
v-for="language in profile.languages"
:key="language.language.id"
:profileLanguage="language"
/>
</div>
</Section>
<Section title="Verfügbarkeit">
<div class="d-flex align-items-center">
<div v-if="profile.availability_status">
<i class="bi bi-check-square me-1"></i>
ja
</div>
<div v-else>
<i class="bi bi-x-square me-1"></i>
nein
</div>
<span
class="ms-3"
v-if="profile.availability_status && profile.availability_hours_per_week">
({{ profile.availability_hours_per_week }} Stunden pro Woche)
</span>
</div>
<div v-if="profile.availability_text" class="mt-3">
<label class="form-label fw-bold">
Anmerkungen
</label>
<div class="line-break-text">{{ profile.availability_text }}</div>
</div>
</Section>
<Section
v-if="profile.contacts && profile.contacts.length > 0"
title="Meine Kontaktmöglichkeiten"
>
<div style="margin-bottom: -.5rem;">
<Contact
class="me-2 mb-2"
v-for="profileContact in profile.contacts"
:key="profileContact.id"
:profileContact="profileContact"
/>
</div>
</Section>
<Section
v-if="profile.volunteerwork || profile.freetext"
title="Sonstiges">
<div v-if="profile.freetext" :class="{ 'lh-base': true, 'mb-4': profile.volunteerwork }">
<h5>Über mich</h5>
<div class="line-break-text">{{ profile.freetext }}</div>
</div>
<div v-if="profile.volunteerwork" class="lh-base">
<h5>Ehrenamtliche Arbeit</h5>
<div class="line-break-text">{{ profile.volunteerwork }}</div>
</div>
</Section>
</template>
<div v-if="profile" class="container">
<h1>
{{profile.nickname}}
<span v-if="profile.pronouns">({{profile.pronouns}})</span>
</h1>
<p><label class="fw-bold">Vorstellung: </label> {{profile.freetext}}</p>
<p><label class="fw-bold">Ehrentamtliche Arbeit: </label> {{profile.volunteerwork}}</p>
<p><label class="fw-bold">Verfügbarkeit: </label> {{profile.availability}}</p>
<h3>Das kann ich:</h3>
<profile-list
:values="profile.skills"
type="skill"
></profile-list>
<h3>Das suche ich:</h3>
<profile-list
:values="profile.searchtopics"
type="skill"
></profile-list>
<h3>Meine Kontaktmöglichkeiten:</h3>
<profile-list
:values="profile.contacts"
type="contacttype"
></profile-list>
<h3>Ich Spreche Folgende Sprachen:</h3>
<profile-list
:values="profile.languages"
type="language"
></profile-list>
<div v-if="profile.address">
<h3>Meine Location:</h3>
{{profile.address.city}} ({{profile.address.postcode}}), {{profile.address.country}}
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import RequestMixin from "@/mixins/request.mixin"
import ViewError from '@/components/ViewError'
import ProfileHeader from '@/components/profile/Header'
import Section from '@/components/profile/Section'
import Contact from '@/components/profile/Contact'
import Language from '@/components/profile/Language'
import Skill from '@/components/Skill'
import ProfileList from "@/components/ProfileList";
export default {
name: 'profileView',
name: "profileView",
mixins: [RequestMixin],
components: {
Contact,
Language,
ProfileHeader,
Section,
Skill,
ViewError,
ProfileList,
},
methods: {
...mapActions({
loadProfile: 'profile/load',
clearStore: 'profile/clear',
})
},
computed: {
...mapState({
profile: state => state.profile.profile,
error: state => state.profile.error,
notFound: state => state.profile.notFound,
isOwnProfile: state => state.profile.isOwnProfile,
showSpinner: state => state.profile.showSpinner
})
data() {
return {
profile: null
};
},
async created() {
const id = parseInt(this.$route.params.memberId, 10)
this.loadProfile(id)
},
unmounted() {
this.clearStore()
},
await this.initViewPage();
}
};
</script>
<style>
.line-break-text{
white-space: pre-line;
}
</style>