Compare commits
1 Commits
main
...
2440c5357b
Author | SHA1 | Date | |
---|---|---|---|
2440c5357b |
@ -1,11 +1,2 @@
|
||||
.browserslistrc
|
||||
.dockerignore
|
||||
.drone.yml
|
||||
.editorconfig
|
||||
.git
|
||||
.gitignore
|
||||
.reuse
|
||||
Dockerfile
|
||||
LICENSES
|
||||
README.md
|
||||
node_modules
|
||||
|
129
.drone.yml
@ -4,118 +4,21 @@
|
||||
# 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:5.0.2-debian@sha256:7928d25ed14a1bc22758d917ebc6aecbb8bcd1a4da7aa748d7179c9011bbfb0b
|
||||
- name: lint
|
||||
image: node:20.18.1-alpine@sha256:e44837841abf6177b308a7c627c8fd7820c1ae6ed09ffa4d60d700e5fbba1b1a
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run lint -- --no-fix
|
||||
- name: audit
|
||||
image: node:20.18.1-alpine@sha256:e44837841abf6177b308a7c627c8fd7820c1ae6ed09ffa4d60d700e5fbba1b1a
|
||||
commands:
|
||||
- npm install -g better-npm-audit
|
||||
- better-npm-audit audit --production --level=moderate
|
||||
- name: docker-dry-run
|
||||
image: plugins/docker:20.18.6@sha256:59c993e3c4e6c097a0e2d274419aac0d7d8e929773f0ba1af44078e54389834f
|
||||
settings:
|
||||
registry: git.wtf-eg.de
|
||||
repo: git.wtf-eg.de/kompetenzinventar/frontend
|
||||
target: ki-frontend
|
||||
dry_run: true
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
- main
|
||||
|
||||
depends_on:
|
||||
- qa
|
||||
|
||||
steps:
|
||||
- name: docker-publish
|
||||
image: plugins/docker:20.18.6@sha256:59c993e3c4e6c097a0e2d274419aac0d7d8e929773f0ba1af44078e54389834f
|
||||
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:1.7.5@sha256:995677e073454912f26d4c0fdd2f9df2e1f5a30d6603d3f2ece667311b6babb3
|
||||
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:5.0.2-debian@sha256:7928d25ed14a1bc22758d917ebc6aecbb8bcd1a4da7aa748d7179c9011bbfb0b
|
||||
- name: lint
|
||||
image: node:20.18.1-alpine@sha256:e44837841abf6177b308a7c627c8fd7820c1ae6ed09ffa4d60d700e5fbba1b1a
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run lint -- --no-fix
|
||||
- name: docker-publish
|
||||
image: plugins/docker:20.18.6@sha256:59c993e3c4e6c097a0e2d274419aac0d7d8e929773f0ba1af44078e54389834f
|
||||
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: reuse
|
||||
image: fsfe/reuse:latest
|
||||
commands:
|
||||
- reuse lint
|
||||
- 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"
|
||||
|
@ -8,13 +8,10 @@ module.exports = {
|
||||
'eslint:recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser'
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-useless-template-attributes': 'off',
|
||||
'vue/no-reserved-component-names': 'off'
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
}
|
||||
}
|
||||
|
28
.reuse/dep5
Normal file
@ -0,0 +1,28 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
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: public/img/wtf_logo.svg src/assets/img/wtf_logo_white.svg
|
||||
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
|
21
Dockerfile
@ -2,27 +2,14 @@
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
FROM node:20.18.1-alpine@sha256:e44837841abf6177b308a7c627c8fd7820c1ae6ed09ffa4d60d700e5fbba1b1a as builder
|
||||
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:1.27-alpine@sha256:4efa432b751239898e576a2178702fb156fc483f6d456e0ad5899b3bf5c0445a 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
|
||||
FROM nginx as ki-frontend
|
||||
|
||||
COPY --from=builder /dist/ /usr/share/nginx/html/
|
||||
COPY etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /dist .
|
||||
|
9
LICENSES/MIT.txt
Normal 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.
|
10
README.md
@ -7,7 +7,6 @@ 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
|
||||
|
||||
@ -41,14 +40,6 @@ Folgende Kanäle gibt es für die Kommunikation über das Kompetenzinventar:
|
||||
npm ci
|
||||
```
|
||||
|
||||
### Pre requirements
|
||||
|
||||
* Node 20
|
||||
* Wenn du eine andere node version installiert hast, kannst du [nvm](https://github.com/nvm-sh/nvm) benutzen um schnell zwischen node version zu wechseln
|
||||
* NPM
|
||||
* (KI-backend)[https://git.wtf-eg.de/kompetenzinventar/ki-backend] muss lokal laufen
|
||||
|
||||
|
||||
### Konfigurationsdatei anpassen
|
||||
|
||||
```
|
||||
@ -56,6 +47,7 @@ cp public/config.js.dev public/config.js
|
||||
vi public/config.js
|
||||
```
|
||||
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
|
40
REUSE.toml
@ -1,40 +0,0 @@
|
||||
version = 1
|
||||
SPDX-PackageName = "ki-frontend"
|
||||
SPDX-PackageSupplier = "Scammo <kontakt@samuelbrinkmann.de>"
|
||||
SPDX-PackageDownloadLocation = "https://git.wtf-eg.de/kompetenzinventar/ki-frontend"
|
||||
|
||||
[[annotations]]
|
||||
path = ["package.json", "package-lock.json", "renovate.json"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
|
||||
SPDX-License-Identifier = "AGPL-3.0-or-later"
|
||||
|
||||
[[annotations]]
|
||||
path = [".browserslistrc", ".dockerignore", ".eslintrc.js", ".gitignore", "REUSE.toml"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
|
||||
SPDX-License-Identifier = "AGPL-3.0-or-later"
|
||||
|
||||
[[annotations]]
|
||||
path = "src/assets/img/wtf**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
|
||||
SPDX-License-Identifier = "LicenseRef-WTF"
|
||||
|
||||
[[annotations]]
|
||||
path = ["src/assets/language_level.json", "src/assets/skill_level.json"]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "WTF Kooperative eG <https://wtf-eg.de/>"
|
||||
SPDX-License-Identifier = "AGPL-3.0-or-later"
|
||||
|
||||
[[annotations]]
|
||||
path = "public/img/bootstrap-icons-1.5.0/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "Copyright (c) 2019-2020 The Bootstrap Authors"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
|
||||
[[annotations]]
|
||||
path = "public/fonts/Lato**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2010-2015, Łukasz Dziedzic (dziedzic@typoland.com)"
|
||||
SPDX-License-Identifier = "OFL-1.1-RFN"
|
@ -9,24 +9,10 @@ server {
|
||||
|
||||
#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;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
31963
package-lock.json
generated
37
package.json
@ -1,31 +1,26 @@
|
||||
{
|
||||
"name": "@wtf/ki-frontend",
|
||||
"version": "1.1.0",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.25.9",
|
||||
"@vue/cli-plugin-babel": "5.0.8",
|
||||
"@vue/cli-plugin-eslint": "5.0.8",
|
||||
"@vue/cli-plugin-router": "5.0.8",
|
||||
"@vue/cli-service": "5.0.8",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"bootstrap": "5.3.3",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"core-js": "3.39.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"sass": "1.83.1",
|
||||
"sass-loader": "16.0.4",
|
||||
"v-tooltip": "4.0.0-beta.17",
|
||||
"vue": "3.5.13",
|
||||
"vue-router": "4.5.0",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sass-embedded": "1.83.1"
|
||||
"@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",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"sass": "^1.37.5",
|
||||
"sass-loader": "^10.2.0",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0-0"
|
||||
}
|
||||
}
|
||||
|
1
public/img/bootstrap-icons-1.5.0/pencil.license
Normal file
@ -0,0 +1 @@
|
||||
SPDX-License-Identifier: MIT
|
3
public/img/bootstrap-icons-1.5.0/pencil.svg
Normal 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 |
3
public/img/bootstrap-icons-1.5.0/plus-lg.svg
Normal 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 |
1
public/img/bootstrap-icons-1.5.0/plus-lg.svg.license
Normal file
@ -0,0 +1 @@
|
||||
SPDX-License-Identifier: MIT
|
3
public/img/bootstrap-icons-1.5.0/search.svg
Normal 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 |
1
public/img/bootstrap-icons-1.5.0/search.svg.license
Normal file
@ -0,0 +1 @@
|
||||
SPDX-License-Identifier: MIT
|
4
public/img/bootstrap-icons-1.5.0/trash.svg
Normal 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 |
1
public/img/bootstrap-icons-1.5.0/trash.svg.license
Normal file
@ -0,0 +1 @@
|
||||
SPDX-License-Identifier: MIT
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:best-practices",
|
||||
"group:linters",
|
||||
"group:test",
|
||||
"npm:unpublishSafe",
|
||||
":disableDependencyDashboard",
|
||||
":maintainLockFilesWeekly",
|
||||
":pinAllExceptPeerDependencies",
|
||||
":separateMultipleMajorReleases"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"node"
|
||||
],
|
||||
"allowedVersions": "/^[1-9][02468]\\./"
|
||||
}
|
||||
]
|
||||
}
|
114
src/App.vue
@ -5,21 +5,119 @@ 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 target="_blank" 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" />
|
||||
<router-view />
|
||||
<Footer />
|
||||
</template>
|
||||
<script>
|
||||
import Footer from '@/components/Footer'
|
||||
import Navbar from '@/components/Navbar'
|
||||
import Footer from '@/views/partials/Footer.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
name: "App",
|
||||
components: {
|
||||
Footer,
|
||||
Navbar,
|
||||
}
|
||||
Footer
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showMobileNavbar: false,
|
||||
memberId: null,
|
||||
searchText: "",
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.memberId = localStorage.getItem("user_id");
|
||||
},
|
||||
updated() {
|
||||
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>
|
||||
|
@ -6,18 +6,3 @@
|
||||
|
||||
@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%;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 107 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"1": "Keine Angabe",
|
||||
"2": "Grundkenntnisse",
|
||||
"2": "Grundkentnisse",
|
||||
"3": "Gut",
|
||||
"4": "Fließend",
|
||||
"5": "Muttersprache"
|
||||
|
@ -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"
|
||||
}
|
@ -4,15 +4,5 @@
|
||||
* 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;
|
||||
$dark: #344b5d;
|
||||
|
@ -5,46 +5,21 @@ 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 bg-white">
|
||||
<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 bg-white"
|
||||
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 +27,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 +78,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 +98,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 +119,7 @@ export default {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.$emit('update-values', newValues);
|
||||
this.$emit("update-values", newValues);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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>
|
@ -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>
|
@ -1,50 +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"
|
||||
:class="{ 'bg-white': page !== current }"
|
||||
@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,72 +5,87 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li
|
||||
class="list-group-item bg-white"
|
||||
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><span v-if="value.level">({{ levelSelection[value.level] }})</span></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><span v-if="value.level">({{languagesSelection[value.level]}})</span></div>
|
||||
</div>
|
||||
<div v-else-if="type == 'contacttype'">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
maxlength="200"
|
||||
v-model="editableValues[valueKey].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 +93,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 +106,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 +126,3 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scope>
|
||||
.list-icon {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
</style>
|
||||
|
@ -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 bg-white">
|
||||
<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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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 bg-white">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: String
|
||||
}
|
||||
}
|
||||
</script>
|
@ -5,18 +5,12 @@
|
||||
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
|
||||
|
||||
|
@ -2,125 +2,90 @@
|
||||
//
|
||||
// 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 +93,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;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -3,9 +3,6 @@
|
||||
// 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 +16,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 +43,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 +52,4 @@ const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
export default router
|
@ -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')
|
||||
}
|
||||
}
|
||||
})
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
@ -5,52 +5,48 @@ 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 +62,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>
|
||||
|
@ -5,182 +5,85 @@ 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>
|
||||
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
<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>
|
||||
<li><a href="https://git.wtf-eg.de/kompetenzinventar/ki-frontend/issues/new">Problem melden</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
@ -5,249 +5,190 @@ 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 bg-white">
|
||||
<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 +199,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>
|
||||
|
@ -5,156 +5,58 @@ 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}}<span v-if="profile.address && profile.address.postcode"> ({{profile.address.postcode}})</span>, {{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>
|
||||
|