Initial commit

This commit is contained in:
Torsten Schulz
2024-07-17 22:24:56 +02:00
commit 3880a265eb
126 changed files with 10959 additions and 0 deletions

20
backend/app.js Normal file
View File

@@ -0,0 +1,20 @@
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import chatRouter from './routers/chatRouter.js';
import bodyParser from 'body-parser';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
app.use(bodyParser.json());
app.use('/api/chat', chatRouter);
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
});
export default app;

View File

@@ -0,0 +1,43 @@
import { getMessages as getMessagesService, findMatch, registerUser as registerUserService, addMessage, endChat } from '../services/chatService.js';
export const getMessages = (req, res) => {
const { to, from } = req.body;
const messages = getMessagesService(to, from);
res.status(200).json(messages);
};
export const findRandomChatMatch = (req, res) => {
const { genders, age, id } = req.body;
const match = findMatch(genders, age, id);
if (match) {
res.status(200).json({ status: 'matched', user: match });
} else {
res.status(200).json({ status: 'waiting' });
}
};
export const registerUser = (req, res) => {
const { gender, age } = req.body;
const userId = registerUserService(gender, age);
res.status(200).json({ id: userId });
};
export const sendMessage = (req, res) => {
const from = req.body.from;
const to = req.body.to;
const text = req.body.text;
const message = addMessage(from, to, text);
res.status(200).json(message);
};
export const removeUser = (req, res) => {
const { id } = req.body;
removeUserService(id);
res.sendStatus(200);
};
export const stopChat = (req, res) => {
const { id } = req.body;
endChat(id);
res.sendStatus(200);
}

1
backend/dist/index.html vendored Normal file
View File

@@ -0,0 +1 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>YourPart</title><script defer="defer" src="/js/chunk-vendors.e0723df7.js"></script><script defer="defer" src="/js/app.989c1f98.js"></script></head><body><div id="app"></div></body></html>

2
backend/dist/js/app.989c1f98.js vendored Normal file
View File

@@ -0,0 +1,2 @@
(()=>{"use strict";var e={855:(e,t,n)=>{var s=n(756),r=n(641),o=n(33),a=n(751);function i(e,t,n,s,i,u){return(0,r.uX)(),(0,r.CE)("div",null,[((0,r.uX)(!0),(0,r.CE)(r.FK,null,(0,r.pI)(i.messages,(e=>((0,r.uX)(),(0,r.CE)("div",{key:e.id},(0,o.v_)(e.text),1)))),128)),(0,r.bo)((0,r.Lk)("input",{"onUpdate:modelValue":t[0]||(t[0]=e=>i.newMessage=e),onKeyup:t[1]||(t[1]=(0,a.jR)(((...e)=>u.sendMessage&&u.sendMessage(...e)),["enter"]))},null,544),[[a.Jo,i.newMessage]])])}var u=n(373);const l={data(){return{socket:null,messages:[],newMessage:""}},created(){this.socket=(0,u.Ay)("http://localhost:3001"),this.socket.on("newMessage",(e=>{this.messages.push(e)})),fetch("/api/chat/messages").then((e=>e.json())).then((e=>{this.messages=e}))},methods:{sendMessage(){if(""!==this.newMessage.trim()){const e={id:Date.now(),text:this.newMessage};this.socket.emit("newMessage",e),this.newMessage=""}}}};var d=n(262);const c=(0,d.A)(l,[["render",i]]),f=c;s["default"].config.productionTip=!1,new s["default"]({render:e=>e(f)}).$mount("#app")}},t={};function n(s){var r=t[s];if(void 0!==r)return r.exports;var o=t[s]={exports:{}};return e[s](o,o.exports,n),o.exports}n.m=e,(()=>{var e=[];n.O=(t,s,r,o)=>{if(!s){var a=1/0;for(d=0;d<e.length;d++){for(var[s,r,o]=e[d],i=!0,u=0;u<s.length;u++)(!1&o||a>=o)&&Object.keys(n.O).every((e=>n.O[e](s[u])))?s.splice(u--,1):(i=!1,o<a&&(a=o));if(i){e.splice(d--,1);var l=r();void 0!==l&&(t=l)}}return t}o=o||0;for(var d=e.length;d>0&&e[d-1][2]>o;d--)e[d]=e[d-1];e[d]=[s,r,o]}})(),(()=>{n.d=(e,t)=>{for(var s in t)n.o(t,s)&&!n.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})}})(),(()=>{n.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}()})(),(()=>{n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t)})(),(()=>{n.r=e=>{"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}})(),(()=>{var e={524:0};n.O.j=t=>0===e[t];var t=(t,s)=>{var r,o,[a,i,u]=s,l=0;if(a.some((t=>0!==e[t]))){for(r in i)n.o(i,r)&&(n.m[r]=i[r]);if(u)var d=u(n)}for(t&&t(s);l<a.length;l++)o=a[l],n.o(e,o)&&e[o]&&e[o][0](),e[o]=0;return n.O(d)},s=self["webpackChunkfrontend"]=self["webpackChunkfrontend"]||[];s.forEach(t.bind(null,0)),s.push=t.bind(null,s.push.bind(s))})();var s=n.O(void 0,[504],(()=>n(855)));s=n.O(s)})();
//# sourceMappingURL=app.989c1f98.js.map

1
backend/dist/js/app.989c1f98.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1026
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
backend/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"amqplib": "^0.10.4",
"express": "^4.19.2",
"socket.io": "^4.7.5",
"uuid": "^10.0.0"
}
}

View File

@@ -0,0 +1,14 @@
import { Router } from 'express';
import { getMessages, findRandomChatMatch, registerUser, sendMessage, stopChat, removeUser } from '../controllers/chatController.js';
const router = Router();
router.get('/messages', getMessages);
router.post('/findMatch', findRandomChatMatch);
router.post('/register', registerUser);
router.post('/sendMessage', sendMessage);
router.post('/getMessages', getMessages);
router.post('/leave', stopChat);
router.post('/exit', removeUser);
export default router;

55
backend/server.js Normal file
View File

@@ -0,0 +1,55 @@
import http from 'http';
import { Server } from 'socket.io';
import amqp from 'amqplib/callback_api.js';
import app from './app.js';
import path from 'path';
import express from 'express';
const server = http.createServer(app);
const io = new Server(server);
const RABBITMQ_URL = 'amqp://localhost';
const QUEUE = 'chat_messages';
const __dirname = path.resolve();
const frontendPath = path.join(__dirname, 'path/to/your/frontend/build/folder');
app.use(express.static(frontendPath));
app.get('*', (req, res) => {
res.sendFile(path.join(frontendPath, 'index.html'));
});
amqp.connect(RABBITMQ_URL, (err, connection) => {
if (err) {
throw err;
}
connection.createChannel((err, channel) => {
if (err) {
throw err;
}
channel.assertQueue(QUEUE, { durable: false });
io.on('connection', (socket) => {
console.log('A user connected');
channel.consume(QUEUE, (msg) => {
const message = JSON.parse(msg.content.toString());
io.emit('newMessage', message);
}, { noAck: true });
socket.on('newMessage', (message) => {
channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(message)));
});
socket.on('disconnect', () => {
console.log('A user disconnected');
});
});
server.listen(3001, () => {
console.log('Server is running on port 3001');
});
});
});

View File

@@ -0,0 +1,76 @@
import { v4 as uuidv4 } from 'uuid';
let messages = [];
let searchQueue = [];
let users = [];
let currentChats = [];
export const getMessages = (toId, fromId) => {
const userChats = currentChats.filter(chat => chat.includes(toId) && chat.includes(fromId));
if (userChats.length === 0) {
fromId = '';
}
const userMessages = messages.filter(message => message.to = toId && ["system", fromId].includes(message.from));
messages = messages.filter(message => message.to === toId && ["system", fromId].includes(message.from));
return userMessages;
};
export const addMessage = (from, to, text) => {
const userChats = currentChats.filter(chat => chat.includes(from) && chat.includes(to));
if (userChats.length === 0) {
return;
}
messages.push({ from: from, to: to, text: text });
return { text: text };
};
export const findMatch = (genders, age, id) => {
const currentUsersChat = currentChats.filter(chat => chat.includes(id));
if (currentUsersChat.length > 0) {
return findUser(currentUsersChat[0][0] === id ? currentUsersChat[0][1] : currentUsersChat[0][0]);
}
let filteredSearchQueue = users.filter(user =>
searchQueue.some(sq => sq.id === user.id) && user.id !== id
&& currentChats.filter(chat => chat.includes(user.id)).length === 0
).sort(() => Math.random() - 0.5);
for (let i = 0; i < filteredSearchQueue.length; i++) {
const user = filteredSearchQueue[i];
const ageMatch = user.age >= age.min && user.age <= age.max;
const genderMatch = genders.includes(user.gender);
if (ageMatch && genderMatch) {
for (let j = searchQueue.length - 1; j >= 0; j--) {
if ([id, user.id].includes(searchQueue[j].id)) {
searchQueue.splice(j, 1);
}
}
currentChats.push([user.id, id]);
return user;
}
}
if (!searchQueue.find(user => user.id === id)) {
searchQueue.push({ id, genders, age });
}
return null;
};
const findUser = (id) => {
return users.find(user => user.id === id);
};
export const registerUser = (gender, age) => {
const id = uuidv4();
users.push({ gender, age, id });
return id;
};
export const removeUser = (id) => {
searchQueue = searchQueue.filter(user => user.id !== id);
users = users.filter(user => user.id !== id);
currentChats = currentChats.filter(pair => pair[0] === id || pair[1] === id);
messages = messages.filter(message => message.from === id || message.to === id);
};
export const endChat = (userId) => {
currentChats = currentChats.filter(chat => !chat.includes(userId));
messages.push({ to: userId, from: 'system', activity: 'otheruserleft'})
}

8008
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
frontend/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "frontend",
"version": "1.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.7.2",
"vue": "~3.4.31",
"vue-i18n": "^10.0.0-beta.2",
"vue-router": "^4.0.13",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vue/cli-service": "^5.0.8",
"sass": "^1.77.8",
"sass-loader": "^10.5.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

41
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,41 @@
<template>
<div id="app">
<AppHeader />
<AppNavigation v-if="isLoggedIn" />
<AppContent />
<AppFooter />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import AppHeader from './components/AppHeader.vue';
import AppNavigation from './components/AppNavigation.vue';
import AppContent from './components/AppContent.vue';
import AppFooter from './components/AppFooter.vue';
export default {
name: 'App',
mounted() {
document.title = 'yourPart';
},
computed: {
...mapGetters(['isLoggedIn'])
},
components: {
AppHeader,
AppNavigation,
AppContent,
AppFooter
}
};
</script>
<style>
#app {
display: flex;
flex-direction: column;
height: 100%;
}
</style>

View File

@@ -0,0 +1,49 @@
html,
body {
height: 100%;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
}
a {
text-decoration: none;
color: inherit;
}
button {
margin-left: 10px;
padding: 5px 12px;
cursor: pointer;
background: #F9A22C;
color: #000000;
border: none;
border-radius: 4px;
transition: background 0.05s;
border: 1px solid transparent;
}
button:hover {
background: #fdf1db;
color: #7E471B;
border: 1px solid #7E471B;
}
.rc-system {
font-style: italic;
}
.rc-self {
color: #ff0000;
font-weight: bold;
}
.rc-partner {
color: #0000ff;
font-weight: bold;
}

View File

@@ -0,0 +1,20 @@
<template>
<main>
<router-view></router-view>
</main>
</template>
<script>
export default {
name: 'AppContent'
};
</script>
<style scoped>
main {
padding: 20px;
background-color: #ffffff;
flex: 1;
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<footer>
<div class="logo"><img src="/images/icons/logo_color.png"></div>
<div class="window-bar">
<button v-for="dialog in openDialogs" :key="dialog.dialog.name" class="dialog-button"
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.title">
<img v-if="dialog.dialog.icon" :src="'/images/icons/' + dialog.dialog.icon" />
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.title) : dialog.dialog.title
}}</span>
</button>
</div>
<div class="static-block">
<a href="#" @click.prevent="openImprintDialog">Impressum</a>
<a href="#" @click.prevent="openDataPrivacyDialog">Datenschutzerklärung</a>
</div>
<ImprintDialog ref="imprintDialog" name="imprintDialog" />
<DataPrivacyDialog ref="dataPrivacyDialog" name="dataPrivacyDialog" />
</footer>
</template>
<script>
import { mapGetters } from 'vuex';
import ImprintDialog from '../dialogues/standard/ImprintDialog.vue';
import DataPrivacyDialog from '../dialogues/standard/DataPrivacyDialog.vue';
export default {
name: 'AppFooter',
components: {
ImprintDialog,
DataPrivacyDialog,
},
computed: {
...mapGetters('dialogs', ['openDialogs'])
},
methods: {
openImprintDialog() {
this.$refs.imprintDialog.open();
},
openDataPrivacyDialog() {
this.$refs.dataPrivacyDialog.open();
},
toggleDialogMinimize(dialogName) {
this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName);
}
}
};
</script>
<style scoped>
footer {
display: flex;
background-color: #7BBE55;
height: 38px;
width: 100%;
color: #7E471B;
}
.logo,
.window-bar,
.static-block {
text-align: center;
}
.window-bar {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding-left: 10px;
}
.dialog-button {
max-width: 12em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
background: none;
height: 1.8em;
border: 1px solid #0a4337;
box-shadow: 1px 1px 2px #484949;
}
.dialog-button>img {
height: 16px;
}
.button-text {
margin-left: 5px;
}
.logo>img {
width: 36px;
height: 36px;
}
.static-block {
line-height: 38px;
}
.static-block>a {
padding-right: 1.5em;
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<header>
<div class="logo"><img src="/images/logos/logo.png" /></div>
<div class="advertisement">Advertisement</div>
</header>
</template>
<script>
export default {
name: 'AppHeader'
};
</script>
<style scoped>
header {
display: flex;
justify-content: space-between;
padding: 10px;
background-color: #f8f9fa;
}
.logo, .title, .advertisement {
text-align: center;
}
.advertisement {
flex: 1;
}
.logo > img {
max-height: 50px;
}
</style>

Some files were not shown because too many files have changed in this diff Show More