Compare commits
42 Commits
fcbf3ee796
...
nginx-cach
Author | SHA1 | Date | |
---|---|---|---|
26edf1d4b2
|
|||
e7ff487aeb | |||
c5bda80f11 | |||
379ddaf5b9 | |||
13fb15e033 | |||
aaa0883692 | |||
e404bef2a9
|
|||
cbbcfd0f37 | |||
5c5f157a77
|
|||
3017c001b2 | |||
fefe9a034d | |||
bac8731e17
|
|||
dc883ac302
|
|||
16feb41f8a | |||
04cb8a7217 | |||
e3115f9944 | |||
122b13b6a2 | |||
872d544075 | |||
fba33d20a7 | |||
e035fa3289
|
|||
324203216a
|
|||
b270a5d56a | |||
6ecf80f34c
|
|||
016a1bd959 | |||
4e8390cf96
|
|||
04d59d5520 | |||
0f0d3cd861
|
|||
2b63603957 | |||
9a51b416e5
|
|||
3ea7eb48b4
|
|||
8c8021bedc | |||
46fcaa2db6
|
|||
2d700c77dc
|
|||
c1d78fa8c1
|
|||
7c8a1bb423
|
|||
8e42c7fdbe | |||
c25639b40c | |||
a2048d0eb9 | |||
b19a770d61 | |||
8098c54c06 | |||
73847022e2
|
|||
93cb302ca7
|
21
.drone.yml
21
.drone.yml
@ -9,6 +9,7 @@ name: default
|
|||||||
steps:
|
steps:
|
||||||
- name: reuse
|
- name: reuse
|
||||||
image: fsfe/reuse:latest
|
image: fsfe/reuse:latest
|
||||||
|
|
||||||
- name: docker-publish
|
- name: docker-publish
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
@ -20,3 +21,23 @@ steps:
|
|||||||
from_secret: "docker_username"
|
from_secret: "docker_username"
|
||||||
password:
|
password:
|
||||||
from_secret: "docker_password"
|
from_secret: "docker_password"
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
|
||||||
|
- name: docker-publish-tag
|
||||||
|
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"
|
||||||
|
when:
|
||||||
|
event:
|
||||||
|
- tag
|
||||||
|
@ -11,7 +11,7 @@ Files: .browserslistrc .dockerignore .eslintrc.js .gitignore
|
|||||||
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
|
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
|
||||||
License: AGPL-3.0-or-later
|
License: AGPL-3.0-or-later
|
||||||
|
|
||||||
Files: src/assets/img/wtf_logo*
|
Files: src/assets/img/wtf*
|
||||||
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
|
Copyright: WTF Kooperative eG <https://wtf-eg.de/>
|
||||||
License: LicenseRef-WTF
|
License: LicenseRef-WTF
|
||||||
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
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.
|
|
@ -9,10 +9,24 @@ server {
|
|||||||
|
|
||||||
#access_log /var/log/nginx/host.access.log main;
|
#access_log /var/log/nginx/host.access.log main;
|
||||||
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html index.htm;
|
|
||||||
try_files $uri $uri/ /index.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;
|
#error_page 404 /404.html;
|
||||||
|
14743
package-lock.json
generated
14743
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -7,18 +7,20 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "^4.5.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
"@vue/cli-plugin-eslint": "^4.5.0",
|
||||||
"@vue/cli-plugin-router": "~4.5.0",
|
"@vue/cli-plugin-router": "^4.5.0",
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "^4.5.0",
|
||||||
"@vue/compiler-sfc": "^3.0.0",
|
"@vue/compiler-sfc": "^3.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"bootstrap": "^5.0.1",
|
"bootstrap": "^5.0.1",
|
||||||
|
"bootstrap-icons": "^1.5.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.0.0",
|
||||||
"sass": "^1.37.5",
|
"sass": "^1.37.5",
|
||||||
"sass-loader": "^10.2.0",
|
"sass-loader": "^10.2.0",
|
||||||
|
"v-tooltip": "^4.0.0-alpha.1",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
"vuex": "^4.0.2"
|
"vuex": "^4.0.2"
|
||||||
|
@ -1 +0,0 @@
|
|||||||
SPDX-License-Identifier: MIT
|
|
@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 548 B |
@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 238 B |
@ -1 +0,0 @@
|
|||||||
SPDX-License-Identifier: MIT
|
|
@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 331 B |
@ -1 +0,0 @@
|
|||||||
SPDX-License-Identifier: MIT
|
|
@ -1,4 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 573 B |
@ -1 +0,0 @@
|
|||||||
SPDX-License-Identifier: MIT
|
|
@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
<Footer />
|
<Footer />
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Footer from '@/components/Footer.vue'
|
import Footer from '@/components/Footer'
|
||||||
import Navbar from '@/components/Navbar.vue'
|
import Navbar from '@/components/Navbar'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
@ -7,6 +7,13 @@
|
|||||||
@import "variables";
|
@import "variables";
|
||||||
@import "bootstrap/scss/bootstrap";
|
@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 {
|
.btn-primary {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
BIN
src/assets/img/wtf-header-bg.jpg
Normal file
BIN
src/assets/img/wtf-header-bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"1": "Keine Angabe",
|
"1": "Keine Angabe",
|
||||||
"2": "Grundkentnisse",
|
"2": "Grundkenntnisse",
|
||||||
"3": "Gut",
|
"3": "Gut",
|
||||||
"4": "Fließend",
|
"4": "Fließend",
|
||||||
"5": "Muttersprache"
|
"5": "Muttersprache"
|
||||||
|
@ -1,7 +1,22 @@
|
|||||||
{
|
{
|
||||||
"1": "bis 6 Monate",
|
"1": {
|
||||||
"2": "bis 1 Jahr",
|
"short": "≤ 6M",
|
||||||
"3": "bis 3 Jahre",
|
"long": "bis 6 Monate"
|
||||||
"4": "bis 5 Jahre",
|
},
|
||||||
"5": "mehr als 5 Jahre"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
@ -14,3 +14,5 @@ $body-bg: $gray-100;
|
|||||||
|
|
||||||
$link-decoration: none;
|
$link-decoration: none;
|
||||||
$link-hover-decoration: underline;
|
$link-hover-decoration: underline;
|
||||||
|
|
||||||
|
$spinner-animation-speed: 1s;
|
||||||
|
@ -5,37 +5,33 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<profile-list
|
||||||
<label for="searchText" class="form-label fw-bold">{{ label }}</label>
|
:values="values"
|
||||||
<div class="row mb-2">
|
:type="type"
|
||||||
<div class="col">
|
: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">
|
||||||
<input
|
<input
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control form-control-sm"
|
||||||
id="searchText"
|
id="searchText"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:placeholder="placeholder"
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
@keyup="search()"
|
@input="search()"
|
||||||
@keyup.enter="addResult()"
|
@keyup.enter="addResult()"
|
||||||
/>
|
/>
|
||||||
</div>
|
<div v-if="searchResults">
|
||||||
<div class="col">
|
|
||||||
<button
|
|
||||||
v-if="searchText != ''"
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-success"
|
|
||||||
aria-label="Hinzufügen"
|
|
||||||
@click="addResult()"
|
|
||||||
>
|
|
||||||
<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">
|
<ul class="list-group">
|
||||||
<li
|
<li
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
@ -47,24 +43,30 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<profile-list
|
</div>
|
||||||
:values="values"
|
<div class="col-md-2">
|
||||||
:type="type"
|
<button
|
||||||
:editable="true"
|
v-if="searchText != ''"
|
||||||
@remove-value="removeValue($event)"
|
type="button"
|
||||||
@update-values="this.$emit('update-values', this.values)"
|
class="btn btn-outline-success"
|
||||||
|
aria-label="Hinzufügen"
|
||||||
|
@click="addResult()"
|
||||||
>
|
>
|
||||||
</profile-list>
|
<i clas="bi-plus-lg"></i>
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
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 {
|
export default {
|
||||||
name: "AutoComplete",
|
name: 'AutoComplete',
|
||||||
mixins: [RequestMixin],
|
mixins: [RequestMixin],
|
||||||
components: {
|
components: {
|
||||||
ProfileList,
|
ProfileList,
|
||||||
@ -79,21 +81,33 @@ export default {
|
|||||||
values: {
|
values: {
|
||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
|
showSecondary: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
iconUrl: this.apiUrl,
|
iconUrl: this.apiUrl,
|
||||||
searchText: "",
|
searchText: '',
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
showErrorMessage: false,
|
showErrorMessage: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['currentUserId'])
|
...mapState(['currentUserId']),
|
||||||
|
maxlength() {
|
||||||
|
return this.type === 'skill' ? 50 : 25
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addResult(result = false) {
|
addResult(result = false) {
|
||||||
if (!result) result = this.searchResults[0];
|
if (!result) result = this.searchResults[0];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.values.map((item) => item[this.type].name).includes(result.name)
|
this.values.map((item) => item[this.type].name).includes(result.name)
|
||||||
) {
|
) {
|
||||||
@ -104,16 +118,19 @@ export default {
|
|||||||
let newValue = {
|
let newValue = {
|
||||||
profile_id: this.currentUserId,
|
profile_id: this.currentUserId,
|
||||||
};
|
};
|
||||||
if (this.type != "contacttype") {
|
|
||||||
|
if (this.type != 'contacttype') {
|
||||||
newValue.level = 1;
|
newValue.level = 1;
|
||||||
} else {
|
} else {
|
||||||
newValue.content = "";
|
newValue.content = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
newValue[this.type] = result;
|
newValue[this.type] = result;
|
||||||
changeValues.unshift(newValue);
|
changeValues.unshift(newValue);
|
||||||
this.searchText = "";
|
|
||||||
|
this.searchText = '';
|
||||||
this.searchResults = [];
|
this.searchResults = [];
|
||||||
this.$emit("update-values", changeValues);
|
this.$emit('update-values', changeValues);
|
||||||
},
|
},
|
||||||
removeValue(valueName) {
|
removeValue(valueName) {
|
||||||
const newValues = this.values.filter((value) => {
|
const newValues = this.values.filter((value) => {
|
||||||
@ -123,7 +140,7 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.$emit("update-values", newValues);
|
this.$emit('update-values', newValues);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
37
src/components/Avatar.vue
Normal file
37
src/components/Avatar.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
<div class="fw-bold text-white mb-2">Kompetenzinventar</div>
|
<div class="fw-bold text-white mb-2">Kompetenzinventar</div>
|
||||||
<ul class="list-unstyled">
|
<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">Quellcode</a></li>
|
||||||
<li><a href="https://git.wtf-eg.de/kompetenzinventar/ki-frontend/issues/new">Problem melden</a></li>
|
<li><a href="https://git.wtf-eg.de/kompetenzinventar/ki-doku/issues/new/choose">Problem melden</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,11 +26,11 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="collapse navbar-collapse"
|
class="collapse navbar-collapse d-lg-flex"
|
||||||
:class="{ show: showMobileNav }"
|
:class="{ show: showMobileNav }"
|
||||||
id="navbarSupportedContent"
|
id="navbarSupportedContent"
|
||||||
>
|
>
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
<ul class="navbar-nav mb-2 mb-lg-0 me-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<router-link
|
<router-link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
@ -55,30 +55,15 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
>Bearbeiten</router-link
|
>Bearbeiten</router-link
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<button class="btn btn-outline-danger" @click="logout()">
|
<button class="btn btn-danger w-100" @click="logout()">
|
||||||
|
<i class="bi bi-box-arrow-right"></i>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -95,7 +80,6 @@ export default {
|
|||||||
mixins: [RequestMixin],
|
mixins: [RequestMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchText: '',
|
|
||||||
showMobileNav: false
|
showMobileNav: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -110,9 +94,6 @@ export default {
|
|||||||
this.$store.dispatch('clear')
|
this.$store.dispatch('clear')
|
||||||
this.$router.push({ path: '/' });
|
this.$router.push({ path: '/' });
|
||||||
},
|
},
|
||||||
searchRedirect() {
|
|
||||||
this.$router.push({ path: `/s/search?text`, query: { query: this.searchText } } );
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
49
src/components/Paginator.vue
Normal file
49
src/components/Paginator.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
@ -5,24 +5,27 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul>
|
<ul class="list-group list-group-flush">
|
||||||
<li v-for="(value, valueKey) in values" :key="value.id">
|
<li
|
||||||
<div class="row m-1">
|
class="list-group-item"
|
||||||
<div class="col">
|
v-for="(value, valueKey) in values"
|
||||||
<img
|
:key="value.id"
|
||||||
style="max-width: 64px"
|
>
|
||||||
|
<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}'` }"
|
||||||
v-if="value[type].icon_url"
|
v-if="value[type].icon_url"
|
||||||
:src="iconUrl + value[type].icon_url"
|
|
||||||
:alt="`${value[type].name} Logo`"
|
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
{{ value[type].name }}
|
{{ value[type].name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
</div>
|
||||||
<div v-if="type == 'skill'">
|
<div class="col-10 col-md-5">
|
||||||
<div v-if="editable">
|
<div v-if="type == 'skill' && showSecondary">
|
||||||
<select
|
<select
|
||||||
v-if="editableValues[valueKey]"
|
class="form-select form-select-sm"
|
||||||
class="form-select"
|
|
||||||
aria-label="Selektiere dein Level"
|
aria-label="Selektiere dein Level"
|
||||||
v-model="editableValues[valueKey].level"
|
v-model="editableValues[valueKey].level"
|
||||||
@input="$emit('update-values', editableValues)"
|
@input="$emit('update-values', editableValues)"
|
||||||
@ -32,18 +35,13 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
:value="key"
|
:value="key"
|
||||||
:key="key"
|
:key="key"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ value.long || value }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-else><span v-if="value.level">({{ levelSelection[value.level] }})</span></div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="type == 'language'">
|
<div v-else-if="type == 'language'">
|
||||||
<div v-if="editable">
|
|
||||||
<div v-if="editable">
|
|
||||||
<select
|
<select
|
||||||
v-if="editableValues[valueKey]"
|
class="form-select form-select-sm"
|
||||||
class="form-select"
|
|
||||||
aria-label="Selektiere dein Level"
|
aria-label="Selektiere dein Level"
|
||||||
v-model="editableValues[valueKey].level"
|
v-model="editableValues[valueKey].level"
|
||||||
@input="$emit('update-values', editableValues)"
|
@input="$emit('update-values', editableValues)"
|
||||||
@ -57,35 +55,22 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div v-else><span v-if="value.level">({{languagesSelection[value.level]}})</span></div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="type == 'contacttype'">
|
<div v-else-if="type == 'contacttype'">
|
||||||
<div v-if="editable">
|
<input
|
||||||
<input class="form-control" v-model="editableValues[valueKey].content"/>
|
class="form-control form-control-sm"
|
||||||
</div>
|
maxlength="200"
|
||||||
<div v-else>
|
v-model="editableValues[valueKey].content"
|
||||||
<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>
|
||||||
</div>
|
<div class="col-2 text-end">
|
||||||
<div class="col">
|
|
||||||
<button
|
<button
|
||||||
v-if="editable"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-danger"
|
class="btn btn-sm btn-light"
|
||||||
aria-label="Löschen"
|
aria-label="Löschen"
|
||||||
@click="$emit('removeValue', value[type].name)"
|
@click="$emit('removeValue', value[type].name)"
|
||||||
>
|
>
|
||||||
<img
|
<i class="text-danger bi bi-x-circle"></i>
|
||||||
src="/img/bootstrap-icons-1.5.0/trash.svg"
|
|
||||||
alt="Löschen Icon"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,11 +78,11 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import levelJson from "@/assets/skill_level.json";
|
import levelJson from '@/assets/skill_level.json';
|
||||||
import languagesJson from "@/assets/language_level.json";
|
import languagesJson from '@/assets/language_level.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProfileList",
|
name: 'ProfileList',
|
||||||
props: {
|
props: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
@ -106,14 +91,14 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
editable: {
|
showSecondary: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: true
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
iconUrl: this.apiUrl,
|
iconBaseUrl: this.apiUrl,
|
||||||
levelSelection: levelJson,
|
levelSelection: levelJson,
|
||||||
languagesSelection: languagesJson,
|
languagesSelection: languagesJson,
|
||||||
editableValues: this.values,
|
editableValues: this.values,
|
||||||
@ -126,3 +111,13 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scope>
|
||||||
|
.list-icon {
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
54
src/components/SearchResult.vue
Normal file
54
src/components/SearchResult.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
81
src/components/Skill.vue
Normal file
81
src/components/Skill.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
9
src/components/Spinner.vue
Normal file
9
src/components/Spinner.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
39
src/components/ViewError.vue
Normal file
39
src/components/ViewError.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
|
|
39
src/components/profile/Contact.vue
Normal file
39
src/components/profile/Contact.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
79
src/components/profile/Header.vue
Normal file
79
src/components/profile/Header.vue
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
69
src/components/profile/Language.vue
Normal file
69
src/components/profile/Language.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
28
src/components/profile/Section.vue
Normal file
28
src/components/profile/Section.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
@ -6,11 +6,15 @@ import { createApp } from 'vue/dist/vue.esm-bundler'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import store from '@/store'
|
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'
|
import './assets/global.scss'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(VTooltipPlugin)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(store)
|
app.use(store)
|
||||||
|
|
||||||
|
@ -37,6 +37,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async search() {
|
async search() {
|
||||||
|
if (!this.searchText) {
|
||||||
|
this.searchResults = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.apiUrl}/${this.type}s?search=${this.searchText}`, {
|
const response = await fetch(`${this.apiUrl}/${this.type}s?search=${this.searchText}`, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -52,15 +57,16 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await response.json()
|
const responseData = await response.json()
|
||||||
this.searchResults = responseData[`${this.type}s`];
|
const searchResults = responseData[`${this.type}s`];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.values
|
!searchResults.map((item) => item.name.toLowerCase())
|
||||||
.map((item) => item[this.type].name.toLowerCase())
|
|
||||||
.includes(this.searchText.toLowerCase())
|
.includes(this.searchText.toLowerCase())
|
||||||
) {
|
) {
|
||||||
this.searchResults.unshift({ name: this.searchText });
|
searchResults.unshift({ name: this.searchText });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.searchResults = searchResults
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error();
|
console.error();
|
||||||
this.showErrorMessage = true;
|
this.showErrorMessage = true;
|
||||||
|
@ -46,7 +46,14 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Index',
|
name: 'Index',
|
||||||
component: Index
|
component: Index,
|
||||||
|
beforeEnter: (_to, _from, next) => {
|
||||||
|
if (store.state.token) {
|
||||||
|
next({name: 'Search'})
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4,17 +4,23 @@
|
|||||||
|
|
||||||
import { createStore } from 'vuex'
|
import { createStore } from 'vuex'
|
||||||
|
|
||||||
|
import profile from './profile'
|
||||||
|
import search from './search'
|
||||||
|
|
||||||
const localStorageKeys = {
|
const localStorageKeys = {
|
||||||
currentUserId: 'ki_current_user_id',
|
currentUserId: 'ki_current_user_id',
|
||||||
token: 'ki_token',
|
token: 'ki_token',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
|
modules: {
|
||||||
|
profile,
|
||||||
|
search,
|
||||||
|
},
|
||||||
state() {
|
state() {
|
||||||
return {
|
return {
|
||||||
currentUserId: JSON.parse(localStorage.getItem(localStorageKeys.currentUserId)),
|
currentUserId: JSON.parse(localStorage.getItem(localStorageKeys.currentUserId)),
|
||||||
token: JSON.parse(localStorage.getItem(localStorageKeys.token)),
|
token: JSON.parse(localStorage.getItem(localStorageKeys.token)),
|
||||||
currentProfile: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@ -37,9 +43,6 @@ export default createStore({
|
|||||||
state.token = token
|
state.token = token
|
||||||
localStorage.setItem(localStorageKeys.token, JSON.stringify(token))
|
localStorage.setItem(localStorageKeys.token, JSON.stringify(token))
|
||||||
},
|
},
|
||||||
setCurrentProfile(state, profile) {
|
|
||||||
state.currentProfile = profile
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
clear(context) {
|
clear(context) {
|
120
src/store/profile.js
Normal file
120
src/store/profile.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
// 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
122
src/store/search.js
Normal file
122
src/store/search.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,12 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="bg-wtf">
|
||||||
<div class="container pt-5">
|
<div class="container pt-5">
|
||||||
<div class="text-center mb-5">
|
<div class="text-center mb-5">
|
||||||
<img class="wtf-logo wtf-logo--index" src="@/assets/img/wtf_logo.svg">
|
<img class="wtf-logo wtf-logo--index" src="@/assets/img/wtf_logo.svg">
|
||||||
<h1>Kompetenzinventar</h1>
|
<h1 class="text-white">Kompetenzinventar</h1>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitLogin()" class="bg-white p-3 login-form">
|
<form @submit.prevent="submitLogin()" class="card bg-white login-form">
|
||||||
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="exampleInputusername1" class="form-label" >
|
<label for="exampleInputusername1" class="form-label" >
|
||||||
WTF-Benutzername:
|
WTF-Benutzername:
|
||||||
@ -21,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
id="exampleInputusername1"
|
id="exampleInputusername1"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
required
|
required
|
||||||
|
autofocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -42,9 +45,12 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
>
|
>
|
||||||
Dein Benutzername oder Passwort ist falsch.<br>Versuche es noch einmal.
|
Dein Benutzername oder Passwort ist falsch.<br>Versuche es noch einmal.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RequestMixin from "@/mixins/request.mixin"
|
import RequestMixin from "@/mixins/request.mixin"
|
||||||
|
|
||||||
@ -62,6 +68,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
@ -5,88 +5,182 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<div class="bg-wtf text-white pt-3 pb-4">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Suche</h1>
|
<div class="fs-3 text-center lh-1 mb-3">Finde WTF Member</div>
|
||||||
<form @submit.prevent="submitSearch()">
|
<div class="card mx-auto bg-white">
|
||||||
<div class="row">
|
<div class="card-body">
|
||||||
<div class="col">
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<fieldset class="d-flex" :disabled="searching">
|
||||||
|
<div class="flex-grow-1 me-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="searchText"
|
id="searchText"
|
||||||
v-model="searchText"
|
v-model="searchText"
|
||||||
|
placeholder="Nick, Name, Fähigkeit, Sprache"
|
||||||
|
ref="searchTextInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="">
|
||||||
<button type="submit" class="btn btn-primary mb-4">
|
<button type="submit" class="btn btn-primary">
|
||||||
Suche starten
|
<i class="bi-search"></i>
|
||||||
|
<span class="d-none d-md-inline"> Suchen</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
<div
|
|
||||||
class="alert alert-danger mb-4 mt-4"
|
|
||||||
role="alert"
|
|
||||||
v-if="showErrorMessage"
|
|
||||||
>
|
|
||||||
Bei der Suche ist ein Fehler aufgetreten
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="searchTotal == 0">
|
</div>
|
||||||
|
</div>
|
||||||
|
</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.
|
Es wurde kein Suchergebnis gefunden.
|
||||||
<p v-if="searchText !== ''">Probiere eine andere Suche.</p>
|
<p v-if="searchText !== ''">Probiere eine andere Suche.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else-if="showResults">
|
||||||
<div class="row">
|
<div class="d-flex justify-content-around">
|
||||||
<div
|
<Paginator
|
||||||
class="col-4 p-2"
|
:pages="pages"
|
||||||
v-for="result in searchResults"
|
:current="currentPage"
|
||||||
:key="result.user_id"
|
@page="handlePageSelected"
|
||||||
>
|
/>
|
||||||
<router-link
|
|
||||||
class="text-decoration-none"
|
|
||||||
:to="{ path: `/s/profile/${result.user_id}` }"
|
|
||||||
>
|
|
||||||
<div class="card search-card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">
|
|
||||||
{{ result.nickname}}
|
|
||||||
<span v-if="result.pronouns"> ({{ result.pronouns }})</span>
|
|
||||||
</h5>
|
|
||||||
<p
|
|
||||||
class="card-text"
|
|
||||||
v-if="result.skills && result.skills.length > 0"
|
|
||||||
>
|
|
||||||
Fähigkeiten:
|
|
||||||
<span v-for="(skill, index) in result.skills" :key="index"
|
|
||||||
>
|
|
||||||
{{ skill.skill.name}}<span v-if="index != result.skills.length - 1">, </span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<SearchResult
|
||||||
</router-link>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RequestMixin from "@/mixins/request.mixin";
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Paginator from '@/components/Paginator'
|
||||||
|
import SearchResult from '@/components/SearchResult'
|
||||||
|
import Spinner from '@/components/Spinner'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Search",
|
name: 'Search',
|
||||||
mixins: [RequestMixin],
|
components: {
|
||||||
|
Paginator,
|
||||||
|
SearchResult,
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showErrorMessage: false,
|
textChanged: false
|
||||||
searchText: "",
|
}
|
||||||
searchResults: null,
|
},
|
||||||
searchTotal: 0,
|
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 }})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.$route.query.query) this.searchText = this.$route.query.query;
|
if (this.$route.query.query) {
|
||||||
this.submitSearch();
|
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')
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -5,190 +5,260 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="container">
|
||||||
<h1>Profil bearbeiten</h1>
|
|
||||||
<form @submit.prevent="submitFormEdit()">
|
<form @submit.prevent="submitFormEdit()">
|
||||||
<div class="row">
|
<Section title="Grunddaten">
|
||||||
<div class="col">
|
<div class="mb-4">
|
||||||
|
<div class="form-check form-switch">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
class="form-check-input"
|
||||||
id="false"
|
type="checkbox"
|
||||||
:value="false"
|
role="switch"
|
||||||
v-model="profile.visible"
|
v-model="profile.visible"
|
||||||
class="mr-2"
|
id="visibility"
|
||||||
/>
|
>
|
||||||
<label for="false" class="m-2 fw-bold"> Nicht öffentlich</label>
|
<label
|
||||||
</div>
|
class="form-check-label"
|
||||||
<div class="col">
|
for="visibility">
|
||||||
<input
|
Profil für angemeldete Benutzer sichtbar
|
||||||
type="radio"
|
</label>
|
||||||
id="true"
|
|
||||||
:value="true"
|
|
||||||
v-model="profile.visible"
|
|
||||||
/>
|
|
||||||
<label for="true" class="m-2 fw-bold"> Öffentlich</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="visibilityHelp" class="form-text">
|
|
||||||
Erst wenn du dein Profil Öffentlich stellst, können andere Genoss:innen
|
<div class="row mb-4">
|
||||||
darauf zugreifen oder es in der Suche finden.
|
<div class="col-12 col-md-4 mb-3 mb-md-0">
|
||||||
</div>
|
<label class="form-label">Nickname</label>
|
||||||
<div class="row">
|
|
||||||
<div class="col-6 col-xs-12">
|
|
||||||
<label for="nickname" class="form-label fw-bold">Nickname:</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="nickname"
|
id="nickname"
|
||||||
|
maxlength="25"
|
||||||
v-model="profile.nickname"
|
v-model="profile.nickname"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-xs-12">
|
<div class="col-12 col-md-4 mb-3 mb-md-0">
|
||||||
<label for="pronouns" class="form-label fw-bold">Pronomen:</label>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="pronouns"
|
id="pronouns"
|
||||||
|
maxlength="25"
|
||||||
v-model="profile.pronouns"
|
v-model="profile.pronouns"
|
||||||
/>
|
/>
|
||||||
<div for="pronouns" class="form-text">
|
|
||||||
Z.B.: Er/Ihn, Sie/Ihr, Es etc..
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label class="form-label">Anschrift</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-xs-12">
|
<div class="col-12 col-md-4 mb-3 mb-md-0">
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="pzl"
|
id="postcode"
|
||||||
|
maxlength="10"
|
||||||
|
placeholder="Postleitzahl"
|
||||||
v-model="profile.address.postcode"
|
v-model="profile.address.postcode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-12 col-md-4 mb-3 mb-md-0">
|
||||||
<label for="city" class="form-label fw-bold">Stadt</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="city"
|
id="city"
|
||||||
|
maxlength="25"
|
||||||
|
placeholder="Ort"
|
||||||
v-model="profile.address.city"
|
v-model="profile.address.city"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col-12 col-md-4">
|
||||||
<label for="country" class="form-label fw-bold">Land</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="country"
|
id="country"
|
||||||
|
maxlength="25"
|
||||||
|
placeholder="Land"
|
||||||
v-model="profile.address.country"
|
v-model="profile.address.country"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-outline-success mb-4 mt-4 col-12">
|
<Section title="Meine Fähigkeiten">
|
||||||
Speichern
|
<template v-slot:card-body>
|
||||||
</button>
|
<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>
|
||||||
|
<div class="savebar bg-white border-top py-3">
|
||||||
|
<div class="container d-flex align-items-center justify-content-end">
|
||||||
<div
|
<div
|
||||||
class="alert alert-danger mb-4 mt-4"
|
class="text-danger"
|
||||||
role="alert"
|
|
||||||
v-if="showErrorMessage"
|
v-if="showErrorMessage"
|
||||||
>
|
>
|
||||||
Es ist Fehler aufgetreten
|
<i class="bi bi-bug"></i>
|
||||||
|
Beim Speichern ist ein Fehler aufgetreten.
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="alert alert-success mb-4 mt-4"
|
class="text-success"
|
||||||
role="alert"
|
|
||||||
v-if="showSuccessMessage"
|
v-if="showSuccessMessage"
|
||||||
>
|
>
|
||||||
Deine Änderungen wurden erfolgreich gespeichert
|
<i class="bi bi-check-lg"></i>
|
||||||
|
Gespeichert
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary ms-3"
|
||||||
|
@click="submitFormEdit()"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import RequestMixin from "@/mixins/request.mixin"
|
import RequestMixin from "@/mixins/request.mixin"
|
||||||
|
|
||||||
import AutoComplete from "@/components/AutoComplete";
|
import AutoComplete from "@/components/AutoComplete";
|
||||||
|
import Section from '@/components/profile/Section'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "profileEdit",
|
name: "profileEdit",
|
||||||
mixins: [RequestMixin],
|
mixins: [RequestMixin],
|
||||||
components: {
|
components: {
|
||||||
AutoComplete,
|
AutoComplete,
|
||||||
|
Section,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showErrorMessage: false,
|
showErrorMessage: false,
|
||||||
showSuccessMessage: false,
|
showSuccessMessage: false,
|
||||||
|
clearMessagesHandle: null,
|
||||||
profile: {
|
profile: {
|
||||||
visible: false,
|
visible: false,
|
||||||
nickname: "",
|
nickname: "",
|
||||||
pronouns: "",
|
pronouns: "",
|
||||||
volunteerwork: "",
|
volunteerwork: "",
|
||||||
freetext: "",
|
freetext: "",
|
||||||
availability: "",
|
availability_status: false,
|
||||||
|
availability_hours_per_week: null,
|
||||||
|
availability_text: "",
|
||||||
address: {
|
address: {
|
||||||
postcode: "",
|
postcode: "",
|
||||||
city: "",
|
city: "",
|
||||||
@ -199,10 +269,49 @@ export default {
|
|||||||
searchtopics: [],
|
searchtopics: [],
|
||||||
contacts: [],
|
contacts: [],
|
||||||
},
|
},
|
||||||
|
pronounsTooltip: {
|
||||||
|
content: 'Wie möchtest du angesprochen werden?<br>Zum Beispiel "er/ihn" oder "sie/ihre".',
|
||||||
|
html: true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
await this.initEditPage();
|
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>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.savebar {
|
||||||
|
bottom: 0;
|
||||||
|
position: sticky;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -5,60 +5,156 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="profile" class="container">
|
<div>
|
||||||
<h1>
|
<template v-if="error">
|
||||||
{{profile.nickname}}
|
<ViewError :isOwnProfile="isOwnProfile" :notFound="notFound" />
|
||||||
<span v-if="profile.pronouns">({{profile.pronouns}})</span>
|
</template>
|
||||||
</h1>
|
<template
|
||||||
<p><label class="fw-bold">Vorstellung: </label> {{profile.freetext}}</p>
|
v-else-if="profile"
|
||||||
<p><label class="fw-bold">Ehrentamtliche Arbeit: </label> {{profile.volunteerwork}}</p>
|
class="container">
|
||||||
<p><label class="fw-bold">Verfügbarkeit: </label> {{profile.availability}}</p>
|
<ProfileHeader
|
||||||
<h3>Das kann ich:</h3>
|
class="mb-4"
|
||||||
<profile-list
|
:profile="profile" />
|
||||||
:values="profile.skills"
|
|
||||||
type="skill"
|
<Section
|
||||||
></profile-list>
|
v-if="profile.skills && profile.skills.length > 0"
|
||||||
<h3>Das suche ich:</h3>
|
title="Das kann ich">
|
||||||
<profile-list
|
<div style="margin-bottom: -.5rem;">
|
||||||
:values="profile.searchtopics"
|
<Skill
|
||||||
type="skill"
|
class="me-2 mb-2"
|
||||||
></profile-list>
|
v-for="skill in profile.skills"
|
||||||
<h3>Meine Kontaktmöglichkeiten:</h3>
|
:key="skill.skill.id"
|
||||||
<profile-list
|
:profileSkill="skill"
|
||||||
:values="profile.contacts"
|
:showLevel="true" />
|
||||||
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}}<span v-if="profile.address && profile.address.postcode"> ({{profile.address.postcode}})</span>, {{profile.address.country}}
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex'
|
import { mapState, mapActions } from 'vuex'
|
||||||
|
|
||||||
import RequestMixin from '@/mixins/request.mixin'
|
import ViewError from '@/components/ViewError'
|
||||||
|
import ProfileHeader from '@/components/profile/Header'
|
||||||
import ProfileList from '@/components/ProfileList';
|
import Section from '@/components/profile/Section'
|
||||||
|
import Contact from '@/components/profile/Contact'
|
||||||
|
import Language from '@/components/profile/Language'
|
||||||
|
import Skill from '@/components/Skill'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "profileView",
|
name: 'profileView',
|
||||||
mixins: [RequestMixin],
|
|
||||||
components: {
|
components: {
|
||||||
ProfileList,
|
Contact,
|
||||||
|
Language,
|
||||||
|
ProfileHeader,
|
||||||
|
Section,
|
||||||
|
Skill,
|
||||||
|
ViewError,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions({
|
||||||
|
loadProfile: 'profile/load',
|
||||||
|
clearStore: 'profile/clear',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
profile: 'currentProfile'
|
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
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
await this.initViewPage();
|
const id = parseInt(this.$route.params.memberId, 10)
|
||||||
}
|
this.loadProfile(id)
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.clearStore()
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.line-break-text{
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
Reference in New Issue
Block a user