Merge pull request #4 from joschuatonn/feature/chatSettingsComponent
Feature/chat settings component
This commit is contained in:
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# build stage
|
||||||
|
FROM node:lts-alpine as build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# production stage
|
||||||
|
FROM nginx:stable-alpine as production-stage
|
||||||
|
COPY --from=build-stage /app/dist/spa/ /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
71
README.md
71
README.md
@@ -1,41 +1,60 @@
|
|||||||
# Praxistransferprojekt II (praxistransfer-frontend)
|
# Praxistransferprojekt II (ptpChat) - Frontend
|
||||||
|
|
||||||
Praxistransferprojekt | Zweites Semester
|
Dieses Repository beinhaltet das Frontend für das im zweiten Semester realisierte Projekt "ptpChat"
|
||||||
|
|
||||||
## Install the dependencies
|
> **_Notiz:_** Die folgende Anleitung erlaubt ein Setup des Frontends komplett ohne IDE. Durch die Verwendung einer solchen entfallen manche Schritte gegebenenfalls.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
Vor der Durchführung der anderen Schritte sollte sichergestellt werden, dass die folgenden Softwarekomponenten installiert sind. Grundsätzlich kann, je nach persönlicher Präferenz, zwischen `npm` und `yarn` ausgewählt werden:
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/)
|
||||||
|
- [Yarn](https://yarnpkg.com/) oder [npm](https://www.npmjs.com/)
|
||||||
|
|
||||||
|
(In der Entwicklung wurde ausschließlich npm verwendet, was aus diesem Grund hier empfohlen wird)
|
||||||
|
|
||||||
|
Außerdem wird `git` benötigt:
|
||||||
|
|
||||||
|
- [git Donwloads](https://git-scm.com/downloads)
|
||||||
|
|
||||||
|
## Clonen des Repositories
|
||||||
|
Zum Clonen des Repositories muss folgender Befehl in einer beliebigen Shell ausgeführt werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/joschuatonn/praxistransferFrontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Für die folgenden Schritte muss zuerst in das Projektverzeichnis navigiert werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd praxistransferFrontend
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installieren der Dependencies
|
||||||
```bash
|
```bash
|
||||||
yarn
|
yarn
|
||||||
# or
|
# oder
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Start the app in development mode (hot-code reloading, error reporting, etc.)
|
## Starten der Applikation im Development-Modus
|
||||||
|
```bash
|
||||||
|
yarn dev
|
||||||
|
# oder
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Sofern die Quasar-CLI installiert ist, kann auch folgender Befehl verwendet werden:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
quasar dev
|
quasar dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Nutzung der Applikation
|
||||||
|
|
||||||
### Lint the files
|
Das Setup des Frontends ist nun abgeschlossen. Standardmäßig ist dieses über `http://localhost:8080` erreichbar. Um die reibungslose Funktionalität der gesamten Applikation zu gewährleisten, wird von einer Änderung der Standardeinstellung abgeraten.
|
||||||
```bash
|
|
||||||
yarn lint
|
|
||||||
# or
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Um die Installation der Applikation vollständig abzuschließen, müssen außerdem die Anleitungen in folgenden Repos befolgt werden:
|
||||||
|
|
||||||
### Format the files
|
[ptpChat-Services](https://github.com/joschuatonn/praxistransferServices) --> Enthält die Installation von Keycloak und Postgres
|
||||||
```bash
|
|
||||||
yarn format
|
|
||||||
# or
|
|
||||||
npm run format
|
|
||||||
```
|
|
||||||
|
|
||||||
|
[ptpChat-Backend](https://github.com/joschuatonn/praxistransferBackend) --> Enthält das Java-SpringBoot Backend
|
||||||
|
|
||||||
### Build the app for production
|
|
||||||
```bash
|
|
||||||
quasar build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Customize the configuration
|
|
||||||
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).
|
|
||||||
|
|||||||
98
src/App.vue
98
src/App.vue
@@ -1,46 +1,78 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
import ProtectedData from './components/ProtectedData.vue';
|
import ProtectedData from './components/ProtectedData.vue';
|
||||||
import IndexPage from 'pages/IndexPage.vue';
|
import IndexPage from 'pages/IndexPage.vue';
|
||||||
defineOptions({
|
import { useUserStore } from './stores/user-store';
|
||||||
name: 'App'
|
import { useQuasar } from 'quasar';
|
||||||
});
|
defineOptions({
|
||||||
|
name: 'App'
|
||||||
|
});
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<IndexPage></IndexPage>
|
<IndexPage v-if="userStore.userLoaded"></IndexPage>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center;justify-content: center;padding-top: 45vh;flex-direction: column;width: 100vw; height: 100vh;background: white;" v-else>
|
||||||
|
<div>
|
||||||
|
<q-spinner
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="text-h6" v-if="userStore.reconnectTries < 2">ptpChat wird geladen...</div>
|
||||||
|
<div class="text-h6" v-else-if="userStore.reconnectTries < 20">Verbindung wird wiederhergestellt (Versuch {{ userStore.reconnectTries }})</div>
|
||||||
|
<div class="text-h6" v-else>Also langsam wirds peinlich</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 7vh;
|
padding-top: 7vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CUSTOM SCROLLBAR DESIGN START */
|
/* CUSTOM SCROLLBAR DESIGN START */
|
||||||
/* width */
|
/* width */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Track */
|
/* Track */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #888;
|
background: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Handle on hover */
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
/* CUSTOM SCROLLBAR DESIGN ENDE */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--defaultGridBorderRadius: 1rem;
|
||||||
|
--defaultElementBackground: rgb(197, 197, 197);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: rgb(50, 50, 50);
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Handle on hover */
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #555;
|
|
||||||
}
|
|
||||||
/* CUSTOM SCROLLBAR DESIGN ENDE */
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,48 +1,63 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {message, ptpTestUser} from 'src/models';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
chat: Object,
|
chat: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
|
lastMessage: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="profileCard" :class="selected ? 'selected' : ''">
|
<div class="profileCard" :class="{ selected: props.selected }">
|
||||||
<div class="profilePicture" :style="{'background-image' : 'url(https://i.pinimg.com/736x/f2/6d/38/f26d38b9685a48e8a43481391f75471c.jpg)' }"></div>
|
<div class="profilePicture" :style="{ backgroundImage: 'url(' + props.chat.profilepicture + ')' }"></div>
|
||||||
<div style="margin-left:10px;">
|
<div class="profileInfo">
|
||||||
<p style="margin-bottom: 0;line-height: 25px;vertical-align: middle;height: 25px;display: table-cell;">
|
<p class="displayName">{{ props.chat.displayName }}</p>
|
||||||
<span style="font-weight: bold;font-size: 1.2rem;">{{props.chat!.displayName}}</span>
|
<p class="lastMessage">{{ props.lastMessage }}</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.profileCard {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
transition: 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profileCard:hover,
|
||||||
|
.selected {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.profilePicture {
|
.profilePicture {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileCard {
|
.profileInfo {
|
||||||
display: flex;
|
margin-left: 10px;
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
transition: .3s;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileCard:hover {
|
.displayName {
|
||||||
transition: .3s;
|
font-weight: bold;
|
||||||
background: rgba(0,0,0,0.2);
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.lastMessage {
|
||||||
background: rgba(0,0,0,0.2);
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 20rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
308
src/components/ChatSettingsComponent.vue
Normal file
308
src/components/ChatSettingsComponent.vue
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog v-bind:model-value="internalShowDialog" @update:model-value="closeDialog" style="overflow-x: hidden;" full-width>
|
||||||
|
<q-card style="margin: 3rem 15rem;height: 80vh;padding: 20px;overflow: auto !important;">
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<div class="text-h4">{{ props.chat!.displayName }}</div>
|
||||||
|
<q-space />
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<div class="grid-container" v-if="chat?.members">
|
||||||
|
<q-card-section style="grid-row: span 2;">
|
||||||
|
<div class="text-h5">Mitglieder ({{ props.chat!.members.length }})</div>
|
||||||
|
<div style="height:15rem;overflow-y: auto;">
|
||||||
|
<table>
|
||||||
|
<tr v-for="user in props.chat!.members">
|
||||||
|
<td>
|
||||||
|
<ProfileCardComponent :user="user" :selected="false" :showAdminTag="isUserAdmin(user.id)"></ProfileCardComponent>
|
||||||
|
</td>
|
||||||
|
<td v-if="user.id != userStore.user!.id && currentUserIsAdmin">
|
||||||
|
<q-btn round flat icon="person_remove" style="color: darkred;" title="Nutzer entfernen" @click="removeUser(user.id)"></q-btn>
|
||||||
|
<q-btn round flat icon="arrow_downward" title="Adminrechte entziehen" @click="removeAdmin(user.id)" v-if="isUserAdmin(user.id)"></q-btn>
|
||||||
|
<q-btn round flat icon="arrow_upward" title="Nutzer zum Admin machen" @click="addAdmin(user.id)" v-if="!isUserAdmin(user.id)"></q-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<!-- Namen des Chats ändern -->
|
||||||
|
<q-card-section v-if="currentUserIsAdmin">
|
||||||
|
<div class="text-h5">Namen ändern</div>
|
||||||
|
<q-input filled v-model="props.chat!.displayName" maxlength="256" counter @update:model-value="showUnsavedChangesAlert" style="width: 30rem"/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="currentUserIsAdmin">
|
||||||
|
<div class="text-h5">Gruppenbild ändern</div>
|
||||||
|
|
||||||
|
<div style="display: flex;" v-if="chatStore.profilePicturesLoaded">
|
||||||
|
<div
|
||||||
|
v-for="(image, index) in chatStore.profilePictures"
|
||||||
|
:key="index"
|
||||||
|
:style="{'background-image' : 'url('+image.url+')' , 'outline' : image.url == chat.profilepicture ? 'dashed grey' : 'none'}"
|
||||||
|
style="color: red;"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
outline
|
||||||
|
class="profilePictureSelector"
|
||||||
|
@click="selectProfilePicture(image)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<q-spinner
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="currentUserIsAdmin">
|
||||||
|
<div class="text-h5">Mitglieder hinzufügen</div>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="Nach Nutzern suchen"
|
||||||
|
@update:model-value="updateSearchResults"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div style="height: 12rem; overflow-y: auto;">
|
||||||
|
<div v-for="(user, index) in searchResults" class="grid-container-search">
|
||||||
|
<div class="wrapper-div">
|
||||||
|
<q-btn
|
||||||
|
v-if="!props.chat!.members.find((u : ptpUser) => user.id == u.id)"
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
icon="add"
|
||||||
|
title="Hinzufügen"
|
||||||
|
style="width:3rem;height:3rem;"
|
||||||
|
class="button"
|
||||||
|
@click="handleClickEvent(user)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ProfileCardComponent :user="user" :selected="false"></ProfileCardComponent>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h5">Aktionen</div>
|
||||||
|
<q-btn class="deleteButton" @click="removeUser(userStore.user!.id)">Gruppenchat verlassen</q-btn>
|
||||||
|
<q-btn class="deleteButton" v-if="currentUserIsAdmin" @click="deleteChat">Gruppenchat löschen</q-btn>
|
||||||
|
</q-card-section>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import ProfileCardComponent from './ProfileCardComponent.vue';
|
||||||
|
import { useUserStore } from 'src/stores/user-store';
|
||||||
|
import { profilePicture, ptpUser, responseModel } from 'src/models';
|
||||||
|
import type { chat } from 'src/models';
|
||||||
|
import { useChatStore } from 'src/stores/chat-store';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { sendNotification, getFilteredUserList } from 'src/utils';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
const showingAlert = ref(false);
|
||||||
|
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const searchResults = ref([] as ptpUser[]);
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:showDialog', 'showModal']);
|
||||||
|
const props = defineProps({
|
||||||
|
showDialog: Boolean,
|
||||||
|
chatID: Number,
|
||||||
|
chat: Object,
|
||||||
|
leaveChat: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const internalShowDialog = computed({
|
||||||
|
get: () => props.showDialog,
|
||||||
|
set: (value) => emit('update:showDialog', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
emit('showModal', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectProfilePicture = (image: profilePicture) => {
|
||||||
|
chat.value!.profilepicture = image.url;
|
||||||
|
//props.chat!.profilepictureAsString = image.url;
|
||||||
|
showUnsavedChangesAlert();
|
||||||
|
}
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const chat = ref(chatStore.chats.find((chat) => chat.id == props.chat!.id));
|
||||||
|
|
||||||
|
const currentUserIsAdmin = computed(() => {
|
||||||
|
return props.chat!.adminIds.includes(userStore.user!.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateSearchResults = () => {
|
||||||
|
searchResults.value = getFilteredUserList(searchQuery.value, userStore.users, userStore.user!.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickEvent = (user : ptpUser) => {
|
||||||
|
chatStore.addUserToChat(user.id, props.chatID!).then((response : responseModel) => {
|
||||||
|
chatStore.loadChatsByUser().then(() => {
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
props.chat!.members.unshift(user);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeUser = (keycloakID : string) => {
|
||||||
|
chatStore.removeUserFromChat(keycloakID, props.chatID!).then((response : responseModel) => {
|
||||||
|
chatStore.loadChatsByUser().then(() => {
|
||||||
|
if(response.success) {
|
||||||
|
if(userStore.user!.id == keycloakID) {
|
||||||
|
sendNotification("Du hast den Chat verlassen", true);
|
||||||
|
props.leaveChat();
|
||||||
|
closeDialog();
|
||||||
|
} else {
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
}
|
||||||
|
|
||||||
|
//props.chat!.members.shift();
|
||||||
|
const index = props.chat!.members.indexOf(props.chat!.members.find((user : ptpUser) => user.id == keycloakID));
|
||||||
|
if (index > -1) {
|
||||||
|
props.chat!.members.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//chat.value = chatStore.chats.find((chat) => chat.id == props.chatID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteChat = () => {
|
||||||
|
chatStore.deleteChat(props.chatID!).then((response : responseModel) => {
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
props.leaveChat();
|
||||||
|
closeDialog();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAdmin = (keycloakID : string) => {
|
||||||
|
chatStore.addAdmin(props.chatID!, keycloakID).then((response : responseModel) => {
|
||||||
|
chatStore.loadChatsByUser().then(() => {
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
chat.value = chatStore.chats.find((chat) => chat.id == props.chatID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAdmin = (keycloakID : string) => {
|
||||||
|
chatStore.removeAdmin(props.chatID!, keycloakID).then((response : responseModel) => {
|
||||||
|
chatStore.loadChatsByUser().then(() => {
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
chat.value = chatStore.chats.find((chat) => chat.id == props.chatID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserAdmin = (keycloakID : string) => {
|
||||||
|
return chat.value!.adminIds.includes(keycloakID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveChatChanges = () => {
|
||||||
|
chatStore.updateChat(chat.value as chat).then((response : responseModel) => {
|
||||||
|
showingAlert.value = false;
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const showUnsavedChangesAlert = () => {
|
||||||
|
if(!showingAlert.value) {
|
||||||
|
$q.notify({
|
||||||
|
message: 'Du hast Änderungen vorgenommen, die noch nicht gespeichert wurden',
|
||||||
|
position: 'top',
|
||||||
|
timeout: 0,
|
||||||
|
color: "green",
|
||||||
|
icon: 'announcement',
|
||||||
|
actions: [
|
||||||
|
{ label: 'Speichern', color: 'white', handler: () => { saveChatChanges() } },
|
||||||
|
{ label: 'Verwerfen', color: 'white', handler: () => { discardChanges() } },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
showingAlert.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const discardChanges = () => {
|
||||||
|
showingAlert.value = false;
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.deleteButton {
|
||||||
|
color:darkred;
|
||||||
|
border:2px solid darkred;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container > div:nth-child(odd) {
|
||||||
|
margin-right: 50px;
|
||||||
|
min-width: 30vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePictureSelector {
|
||||||
|
transition: .3s;
|
||||||
|
background-size: cover;
|
||||||
|
background-position:center;
|
||||||
|
width: 6rem;
|
||||||
|
height:6rem;
|
||||||
|
margin: 15px;
|
||||||
|
border-radius: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profilePictureSelector:hover {
|
||||||
|
transition: .3s;
|
||||||
|
filter: brightness(0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MIGHT BE MOVED LATER */
|
||||||
|
.grid-container-search {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 70px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container-search > div:nth-child(even) {
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper-div {
|
||||||
|
height:100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
166
src/components/CreateChatComponent.vue
Normal file
166
src/components/CreateChatComponent.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog v-bind:model-value="internalShowDialog" @update:model-value="closeDialog" full-width>
|
||||||
|
<q-card style="padding: 20px;margin: 0 40rem;height: 80vh;">
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<div class="text-h4">Chat erstellen</div>
|
||||||
|
<q-space />
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup />
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<div style="display: flex;overflow-x: auto;height: 42px;">
|
||||||
|
<div v-for="user in usersForChat" style="background: #e0e0e0;border-radius: 10px;display: flex;padding: 3px;margin: 3px;">
|
||||||
|
<div
|
||||||
|
style="width: 30px; height: 30px; background-size: cover;background-position: center;border-radius: 100%;"
|
||||||
|
:style="{'background-image' : 'url('+user.profilepicture+')'}"
|
||||||
|
></div>
|
||||||
|
<div class="text-body" style="margin: 3px 10px 0 10px;white-space: nowrap;">{{ user.firstName }} {{ user.lastName }}</div>
|
||||||
|
<q-btn round flat icon="close" style="font-size: 10px;" @click="handleClickEvent(user)"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="chatName"
|
||||||
|
placeholder="Name des Chats"
|
||||||
|
maxlength="256"
|
||||||
|
counter
|
||||||
|
style="margin: 20px 0;"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="edit"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="searchQuery"
|
||||||
|
placeholder="Nach Nutzern suchen"
|
||||||
|
@update:model-value="updateSearchResults"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div style="height: 50%;">
|
||||||
|
<div v-for="(user, index) in searchResults" class="grid-container">
|
||||||
|
<div class="wrapper-div">
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:icon="isUserSelected(user) ? 'remove' : 'add'"
|
||||||
|
:title="isUserSelected(user) ? 'Entfernen' : 'Hinzufügen'"
|
||||||
|
style="width:3rem;height:3rem;"
|
||||||
|
class="button"
|
||||||
|
@click="handleClickEvent(user)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ProfileCardComponent :user="user" :selected="false"></ProfileCardComponent>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn style="width: 100%;" @click="createChat">Chat erstellen</q-btn>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import ProfileCardComponent from './ProfileCardComponent.vue';
|
||||||
|
import { useUserStore } from 'src/stores/user-store';
|
||||||
|
import type { ptpUser, responseModel } from 'src/models';
|
||||||
|
import {getFilteredUserList, sendNotification} from 'src/utils';
|
||||||
|
import { useChatStore } from 'src/stores/chat-store';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const chatName = ref("");
|
||||||
|
const usersForChat = ref([] as ptpUser[]);
|
||||||
|
|
||||||
|
const searchResults = ref([] as ptpUser[]);
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:showDialog', 'showModal']);
|
||||||
|
const props = defineProps({
|
||||||
|
showDialog: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const internalShowDialog = computed({
|
||||||
|
get: () => props.showDialog,
|
||||||
|
set: (value) => emit('update:showDialog', value)
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
emit('showModal', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSearchResults = () => {
|
||||||
|
searchResults.value = getFilteredUserList(searchQuery.value, userStore.users, userStore.user!.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserSelected = (user : ptpUser) => {
|
||||||
|
return usersForChat.value.indexOf(user) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickEvent = (user : ptpUser) => {
|
||||||
|
if(isUserSelected(user)) {
|
||||||
|
const index = usersForChat.value.indexOf(user);
|
||||||
|
usersForChat.value.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
usersForChat.value.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChat = () => {
|
||||||
|
let errorOccured = false;
|
||||||
|
|
||||||
|
if(chatName.value == "") {
|
||||||
|
sendNotification("Du hast keinen Namen festgelegt", false);
|
||||||
|
errorOccured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!errorOccured) {
|
||||||
|
setupChat(chatName.value);
|
||||||
|
closeDialog();
|
||||||
|
chatName.value = "";
|
||||||
|
usersForChat.value = [] as ptpUser[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
const setupChat = async(chatName: string)=>{
|
||||||
|
chatStore.createChat(chatName, usersForChat.value.map((user) => user.id), userStore.user!.id).then((response : responseModel) => {
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
reloadChats();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadChats = async() => {
|
||||||
|
await chatStore.loadChatsByUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 70px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-container > div:nth-child(even) {
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper-div {
|
||||||
|
height:100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<template>
|
|
||||||
<q-item
|
|
||||||
clickable
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
:href="link"
|
|
||||||
>
|
|
||||||
<q-item-section
|
|
||||||
v-if="icon"
|
|
||||||
avatar
|
|
||||||
>
|
|
||||||
<q-icon :name="icon" />
|
|
||||||
</q-item-section>
|
|
||||||
|
|
||||||
<q-item-section>
|
|
||||||
<q-item-label>{{ title }}</q-item-label>
|
|
||||||
<q-item-label caption>{{ caption }}</q-item-label>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
defineOptions({
|
|
||||||
name: 'EssentialLink'
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface EssentialLinkProps {
|
|
||||||
title: string;
|
|
||||||
caption?: string;
|
|
||||||
link?: string;
|
|
||||||
icon?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
withDefaults(defineProps<EssentialLinkProps>(), {
|
|
||||||
caption: '',
|
|
||||||
link: '#',
|
|
||||||
icon: '',
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<p>{{ title }}</p>
|
|
||||||
<ul>
|
|
||||||
<li v-for="todo in todos" :key="todo.id" @click="increment">
|
|
||||||
{{ todo.id }} - {{ todo.content }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>Count: {{ todoCount }} / {{ meta.totalCount }}</p>
|
|
||||||
<p>Active: {{ active ? 'yes' : 'no' }}</p>
|
|
||||||
<p>Clicks on todos: {{ clickCount }}</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { Todo, Meta } from './models';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
todos?: Todo[];
|
|
||||||
meta: Meta;
|
|
||||||
active: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
todos: () => []
|
|
||||||
});
|
|
||||||
|
|
||||||
const clickCount = ref(0);
|
|
||||||
function increment() {
|
|
||||||
clickCount.value += 1;
|
|
||||||
return clickCount.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const todoCount = computed(() => props.todos.length);
|
|
||||||
</script>
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
:style="{'background-image' : 'url('+props.image+')' }"
|
:style="{'background-image' : 'url('+props.image+')' }"
|
||||||
class="image"
|
class="image"
|
||||||
></div>
|
></div>
|
||||||
<h6 class="displayName">{{ props.displayName }}</h6>
|
<h6 class="displayName" :title="props.displayName">{{ props.displayName }}</h6>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
.displayName {
|
.displayName {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0.5rem 1rem;
|
margin: 0.5rem 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
|
|||||||
@@ -1,22 +1,46 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getDaysOffset } from 'src/utils';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
ownMessage: Boolean,
|
ownMessage: Boolean,
|
||||||
timestamp: String,
|
showSender: Boolean,
|
||||||
message: String,
|
nametagColor: String,
|
||||||
gif: Boolean,
|
message: {
|
||||||
link: Boolean,
|
type: Object,
|
||||||
url: String,
|
required: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
};
|
||||||
|
|
||||||
|
const date = new Date(Number(props.message.timestamp));
|
||||||
|
let timestampFormatted = date.toLocaleString('de-DE', options);
|
||||||
|
const daysOffset = getDaysOffset(date, new Date());
|
||||||
|
|
||||||
|
if(daysOffset <= 2) {
|
||||||
|
const descriptions = ['Heute', 'Gestern', 'Vorgestern'];
|
||||||
|
timestampFormatted = descriptions[daysOffset] + ', ' + date.toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="message" :class="{'sent' : props.ownMessage, 'received' : !props.ownMessage}">
|
<div class="message" :class="{'sent' : props.ownMessage, 'received' : !props.ownMessage}">
|
||||||
<p v-if="!props.gif && !props.link" style="margin-bottom: 0;">{{props.message}}</p>
|
<div v-if="props.showSender && !props.ownMessage" style="display: flex; justify-content: space-between;">
|
||||||
<img v-if="props.gif" :src="props.url" alt="GIF" style="width: 100%;border-radius: 10px;" />
|
<div class="text-body" style="font-weight: bold;" :style="{'color' : props.nametagColor }">{{ props.message!.sender.firstName }} {{ props.message!.sender.lastName }}</div>
|
||||||
<a v-if="props.link" :href="props.url" target="_blank">{{props.url}}</a>
|
<div class="text-body" style="color: #505050"><small>@{{ props.message!.sender.username }}</small></div>
|
||||||
|
</div>
|
||||||
|
<p v-if="!props.message.gif && !props.message.link" style="margin-bottom: 0;">{{props.message.content}}</p>
|
||||||
|
<img v-if="props.message.gif" :src="props.message.content" alt="GIF" style="width: 100%;border-radius: 10px;" />
|
||||||
|
<a v-if="props.message.link" :href="props.message.content" target="_blank">{{props.message.content}}</a>
|
||||||
|
|
||||||
<div style="text-align: right;">
|
<div style="text-align: right;">
|
||||||
<small style="margin-top: 0;">{{props.timestamp}}</small>
|
<small style="margin-top: 0;color: #505050;">{{timestampFormatted}}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -24,7 +48,8 @@
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.message {
|
.message {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width: 15rem;
|
max-width: 20rem;
|
||||||
|
width: auto;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
margin:3px;
|
margin:3px;
|
||||||
box-shadow: 1px 1px 5px 0 rgba(0,0,0,0.15);
|
box-shadow: 1px 1px 5px 0 rgba(0,0,0,0.15);
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
Diese Komponente erlaubt das Anzeigen eines Nutzers
|
||||||
|
Sie wird unter anderem bei der Suche verwendet
|
||||||
|
-->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {message, ptpTestUser} from 'src/models';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
user: Object,
|
user: Object,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
|
slim: Boolean,
|
||||||
|
lastMessage: String,
|
||||||
|
showAdminTag: Boolean
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="profileCard" :class="selected ? 'selected' : ''">
|
<div class="profileCard" :class="selected ? 'selected' : ''">
|
||||||
<div class="profilePicture" :style="{'background-image' : 'url('+props.user!.profilePictureUrl+')' }"></div>
|
<div class="profilePicture" :style="{'background-image' : 'url('+props.user!.profilepicture+')' }"></div>
|
||||||
<div style="margin-left:10px;">
|
<div style="margin-left:10px;">
|
||||||
<p style="margin-bottom: 0;line-height: 25px;vertical-align: middle;height: 25px;display: table-cell;">
|
<p style="margin-bottom: 0;line-height: 25px;vertical-align: middle;height: 25px;display: table-cell;">
|
||||||
<span style="font-weight: bold;font-size: 1.2rem;">{{props.user!.firstName}} {{ props.user!.lastName }}</span>
|
<span style="font-weight: bold;font-size: 1.2rem;">{{props.user!.firstName}} {{ props.user!.lastName }}</span>
|
||||||
@{{props.user!.username}}
|
<span v-if="!props.slim">@{{props.user!.username}}</span>
|
||||||
|
<span v-if="props.showAdminTag" style="color: darkred;"> [Administrator]</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0;">{{ props.user!.status }}</p>
|
<p v-if="!props.slim" style="margin: 0;">{{ props.user!.status }}</p>
|
||||||
|
<p v-else style="margin: 0;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;width:20rem;">{{ props.lastMessage }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
Diese Komponente erlaubt dem Nutzer die Änderung seiner Accountdaten
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="internalShowDialog" backdrop-filter="brightness(60%)" full-width>
|
<q-dialog v-bind:model-value="showDeleteAccountPopup">
|
||||||
|
<q-card style="padding: 20px;">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h4">Account löschen</div>
|
||||||
|
<div class="text-body">Bist du sicher, dass du deinen Account löschen möchtest? (Dieses Feature ist momentan noch nicht aktiv)</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn label="Abbrechen" class="greyButton" style="width: 48%;" @click="showDeleteAccountPopup = false" />
|
||||||
|
<q-btn label="Account löschen" class="buttonRed" style="width: 48%" @click="" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
<q-dialog v-bind:model-value="internalShowDialog" @update:model-value="closeDialog" backdrop-filter="brightness(60%)" full-width>
|
||||||
<q-card style="margin: 3rem 20rem;padding: 30px;height: 80vh;overflow-y:scroll" v-if="userStore.userLoaded">
|
<q-card style="margin: 3rem 20rem;padding: 30px;height: 80vh;overflow-y:scroll" v-if="userStore.userLoaded">
|
||||||
<!--<h1>{{ internalShowDialog }}</h1>-->
|
|
||||||
|
|
||||||
<!-- HEADLINE -->
|
<!-- HEADLINE -->
|
||||||
<q-card-section class="row items-center q-pb-none">
|
<q-card-section class="row items-center q-pb-none">
|
||||||
<h4 style="margin: 0;">Einstellungen</h4>
|
<h4 style="margin: 0;">Einstellungen</h4>
|
||||||
@@ -15,20 +28,24 @@
|
|||||||
<h5>Nutzerdaten</h5>
|
<h5>Nutzerdaten</h5>
|
||||||
<div style="display: flex; flex-wrap: nowrap">
|
<div style="display: flex; flex-wrap: nowrap">
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Vorname:</td>
|
<td>Vorname:</td>
|
||||||
<td>{{ userStore.user!.firstName }}</td>
|
<td>{{ userStore.user!.firstName }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Nachname:</td>
|
<td>Nachname:</td>
|
||||||
<td>{{ userStore.user!.lastName }}</td>
|
<td>{{ userStore.user!.lastName }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Nutzername:</td>
|
<td>Nutzername:</td>
|
||||||
<td>{{ userStore.user!.username }}</td>
|
<td>{{ userStore.user!.username }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
<tr>
|
||||||
|
<td>Sichtbarkeit:</td>
|
||||||
|
<td>{{ userStore.user!.isPrivate ? 'Privat' : 'Öffentlich' }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -37,26 +54,32 @@
|
|||||||
<h5>Profilbild</h5>
|
<h5>Profilbild</h5>
|
||||||
<p>Du kannst eins der folgenden Porfilbilder aussuchen</p>
|
<p>Du kannst eins der folgenden Porfilbilder aussuchen</p>
|
||||||
|
|
||||||
<div style="display: flex;">
|
<div style="display: flex;" v-if="userStore.profilePicturesLoaded">
|
||||||
<div
|
<div
|
||||||
v-for="(imageUrl, index) in profilePictures"
|
v-for="(image, index) in userStore.profilePictures"
|
||||||
:style="{'background-image' : 'url('+imageUrl+')' , 'outline' : imageUrl == userStore.user!.profilePictureUrl ? 'dashed grey' : 'none'}"
|
:style="{'background-image' : 'url('+image.url+')' , 'outline' : image.url == userStore.user!.profilepicture ? 'dashed grey' : 'none'}"
|
||||||
style="color: red;"
|
style="color: red;"
|
||||||
round
|
round
|
||||||
flat
|
flat
|
||||||
outline
|
outline
|
||||||
class="profilePictureSelector"
|
class="profilePictureSelector"
|
||||||
@click="selectProfilePicture(index)"
|
@click="selectProfilePicture(image)"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<q-spinner
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h5>Aktionen</h5>
|
<h5>Aktionen</h5>
|
||||||
<q-btn @click="logout()" label="Abmelden" style="color:darkred;border:2px solid darkred;border-radius: 6px;width: 100%;margin-bottom: 14px;"/>
|
<q-btn @click="logout()" label="Abmelden" style="color:darkred;border:2px solid darkred;border-radius: 6px;width: 100%;margin-bottom: 14px;"/>
|
||||||
<q-btn @click="logout()" label="Account privatisieren" style="color:darkred;border:2px solid darkred;border-radius: 6px;width: 100%;margin-bottom: 14px;"/>
|
<q-btn @click="toggleSetPrivate()" :label="userStore.user!.isPrivate ? 'Account entprivatisieren' : 'Account privatisieren'" style="color:darkred;border:2px solid darkred;border-radius: 6px;width: 100%;margin-bottom: 14px;"/>
|
||||||
<q-btn @click="" label="Account löschen" style="color:darkred;border:2px solid darkred;border-radius: 6px;width: 100%;"/>
|
<q-btn @click="deleteAccount()" label="Account löschen" style="color:darkred;border:2px solid darkred;border-radius: 6px;width: 100%;"/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
@@ -73,16 +96,17 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import {message, ptpTestUser} from 'src/models';
|
import {profilePicture, responseModel,} from 'src/models';
|
||||||
import { useUserStore } from 'src/stores/user-store';
|
import { useUserStore } from 'src/stores/user-store';
|
||||||
import { computed, onMounted, toRefs } from 'vue';
|
import { sendNotification } from 'src/utils';
|
||||||
|
import { computed } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const showingAlert = ref(false);
|
const showingAlert = ref(false);
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
const emit = defineEmits(['update:showDialog']);
|
const emit = defineEmits(['update:showDialog', 'showModal']);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
showDialog: Boolean
|
showDialog: Boolean
|
||||||
});
|
});
|
||||||
@@ -92,31 +116,38 @@
|
|||||||
set: (value) => emit('update:showDialog', value)
|
set: (value) => emit('update:showDialog', value)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Eine Liste der möglichen Bilder soll später aus dem Backend kommen
|
function selectProfilePicture(image : profilePicture) {
|
||||||
let profilePictures : string[] = [
|
|
||||||
"https://assets.change.org/photos/8/jw/ax/QMjwAxeQAfcpoxs-1600x900-noPad.jpg?1597557421",
|
|
||||||
"https://media.tenor.com/fErt1YElZ2gAAAAe/shrek-rizz-reverse.png",
|
|
||||||
"https://i.pinimg.com/736x/8b/08/d0/8b08d0127947da23310a14135b865f61.jpg",
|
|
||||||
"https://th.bing.com/th/id/R.487fe8708797950ab745a3800c31b7a4?rik=qW3zkDdZfweqmQ&pid=ImgRaw&r=0",
|
|
||||||
"https://media.discordapp.net/attachments/698162708427833355/1241378744032297041/image.png?ex=6649fb8c&is=6648aa0c&hm=2024b84dc61ec55d1923ff2e517af250d8e63cf599fcc223d014801adfe5b4e0&=&format=webp&quality=lossless&width=416&height=350"
|
|
||||||
]
|
|
||||||
|
|
||||||
function selectProfilePicture(id : number) {
|
|
||||||
showUnsavedChangesAlert();
|
showUnsavedChangesAlert();
|
||||||
userStore.user!.profilePictureUrl = profilePictures[id];
|
userStore.user!.profilepicture = image.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveProfileChanges() {
|
function saveProfileChanges() {
|
||||||
showingAlert.value = false;
|
showingAlert.value = false;
|
||||||
// Hier muss ein request ans Backend geschickt werden, welcher den gesamten User enthält
|
userStore.updateUser().then((response : responseModel) => {
|
||||||
|
sendNotification(response.message, true);
|
||||||
|
});
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSetPrivate() {
|
||||||
|
userStore.setPrivateForUser(userStore.user!.id, !userStore.user!.isPrivate).then((response : responseModel) => {
|
||||||
|
userStore.user!.isPrivate = !userStore.user!.isPrivate;
|
||||||
|
sendNotification(response.message, response.success);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
userStore.logout();
|
userStore.logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showDeleteAccountPopup = ref(false);
|
||||||
|
|
||||||
|
const deleteAccount = () => {
|
||||||
|
showDeleteAccountPopup.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
const closeDialog = () => {
|
const closeDialog = () => {
|
||||||
internalShowDialog.value = false;
|
emit('showModal', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showUnsavedChangesAlert = () => {
|
const showUnsavedChangesAlert = () => {
|
||||||
@@ -128,13 +159,19 @@
|
|||||||
color: "green",
|
color: "green",
|
||||||
icon: 'announcement',
|
icon: 'announcement',
|
||||||
actions: [
|
actions: [
|
||||||
{ label: 'Änderungen speichern', color: 'white', handler: () => { saveProfileChanges() } },
|
{ label: 'Speichern', color: 'white', handler: () => { saveProfileChanges() } },
|
||||||
|
{ label: 'Verwerfen', color: 'white', handler: () => { discardChanges() } },
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
showingAlert.value = true;
|
showingAlert.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const discardChanges = () => {
|
||||||
|
showingAlert.value = false;
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -172,4 +209,18 @@
|
|||||||
.grid-container > div:nth-child(odd) {
|
.grid-container > div:nth-child(odd) {
|
||||||
margin-right: 50px;
|
margin-right: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonRed {
|
||||||
|
color: white;
|
||||||
|
background: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonRed:hover, .greyButton:hover {
|
||||||
|
filter: brightness(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.greyButton {
|
||||||
|
color: white;
|
||||||
|
background: grey;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
export interface Todo {
|
|
||||||
id: number;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Meta {
|
|
||||||
totalCount: number;
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
|
||||||
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MainLayout'
|
name: 'MainLayout'
|
||||||
|
|||||||
54
src/main.ts
54
src/main.ts
@@ -5,25 +5,57 @@ import App from './App.vue';
|
|||||||
|
|
||||||
import Keycloak from 'keycloak-js';
|
import Keycloak from 'keycloak-js';
|
||||||
import {useUserStore} from 'stores/user-store';
|
import {useUserStore} from 'stores/user-store';
|
||||||
|
import { useChatStore } from 'stores/chat-store';
|
||||||
|
import { useNotificationStore } from './stores/notification-store';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const keycloak: Keycloak = await userStore.initKeycloak();
|
const chatStore = useChatStore();
|
||||||
|
let keycloak:Keycloak;
|
||||||
|
userStore.initKeycloak().then(r=> {
|
||||||
|
keycloak=r;
|
||||||
|
|
||||||
|
if (keycloak.subject) {
|
||||||
|
userStore.id = keycloak.subject;
|
||||||
|
userStore.getPtpUserById(keycloak.subject).then(() =>
|
||||||
|
userStore.loadPtpUsersById(keycloak.subject||"").then(user => {
|
||||||
|
console.log(JSON.stringify(user));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if(userStore.userLoaded) {
|
||||||
|
chatStore.loadProfilePictures();
|
||||||
|
userStore.loadProfilePictures();
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
userStore.getPtpUserById(keycloak.subject!);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
Notification.requestPermission().then((result) => {
|
||||||
|
useNotificationStore().saveDecision(result === "granted");
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error("hier ist was schlimmes passiert menno");
|
||||||
|
}
|
||||||
|
}).catch(e=>console.error(e));
|
||||||
|
|
||||||
|
|
||||||
|
window.onfocus = function() {
|
||||||
|
useNotificationStore().isTabActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onblur = function() {
|
||||||
|
useNotificationStore().isTabActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Anfrage ans Backend
|
|
||||||
|
|
||||||
if (keycloak.subject) {
|
|
||||||
userStore.id = keycloak.subject;
|
|
||||||
await userStore.getPtpUserById(keycloak.subject);
|
|
||||||
userStore.loadPtpUsersById(keycloak.subject).then(user => {
|
|
||||||
console.log(JSON.stringify(user));
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.error("hier ist was schlimmes passiert menno");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,10 @@ export interface ptpUser {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
username: string;
|
username: string;
|
||||||
status?: string;
|
status: string;
|
||||||
profilePictureUrl?: string;
|
profilepicture: string;
|
||||||
}
|
isPrivate: boolean;
|
||||||
|
nametagColor?: string;
|
||||||
export interface ptpTestUser {
|
|
||||||
id: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
username: string;
|
|
||||||
status?: string;
|
|
||||||
profilePictureUrl?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface responseModel {
|
export interface responseModel {
|
||||||
@@ -27,20 +20,15 @@ export interface chat{
|
|||||||
members: ptpUser[];
|
members: ptpUser[];
|
||||||
messages: message[];
|
messages: message[];
|
||||||
displayName: string;
|
displayName: string;
|
||||||
}
|
profilepicture: string;
|
||||||
|
groupChat: boolean;
|
||||||
export interface user{
|
adminIds: string[];
|
||||||
id: string;
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
username: string;
|
|
||||||
chats: chat[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface message {
|
export interface message {
|
||||||
id: number;
|
id: number;
|
||||||
chat: chat;
|
chat: chat;
|
||||||
sender:user;
|
sender:ptpUser;
|
||||||
content: string;
|
content: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
isOwn: boolean;
|
isOwn: boolean;
|
||||||
@@ -70,3 +58,8 @@ export interface tenorResponseElement {
|
|||||||
export interface tenorResponse {
|
export interface tenorResponse {
|
||||||
results: Array<tenorGif>,
|
results: Array<tenorGif>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface profilePicture {
|
||||||
|
shortName: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="width:100%;display: flex; align-items: center; justify-content: space-between;height:100vh;position: fixed;">
|
<div class="container">
|
||||||
<!-- Hier gibt es die ganzen PopUps und so -->
|
<!-- Hier gibt es die ganzen PopUps und so -->
|
||||||
|
|
||||||
<SettingsPopUp :showDialog="settingsDialog"/>
|
<SettingsPopUp :showDialog="settingsDialog" @showModal="() => settingsDialog = false"/>
|
||||||
|
|
||||||
<div style="background: gray; width: 25%;height:100%;">
|
<CreateChatComponent :showDialog="createChatDialog" @showModal="() => createChatDialog = false"></CreateChatComponent>
|
||||||
|
|
||||||
|
<div class="menu">
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;" class="navigation">
|
<div style="display: flex; align-items: center; justify-content: space-between;" class="navigation">
|
||||||
<div title="Nach Nutzern suchen" @click="activeMenu = 'search'" :class="{menuItemActive : activeMenu === 'search', menuItemInactive : activeMenu !== 'search'}">
|
<div title="Nach Nutzern suchen" @click="selectTab('search')" :class="{menuItemActive : activeMenu === 'search', menuItemInactive : activeMenu !== 'search'}" style="border-top-left-radius: 1rem;">
|
||||||
<q-icon name="search"></q-icon>
|
<q-icon name="search"></q-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div title="Meine Chats anzeigen" @click="activeMenu = 'chats'" :class="{menuItemActive : activeMenu === 'chats', menuItemInactive : activeMenu !== 'chats'}">
|
<div title="Meine Chats anzeigen" @click="selectTab('chats')" :class="{menuItemActive : activeMenu === 'chats', menuItemInactive : activeMenu !== 'chats'}">
|
||||||
<q-icon name="chat"></q-icon>
|
<q-icon name="chat"></q-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div style="margin: 20px;" v-if="activeMenu == 'search'">
|
<div style="margin: 20px;" v-if="activeMenu == 'search'">
|
||||||
<q-input filled v-model="searchQuery" placeholder="Nach Nutzern suchen">
|
<q-input filled v-model="searchQuery" placeholder="Nach Nutzern suchen">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
@@ -23,16 +24,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<div v-if="userStore.usersLoaded && searchQuery.length > 0 && searchQuery.replace(/\s/g, '').length" style="margin-top: 20px;">
|
<div v-if="userStore.usersLoaded && userStore.userLoaded && searchQuery.length > 0 && searchQuery.replace(/\s/g, '').length" style="margin-top: 20px;">
|
||||||
<ProfileCardComponent
|
<ProfileCardComponent
|
||||||
v-for="user in userStore.users.filter(u => (u.username.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()) || (u.firstName + ' ' + u.lastName).toLocaleLowerCase().includes(searchQuery.trimStart().toLocaleLowerCase()) || searchQuery == '*') && u.id != userStore.id && searchQuery.replace(/\s/g, '').length)" :displayName="user.firstName + ' ' + user.lastName" :username="user.username"
|
v-for="user in getFilteredUserList(searchQuery, userStore.users, userStore.user!.id)"
|
||||||
:profilePictureUrl="user.profilePictureUrl"
|
|
||||||
:status="user.status"
|
|
||||||
:user="user"
|
:user="user"
|
||||||
:selected="selectedUser.id == user.id"
|
:selected="selectedUser.id == user.id"
|
||||||
@click="selectUser(user as ptpTestUser)"
|
@click="selectUser(user as ptpUser)"
|
||||||
></ProfileCardComponent>
|
></ProfileCardComponent>
|
||||||
<div v-if="userStore.users.filter(u => u.username.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()) || (u.firstName + ' ' + u.lastName).toLocaleLowerCase().includes(searchQuery.trimStart().toLocaleLowerCase()) || searchQuery == '*').length == 0 || !searchQuery.replace(/\s/g, '').length" style="text-align: center;">
|
|
||||||
|
<div v-if="getFilteredUserList(searchQuery, userStore.users, userStore.user!.id).length == 0" style="text-align: center;">
|
||||||
<img src="https://media.tenor.com/KOZLvzU0o4kAAAAd/no-results.gif" style="border-radius: 1rem;" alt="No search results found">
|
<img src="https://media.tenor.com/KOZLvzU0o4kAAAAd/no-results.gif" style="border-radius: 1rem;" alt="No search results found">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,138 +41,94 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin: 20px;" v-if="activeMenu == 'chats'">
|
<div style="margin: 20px;" v-if="activeMenu == 'chats'">
|
||||||
|
<q-btn round flat icon="group_add" style="float:right;" @click="createChatDialog = true"></q-btn>
|
||||||
<p>Chats, in denen du Mitglied bist:</p>
|
<p>Chats, in denen du Mitglied bist:</p>
|
||||||
<div v-if="chatStore.chatsLoaded">
|
<div v-if="chatStore.chatsLoaded" style="padding-top: 10px;">
|
||||||
<div v-for="chat in chatStore.chats" :key="chat.id">
|
<div v-for="chat in chatStore.chats" :key="chat.id" style="margin-top: 10px;">
|
||||||
|
|
||||||
<ChatCardComponent
|
<ChatCardComponent
|
||||||
v-if="chat.members.length > 2"
|
v-if="chat.groupChat"
|
||||||
:selected="chat.id == selectedChat.id"
|
:selected="chat.id == selectedChat.id"
|
||||||
:chat="chat"
|
:chat="chat"
|
||||||
@click="selectChat(chat as chat)"
|
@click="selectChat(chat as chat)"
|
||||||
|
:lastMessage="getChatPreviewMessage(chat.id)"
|
||||||
></ChatCardComponent>
|
></ChatCardComponent>
|
||||||
|
|
||||||
|
<ProfileCardComponent
|
||||||
<div v-else>
|
|
||||||
<h5>Ist ein Chat für zwei</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--<p v-for="member in chat.members">
|
|
||||||
{{ member.id }}
|
|
||||||
</p>-->
|
|
||||||
<!----<ProfileCardComponent
|
|
||||||
v-else
|
v-else
|
||||||
:user="userStore.users.find((user) => user.id === chat.members.find((member) => member.id !== userStore.user!.id)?.id)"
|
:user="userStore.users.find((user) => user.id === chat.members.find((member) => member.id !== userStore.user!.id)?.id)"
|
||||||
:selected="selectedUser.id == chat.members[0].id"
|
:selected="chat.id == selectedChat.id"
|
||||||
@click="selectUser(chat.members[0] as ptpTestUser)"
|
@click="selectChat(chat as chat)"
|
||||||
></ProfileCardComponent>-->
|
slim
|
||||||
|
:lastMessage="getChatPreviewMessage(chat.id)"
|
||||||
|
></ProfileCardComponent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<br>
|
|
||||||
<q-input
|
|
||||||
v-model="chatName"
|
|
||||||
label="Chat Name"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
<div class="q-gutter-y-md" style="max-width: 300px">
|
|
||||||
<q-input
|
|
||||||
standout
|
|
||||||
bottom-slots
|
|
||||||
v-model="userSearch"
|
|
||||||
label="Search User"
|
|
||||||
clearable
|
|
||||||
filled
|
|
||||||
color="orange"
|
|
||||||
@update:model-value=""
|
|
||||||
>
|
|
||||||
</q-input>
|
|
||||||
</div>
|
|
||||||
<div class="q-pa-lg">
|
|
||||||
<q-option-group
|
|
||||||
v-model="usersForChat"
|
|
||||||
rules="[val => val.length > 0 || 'Please select at least one user']"
|
|
||||||
:options="userStore.users.filter(u => (u.username.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()) || (u.firstName + ' ' + u.lastName).toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()) || searchQuery == '*') && u.id != userStore.id).map((u) => {
|
|
||||||
return {label: u.firstName + ' ' + u.lastName, value: u.id}})"
|
|
||||||
type="checkbox"
|
|
||||||
color="primary"
|
|
||||||
inline
|
|
||||||
>
|
|
||||||
</q-option-group>
|
|
||||||
</div>
|
|
||||||
<q-btn @click="createChat(chatName)" label="Create Chat"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
flat
|
flat
|
||||||
v-if="userStore.userLoaded"
|
v-if="userStore.userLoaded"
|
||||||
:style="{'background-image' : 'url('+userStore.user!.profilePictureUrl+')' }"
|
:style="{'background-image' : 'url('+userStore.user!.profilepicture+')' }"
|
||||||
style="background-size: cover;background-position: center;position: fixed; left:1rem; bottom: 1rem;"
|
style="background-size: cover;background-position: center;position: fixed; left:36px; bottom: 30px;"
|
||||||
title="Profil und Einstellungen"
|
title="Profil und Einstellungen"
|
||||||
@click="settingsDialog = true"
|
@click="settingsDialog = true"
|
||||||
>
|
></q-btn>
|
||||||
</q-btn>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RECHTE SPALTE -->
|
<!-- RECHTE SPALTE -->
|
||||||
|
|
||||||
<div style="width: 75%;height: 100%;background: #DFD0BF;">
|
|
||||||
<div class="header" v-if="selectedUser.id != null || selectedChat.id != null" style="padding: 10px;">
|
<div class="header" v-if="selectedUser.id != null || selectedChat.id != null" style="padding: 10px;">
|
||||||
<HeaderInfoContainerComponent
|
<HeaderInfoContainerComponent
|
||||||
v-if="selectedUser.id != null"
|
v-if="!selectedChat.groupChat"
|
||||||
:displayName="selectedUser.firstName + ' ' + selectedUser.lastName"
|
:displayName="selectedChat.members.find((member) => member.id !== userStore.user!.id)?.firstName + ' ' + selectedChat.members.find((member) => member.id !== userStore.user!.id)?.lastName"
|
||||||
:image="selectedUser.profilePictureUrl"
|
:image="selectedChat.members.find((member) => member.id !== userStore.user!.id)?.profilepicture"
|
||||||
></HeaderInfoContainerComponent>
|
></HeaderInfoContainerComponent>
|
||||||
|
|
||||||
<div v-if="selectedChat.id != null">
|
<div v-else style="display: flex;">
|
||||||
<HeaderInfoContainerComponent
|
<HeaderInfoContainerComponent
|
||||||
:displayName="selectedChat.displayName"
|
:displayName="selectedChat.displayName"
|
||||||
image="https://i.pinimg.com/736x/f2/6d/38/f26d38b9685a48e8a43481391f75471c.jpg"
|
:image="selectedChat.profilepicture"
|
||||||
></HeaderInfoContainerComponent>
|
></HeaderInfoContainerComponent>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p style="overflow: hidden;white-space: nowrap;text-overflow: ellipsis;line-height: 14px;height:50px;margin: 0px;display: table-cell;vertical-align: middle;padding: 10px;max-width: 300px;">
|
||||||
|
<span>Du</span>
|
||||||
|
<span v-if="selectedChat.members.length > 1">, </span>
|
||||||
|
<span v-for="(user, index) in selectedChat.members.filter(u => u.id != userStore.user!.id)">@{{ user.username }}{{ index < selectedChat.members.length - 2 ? ", " : "" }} </span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-space />
|
||||||
|
<q-btn icon="more_vert" round flat @click="chatSettingsDialog = true"></q-btn>
|
||||||
|
|
||||||
|
<ChatSettingsComponent :chat="selectedChat" :chatID="selectedChat.id" :showDialog="chatSettingsDialog" @showModal="() => chatSettingsDialog = false" :leaveChat="resetSelectedChat"></ChatSettingsComponent>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else style="text-align: center; margin-top: 10rem;">
|
||||||
|
<img src="https://media1.tenor.com/m/SpXWQo0Mq7EAAAAd/welcome-michael-scott.gif" style="width:20rem;border-radius: 1rem; box-shadow: 1px 1px 5px 0 rgba(0,0,0,0.15);">
|
||||||
|
<div class="text-h6">Wähle einen Chat aus</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedUser.id != null" id="messageContainer" style="padding: 25px;overflow-y: auto;height: 90%;display: flex;flex-direction: column-reverse;">
|
<div v-if="selectedChat.id != null" class="main" style="overflow-y: auto;display: flex;flex-direction: column-reverse;">
|
||||||
<div v-if="!hasChatWith(selectedUser.id)" style="width: 100%; text-align: center;font-size: 1.2rem;">
|
<div v-if="selectedChat.id == -1" style="text-align: center;">
|
||||||
<p>Das ist der Beginn deines Chats mit <strong>{{selectedUser.firstName}} {{selectedUser.lastName}}</strong></p>
|
<div class="text-h5" style="font-weight: normal;">Das ist der Beginn deines Chats mit <b>{{ selectedChat.displayName }}</b></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
|
||||||
<p>Du hast schonmal mit dem geschrieben wie schön</p>
|
|
||||||
|
|
||||||
<div v-for="(message, index) in messages" :key="index">
|
<MessageComponent
|
||||||
<MessageComponent
|
v-for="(message, index) in (messageStore.messages[selectedChat.id])"
|
||||||
v-if="message.gif"
|
:key="index"
|
||||||
:url="message.content"
|
:message="message"
|
||||||
:timestamp="message.timestamp"
|
:nametagColor="!chatStore.userColors[message.sender.username] ? 'black' : chatStore.userColors[message.sender.username]"
|
||||||
:own-message="message.isOwn"
|
:own-message="message.sender.id == userStore.user!.id"
|
||||||
gif
|
:showSender="selectedChat.groupChat && (index == messageStore.messages[selectedChat.id].length - 1 || messageStore.messages[selectedChat.id][index+1].sender!.id != message.sender.id)"
|
||||||
></MessageComponent>
|
></MessageComponent>
|
||||||
|
|
||||||
<MessageComponent
|
|
||||||
v-if="!message.gif && message.link"
|
|
||||||
:url="message.content"
|
|
||||||
:timestamp="message.timestamp"
|
|
||||||
:own-message="message.isOwn"
|
|
||||||
link
|
|
||||||
></MessageComponent>
|
|
||||||
|
|
||||||
<MessageComponent
|
|
||||||
v-if="!message.gif && !message.link"
|
|
||||||
:message="message.content"
|
|
||||||
:timestamp="message.timestamp"
|
|
||||||
:own-message="message.isOwn"
|
|
||||||
></MessageComponent>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
|
|
||||||
<q-input filled v-model="message" placeholder="Nachricht eingeben" tabindex="0" @keyup.enter.prevent="sendMessage">
|
<q-input filled v-model="message" placeholder="Nachricht eingeben" tabindex="0" @keyup.enter.prevent="sendMessage" :disable="!selectedChat.id">
|
||||||
<template v-slot:after>
|
<template v-slot:after>
|
||||||
<q-btn round dense flat icon="gif_box" id="sendMessageBtn" style="font-size: 1.2rem;" @click="toggleGifMenu"></q-btn>
|
<q-btn round dense flat icon="gif_box" id="sendMessageBtn" style="font-size: 1.2rem;" @click="toggleGifMenu"></q-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -206,23 +162,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- GIF SUCHE ENDE -->
|
<!-- GIF SUCHE ENDE -->
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MessageComponent from 'components/MessageComponent.vue';
|
import MessageComponent from 'components/MessageComponent.vue';
|
||||||
import { useTenorStore } from 'stores/tenor-store';
|
import { useTenorStore } from 'stores/tenor-store';
|
||||||
import ProfileCardComponent from 'components/ProfileCardComponent.vue';
|
import ProfileCardComponent from 'components/ProfileCardComponent.vue';
|
||||||
import ChatCardComponent from 'components/ChatCardComponent.vue';
|
import ChatCardComponent from 'components/ChatCardComponent.vue';
|
||||||
import HeaderInfoContainerComponent from 'components/HeaderInfoContainerComponent.vue';
|
import HeaderInfoContainerComponent from 'components/HeaderInfoContainerComponent.vue';
|
||||||
import {message, ptpTestUser} from 'src/models';
|
import type {message} from 'src/models';
|
||||||
import type {chat} from 'src/models';
|
import type {chat, ptpUser} from 'src/models';
|
||||||
import {useUserStore} from 'stores/user-store';
|
import {useUserStore} from 'stores/user-store';
|
||||||
import { useChatStore } from 'stores/chat-store';
|
import { useChatStore } from 'stores/chat-store';
|
||||||
import SettingsPopUp from 'src/components/SettingsPopUp.vue';
|
import SettingsPopUp from 'src/components/SettingsPopUp.vue';
|
||||||
|
import ChatSettingsComponent from 'src/components/ChatSettingsComponent.vue';
|
||||||
|
import CreateChatComponent from 'src/components/CreateChatComponent.vue';
|
||||||
|
import { useMessageStore } from 'src/stores/message-store';
|
||||||
|
import { sendNotification, getFilteredUserList } from 'src/utils';
|
||||||
|
import { Notify } from 'quasar';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'IndexPage'
|
name: 'IndexPage'
|
||||||
@@ -235,81 +194,72 @@
|
|||||||
const searchQuery = ref("")
|
const searchQuery = ref("")
|
||||||
const message = ref("");
|
const message = ref("");
|
||||||
const gifSearch = ref("");
|
const gifSearch = ref("");
|
||||||
const chatName = ref('');
|
|
||||||
const userSearch = ref('');
|
const messageStore = useMessageStore();
|
||||||
const usersForChat = ref([] as string[]);
|
|
||||||
|
|
||||||
const settingsDialog = ref(false);
|
const settingsDialog = ref(false);
|
||||||
|
const chatSettingsDialog = ref(false);
|
||||||
|
const createChatDialog = ref(false);
|
||||||
|
|
||||||
|
const showGifMenu = ref(false);
|
||||||
|
|
||||||
|
// Speichert, ob der Suchbegriff im GIF-Menü sich geändert hat
|
||||||
|
// Wenn nicht, wird keine neue Anfrage an Tenor geschickt
|
||||||
|
const gifSearchQueryChanged = ref(true);
|
||||||
|
|
||||||
const activeMenu = ref("search");
|
const activeMenu = ref("search");
|
||||||
|
|
||||||
|
const selectedUser = ref({} as ptpUser);
|
||||||
|
const selectedChat = ref({} as chat);
|
||||||
|
|
||||||
let loadedGifsAmount = 10;
|
let loadedGifsAmount = 10;
|
||||||
|
|
||||||
let messages : message[] = [
|
const selectTab = (tab : string) => {
|
||||||
// {content: "Halllo", timestamp: "13:38", isOwn: true, gif: false, link: false},
|
activeMenu.value = tab;
|
||||||
// {content: "https://www.google.com/", timestamp: "13:20", isOwn: false, gif: false, link: false},
|
getMessages();
|
||||||
// {content: "Halllo", timestamp: "13:15", isOwn: false, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:14", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:13", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "https://media.tenor.com/mtiOW6O-k8YAAAAd/shrek-shrek-rizz.gif", timestamp: "13:11", isOwn: false, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Halllo", timestamp: "13:10", isOwn: true, gif: false, link: false},
|
|
||||||
// {content: "Hallo", timestamp: "13:10", isOwn: true, gif: false, link: false}
|
|
||||||
];
|
|
||||||
|
|
||||||
function isUrl(url : string) {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTenorGifUrl(url : string) {
|
const getChatPreviewMessage = (chatId : number) => {
|
||||||
return url.startsWith("https://media.tenor.com/") && url.endsWith(".gif");
|
const messageArray : message[] = messageStore.messages[chatId];
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.forEach((msg) => {
|
if(messageArray && messageArray.length > 0) {
|
||||||
if(isTenorGifUrl(msg.content)) {
|
const newestMessage = messageArray[0];
|
||||||
msg.gif = true;
|
|
||||||
} else if(isUrl(msg.content)) {
|
let messageContent = newestMessage.content;
|
||||||
msg.link = true;
|
let userDescriptor = newestMessage.sender.firstName;
|
||||||
|
|
||||||
|
if(newestMessage.gif) {
|
||||||
|
messageContent = "GIF";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newestMessage.sender.id == userStore.user!.id) {
|
||||||
|
userDescriptor = "Du";
|
||||||
|
}
|
||||||
|
|
||||||
|
return userDescriptor + ": " + messageContent;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return "Der Chat wurde erstellt";
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
const selectedGifURL = ref("");
|
|
||||||
const showGifMenu = ref(false);
|
|
||||||
|
|
||||||
const changed = ref(true);
|
|
||||||
|
|
||||||
function inputChanged() {
|
function inputChanged() {
|
||||||
changed.value = true;
|
gifSearchQueryChanged.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getGifs() {
|
function getGifs() {
|
||||||
let query = gifSearch.value != "" ? gifSearch.value : "shrek";
|
let query = gifSearch.value != "" ? gifSearch.value : "the office";
|
||||||
|
|
||||||
if(changed.value) {
|
if(gifSearchQueryChanged.value) {
|
||||||
tenorStore.getGifsBySearchTerm(query, loadedGifsAmount);
|
tenorStore.getGifsBySearchTerm(query, loadedGifsAmount);
|
||||||
changed.value = false;
|
gifSearchQueryChanged.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleGifMenu() {
|
function toggleGifMenu() {
|
||||||
|
if(!selectedChat.value.id) return;
|
||||||
|
|
||||||
showGifMenu.value = !showGifMenu.value;
|
showGifMenu.value = !showGifMenu.value;
|
||||||
if(showGifMenu.value) {
|
if(showGifMenu.value) {
|
||||||
// Somehow focus the input field
|
// Somehow focus the input field
|
||||||
@@ -318,74 +268,130 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
//messages.unshift({content: message.value, timestamp: new Date().toTimeString().split(' ')[0].substring(0, 5), isOwn: true, gif: false, link: false});
|
if(message.value == "") {
|
||||||
|
sendNotification("Nachricht kann nicht leer sein", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectedChat.value.id != -1) {
|
||||||
|
messageStore.sendMessage(selectedChat.value.id, userStore.user!.id, message.value);
|
||||||
|
} else {
|
||||||
|
messageStore.createChatAndSendFirstMessage(selectedUser.value.id,userStore.user!.id, message.value).then((response) => {
|
||||||
|
reloadChats().then(() => {
|
||||||
|
activeMenu.value = 'chats';
|
||||||
|
selectChat(chatStore.chats.find((chat) => chat.id == response)!);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
message.value = "";
|
message.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendGif(url : string) {
|
function sendGif(url : string) {
|
||||||
//messages.unshift({content: url, timestamp: new Date().toTimeString().split(' ')[0].substring(0, 5), isOwn: true, gif: true, link: false});
|
messageStore.sendMessage(selectedChat.value.id, userStore.user!.id, url);
|
||||||
toggleGifMenu();
|
toggleGifMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMoreGifs() {
|
function loadMoreGifs() {
|
||||||
loadedGifsAmount += 10;
|
loadedGifsAmount += 10;
|
||||||
console.log(loadedGifsAmount);
|
console.log(loadedGifsAmount);
|
||||||
changed.value = true;
|
gifSearchQueryChanged.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMessages() {
|
||||||
|
for(let chat of chatStore.chats) {
|
||||||
|
messageStore.loadMessagesByChatID(chat.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const reloadChats = async() => {
|
||||||
|
await chatStore.loadChatsByUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateChatList = () => {
|
||||||
|
chatStore.loadChatsByUser().then(() => {
|
||||||
|
// Hier wird geprüft, ob man immer noch Mitglied des momentan ausgewählten Chats ist
|
||||||
|
if(chatStore.chats.find((chat) => chat.id == selectedChat.value.id) == null && selectedChat.value.id && selectedChat.value.groupChat) {
|
||||||
|
Notify.create({
|
||||||
|
message: "Der Chat '"+selectedChat.value.displayName+"' existiert nicht mehr, oder du bist kein Teil mehr davon",
|
||||||
|
position: 'top',
|
||||||
|
color: "red",
|
||||||
|
icon: "warning",
|
||||||
|
actions: [
|
||||||
|
{ icon: 'close', color: 'white', round: true, handler: () => { /* ... */ } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
chatSettingsDialog.value = false;
|
||||||
|
|
||||||
|
|
||||||
|
selectedChat.value = {} as chat;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(getMessages, 5000);
|
||||||
|
setInterval(updateChatList, 5000);
|
||||||
setInterval(getGifs, 500);
|
setInterval(getGifs, 500);
|
||||||
|
|
||||||
userStore.getAllPtpUsers();
|
userStore.getAllPtpUsers();
|
||||||
|
|
||||||
|
|
||||||
const createChat = async(chatName: string)=>{
|
|
||||||
if(chatName == ""){
|
|
||||||
chatName = "Chat";
|
|
||||||
}
|
|
||||||
await chatStore.createChat(chatName, usersForChat.value);
|
|
||||||
loadChats();
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadChats = async() => {
|
const loadChats = async() => {
|
||||||
await chatStore.loadChatsByUser();
|
await chatStore.loadChatsByUser();
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedUser = ref({} as ptpTestUser);
|
|
||||||
const selectedChat = ref({} as chat);
|
|
||||||
|
|
||||||
function selectUser(user : ptpTestUser) {
|
|
||||||
selectedUser.value = user;
|
const resetSelectedChat = () => {
|
||||||
selectedChat.value = {} as chat;
|
selectedChat.value = {} as chat
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectUser(user : ptpUser) {
|
||||||
|
// Hier wird zuerst geprüft, ob es bereits einen Chat mit dem Nutzer gibt
|
||||||
|
// Wenn ja, wird dieser Chat ausgewählt
|
||||||
|
// Wenn nein, wird ein Dummy-Objekt erstellt
|
||||||
|
|
||||||
|
let chatExists = false;
|
||||||
|
|
||||||
|
chatStore.chats.forEach((chat) => {
|
||||||
|
if(!chat.groupChat && chat.members.find((member) => member.id == user.id)) {
|
||||||
|
selectedChat.value = chat;
|
||||||
|
getMessages();
|
||||||
|
chatExists = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!chatExists) {
|
||||||
|
selectedUser.value = user;
|
||||||
|
const exampleChat: chat = {
|
||||||
|
id: -1,
|
||||||
|
members: [user, userStore.user!],
|
||||||
|
messages: [],
|
||||||
|
displayName: user.firstName + " " + user.lastName,
|
||||||
|
profilepicture: user.profilepicture,
|
||||||
|
groupChat: false,
|
||||||
|
adminIds: []
|
||||||
|
};
|
||||||
|
selectedChat.value = exampleChat;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectChat(chat : chat) {
|
function selectChat(chat : chat) {
|
||||||
selectedChat.value = chat;
|
selectedChat.value = chat;
|
||||||
selectedUser.value = {} as ptpTestUser;
|
selectedUser.value = {} as ptpUser;
|
||||||
}
|
|
||||||
|
|
||||||
function hasChatWith(id : string) : boolean {
|
if(messageStore.messages[chat.id] == null) {
|
||||||
// Hier wird geprüft, ob es ein Chat mit einem bestimmten anderen Nutzer existiert
|
messageStore.messages[chat.id] = [] as message[];
|
||||||
const chatExists = chatStore.chats.find((chat) => chat.members.length === 2 && chat.members.find((member) => member.id === id));
|
}
|
||||||
|
|
||||||
if(chatExists) return true;
|
messageStore.loadMessagesByChatID(chat.id);
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Init methods
|
// Init methods
|
||||||
|
|
||||||
loadChats();
|
loadChats();
|
||||||
|
|
||||||
|
|
||||||
const user = ref();
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
//alert("mounted");
|
|
||||||
user.value = userStore.user;
|
|
||||||
//alert(user.value.firstName);
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -408,24 +414,6 @@
|
|||||||
filter: brightness(0.5);
|
filter: brightness(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
|
||||||
width: 75%;
|
|
||||||
height:auto;
|
|
||||||
background: gray;
|
|
||||||
position: absolute;
|
|
||||||
bottom:0;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
width: 75%;
|
|
||||||
height: auto;
|
|
||||||
background: gray;
|
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
@@ -442,6 +430,10 @@
|
|||||||
transition: .3s;
|
transition: .3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation:nth-child(1) {
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.menuItemActive {
|
.menuItemActive {
|
||||||
border-bottom: 2px solid black;
|
border-bottom: 2px solid black;
|
||||||
}
|
}
|
||||||
@@ -454,4 +446,55 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CSS GRID THINGS */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
color: #111111;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"menu header"
|
||||||
|
"menu main"
|
||||||
|
"menu footer";
|
||||||
|
height: 97vh;
|
||||||
|
background: #FFF5E1;
|
||||||
|
margin: 1.5vh 20px;
|
||||||
|
border-radius: var(--defaultGridBorderRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
grid-area: menu;
|
||||||
|
background-color: var(--defaultElementBackground);
|
||||||
|
color: black;
|
||||||
|
border-radius: 1rem 0 0 1rem;
|
||||||
|
width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
grid-area: header;
|
||||||
|
background-color: var(--defaultElementBackground);
|
||||||
|
color: black;
|
||||||
|
padding: 10px;
|
||||||
|
border-top-right-radius: 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
grid-area: main;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: var(--defaultGridBorderRadius);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
grid-area: footer;
|
||||||
|
background-color: var(--defaultElementBackground);
|
||||||
|
color: black;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom-right-radius: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,13 +1,22 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import {chat, responseModel} from 'src/models';
|
import {chat, profilePicture, ptpUser, responseModel} from 'src/models';
|
||||||
import { api } from 'src/boot/axios';
|
import { api } from 'src/boot/axios';
|
||||||
import { useUserStore } from 'stores/user-store';
|
import { useUserStore } from 'stores/user-store';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { getRandomHexColor } from 'src/utils';
|
||||||
|
|
||||||
|
interface StringKeyValue {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const useChatStore = defineStore('chatStore',{
|
export const useChatStore = defineStore('chatStore',{
|
||||||
state:() => ({
|
state:() => ({
|
||||||
chats: [] as chat[],
|
chats: [] as chat[],
|
||||||
chatsLoaded: false,
|
chatsLoaded: false,
|
||||||
chatExistsReponse: false,
|
chatExistsReponse: false,
|
||||||
|
profilePictures : [] as profilePicture[],
|
||||||
|
profilePicturesLoaded: false,
|
||||||
|
userColors: {} as StringKeyValue
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async getChatsByUser(): Promise<chat[]> {
|
async getChatsByUser(): Promise<chat[]> {
|
||||||
@@ -18,10 +27,25 @@ export const useChatStore = defineStore('chatStore',{
|
|||||||
},
|
},
|
||||||
async loadChatsByUser() : Promise<chat[]>{
|
async loadChatsByUser() : Promise<chat[]>{
|
||||||
this.chats = (await api.get("/users/chat/" + useUserStore().id)).data.response;
|
this.chats = (await api.get("/users/chat/" + useUserStore().id)).data.response;
|
||||||
|
|
||||||
|
this.chats.forEach((c) => {
|
||||||
|
c.members.forEach((member) => {
|
||||||
|
if(!this.userColors[member.username]){
|
||||||
|
this.userColors[member.username] = getRandomHexColor();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
this.chatsLoaded = true;
|
this.chatsLoaded = true;
|
||||||
return this.chats;
|
return this.chats;
|
||||||
},
|
},
|
||||||
async createChat(chatName: string, members: string[]){
|
async loadProfilePictures(){
|
||||||
|
(await api.get("/chats/profilePictures")).data.forEach((element: string) => {
|
||||||
|
this.profilePictures.push({shortName: element.split(" ")[0].replace(":", ""), url: element.split(" ")[1]});
|
||||||
|
});
|
||||||
|
this.profilePicturesLoaded = true;
|
||||||
|
},
|
||||||
|
async createChat(chatName: string, members: string[], keycloakID: string){
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
if(userStore.id) {
|
if(userStore.id) {
|
||||||
if (!members.includes(userStore.id)) {
|
if (!members.includes(userStore.id)) {
|
||||||
@@ -33,14 +57,42 @@ export const useChatStore = defineStore('chatStore',{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(members);
|
console.log(members);
|
||||||
return (await api.post("/chats/create/" + chatName, members)).data;
|
return (await api.post("/chats/create/" + chatName + "/" + keycloakID, members)).data;
|
||||||
},
|
},
|
||||||
async loadDoesChatExist(keycloakID : string, otherKeycloakID : string) : Promise<responseModel> {
|
|
||||||
return (await api.get("/chats/"+keycloakID+"/"+otherKeycloakID)).data;
|
async updateChat(chat : chat) {
|
||||||
|
|
||||||
|
const updatedChat : chat = {...chat};
|
||||||
|
|
||||||
|
console.log("UpdatedChatUrl: " + updatedChat.profilepicture);
|
||||||
|
|
||||||
|
this.profilePictures.forEach((element: profilePicture) => {
|
||||||
|
if(element.url === updatedChat.profilepicture){
|
||||||
|
updatedChat.profilepicture = element.shortName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await api.post("/chats/update", updatedChat)).data;
|
||||||
},
|
},
|
||||||
doesChatExist(keycloakID : string, otherKeycloakID : string) {
|
|
||||||
this.loadDoesChatExist(keycloakID, otherKeycloakID)
|
async addAdmin(chatID: number, keycloakID: string){
|
||||||
.then(m => this.chatExistsReponse = m.success);
|
return (await api.post("/chats/addAdmin/"+chatID+"/"+keycloakID)).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeAdmin(chatID: number, keycloakID: string){
|
||||||
|
return (await api.post("/chats/removeAdmin/"+chatID+"/"+keycloakID)).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeUserFromChat(keycloakID : string, chatID : number) {
|
||||||
|
return (await api.delete("/chats/removeUser/"+chatID+"/"+keycloakID)).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async addUserToChat(keycloakID : string, chatID : number) {
|
||||||
|
return (await api.post("/chats/addUser/"+chatID+"/"+keycloakID)).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteChat(chatID : number) {
|
||||||
|
return (await api.delete("/chats/"+chatID)).data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
export const useCounterStore = defineStore('counter', {
|
|
||||||
state: () => ({
|
|
||||||
counter: 0,
|
|
||||||
}),
|
|
||||||
getters: {
|
|
||||||
doubleCount: (state) => state.counter * 2,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
increment() {
|
|
||||||
this.counter++;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
64
src/stores/message-store.ts
Normal file
64
src/stores/message-store.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { api } from 'src/boot/axios';
|
||||||
|
import { message } from 'src/models';
|
||||||
|
import { useNotificationStore } from './notification-store';
|
||||||
|
import { useUserStore } from './user-store';
|
||||||
|
|
||||||
|
|
||||||
|
export const useMessageStore = defineStore('messageStore', {
|
||||||
|
state: () => ({
|
||||||
|
messages: [[]] as [message[]],
|
||||||
|
loadedMessagesForChats: [] as number[],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async sendMessage(chatId : number, senderId : string, content : string) {
|
||||||
|
const config = { headers: {'Content-Type': 'application/json'} };
|
||||||
|
await api.post("/messages/"+chatId+"/"+senderId, content, config).then((result) => {
|
||||||
|
this.loadMessagesByChatID(chatId);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async createChatAndSendFirstMessage(receiverId : string, senderId : string, content : string) {
|
||||||
|
const config = { headers: {'Content-Type': 'application/json'} };
|
||||||
|
return (await api.post("/messages/dm/"+senderId+"/"+receiverId, content, config).then((response) => {
|
||||||
|
this.loadMessagesByChatID(response.data.id);
|
||||||
|
|
||||||
|
return response.data.id;
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
loadMessagesByChatID(chatID : number) {
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
const initialLoad = !this.loadedMessagesForChats.includes(chatID);
|
||||||
|
|
||||||
|
this.getAllMessagesByChatID(chatID).then((responseBody) => {
|
||||||
|
responseBody.response.forEach((message : message) => {
|
||||||
|
message.content = message.content.substring(1, message.content.length-1);
|
||||||
|
|
||||||
|
if(!this.loadedMessagesForChats.includes(chatID)){
|
||||||
|
this.messages[chatID] = [];
|
||||||
|
this.loadedMessagesForChats.push(chatID);
|
||||||
|
}
|
||||||
|
if(!initialLoad){
|
||||||
|
if(message.sender.id !== userStore.user!.id) {
|
||||||
|
notificationStore.sendNotificationIfAllowed(message.sender.profilepicture, message.sender.username, message.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages[chatID].unshift(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAllMessagesByChatID(chatID : number) {
|
||||||
|
let timestamp = "0";
|
||||||
|
if(this.loadedMessagesForChats.includes(chatID)){
|
||||||
|
timestamp = this.messages[chatID][0].timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await api.get("/messages/"+chatID+"/"+timestamp)).data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
19
src/stores/notification-store.ts
Normal file
19
src/stores/notification-store.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useNotificationStore = defineStore('notificationStore',{
|
||||||
|
state:() => ({
|
||||||
|
decision: false,
|
||||||
|
isTabActive: true,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
saveDecision(decision : boolean) {
|
||||||
|
this.decision = decision;
|
||||||
|
},
|
||||||
|
|
||||||
|
sendNotificationIfAllowed(image : string, headline : string, message : string) {
|
||||||
|
if(this.decision && !this.isTabActive){
|
||||||
|
new Notification(headline, { body: message, icon: image });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from 'src/boot/axios';
|
import { api } from 'src/boot/axios';
|
||||||
import { ptpUser, responseModel } from 'src/models';
|
import { profilePicture, ptpUser, responseModel } from 'src/models';
|
||||||
import Keycloak, { KeycloakInitOptions, KeycloakLoginOptions } from 'keycloak-js';
|
import Keycloak, { KeycloakInitOptions, KeycloakLoginOptions } from 'keycloak-js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,50 +34,59 @@ export const useUserStore = defineStore('userStore', {
|
|||||||
usersLoaded: false,
|
usersLoaded: false,
|
||||||
keycloak: null as Keycloak | null,
|
keycloak: null as Keycloak | null,
|
||||||
id: "",
|
id: "",
|
||||||
|
reconnectTries: 0,
|
||||||
|
profilePictures: [] as profilePicture[],
|
||||||
|
profilePicturesLoaded: false
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async getPtpUserById(id : string) {
|
async getPtpUserById(id : string) {
|
||||||
await this.loadPtpUsersById(id).then((response : responseModel) => {
|
await this.loadPtpUsersById(id).then((response : responseModel) => {
|
||||||
this.user = response.response;
|
this.user = response.response;
|
||||||
this.userLoaded = true;
|
this.userLoaded = true;
|
||||||
|
}).catch(error => {
|
||||||
if(this.user!.profilePictureUrl == null) {
|
this.reconnectTries++;
|
||||||
this.user!.profilePictureUrl = "https://th.bing.com/th/id/R.487fe8708797950ab745a3800c31b7a4?rik=qW3zkDdZfweqmQ&pid=ImgRaw&r=0";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.user!.status == null) {
|
|
||||||
this.user!.status = "Hey there, I am using ptpChat!";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async loadPtpUsersById(id : string) : Promise<responseModel> {
|
async loadPtpUsersById(id : string) : Promise<responseModel> {
|
||||||
return (await api.get("/users/login/"+id)).data;
|
return (await api.get("/users/login/"+id)).data;
|
||||||
},
|
},
|
||||||
|
async loadProfilePictures() {
|
||||||
|
(await api.get("/users/profilePictures")).data.forEach((element: string) => {
|
||||||
|
this.profilePictures.push({ shortName: element.split(" ")[0].replace(":", ""), url: element.split(" ")[1]});
|
||||||
|
});
|
||||||
|
this.profilePicturesLoaded = true;
|
||||||
|
},
|
||||||
|
|
||||||
getAllPtpUsers() {
|
getAllPtpUsers() {
|
||||||
this.loadAllPtpUsers().then((response : ptpUser[]) => {
|
this.loadAllPtpUsers().then((response : ptpUser[]) => {
|
||||||
this.users = response;
|
this.users = response;
|
||||||
|
|
||||||
this.users.forEach((user : ptpUser) => {
|
|
||||||
if(user.profilePictureUrl == null) {
|
|
||||||
user.profilePictureUrl = "https://th.bing.com/th/id/R.487fe8708797950ab745a3800c31b7a4?rik=qW3zkDdZfweqmQ&pid=ImgRaw&r=0";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(user.status == null) {
|
|
||||||
user.status = "Hey there, I am using ptpChat!";
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.usersLoaded = true;
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.usersLoaded = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async loadAllPtpUsers() {
|
async loadAllPtpUsers() {
|
||||||
return (await api.get("/users/user")).data;
|
return (await api.get("/users")).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUser() {
|
||||||
|
const updatedUser : ptpUser = {...this.user!};
|
||||||
|
|
||||||
|
this.profilePictures.forEach((element: profilePicture) => {
|
||||||
|
if(element.url === this.user!.profilepicture){
|
||||||
|
updatedUser.profilepicture = element.shortName;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (await api.post("/users/update", updatedUser)).data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async setPrivateForUser(keycloakId : string, isPrivate : boolean) {
|
||||||
|
return (await api.post("/users/setPrivate/"+keycloakId+"/"+isPrivate)).data;
|
||||||
},
|
},
|
||||||
|
|
||||||
async initKeycloak() : Promise<Keycloak>{
|
async initKeycloak() : Promise<Keycloak>{
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await keycloak.init(initOptions).then( (auth: boolean): void => {
|
await keycloak.init(initOptions).then( (auth: boolean): void => {
|
||||||
console.log("Authenticated: " + auth);
|
console.log("Authenticated: " + auth);
|
||||||
@@ -88,13 +97,16 @@ export const useUserStore = defineStore('userStore', {
|
|||||||
this.keycloak = keycloak;
|
this.keycloak = keycloak;
|
||||||
return keycloak;
|
return keycloak;
|
||||||
},
|
},
|
||||||
|
|
||||||
async login() {
|
async login() {
|
||||||
if(!this.keycloak){
|
if(!this.keycloak){
|
||||||
await this.initKeycloak();
|
await this.initKeycloak();
|
||||||
}else await this.keycloak?.login( {prompt: 'login'} as KeycloakLoginOptions);
|
}else await this.keycloak?.login( {prompt: 'login'} as KeycloakLoginOptions);
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
window.location.href= 'http://localhost:' + keycloakPort + '/realms/' + realm + '/protocol/openid-connect/logout?post_logout_redirect_uri=http%3A%2F%2Flocalhost:' + frontendPort + '&client_id=' + clientId;
|
window.location.href= 'http://localhost:' + keycloakPort + '/realms/' + realm + '/protocol/openid-connect/logout?post_logout_redirect_uri=http%3A%2F%2Flocalhost:' + frontendPort + '&client_id=' + clientId;
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
61
src/utils.ts
Normal file
61
src/utils.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Notify } from "quasar";
|
||||||
|
import { ptpUser } from "./models";
|
||||||
|
|
||||||
|
export const sendNotification = (message : string, successful : Boolean) => {
|
||||||
|
Notify.create({
|
||||||
|
message: message,
|
||||||
|
position: 'top',
|
||||||
|
timeout: 2000,
|
||||||
|
color: successful ? "green" : "red",
|
||||||
|
icon: successful ? "check" : "warning"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isToday = (date : Date) => {
|
||||||
|
const today = new Date();
|
||||||
|
return date.getFullYear() === today.getFullYear() &&
|
||||||
|
date.getMonth() === today.getMonth() &&
|
||||||
|
date.getDate() === today.getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDaysOffset = (date1: Date, date2: Date) => {
|
||||||
|
// Berechne die Differenz in Millisekunden
|
||||||
|
const diffInMillis = date2.getTime() - date1.getTime();
|
||||||
|
|
||||||
|
// Eine Konstante für die Anzahl der Millisekunden in einem Tag
|
||||||
|
const millisInDay = 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
|
// Berechne die Differenz in vollen Tagen und runde ab
|
||||||
|
return Math.floor(diffInMillis / millisInDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRandomHexColor2 = () => {
|
||||||
|
const hexChars = '0123456789ABCDEF';
|
||||||
|
let color = '#';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
color += hexChars[Math.floor(Math.random() * 16)];
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hslToHex = (h : number, s: number, l : number) => {
|
||||||
|
l /= 100;
|
||||||
|
const a = s * Math.min(l, 1 - l) / 100;
|
||||||
|
const f = (n : number) => {
|
||||||
|
const k = (n + h / 30) % 12;
|
||||||
|
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||||
|
return Math.round(255 * color).toString(16).padStart(2, '0'); // Convert to Hex and pad with zeroes if necessary
|
||||||
|
};
|
||||||
|
return `#${f(0)}${f(8)}${f(4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRandomHexColor = () => {
|
||||||
|
const hue = Math.floor(Math.random() * 360); // Random hue (0-360)
|
||||||
|
const saturation = 100; // Full saturation
|
||||||
|
const lightness = 30; // Adjust this value to set the desired lightness (0-100)
|
||||||
|
return hslToHex(hue, saturation, lightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getFilteredUserList = (searchQuery : string, userList : ptpUser[], ownID : string) => {
|
||||||
|
return userList.filter(u => (u.username.toLocaleLowerCase().includes(searchQuery.toLocaleLowerCase()) || (u.firstName + ' ' + u.lastName).toLocaleLowerCase().includes(searchQuery.trimStart().toLocaleLowerCase()) || searchQuery == '*') && u.id != ownID && searchQuery.replace(/\s/g, '').length);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user