Merge branch 'master' into feature/defaultFunctionality

This commit is contained in:
JonaKl
2024-05-13 08:23:32 +02:00
committed by GitHub
12 changed files with 474 additions and 138 deletions

2
package-lock.json generated
View File

@@ -19,7 +19,7 @@
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
"@quasar/app-vite": "^1.8.0",
"@quasar/app-vite": "^1.8.5",
"@quasar/cli": "^2.4.0",
"@types/node": "^12.20.21",
"@typescript-eslint/eslint-plugin": "^5.10.0",

View File

@@ -1,16 +1,18 @@
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
<script setup lang="ts">
import { RouterView } from 'vue-router';
import ProtectedData from './components/ProtectedData.vue';
import IndexPage from 'pages/IndexPage.vue';
defineOptions({
name: 'App'
});
</script>
<template>
<IndexPage></IndexPage>
<router-view />
</template>
<style scoped>
<style>
header {
line-height: 1.5;
display: flex;
@@ -19,4 +21,26 @@ header {
justify-content: center;
padding-top: 7vh;
}
/* CUSTOM SCROLLBAR DESIGN START */
/* width */
::-webkit-scrollbar {
width: 5px;
}
/* Track */
::-webkit-scrollbar-track {
background: white;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #888;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* CUSTOM SCROLLBAR DESIGN ENDE */
</style>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
const props = defineProps({
ownMessage: Boolean,
timestamp: String,
message: String,
gif: Boolean,
link: Boolean,
url: String,
})
</script>
<template>
<div class="message" :class="{'sent' : props.ownMessage, 'received' : !props.ownMessage}">
<p v-if="!props.gif && !props.link" style="margin-bottom: 0;">{{props.message}}</p>
<img v-if="props.gif" :src="props.url" alt="GIF" style="width: 100%;border-radius: 10px;" />
<a v-if="props.link" :href="props.url" target="_blank">{{props.url}}</a>
<div style="text-align: right;">
<small style="margin-top: 0;">{{props.timestamp}}</small>
</div>
</div>
</template>
<style scoped>
.message {
border-radius: 10px;
width: 15rem;
padding: 7px;
margin:3px;
box-shadow: 1px 1px 5px 0 rgba(0,0,0,0.15);
}
.message p {
max-width: 100%;
overflow-wrap: break-word;
}
.sent {
background: #729EA1;
margin-left: auto;
margin-right: 0;
}
.received {
background: white;
}
</style>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
const props = defineProps({
displayName: String,
username: String,
profilePictureUrl: String,
status: String,
})
const profilePicture = "url("+props.profilePictureUrl+")";
</script>
<template>
<div class="profileCard">
<div class="profilePicture"></div>
<div style="margin-left:10px;">
<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.displayName}}</span>
@{{props.username}}
</p>
<p style="margin: 0;">{{ props.status }}</p>
</div>
</div>
</template>
<style scoped>
.profilePicture {
width: 50px;
height: 50px;
border-radius: 50%;
background-size: cover;
background-position: center;
background-image: v-bind('profilePicture');
}
.profileCard {
display: flex;
border-radius: 10px;
padding: 10px;
transition: .3s;
cursor: pointer;
}
.profileCard:hover {
transition: .3s;
background: rgba(0,0,0,0.2);
}
</style>

View File

@@ -1,47 +1,5 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>
<q-toolbar-title>
Quasar App
</q-toolbar-title>
<div>Quasar v{{ $q.version }}</div>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen"
bordered
>
<q-list>
<q-item-label
header
>
Essential Links
</q-item-label>
<EssentialLink
v-for="link in linksList"
:key="link.title"
v-bind="link"
/>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
@@ -51,55 +9,4 @@ import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue'
defineOptions({
name: 'MainLayout'
});
const linksList: EssentialLinkProps[] = [
{
title: 'Docs',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev'
},
{
title: 'Github',
caption: 'github.com/quasarframework',
icon: 'code',
link: 'https://github.com/quasarframework'
},
{
title: 'Discord Chat Channel',
caption: 'chat.quasar.dev',
icon: 'chat',
link: 'https://chat.quasar.dev'
},
{
title: 'Forum',
caption: 'forum.quasar.dev',
icon: 'record_voice_over',
link: 'https://forum.quasar.dev'
},
{
title: 'Twitter',
caption: '@quasarframework',
icon: 'rss_feed',
link: 'https://twitter.quasar.dev'
},
{
title: 'Facebook',
caption: '@QuasarFramework',
icon: 'public',
link: 'https://facebook.quasar.dev'
},
{
title: 'Quasar Awesome',
caption: 'Community Quasar projects',
icon: 'favorite',
link: 'https://awesome.quasar.dev'
}
];
const leftDrawerOpen = ref(false);
function toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
</script>

View File

@@ -26,9 +26,36 @@ export interface user{
chats: chat[];
}
export interface message{
export interface message {
id: number;
chat: chat;
sender: user;
sender:user;
content: string;
timestamp: string;
isOwn: boolean;
gif: boolean;
link: boolean;
}
export interface tenorGif {
url: string,
duration: number,
preview: string,
dims: Array<number>,
size: number,
}
export interface testType {
nanogif: tenorGif,
mediumgif: tenorGif,
}
export interface tenorResponseElement {
id: string,
title: string,
media_formats: testType,
}
export interface tenorResponse {
results: Array<tenorGif>,
}

View File

@@ -1,25 +1,280 @@
<template>
<q-page class="row items-center justify-evenly">
<q-btn v-if="!userStore.keycloak" @click="login()" label="Login" />
<q-btn @click="$router.push('/test')" label="Test" />
</q-page>
<div style="width:100%;display: flex; align-items: center; justify-content: space-between;height:100vh;position: fixed;">
<div style="background: gray; width: 25%;height:100%;">
<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'}">
<q-icon name="search"></q-icon>
</div>
<div title="Meine Chats anzeigen" @click="activeMenu = 'chats'" :class="{menuItemActive : activeMenu === 'chats', menuItemInactive : activeMenu !== 'chats'}">
<q-icon name="chat"></q-icon>
</div>
</div>
<div style="margin: 20px;" v-if="activeMenu == 'search'">
<q-input filled v-model="searchQuery" placeholder="Nach Nutzern suchen">
<template v-slot:prepend>
<q-icon name="search"></q-icon>
</template>
</q-input>
<div v-if="userStore.usersLoaded && searchQuery.length > 0" style="margin-top: 20px;">
<ProfileCardComponent v-for="user in userStore.users.filter(u => u.username.includes(searchQuery) || searchQuery == '*')" :displayName="user.firstName + ' ' + user.lastName" :username="user.username" profilePictureUrl="https://th.bing.com/th/id/OIP.ab5MRsPm1MXfNDVSMW6cjQHaHa?rs=1&pid=ImgDetMain" status="Hey there!"></ProfileCardComponent>
<div v-if="userStore.users.filter(u => u.username.includes(searchQuery) || searchQuery == '*').length == 0" style="text-align: center;">
<!--<q-icon name="error"></q-icon>
<p>Wir konnten keine Nutzer mit diesem Nutzernamen finden</p>-->
<img src="https://media.tenor.com/KOZLvzU0o4kAAAAd/no-results.gif" style="border-radius: 1rem;" alt="No search results found">
</div>
</div>
<p v-else>Gib einen Nutzername ein, um die Suche zu beginnen</p>
</div>
<div style="margin: 20px;" v-if="activeMenu == 'chats'">
<p>Hier werden dann die Chats angezeigt</p>
</div>
</div>
<div style="width: 75%;height: 100%;background: #DFD0BF;">
<div id="messageContainer" style="padding: 25px;overflow-y: auto;height: 90%;display: flex;flex-direction: column-reverse;">
<div v-for="(message, index) in messages" :key="index">
<MessageComponent
v-if="message.gif"
:url="message.content"
:timestamp="message.timestamp"
:own-message="message.isOwn"
gif
></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 class="footer">
<q-input filled v-model="message" placeholder="Nachricht eingeben" tabindex="0" @keyup.enter.prevent="sendMessage">
<template v-slot:after>
<q-btn round dense flat icon="gif_box" id="sendMessageBtn" style="font-size: 1.2rem;" @click="toggleGifMenu"></q-btn>
</template>
<template v-slot:append>
<q-btn round dense flat icon="send" id="sendMessageBtn" @click="sendMessage"></q-btn>
</template>
</q-input>
</div>
<!-- GIF SUCHE -->
<div style="background: white;padding: 10px;border-radius: 10px;width: 20rem;height: 30rem;overflow-y:auto;position: absolute;right:20px;bottom:5rem;" v-if="showGifMenu" id="gifSearchMenu">
<q-input v-model="gifSearch" @update:model-value="inputChanged" dense placeholder="Nach GIF suchen" id="gifSearchBar">
<template v-slot:append>
<q-icon name="search"></q-icon>
</template>
</q-input>
<div v-if="tenorStore.gifsLoaded" style="display: flex; flex-wrap: nowrap;margin-top: 20px;">
<div v-for="i in [0,1]">
<div style="display: flex;flex-wrap: wrap;">
<div v-for="gif in tenorStore.gifs.filter((e, index) => index % 2 == i)" style="margin: 3px;width: 100%;max-height: 100%;">
<img :src="gif.media_formats.nanogif.url" alt="Gif" style="width: 100%;border-radius: 10px;" class="gifPreview" @click="sendGif(gif.media_formats.mediumgif.url)">
</div>
</div>
</div>
</div>
<div style="text-align: center;" v-if="loadedGifsAmount < 50">
<q-btn round dense flat @click="loadMoreGifs" title="Mehr anzeigen">+</q-btn>
</div>
</div>
<!-- GIF SUCHE ENDE -->
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useUserStore } from 'stores/user-store';
<script setup lang="ts">
import { ref } from 'vue';
import MessageComponent from 'components/MessageComponent.vue';
import { useTenorStore } from 'stores/tenor-store';
import ProfileCardComponent from 'components/ProfileCardComponent.vue';
import {message} from 'src/models';
import {useUserStore} from 'stores/user-store';
defineOptions({
name: 'IndexPage'
});
export default defineComponent({
components: {
const searchQuery = ref("")
const message = ref("");
const gifSearch = ref("");
},
setup() {
const userStore = useUserStore();
const login = () => {
userStore.login();
};
return { login, userStore };
const activeMenu = ref("search");
let loadedGifsAmount = 10;
let messages : message[] = [
{content: "Halllo", timestamp: "13:38", isOwn: true, gif: false, link: false},
{content: "https://www.google.com/", timestamp: "13:20", isOwn: false, gif: false, link: false},
{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) {
return url.startsWith("https://media.tenor.com/") && url.endsWith(".gif");
}
messages.forEach((msg) => {
if(isTenorGifUrl(msg.content)) {
msg.gif = true;
} else if(isUrl(msg.content)) {
msg.link = true;
}
})
const selectedGifURL = ref("");
const showGifMenu = ref(false);
const changed = ref(true);
function inputChanged() {
changed.value = true;
}
const tenorStore = useTenorStore();
const userStore = useUserStore();
function getGifs() {
let query = gifSearch.value != "" ? gifSearch.value : "shrek";
if(changed.value) {
tenorStore.getGifsBySearchTerm(query, loadedGifsAmount);
changed.value = false;
}
}
function toggleGifMenu() {
showGifMenu.value = !showGifMenu.value;
if(showGifMenu.value) {
// Somehow focus the input field
//document.getElementById("gifSearchBar")!.focus();
}
}
function sendMessage() {
messages.unshift({content: message.value, timestamp: new Date().toTimeString().split(' ')[0].substring(0, 5), isOwn: true, gif: false, link: false});
message.value = "";
}
function sendGif(url : string) {
messages.unshift({content: url, timestamp: new Date().toTimeString().split(' ')[0].substring(0, 5), isOwn: true, gif: true, link: false});
toggleGifMenu();
}
function loadMoreGifs() {
loadedGifsAmount += 10;
console.log(loadedGifsAmount);
changed.value = true;
}
setInterval(getGifs, 500);
userStore.getAllPtpUsers();
</script>
<style scoped>
#sendMessageBtn {
transition: .3s;
}
#sendMessageBtn:hover {
transition: .3s;
background: white;
}
.gifPreview {
cursor: pointer;
transition: .3s;
}
.gifPreview:hover {
transition: .3s;
filter: brightness(0.5);
}
.footer {
width: 75%;
height:auto;
background: gray;
position: absolute;
bottom:0;
padding: 10px;
}
.navigation {
font-size: 2rem;
}
.navigation > div {
width: 50%;
text-align: center;
cursor: pointer;
transition: .3s;
}
.navigation > div:hover {
background: #555555;
transition: .3s;
}
.menuItemActive {
border-bottom: 2px solid black;
}
.menuItemInactive {
border-bottom: 2px solid transparent;
}
</style>

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
import ProtectedData from 'components/ProtectedData.vue';
</script>
<template>
<h1>Hallloooooooooo</h1>
<ProtectedData />
</template>
<style scoped>
</style>

View File

@@ -6,10 +6,6 @@ const routes: RouteRecordRaw[] = [
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/IndexPage.vue') }],
},
{
path: '/test',
component: () => import('pages/TestPage.vue')
},
// Always leave this as last one,
// but you can also remove it

26
src/stores/tenor-store.ts Normal file
View File

@@ -0,0 +1,26 @@
import { defineStore } from 'pinia';
import {api} from 'boot/axios';
import { ptpUser, responseModel, tenorResponseElement } from 'src/models';
import Keycloak from 'keycloak-js';
export const useTenorStore = defineStore('tenorStore', {
state: () => ({
gifs: [] as Array<tenorResponseElement>,
gifsLoaded: false,
}),
getters: {
},
actions: {
getGifsBySearchTerm(searchTerm : string, amount : number) {
this.loadGifsBySearchTerm(searchTerm, amount).then((response : any) => {
this.gifs = response.data.results;
this.gifsLoaded = true;
console.log(this.gifsLoaded + " / from store");
});
},
async loadGifsBySearchTerm(searchTerm : string, amount : number) : Promise<any> {
return await api.get("https://tenor.googleapis.com/v2/search?q="+searchTerm+"&key=AIzaSyBXsUPCjnmjXwcdqMLsyhrH1zm_6VYH_fY&client_key=praxistransferProjekt&limit="+amount);
}
},
});

View File

@@ -30,6 +30,8 @@ export const useUserStore = defineStore('userStore', {
state: () => ({
user: null as ptpUser | null,
userLoaded: false,
users: [] as ptpUser[],
usersLoaded: false,
keycloak: null as Keycloak | null,
id: "",
users: [] as ptpUser[],
@@ -44,10 +46,18 @@ export const useUserStore = defineStore('userStore', {
async loadPtpUsersById(id : string) : Promise<responseModel> {
return (await api.get("/users/login/"+id)).data;
},
async loadAllUsers(): Promise<ptpUser[]> {
this.users = (await api.get("/users/user")).data;
return this.users;
getAllPtpUsers() {
this.loadAllPtpUsers().then((response : ptpUser[]) => {
this.users = response;
this.usersLoaded = true;
})
},
async loadAllPtpUsers() {
return (await api.get("/users/user")).data;
},
async initKeycloak() : Promise<Keycloak>{
try {

View File

@@ -3,4 +3,12 @@
"compilerOptions": {
"baseUrl": "."
},
"exclude": [
"./dist",
"./.quasar",
"./node_modules",
"./src-capacitor",
"./src-cordova",
"./quasar.config.*.temporary.compiled*"
]
}