Bugs in settings fixed, profile added
This commit is contained in:
@@ -8,6 +8,7 @@ import settingsRouter from './routers/settingsRouter.js';
|
||||
import adminRouter from './routers/adminRouter.js';
|
||||
import contactRouter from './routers/contactRouter.js';
|
||||
import cors from 'cors';
|
||||
import socialnetworkRouter from './routers/socialnetworkRouter.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -31,6 +32,7 @@ app.use('/api/settings', settingsRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
|
||||
app.use('/api/contact', contactRouter);
|
||||
app.use('/api/socialnetwork', socialnetworkRouter);
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).send('404 Not Found');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AdminService from '../services/adminService.js';
|
||||
import Joi from 'joi';
|
||||
|
||||
export const getOpenInterests = async (req, res) => {
|
||||
try {
|
||||
@@ -52,3 +53,27 @@ export const getOpenContacts = async (req, res) => {
|
||||
res.status(403).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export const answerContact = async (req, res) => {
|
||||
try {
|
||||
const schema = Joi.object({
|
||||
id: Joi.number().integer().required(),
|
||||
answer: Joi.string().min(1).required()
|
||||
});
|
||||
|
||||
const { error, value } = schema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
|
||||
const { id, answer } = value;
|
||||
|
||||
await AdminService.answerContact(id, answer);
|
||||
|
||||
res.status(200).json({ status: 'ok' });
|
||||
} catch (error) {
|
||||
console.error('Error in answerContact:', error);
|
||||
res.status(error.status || 500).json({ error: error.message || 'Internal Server Error' });
|
||||
}
|
||||
};
|
||||
@@ -162,6 +162,10 @@ const menuStructure = {
|
||||
visible: ["all"],
|
||||
path: "/settings/interests"
|
||||
},
|
||||
flirt: {
|
||||
visible: ["over14"],
|
||||
path: "/settings/flirt"
|
||||
},
|
||||
sexuality: {
|
||||
visible: ["over14"],
|
||||
path: "/settings/sexuality"
|
||||
|
||||
@@ -133,3 +133,25 @@ export const removeInterest = async (req, res) => {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
export const getVisibilities = async (req, res) => {
|
||||
try {
|
||||
const visibilities = await settingsService.getVisibilities();
|
||||
res.status(200).json(visibilities);
|
||||
} catch (error) {
|
||||
console.error('Error retrieving visibilities:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
export const updateVisibility = async (req, res) => {
|
||||
const { userParamTypeId, visibilityId } = req.body;
|
||||
const hashedUserId = req.headers.userid;
|
||||
try {
|
||||
await settingsService.updateVisibility(hashedUserId, userParamTypeId, visibilityId);
|
||||
res.status(200).json({ message: 'Visibility updated successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error updating visibility:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
37
backend/controllers/socialnetworkController.js
Normal file
37
backend/controllers/socialnetworkController.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import SocialNetworkService from '../services/socialnetworkService.js';
|
||||
|
||||
class SocialNetworkController {
|
||||
constructor() {
|
||||
this.socialNetworkService = new SocialNetworkService();
|
||||
this.userSearch = this.userSearch.bind(this);
|
||||
this.profile = this.profile.bind(this);
|
||||
}
|
||||
|
||||
async userSearch(req, res) {
|
||||
try {
|
||||
const { username, ageFrom, ageTo, genders } = req.body;
|
||||
const users = await this.socialNetworkService.searchUsers({ username, ageFrom, ageTo, genders });
|
||||
res.status(200).json(users);
|
||||
} catch (error) {
|
||||
console.error('Error in userSearch:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async profile(req, res) {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const requestingUserId = req.headers.userid;
|
||||
if (!userId || !requestingUserId) {
|
||||
return res.status(400).json({ error: 'Invalid user or requesting user ID.' });
|
||||
}
|
||||
const profile = await this.socialNetworkService.getProfile(userId, requestingUserId);
|
||||
res.status(200).json(profile);
|
||||
} catch (error) {
|
||||
console.error('Error in profile:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SocialNetworkController;
|
||||
@@ -2,5 +2,19 @@
|
||||
"account_activation_subject": "Aktivierung Deines yourPart-Zugangs",
|
||||
"account_activation_html": "<p>Hallo {{username}},</p><p>Herzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:</p><p><a href='{{activationLink}}'>{{activationLink}}</a></p><p>Alternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:</p><p>{{resetToken}}</p><p>Dein yourPart-Team</p>",
|
||||
"account_activation_text": "Hallo {{username}},\n\nHerzlichen Dank für Deine Registrierung bei yourPart. Um Deinen Zugang zu erhalten, musst Du Ihn noch aktivieren. Dazu klicke bitte folgenden Link an:\n\n{{activationLink}}\n\nAlternativ kannst Du auch nachfolgenden Code eingeben, wenn Du danach gefragt wirst:\n\n{{resetToken}}\n\nDein yourPart-Team",
|
||||
"welcome": "welcome"
|
||||
"welcome": "Willkommen",
|
||||
"your_contact_request_answered_subject": "Ihre Kontaktanfrage wurde beantwortet",
|
||||
"your_contact_request_answered_text": "Hallo,\n\nIhre Kontaktanfrage wurde beantwortet:\n\n{{answer}}\n\nMit freundlichen Grüßen,\nSupport Team",
|
||||
"error": {
|
||||
"emailrequired": "E-Mail ist erforderlich.",
|
||||
"Contact not found": "Kontaktanfrage nicht gefunden.",
|
||||
"id and answer are required": "ID und Antwort sind erforderlich.",
|
||||
"Validation error": "Validierungsfehler: {{message}}"
|
||||
},
|
||||
"admin": {
|
||||
"editcontactrequest": {
|
||||
"title": "Kontaktanfrage bearbeiten"
|
||||
}
|
||||
},
|
||||
"error.title": "Fehler"
|
||||
}
|
||||
@@ -2,5 +2,7 @@
|
||||
"account_activation_subject": "account_activation_subject",
|
||||
"account_activation_text": "account_activation_text",
|
||||
"welcome": "welcome",
|
||||
"account_activation_html": "account_activation_html"
|
||||
"account_activation_html": "account_activation_html",
|
||||
"your_contact_request_answered_subject": "your_contact_request_answered_subject",
|
||||
"your_contact_request_answered_text": "your_contact_request_answered_text"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn({
|
||||
tableName: 'contact_message',
|
||||
schema: 'service'
|
||||
}, 'answer', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true
|
||||
});
|
||||
await queryInterface.addColumn({
|
||||
tableName: 'contact_message',
|
||||
schema: 'service'
|
||||
}, 'answered_at', {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true
|
||||
});
|
||||
await queryInterface.addColumn({
|
||||
tableName: 'contact_message',
|
||||
schema: 'service'
|
||||
}, 'is_answered', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn({
|
||||
tableName: 'contact_message',
|
||||
schema: 'service'
|
||||
}, 'answer');
|
||||
await queryInterface.removeColumn({
|
||||
tableName: 'contact_message',
|
||||
schema: 'service'
|
||||
}, 'answered_at');
|
||||
await queryInterface.removeColumn({
|
||||
tableName: 'contact_message',
|
||||
schema: 'service'
|
||||
}, 'is_answered');
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,8 @@ import UserParamValue from './type/user_param_value.js';
|
||||
import InterestType from './type/interest.js';
|
||||
import InterestTranslationType from './type/interest_translation.js';
|
||||
import Interest from './community/interest.js';
|
||||
import UserParamVisibilityType from './type/user_param_visibility.js';
|
||||
import UserParamVisibility from './community/user_param_visibility.js';
|
||||
|
||||
export default function setupAssociations() {
|
||||
SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' });
|
||||
@@ -16,11 +18,12 @@ export default function setupAssociations() {
|
||||
UserParamType.hasMany(UserParam, { foreignKey: 'paramTypeId', as: 'user_params' });
|
||||
UserParam.belongsTo(UserParamType, { foreignKey: 'paramTypeId', as: 'paramType' });
|
||||
|
||||
User.hasMany(UserParam, { foreignKey: 'userId', as: 'user_params' });
|
||||
UserParam.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
|
||||
UserRight.belongsTo(User, { foreignKey: 'userId' });
|
||||
UserRight.belongsTo(User, { foreignKey: 'userId', as: 'user_with_rights' });
|
||||
UserRight.belongsTo(UserRightType, { foreignKey: 'rightTypeId', as: 'rightType' });
|
||||
UserRightType.hasMany(UserRight, { foreignKey: 'rightTypeId', as: 'rightType' });
|
||||
UserRightType.hasMany(UserRight, { foreignKey: 'rightTypeId', as: 'user_rights' });
|
||||
|
||||
UserParamType.hasMany(UserParamValue, { foreignKey: 'userParamTypeId', as: 'user_param_values' });
|
||||
UserParamValue.belongsTo(UserParamType, { foreignKey: 'userParamTypeId', as: 'user_param_type' });
|
||||
@@ -28,15 +31,16 @@ export default function setupAssociations() {
|
||||
InterestType.hasMany(InterestTranslationType, { foreignKey: 'interestsId', as: 'interest_translations' });
|
||||
InterestTranslationType.belongsTo(InterestType, { foreignKey: 'interestsId', as: 'interest_translations' });
|
||||
|
||||
InterestType.hasMany(Interest, { foreignKey: 'userinterestId', as: 'user_interest_type'} );
|
||||
User.hasMany(Interest, { foreignKey: 'userId', as: 'user_interest' });
|
||||
Interest.belongsTo(InterestType, { foreignKey: 'userinterestId', as: 'user_interest_type' });
|
||||
Interest.belongsTo(User, { foreignKey: 'userId', as: 'user_interest' });
|
||||
InterestType.hasMany(Interest, { foreignKey: 'userinterestId', as: 'user_interest_type' });
|
||||
User.hasMany(Interest, { foreignKey: 'userId', as: 'user_interests' });
|
||||
Interest.belongsTo(InterestType, { foreignKey: 'userinterestId', as: 'interest_type' });
|
||||
Interest.belongsTo(User, { foreignKey: 'userId', as: 'interest_owner' });
|
||||
|
||||
InterestTranslationType.belongsTo(UserParamValue, {
|
||||
foreignKey: 'language',
|
||||
targetKey: 'id',
|
||||
as: 'user_param_value'
|
||||
});
|
||||
InterestTranslationType.belongsTo(UserParamValue, { foreignKey: 'language', targetKey: 'id', as: 'user_param_value' });
|
||||
|
||||
UserParam.hasMany(UserParamVisibility, { foreignKey: 'param_id', as: 'param_visibilities' });
|
||||
UserParamVisibility.belongsTo(UserParam, { foreignKey: 'param_id', as: 'param' });
|
||||
|
||||
UserParamVisibility.belongsTo(UserParamVisibilityType, { foreignKey: 'visibility', as: 'visibility_type' });
|
||||
UserParamVisibilityType.hasMany(UserParamVisibility, { foreignKey: 'visibility', as: 'user_param_visibilities' });
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import crypto from 'crypto';
|
||||
|
||||
const User = sequelize.define('user', {
|
||||
email: {
|
||||
type: DataTypes.BLOB, // Verwende BLOB, um die E-Mail als bytea zu speichern
|
||||
type: DataTypes.BLOB,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
set(value) {
|
||||
|
||||
@@ -61,22 +61,4 @@ const UserParam = sequelize.define('user_param', {
|
||||
]
|
||||
});
|
||||
|
||||
UserParam.upsertParam = async function (userId, paramTypeId, value) {
|
||||
try {
|
||||
const val = value !== null && value !== undefined ? value.toString() : '';
|
||||
const [userParam, created] = await UserParam.findOrCreate({
|
||||
where: { userId, paramTypeId },
|
||||
defaults: { value: val }
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
userParam.value = value !== null && value !== undefined ? value.toString() : '';
|
||||
await userParam.save();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in upsertParam:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default UserParam;
|
||||
|
||||
26
backend/models/community/user_param_visibility.js
Normal file
26
backend/models/community/user_param_visibility.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const UserParamVisibility = sequelize.define('user_param_visibility', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
param_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
visibility: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'user_param_visibility',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
schema: 'community'
|
||||
});
|
||||
|
||||
export default UserParamVisibility;
|
||||
@@ -10,6 +10,8 @@ import InterestType from './type/interest.js';
|
||||
import InterestTranslationType from './type/interest_translation.js';
|
||||
import Interest from './community/interest.js';
|
||||
import ContactMessage from './service/contactmessage.js';
|
||||
import UserParamVisibilityType from './type/user_param_visibility.js';
|
||||
import UserParamVisibility from './community/user_param_visibility.js';
|
||||
|
||||
const models = {
|
||||
SettingsType,
|
||||
@@ -24,6 +26,8 @@ const models = {
|
||||
InterestTranslationType,
|
||||
Interest,
|
||||
ContactMessage,
|
||||
UserParamVisibilityType,
|
||||
UserParamVisibility,
|
||||
};
|
||||
|
||||
export default models;
|
||||
|
||||
@@ -59,6 +59,32 @@ const ContactMessage = sequelize.define('contact_message', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
// Neue Felder für die Antwort
|
||||
answer: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
set(value) {
|
||||
if (value) {
|
||||
const encryptedValue = encrypt(value);
|
||||
this.setDataValue('answer', encryptedValue.toString('hex'));
|
||||
}
|
||||
},
|
||||
get() {
|
||||
const value = this.getDataValue('answer');
|
||||
if (value) {
|
||||
return decrypt(Buffer.from(value, 'hex'));
|
||||
}
|
||||
}
|
||||
},
|
||||
answeredAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
isAnswered: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'contact_message',
|
||||
@@ -68,4 +94,3 @@ const ContactMessage = sequelize.define('contact_message', {
|
||||
});
|
||||
|
||||
export default ContactMessage;
|
||||
|
||||
47
backend/models/trigger.js
Normal file
47
backend/models/trigger.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
|
||||
export async function createTriggers() {
|
||||
const createTriggerFunction = `
|
||||
CREATE OR REPLACE FUNCTION create_user_param_visibility_trigger()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Check if UserParamVisibility already exists for this UserParam
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM community.user_param_visibility
|
||||
WHERE param_id = NEW.id
|
||||
) THEN
|
||||
-- Insert the default visibility (Invisible)
|
||||
INSERT INTO community.user_param_visibility (param_id, visibility)
|
||||
VALUES (NEW.id, (
|
||||
SELECT id FROM type.user_param_visibility WHERE description = 'Invisible'
|
||||
));
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`;
|
||||
|
||||
const createInsertTrigger = `
|
||||
CREATE TRIGGER trigger_create_user_param_visibility
|
||||
AFTER INSERT ON community.user_param
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_user_param_visibility_trigger();
|
||||
`;
|
||||
|
||||
const createUpdateTrigger = `
|
||||
CREATE TRIGGER trigger_update_user_param_visibility
|
||||
AFTER UPDATE ON community.user_param
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_user_param_visibility_trigger();
|
||||
`;
|
||||
|
||||
try {
|
||||
await sequelize.query(createTriggerFunction);
|
||||
await sequelize.query(createInsertTrigger);
|
||||
await sequelize.query(createUpdateTrigger);
|
||||
|
||||
console.log('Triggers created successfully');
|
||||
} catch (error) {
|
||||
console.error('Error creating triggers:', error);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,15 @@ const UserParamType = sequelize.define('user_param_type', {
|
||||
model: 'settings',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
orderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
unit: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
tableName: 'user_param',
|
||||
|
||||
@@ -10,6 +10,11 @@ const UserParamValue = sequelize.define('user_param_value', {
|
||||
value: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
orderId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
22
backend/models/type/user_param_visibility.js
Normal file
22
backend/models/type/user_param_visibility.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { sequelize } from '../../utils/sequelize.js';
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
const UserParamVisibilityType = sequelize.define('user_param_visibility_type', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
}, {
|
||||
tableName: 'user_param_visibility_type',
|
||||
timestamps: false,
|
||||
underscored: true,
|
||||
schema: 'type'
|
||||
});
|
||||
|
||||
export default UserParamVisibilityType;
|
||||
135
backend/package-lock.json
generated
135
backend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"pg": "^8.12.0",
|
||||
@@ -40,6 +41,19 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
|
||||
},
|
||||
"node_modules/@hapi/topo": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
|
||||
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -210,6 +224,24 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/formula": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
|
||||
},
|
||||
"node_modules/@sideway/pinpoint": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
@@ -432,9 +464,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
@@ -444,7 +476,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
@@ -842,9 +874,9 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -1004,36 +1036,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
|
||||
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.2",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.11.0",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
@@ -1097,12 +1129,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
@@ -1515,6 +1547,18 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "17.13.3",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
|
||||
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.3.0",
|
||||
"@hapi/topo": "^5.1.0",
|
||||
"@sideway/address": "^4.1.5",
|
||||
"@sideway/formula": "^3.0.1",
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-beautify": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
|
||||
@@ -1722,9 +1766,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
@@ -2075,9 +2122,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.12.0",
|
||||
@@ -2225,11 +2272,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
@@ -2354,9 +2401,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@@ -2389,6 +2436,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -2499,14 +2554,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~1.0.2",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.18.0"
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"mysql2": "^3.10.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"pg": "^8.12.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router } from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { getOpenInterests, changeInterest, deleteInterest, changeTranslation, getOpenContacts } from '../controllers/adminController.js';
|
||||
import { getOpenInterests, changeInterest, deleteInterest, changeTranslation, getOpenContacts, answerContact } from '../controllers/adminController.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -10,5 +10,7 @@ router.post('/interest/translation', authenticate, changeTranslation);
|
||||
router.delete('/interest/:id', authenticate, deleteInterest);
|
||||
|
||||
router.get('/opencontacts', authenticate, getOpenContacts);
|
||||
router.post('/contacts/answer', answerContact);
|
||||
|
||||
|
||||
export default router;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import { filterSettings, updateSetting, getTypeParamValueId, getTypeParamValues, getTypeParamValue, getAccountSettings,
|
||||
getPossibleInterests, getInterests, addInterest, addUserInterest, removeInterest } from '../controllers/settingsController.js';
|
||||
getPossibleInterests, getInterests, addInterest, addUserInterest, removeInterest, getVisibilities, updateVisibility }
|
||||
from '../controllers/settingsController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -17,5 +18,7 @@ router.get('/getuserinterests', authenticate, getInterests);
|
||||
router.post('/addinterest', authenticate, addInterest);
|
||||
router.post('/setinterest', authenticate, addUserInterest);
|
||||
router.get('/removeinterest/:id', authenticate, removeInterest);
|
||||
router.get('/visibilities', authenticate, getVisibilities);
|
||||
router.post('/update-visibility', authenticate, updateVisibility);
|
||||
|
||||
export default router;
|
||||
|
||||
11
backend/routers/socialnetworkRouter.js
Normal file
11
backend/routers/socialnetworkRouter.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import SocialNetworkController from '../controllers/socialnetworkController.js';
|
||||
|
||||
const router = express.Router();
|
||||
const socialNetworkController = new SocialNetworkController();
|
||||
|
||||
router.post('/usersearch', authenticate, socialNetworkController.userSearch);
|
||||
router.get('/profile/:userId', authenticate, socialNetworkController.profile);
|
||||
|
||||
export default router;
|
||||
@@ -32,7 +32,6 @@ amqp.connect(RABBITMQ_URL, (err, connection) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Sync database before starting the server
|
||||
syncDatabase().then(() => {
|
||||
server.listen(3001, () => {
|
||||
console.log('Server is running on port 3001');
|
||||
|
||||
56
backend/services/BaseService.js
Normal file
56
backend/services/BaseService.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import User from '../models/community/user.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
import UserParamType from '../models/type/user_param.js';
|
||||
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
||||
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
class BaseService {
|
||||
async getUserByHashedId(hashedId) {
|
||||
const user = await User.findOne({ where: { hashedId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async getUserById(userId) {
|
||||
const user = await User.findOne({ where: { id: userId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async getUserParams(userId, paramDescriptions) {
|
||||
return await UserParam.findAll({
|
||||
where: { userId },
|
||||
include: [
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: { [Op.in]: paramDescriptions } }
|
||||
},
|
||||
{
|
||||
model: UserParamVisibility,
|
||||
as: 'param_visibilities',
|
||||
include: [
|
||||
{
|
||||
model: UserParamVisibilityType,
|
||||
as: 'visibility_type'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
calculateAge(birthdate) {
|
||||
const birthDate = new Date(birthdate);
|
||||
const ageDifMs = Date.now() - birthDate.getTime();
|
||||
const ageDate = new Date(ageDifMs);
|
||||
return Math.abs(ageDate.getUTCFullYear() - 1970);
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseService;
|
||||
@@ -10,13 +10,30 @@ class ContactService {
|
||||
name = '';
|
||||
email = '';
|
||||
}
|
||||
ContactMessage.create({
|
||||
await ContactMessage.create({
|
||||
email,
|
||||
name,
|
||||
message,
|
||||
allowDataSave: acceptDataSave
|
||||
});
|
||||
}
|
||||
|
||||
async getContactById(id) {
|
||||
const contact = await ContactMessage.findByPk(id);
|
||||
if (!contact) {
|
||||
const error = new Error('Contact not found');
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
return contact;
|
||||
}
|
||||
|
||||
async saveAnswer(contact, answer) {
|
||||
contact.answer = answer;
|
||||
contact.answeredAt = new Date();
|
||||
contact.isFinished = true;
|
||||
await contact.save();
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactService();
|
||||
@@ -5,6 +5,8 @@ import InterestTranslationType from "../models/type/interest_translation.js"
|
||||
import User from "../models/community/user.js";
|
||||
import UserParamValue from "../models/type/user_param_value.js";
|
||||
import ContactMessage from "../models/service/contactmessage.js";
|
||||
import ContactService from "./ContactService.js";
|
||||
import { sendAnswerEmail } from './emailService.js';
|
||||
|
||||
class AdminService {
|
||||
async hasUserAccess(userId, section) {
|
||||
@@ -18,7 +20,7 @@ class AdminService {
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'user',
|
||||
as: 'user_with_rights',
|
||||
where: {
|
||||
hashedId: userId,
|
||||
}
|
||||
@@ -129,6 +131,13 @@ class AdminService {
|
||||
})
|
||||
return openContacts;
|
||||
}
|
||||
|
||||
async answerContact(contactId, answer) {
|
||||
const contact = await ContactService.getContactById(contactId);
|
||||
await ContactService.saveAnswer(contact, answer);
|
||||
await sendAnswerEmail(contact.email, answer, contact.language || 'en');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new AdminService();
|
||||
@@ -42,3 +42,16 @@ export const sendAccountActivationEmail = async (email, activationLink, username
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
};
|
||||
|
||||
export const sendAnswerEmail = async (toEmail, answer, language) => {
|
||||
i18n.setLocale(language);
|
||||
const mailOptions = {
|
||||
from: process.env.SMTP_FROM,
|
||||
to: toEmail,
|
||||
subject: 'yourPart',
|
||||
text: answer,
|
||||
html: `<p>${ answer }</p>`
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
};
|
||||
@@ -1,23 +1,18 @@
|
||||
import BaseService from './BaseService.js';
|
||||
import UserParamType from '../models/type/user_param.js';
|
||||
import SettingsType from '../models/type/settings.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
import User from '../models/community/user.js';
|
||||
import UserParamValue from '../models/type/user_param_value.js';
|
||||
import Interest from '../models/type/interest.js';
|
||||
import UserInterest from '../models/community/interest.js'
|
||||
import UserInterest from '../models/community/interest.js';
|
||||
import InterestTranslation from '../models/type/interest_translation.js';
|
||||
import { calculateAge } from '../utils/userdata.js';
|
||||
import { Op } from 'sequelize';
|
||||
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
||||
import { generateIv } from '../utils/encryption.js';
|
||||
|
||||
class SettingsService {
|
||||
async getUser(userId) {
|
||||
const user = await User.findOne({ where: { hashedId: userId } });
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
class SettingsService extends BaseService{
|
||||
async getUserParams(userId, paramDescriptions) {
|
||||
return await UserParam.findAll({
|
||||
where: { userId },
|
||||
@@ -25,7 +20,8 @@ class SettingsService {
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: { [Op.in]: paramDescriptions } }
|
||||
where: { description: { [Op.in]: paramDescriptions } },
|
||||
order: [[ 'order_id', 'asc' ]]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -34,16 +30,16 @@ class SettingsService {
|
||||
async getFieldOptions(field) {
|
||||
if (['singleselect', 'multiselect'].includes(field.datatype)) {
|
||||
return await UserParamValue.findAll({
|
||||
where: { userParamTypeId: field.id }
|
||||
where: { userParamTypeId: field.id },
|
||||
order: [[ 'order_id', 'asc' ]]
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async filterSettings(userId, type) {
|
||||
const user = await this.getUser(userId);
|
||||
async filterSettings(hashedUserId, type) {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']);
|
||||
|
||||
let birthdate = null;
|
||||
let gender = null;
|
||||
for (const param of userParams) {
|
||||
@@ -55,8 +51,7 @@ class SettingsService {
|
||||
gender = genderResult ? genderResult.dataValues?.value : null;
|
||||
}
|
||||
}
|
||||
|
||||
const age = birthdate ? calculateAge(birthdate) : null;
|
||||
const age = birthdate ? this.calculateAge(birthdate) : null;
|
||||
const fields = await UserParamType.findAll({
|
||||
include: [
|
||||
{
|
||||
@@ -72,7 +67,18 @@ class SettingsService {
|
||||
{
|
||||
model: User,
|
||||
as: 'user',
|
||||
where: { hashedId: userId }
|
||||
where: { id: user.id }
|
||||
},
|
||||
{
|
||||
model: UserParamVisibility,
|
||||
as: 'param_visibilities',
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: UserParamVisibilityType,
|
||||
as: 'visibility_type'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -84,28 +90,55 @@ class SettingsService {
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return await Promise.all(fields.map(async (field) => {
|
||||
const options = await this.getFieldOptions(field);
|
||||
const visibilityData = field.user_params[0]?.param_visibilities?.[0];
|
||||
const visibility = visibilityData
|
||||
? { id: visibilityData.visibility_type?.id, description: visibilityData.visibility_type?.description }
|
||||
: { id: null, description: 'Invisible' };
|
||||
return {
|
||||
id: field.id,
|
||||
name: field.description,
|
||||
minAge: field.minAge,
|
||||
gender: field.gender,
|
||||
datatype: field.datatype,
|
||||
unit: field.unit,
|
||||
value: field.user_params.length > 0 ? field.user_params[0].value : null,
|
||||
options: options.map(opt => ({ id: opt.id, value: opt.value }))
|
||||
options: options.map(opt => ({ id: opt.id, value: opt.value })),
|
||||
visibility
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
async updateSetting(userId, settingId, value) {
|
||||
const user = await this.getUser(userId);
|
||||
const paramType = await UserParamType.findOne({ where: { id: settingId } });
|
||||
if (!paramType) {
|
||||
throw new Error('Parameter type not found');
|
||||
async updateSetting(hashedUserId, settingId, value) {
|
||||
try {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
const paramType = await UserParamType.findOne({ where: { id: settingId } });
|
||||
if (!paramType) {
|
||||
throw new Error('Parameter type not found');
|
||||
}
|
||||
const userParam = await UserParam.findOne({
|
||||
where: { userId: user.id, paramTypeId: settingId }
|
||||
});
|
||||
if (userParam) {
|
||||
console.log('update param with ', value)
|
||||
if (typeof value === 'boolean') {
|
||||
value = value ? 'true' : 'false';
|
||||
}
|
||||
await userParam.update({value: value});
|
||||
} else {
|
||||
await UserParam.create(
|
||||
{
|
||||
userId: user.id,
|
||||
paramTypeId: settingId,
|
||||
value: value
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating setting:', hashedUserId, settingId, value, error);
|
||||
throw error;
|
||||
}
|
||||
await UserParam.upsertParam(user.id, paramType.id, value);
|
||||
}
|
||||
|
||||
async getTypeParamValueId(paramValue) {
|
||||
@@ -141,171 +174,123 @@ class SettingsService {
|
||||
return userParamValueObject.value;
|
||||
}
|
||||
|
||||
async getAccountSettings(userId) {
|
||||
const user = await this.getUser(userId);
|
||||
const email = user.email;
|
||||
return { username: user.username, email, showinsearch: user.searchable };
|
||||
async addInterest(hashedUserId, name) {
|
||||
try {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
|
||||
const existingInterests = await Interest.findAll({ where: { name: name.toLowerCase() } });
|
||||
if (existingInterests.length > 0) {
|
||||
throw new Error('Interest already exists');
|
||||
}
|
||||
|
||||
const userParam = await this.getUserParams(user.id, ['language']);
|
||||
let language = 'en';
|
||||
if (userParam.length > 0) {
|
||||
const userParamValue = await UserParamValue.findOne({
|
||||
where: { id: userParam[0].value }
|
||||
});
|
||||
language = userParamValue ? userParamValue.value : 'en';
|
||||
}
|
||||
|
||||
const languageParam = await UserParamValue.findOne({ where: { value: language } });
|
||||
const languageId = languageParam.id;
|
||||
const interest = await Interest.create({ name: name.toLowerCase(), allowed: false, adultOnly: true });
|
||||
await InterestTranslation.create({ interestsId: interest.id, language: languageId, translation: name });
|
||||
return interest;
|
||||
} catch (error) {
|
||||
console.error('Error adding interest:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setAccountSettings(data) {
|
||||
const { userId, username, email, searchable, oldpassword, newpassword, newpasswordrepeat } = data;
|
||||
const user = await this.getUser(userId);
|
||||
async addUserInterest(hashedUserId, interestId) {
|
||||
try {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
|
||||
user.searchable = searchable;
|
||||
|
||||
if (user.password !== oldpassword) {
|
||||
throw new Error('Wrong password');
|
||||
}
|
||||
|
||||
const updateUser = {};
|
||||
if (username.toLowerCase() !== user.username.toLowerCase()) {
|
||||
const isUsernameTaken = (await User.findAll({ where: { username: username } })).length > 0;
|
||||
if (isUsernameTaken) {
|
||||
throw new Error('Username already taken');
|
||||
const userParams = await this.getUserParams(user.id, ['birthdate']);
|
||||
let birthdate = null;
|
||||
for (const param of userParams) {
|
||||
if (param.paramType.description === 'birthdate') {
|
||||
birthdate = param.value;
|
||||
}
|
||||
}
|
||||
updateUser.username = username;
|
||||
}
|
||||
|
||||
if (newpassword.trim().length > 0) {
|
||||
if (newpassword.length < 6) {
|
||||
throw new Error('Password too short');
|
||||
}
|
||||
if (newpassword !== newpasswordrepeat) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
updateUser.password = newpassword;
|
||||
}
|
||||
await user.update(updateUser);
|
||||
}
|
||||
|
||||
async getPossibleInterests(userId) {
|
||||
const user = await this.getUser(userId);
|
||||
const userParams = await this.getUserParams(user.id, ['birthdate']);
|
||||
let birthdate = null;
|
||||
for (const param of userParams) {
|
||||
if (param.paramType.description === 'birthdate') {
|
||||
birthdate = param.value;
|
||||
}
|
||||
}
|
||||
const age = birthdate ? calculateAge(birthdate) : 0;
|
||||
const filter = {
|
||||
where: age >= 18 ? {
|
||||
allowed: true,
|
||||
} : {
|
||||
allowed: true,
|
||||
[Op.or]: [
|
||||
const age = birthdate ? this.calculateAge(birthdate) : 0;
|
||||
const interestsFilter = { id: interestId, allowed: true };
|
||||
if (age < 18) {
|
||||
interestsFilter[Op.or] = [
|
||||
{ adultOnly: false },
|
||||
{ adultOnly: { [Op.eq]: null } }
|
||||
]
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: InterestTranslation,
|
||||
as: 'interest_translations',
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: UserParamValue,
|
||||
as: 'user_param_value',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return await Interest.findAll(filter);
|
||||
}
|
||||
|
||||
async getInterests(userId) {
|
||||
const user = await this.getUser(userId);
|
||||
return await UserInterest.findAll({
|
||||
where: { userId: user.id },
|
||||
include: [
|
||||
{
|
||||
model: Interest,
|
||||
as: 'user_interest_type',
|
||||
include: [
|
||||
{
|
||||
model: InterestTranslation,
|
||||
as: 'interest_translations',
|
||||
include: [
|
||||
{
|
||||
model: UserParamValue,
|
||||
as: 'user_param_value',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async addInterest(userId, name) {
|
||||
const user = await this.getUser(userId);
|
||||
const existingInterests = await Interest.findAll({ where: { name: name.toLowerCase() } });
|
||||
if (existingInterests.length > 0) {
|
||||
throw new Error('Interest already exists');
|
||||
}
|
||||
const userParam = await this.getUserParams(user.id, ['language']);
|
||||
let language = 'en';
|
||||
if (userParam) {
|
||||
const userParamValue = await UserParamValue.findOne({
|
||||
where: {
|
||||
id: userParam[0].value
|
||||
}
|
||||
});
|
||||
language = userParamValue && userParamValue.value ? userParamValue.value : 'en';
|
||||
}
|
||||
const languageParam = await UserParamValue.findOne({ where: { value: language } });
|
||||
const languageId = languageParam.id;
|
||||
const interest = await Interest.create({ name: name.toLowerCase(), allowed: false, adultOnly: true });
|
||||
await InterestTranslation.create({ interestsId: interest.id, language: languageId, translation: name });
|
||||
return interest;
|
||||
}
|
||||
|
||||
async addUserInterest(userId, interestId) {
|
||||
const user = await this.getUser(userId);
|
||||
const interestsFilter = {
|
||||
id: interestId,
|
||||
allowed: true,
|
||||
};
|
||||
const userParams = await this.getUserParams(user.id, ['birthdate']);
|
||||
let birthdate = null;
|
||||
for (const param of userParams) {
|
||||
if (param.paramType.description === 'birthdate') {
|
||||
birthdate = param.value;
|
||||
];
|
||||
}
|
||||
|
||||
const existingInterests = await Interest.findAll({ where: interestsFilter });
|
||||
if (existingInterests.length === 0) {
|
||||
throw new Error('Interest not found');
|
||||
};
|
||||
|
||||
const interest = await UserInterest.findAll({
|
||||
where: { userId: user.id, userinterestId: interestId }
|
||||
});
|
||||
if (interest.length > 0) {
|
||||
throw new Error('Interest already exists');
|
||||
}
|
||||
await UserInterest.create({ userId: user.id, userinterestId: interestId });
|
||||
} catch (error) {
|
||||
console.error('Error adding user interest:', error);
|
||||
throw error;
|
||||
}
|
||||
const age = birthdate ? calculateAge(birthdate) : 0;
|
||||
if (age < 18) {
|
||||
interestsFilter[Op.or] = [
|
||||
{ adultOnly: false },
|
||||
{ adultOnly: { [Op.eq]: null } }
|
||||
];
|
||||
}
|
||||
const existingInterests = await Interest.findAll({ where: interestsFilter });
|
||||
if (existingInterests.length === 0) {
|
||||
throw new Error('Interest not found');
|
||||
};
|
||||
const interest = await UserInterest.findAll({
|
||||
where: { userId: user.id, userinterestId: interestId }
|
||||
});
|
||||
if (interest.length > 0) {
|
||||
throw new Error('Interest already exists');
|
||||
}
|
||||
await UserInterest.create({ userId: user.id, userinterestId: interestId });
|
||||
}
|
||||
|
||||
async removeInterest(userId, interestId) {
|
||||
const user = await this.getUser(userId);
|
||||
const interests = await UserInterest.findAll({
|
||||
where: { userId: user.id, userinterestId: interestId }
|
||||
});
|
||||
for (const interest of interests) {
|
||||
await interest.destroy();
|
||||
async removeInterest(hashedUserId, interestId) {
|
||||
try {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
const interests = await UserInterest.findAll({
|
||||
where: { userId: user.id, userinterestId: interestId }
|
||||
});
|
||||
for (const interest of interests) {
|
||||
await interest.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing interest:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getVisibilities() {
|
||||
return UserParamVisibilityType.findAll();
|
||||
}
|
||||
|
||||
async updateVisibility(hashedUserId, userParamTypeId, visibilityId) {
|
||||
try {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
console.log(JSON.stringify(user));
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const userParam = await UserParam.findOne({
|
||||
where: { paramTypeId: userParamTypeId, userId: user.id }
|
||||
});
|
||||
if (!userParam) {
|
||||
console.error(`UserParam not found for settingId: ${userParamTypeId} and userId: ${user.id}`);
|
||||
throw new Error('User parameter not found or does not belong to the user');
|
||||
}
|
||||
let userParamVisibility = await UserParamVisibility.findOne({
|
||||
where: { param_id: userParam.id }
|
||||
});
|
||||
if (userParamVisibility) {
|
||||
userParamVisibility.visibility = visibilityId;
|
||||
await userParamVisibility.save();
|
||||
} else {
|
||||
await UserParamVisibility.create({
|
||||
param_id: userParam.id,
|
||||
visibility: visibilityId
|
||||
});
|
||||
}
|
||||
console.log(`Visibility updated for settingId: ${userParamTypeId} with visibilityId: ${visibilityId}`);
|
||||
} catch (error) {
|
||||
console.error('Error updating visibility:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
187
backend/services/socialnetworkService.js
Normal file
187
backend/services/socialnetworkService.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import BaseService from './BaseService.js';
|
||||
import { Op, col } from 'sequelize';
|
||||
import User from '../models/community/user.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
import UserParamType from '../models/type/user_param.js';
|
||||
import UserParamValue from '../models/type/user_param_value.js';
|
||||
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
||||
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||
|
||||
class SocialNetworkService extends BaseService {
|
||||
async searchUsers({ username, ageFrom, ageTo, genders }) {
|
||||
try {
|
||||
const whereClause = {
|
||||
active: true,
|
||||
searchable: true
|
||||
};
|
||||
if (username) {
|
||||
whereClause.username = { [Op.iLike]: `%${username}%` };
|
||||
}
|
||||
const users = await User.findAll({
|
||||
where: whereClause,
|
||||
include: [
|
||||
{
|
||||
model: UserParam,
|
||||
as: 'user_params',
|
||||
include: [
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: {
|
||||
description: {
|
||||
[Op.in]: ['gender', 'birthdate']
|
||||
}
|
||||
},
|
||||
required: true
|
||||
}
|
||||
],
|
||||
required: true
|
||||
}
|
||||
]
|
||||
});
|
||||
const results = [];
|
||||
for (const user of users) {
|
||||
const id = user.hashedId;
|
||||
const birthdateParam = user.user_params.find(param => param.paramType.description === 'birthdate');
|
||||
const genderParam = user.user_params.find(param => param.paramType.description === 'gender');
|
||||
const age = birthdateParam ? this.calculateAge(birthdateParam.value) : null;
|
||||
const decryptedGenderValue = genderParam ? genderParam.value : null;
|
||||
let gender = null;
|
||||
if (decryptedGenderValue) {
|
||||
const genderValue = await UserParamValue.findOne({
|
||||
where: {
|
||||
id: decryptedGenderValue
|
||||
}
|
||||
});
|
||||
gender = genderValue ? genderValue.value : null;
|
||||
}
|
||||
const isWithinAgeRange = (!ageFrom || age >= ageFrom) && (!ageTo || age <= ageTo);
|
||||
if (isWithinAgeRange && (!genders || !genders.length || (gender && genders.includes(gender))) && age >= 14) {
|
||||
results.push({
|
||||
id: id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
gender: gender,
|
||||
age: age
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error in searchUsers:', error);
|
||||
throw new Error('Error searching users');
|
||||
}
|
||||
}
|
||||
|
||||
async getProfile(hashedUserId, requestingUserId) {
|
||||
try {
|
||||
const requestingUser = await this.getUserByHashedId(requestingUserId);
|
||||
const requestingUserParams = await this.getUserParams(requestingUser.id, ['birthdate']);
|
||||
let requestingUserAge = 0;
|
||||
|
||||
for (const param of requestingUserParams) {
|
||||
if (param.paramType.description === 'birthdate') {
|
||||
requestingUserAge = this.calculateAge(param.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
hashedId: hashedUserId,
|
||||
active: true,
|
||||
searchable: true,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: UserParam,
|
||||
as: 'user_params',
|
||||
include: [
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
},
|
||||
{
|
||||
model: UserParamVisibility,
|
||||
as: 'param_visibilities',
|
||||
include: [
|
||||
{
|
||||
model: UserParamVisibilityType,
|
||||
as: 'visibility_type'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [[ 'order_id', 'asc']]
|
||||
}
|
||||
]
|
||||
});
|
||||
if (user) {
|
||||
const userParams = {};
|
||||
await Promise.all(user.user_params.map(async (param) => {
|
||||
const visibilityData = param.param_visibilities?.[0]?.visibility_type;
|
||||
const visibility = visibilityData ? visibilityData.description : 'Invisible';
|
||||
let paramValue = param.value;
|
||||
let paramValueChanged = false;
|
||||
try {
|
||||
const parsedValue = JSON.parse(paramValue);
|
||||
if (Array.isArray(parsedValue)) {
|
||||
paramValue = await Promise.all(parsedValue.map(async (value) => {
|
||||
if (/^\d+$/.test(value)) {
|
||||
const userParamValue = await UserParamValue.findOne({
|
||||
where: {
|
||||
id: parseInt(value, 10),
|
||||
userParamTypeId: param.paramTypeId
|
||||
}
|
||||
});
|
||||
paramValueChanged = true;
|
||||
return userParamValue ? userParamValue.value : value;
|
||||
}
|
||||
return value;
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
if (!paramValueChanged) {
|
||||
if (/^\d+$/.test(paramValue)) {
|
||||
const userParamValue = await UserParamValue.findOne({
|
||||
where: {
|
||||
id: parseInt(paramValue, 10),
|
||||
userParamTypeId: param.paramTypeId
|
||||
}
|
||||
});
|
||||
if (userParamValue) {
|
||||
paramValue = userParamValue.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const paramTypeDescription = param.paramType.description;
|
||||
if (visibility === 'Invisible') {
|
||||
return;
|
||||
}
|
||||
if (visibility === 'All' || (visibility === 'FriendsAndAdults' && requestingUserAge >= 18) || (visibility === 'AdultsOnly' && requestingUserAge >= 18)) {
|
||||
userParams[paramTypeDescription] = {
|
||||
type: param.paramType.datatype,
|
||||
value: paramValue
|
||||
};
|
||||
if (paramTypeDescription === 'birthdate') {
|
||||
userParams['age'] = { value: this.calculateAge(Date.parse(paramValue)), type: "int"};
|
||||
}
|
||||
}
|
||||
}));
|
||||
const userProfile = {
|
||||
username: user.username,
|
||||
registrationDate: user.registrationDate,
|
||||
params: userParams
|
||||
};
|
||||
|
||||
return userProfile;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error in getProfile:', error);
|
||||
throw new Error('Error getting profile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SocialNetworkService;
|
||||
@@ -13,6 +13,10 @@ const initializeSettings = async () => {
|
||||
where: { name: 'sexuality' },
|
||||
defaults: { name: 'sexuality' }
|
||||
});
|
||||
await SettingsType.findOrCreate({
|
||||
where: { name: 'flirt' },
|
||||
defaults: { name: 'flirt' }
|
||||
});
|
||||
};
|
||||
|
||||
export default initializeSettings;
|
||||
@@ -4,7 +4,7 @@ import UserParamValue from '../models/type/user_param_value.js';
|
||||
import Interest from '../models/type/interest.js';
|
||||
import { Op, } from 'sequelize';
|
||||
import InterestTranslation from '../models/type/interest_translation.js';
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||
|
||||
const initializeTypes = async () => {
|
||||
const settingsTypes = await SettingsType.findAll();
|
||||
@@ -27,8 +27,8 @@ const initializeTypes = async () => {
|
||||
birthdate: { type: 'date', setting: 'personal' },
|
||||
zip: { type: 'string', setting: 'personal' },
|
||||
town: { type: 'string', setting: 'personal' },
|
||||
bodyheight: { type: 'float', setting: 'view' },
|
||||
weight: { type: 'float', setting: 'view' },
|
||||
bodyheight: { type: 'float', setting: 'view', unit: 'cm' },
|
||||
weight: { type: 'float', setting: 'view', unit: 'kg' },
|
||||
eyecolor: { type: 'singleselect', setting: 'view' },
|
||||
haircolor: { type: 'singleselect', setting: 'view' },
|
||||
hairlength: { type: 'singleselect', setting: 'view' },
|
||||
@@ -39,19 +39,26 @@ const initializeTypes = async () => {
|
||||
sexualpreference: { type: 'singleselect', 'setting': 'sexuality', minAge: 14 },
|
||||
gender: { type: 'singleselect', setting: 'personal' },
|
||||
pubichair: { type: 'singleselect', setting: 'sexuality', minAge: 14 },
|
||||
penislenght: { type: 'int', setting: 'sexuality', minAge: 14, gender: 'male' },
|
||||
brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'female' }
|
||||
penislength: { type: 'int', setting: 'sexuality', minAge: 14, gender: 'male', unit: 'cm' },
|
||||
brasize: { type: 'string', setting: 'sexuality', minAge: 14, gender: 'female' },
|
||||
interestedInGender: { type: 'multiselect', setting: 'flirt', minAge: 14},
|
||||
hasChildren: { type: 'bool', setting: 'flirt', minAge: 14 },
|
||||
willChildren: { type: 'bool', setting: 'flirt', minAge: 14 },
|
||||
smokes: { type: 'singleselect', setting: 'flirt', minAge: 14},
|
||||
drinks: { type: 'singleselect', setting: 'flirt', minAge: 14 },
|
||||
};
|
||||
Object.keys(userParams).forEach(async (key) => {
|
||||
let orderId = 1;
|
||||
for (const key of Object.keys(userParams)) {
|
||||
const item = userParams[key];
|
||||
const createItem = { description: key, datatype: item.type, settingsId: getSettingsTypeId(item.setting) };
|
||||
const createItem = { description: key, datatype: item.type, settingsId: getSettingsTypeId(item.setting), orderId: orderId++ };
|
||||
if (item.minAge) createItem.minAge = item.minAge;
|
||||
if (item.gender) createItem.gender = item.gender;
|
||||
if (item.unit) createItem.unit = item.unit;
|
||||
await UserParamType.findOrCreate({
|
||||
where: { description: key },
|
||||
defaults: createItem
|
||||
});
|
||||
});
|
||||
}
|
||||
const valuesList = {
|
||||
gender: ['male', 'female', 'transfemale', 'transmale', 'nonbinary'],
|
||||
language: ['de', 'en'],
|
||||
@@ -62,17 +69,22 @@ const initializeTypes = async () => {
|
||||
freckles: ['much', 'medium', 'less', 'none'],
|
||||
sexualpreference: ['straight', 'gay', 'bi', 'pan', 'asexual'],
|
||||
pubichair: ['none', 'short', 'medium', 'long', 'hairy', 'waxed', 'landingstrip', 'bikinizone', 'other'],
|
||||
interestedInGender: ['male', 'female'],
|
||||
smokes: ['never', 'socially', 'often', 'daily'],
|
||||
drinks: ['never', 'socially', 'often', 'daily'],
|
||||
brasize: ['Keine', 'AA', 'A', 'B', 'C', 'D', 'E (DD)', 'F (E)', 'G (F)', 'H (FF)', 'I (G)', 'J (GG)', 'K (H)']
|
||||
};
|
||||
Object.keys(valuesList).forEach(async (key) => {
|
||||
const values = valuesList[key];
|
||||
const userParamTypeId = await getUserParamTypeId(key);
|
||||
let orderId = 1;
|
||||
values.forEach(async (value) => {
|
||||
await UserParamValue.findOrCreate({
|
||||
where: {
|
||||
userParamTypeId: userParamTypeId,
|
||||
value: value
|
||||
},
|
||||
defaults: { userParamTypeId: userParamTypeId, value: value }
|
||||
defaults: { userParamTypeId: userParamTypeId, value: value, orderId: orderId++ }
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -161,6 +173,18 @@ const initializeTypes = async () => {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const visibilityTypes = ['Invisible', 'OnlyFriends', 'FriendsAndAdults', 'AdultsOnly', 'All'];
|
||||
for (const type of visibilityTypes) {
|
||||
try {
|
||||
await UserParamVisibilityType.findOrCreate({
|
||||
where: { description: type },
|
||||
defaults: { description: type }
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default initializeTypes;
|
||||
|
||||
@@ -4,6 +4,7 @@ import initializeSettings from './initializeSettings.js';
|
||||
import initializeUserRights from './initializeUserRights.js';
|
||||
import setupAssociations from '../models/associations.js';
|
||||
import models from '../models/index.js';
|
||||
import { createTriggers } from '../models/trigger.js';
|
||||
|
||||
const syncDatabase = async () => {
|
||||
try {
|
||||
@@ -12,6 +13,7 @@ const syncDatabase = async () => {
|
||||
for (const model of Object.values(models)) {
|
||||
await model.sync();
|
||||
}
|
||||
createTriggers();
|
||||
|
||||
await initializeSettings();
|
||||
await initializeTypes();
|
||||
|
||||
14
frontend/index.html
Normal file
14
frontend/index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>YourPart</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
8192
frontend/package-lock.json
generated
8192
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,32 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tinymce/tinymce-vue": "^6.0.1",
|
||||
"axios": "^1.7.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"tinymce": "^7.3.0",
|
||||
"vue": "~3.4.31",
|
||||
"vue-i18n": "^10.0.0-beta.2",
|
||||
"vue-multiselect": "^3.1.0",
|
||||
"vue-router": "^4.0.13",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "^5.0.8",
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@vitejs/plugin-vue": "^5.1.3",
|
||||
"assert": "^2.1.0",
|
||||
"sass": "^1.77.8",
|
||||
"sass-loader": "^10.5.2"
|
||||
"stream-browserify": "^3.0.0",
|
||||
"util": "^0.12.5",
|
||||
"vite": "^5.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ button {
|
||||
cursor: pointer;
|
||||
background: #F9A22C;
|
||||
color: #000000;
|
||||
border: none;
|
||||
border: 1px solid #F9A22C;
|
||||
border-radius: 4px;
|
||||
transition: background 0.05s;
|
||||
border: 1px solid transparent;
|
||||
@@ -52,3 +52,18 @@ button:hover {
|
||||
color: #F9A22C;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.multiselect__option--highlight,
|
||||
.multiselect__option--highlight::after,
|
||||
.multiselect__tag,
|
||||
.multiselect__option--highlight[data-select],
|
||||
.multiselect__option--highlight[data-selected],
|
||||
.multiselect__option--highlight[data-deselect] {
|
||||
background: none;
|
||||
background-color: #F9A22C;
|
||||
color: #000;
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
<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">
|
||||
@click="toggleDialogMinimize(dialog.dialog.name)" :title="dialog.dialog.localTitle">
|
||||
<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>
|
||||
<span class="button-text">{{ dialog.dialog.isTitleTranslated ? $t(dialog.dialog.localTitle) :
|
||||
dialog.dialog.localTitle }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="static-block">
|
||||
|
||||
@@ -60,6 +60,7 @@ nav>ul {
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<span v-if="icon" class="dialog-icon">
|
||||
<img :src="'/images/icons/' + icon" alt="Icon" />
|
||||
</span>
|
||||
<span class="dialog-title">{{ isTitleTranslated ? $t(title) : title }}</span>
|
||||
<span class="dialog-title">{{ localIsTitleTranslated ? $t(localTitle) : localTitle }}</span>
|
||||
<span v-if="!modal" class="dialog-minimize" @click="minimize">_</span>
|
||||
<span v-if="showClose" class="dialog-close" @click="close">✖</span>
|
||||
</div>
|
||||
@@ -73,12 +73,13 @@ export default {
|
||||
isDragging: false,
|
||||
dragOffsetX: 0,
|
||||
dragOffsetY: 0,
|
||||
localTitle: this.title,
|
||||
localIsTitleTranslated: this.isTitleTranslated,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dialogWidth() {
|
||||
const val = this.width || '70%';
|
||||
console.log(val);
|
||||
return val;
|
||||
},
|
||||
dialogHeight() {
|
||||
@@ -90,6 +91,9 @@ export default {
|
||||
if (!newValue) {
|
||||
this.minimized = false;
|
||||
}
|
||||
},
|
||||
title(newValue) {
|
||||
this.updateTitle(newValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -107,9 +111,13 @@ export default {
|
||||
this.$store.dispatch('dialogs/removeOpenDialog', this.name);
|
||||
},
|
||||
buttonClick(action) {
|
||||
this.$emit(action);
|
||||
if (action === 'close') {
|
||||
this.close();
|
||||
if (typeof action === 'function') {
|
||||
action(); // Wenn action eine Funktion ist, rufe sie direkt auf
|
||||
} else {
|
||||
this.$emit(action);
|
||||
if (action === 'close') {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
handleOverlayClick() {
|
||||
@@ -131,7 +139,6 @@ export default {
|
||||
this.dragOffsetY = event.clientY - dialog.offsetTop;
|
||||
document.addEventListener('mousemove', this.onDrag);
|
||||
document.addEventListener('mouseup', this.stopDragging);
|
||||
console.log('dragging started');
|
||||
},
|
||||
onDrag(event) {
|
||||
if (!this.isDragging) return;
|
||||
@@ -143,13 +150,15 @@ export default {
|
||||
document.removeEventListener('mousemove', this.onDrag);
|
||||
document.removeEventListener('mouseup', this.stopDragging);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type === 'dialogs/toggleDialogMinimize' && mutation.payload === this.name) {
|
||||
this.minimized = !this.minimized;
|
||||
}
|
||||
});
|
||||
updateTitle(newTitle, newIsTitleTranslated) {
|
||||
this.localTitle = newTitle;
|
||||
this.localIsTitleTranslated = newIsTitleTranslated;
|
||||
this.$store.dispatch('dialogs/updateDialogTitle', {
|
||||
name: this.name,
|
||||
newTitle: newTitle,
|
||||
isTitleTranslated: this.localIsTitleTranslated
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
76
frontend/src/components/QuillEditor.vue
Normal file
76
frontend/src/components/QuillEditor.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div ref="quillEditor" class="quill-editor"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Quill from 'quill/core';
|
||||
import Toolbar from 'quill/modules/toolbar';
|
||||
import Snow from 'quill/themes/snow';
|
||||
import Bold from 'quill/formats/bold';
|
||||
import Italic from 'quill/formats/italic';
|
||||
import Underline from 'quill/formats/underline';
|
||||
import List from 'quill/formats/list';
|
||||
|
||||
Quill.register({
|
||||
'modules/toolbar': Toolbar,
|
||||
'themes/snow': Snow,
|
||||
'formats/bold': Bold,
|
||||
'formats/italic': Italic,
|
||||
'formats/underline': Underline,
|
||||
'formats/list': List
|
||||
});
|
||||
|
||||
export default {
|
||||
name: 'QuillEditor',
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Compose an epic...'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.editor = new Quill(this.$refs.quillEditor, {
|
||||
theme: 'snow',
|
||||
placeholder: this.placeholder,
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['link', 'image'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
this.editor.on('text-change', () => {
|
||||
this.$emit('update:content', this.editor.root.innerHTML);
|
||||
});
|
||||
|
||||
this.editor.root.innerHTML = this.content;
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('text-change');
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quill-editor {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,58 @@
|
||||
<template>
|
||||
<div class="settings-widget">
|
||||
<template v-for="setting in settings">
|
||||
<InputStringWidget v-if="setting.datatype == 'string'" :labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value :list="languagesList()"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<DateInputWidget v-else-if="setting.datatype == 'date'" :labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
:list="getSettingOptions(setting.name, setting.options)" @input="handleInput(setting.id, $event)" />
|
||||
<InputNumberWidget v-else-if="setting.datatype == 'int'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToInt(setting.value)" min="0"
|
||||
max="200" @input="handleInput(setting.id, $event)" />
|
||||
<FloatInputWidget v-else-if="setting.datatype == 'float'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToFloat(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<CheckboxWidget v-else-if="setting.datatype == 'bool'" :labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToBool(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
<div v-else>{{ setting }}
|
||||
</div>
|
||||
</template>
|
||||
<table>
|
||||
<tr v-for="setting in settings" :key="setting.id">
|
||||
<td>
|
||||
<InputStringWidget v-if="setting.datatype == 'string'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
:list="languagesList()" @input="handleInput(setting.id, $event)" />
|
||||
|
||||
<DateInputWidget v-else-if="setting.datatype == 'date'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<SelectDropdownWidget v-else-if="setting.datatype == 'singleselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value=setting.value
|
||||
:list="getSettingOptions(setting.name, setting.options)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<InputNumberWidget v-else-if="setting.datatype == 'int'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToInt(setting.value)"
|
||||
min="0" max="200" @input="handleInput(setting.id, $event)" />
|
||||
|
||||
<FloatInputWidget v-else-if="setting.datatype == 'float'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToFloat(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<CheckboxWidget v-else-if="setting.datatype == 'bool'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="convertToBool(setting.value)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<MultiselectWidget v-else-if="setting.datatype == 'multiselect'"
|
||||
:labelTr="`settings.personal.label.${setting.name}`"
|
||||
:tooltipTr="`settings.personal.tooltip.${setting.name}`" :value="setting.value"
|
||||
:list="getSettingOptions(setting.name, setting.options)"
|
||||
@input="handleInput(setting.id, $event)" />
|
||||
|
||||
<div v-else>{{ setting }}</div>
|
||||
<span v-if="setting.unit"> {{ setting.unit }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<select v-model="setting.visibility.id"
|
||||
@change="handleVisibilityChange(setting.id, setting.visibility.id)">
|
||||
<option v-for="visibility in possibleVisibilities" :key="visibility.id" :value="visibility.id">
|
||||
{{ $t(`settings.visibility.${visibility.description}`) }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,10 +61,11 @@ import apiClient from '@/utils/axios.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import InputStringWidget from '@/components/form/InputStringWidget.vue';
|
||||
import DateInputWidget from '@/components/form/DateInputWidget.vue';
|
||||
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget';
|
||||
import InputNumberWidget from '@/components/form/InputNumberWidget';
|
||||
import FloatInputWidget from '@/components/form/FloatInputWidget';
|
||||
import CheckboxWidget from '@/components/form/CheckboxWidget';
|
||||
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
|
||||
import InputNumberWidget from '@/components/form/InputNumberWidget.vue';
|
||||
import FloatInputWidget from '@/components/form/FloatInputWidget.vue';
|
||||
import CheckboxWidget from '@/components/form/CheckboxWidget.vue';
|
||||
import MultiselectWidget from '@/components/form/MultiselectWidget.vue';
|
||||
|
||||
export default {
|
||||
name: "SettingsWidget",
|
||||
@@ -46,7 +75,8 @@ export default {
|
||||
SelectDropdownWidget,
|
||||
InputNumberWidget,
|
||||
FloatInputWidget,
|
||||
CheckboxWidget
|
||||
CheckboxWidget,
|
||||
MultiselectWidget
|
||||
},
|
||||
props: {
|
||||
settingsType: {
|
||||
@@ -54,6 +84,10 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data: {
|
||||
settings: [],
|
||||
possibleVisibilities: [],
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
},
|
||||
@@ -64,6 +98,8 @@ export default {
|
||||
async fetchSettings() {
|
||||
if (this.user && this.user.id) {
|
||||
try {
|
||||
const visibilityResponse = await apiClient.get('/api/settings/visibilities');
|
||||
this.possibleVisibilities = visibilityResponse.data;
|
||||
const userid = this.user.id;
|
||||
const response = await apiClient.post('/api/settings/filter', {
|
||||
userid: userid,
|
||||
@@ -94,6 +130,7 @@ export default {
|
||||
settingId: settingId,
|
||||
value: value
|
||||
});
|
||||
this.fetchSettings();
|
||||
} catch (err) {
|
||||
console.error('Error updating setting:', err);
|
||||
}
|
||||
@@ -120,7 +157,17 @@ export default {
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleVisibilityChange(settingId, visibilityId) {
|
||||
try {
|
||||
await apiClient.post('/api/settings/update-visibility', {
|
||||
userParamTypeId: settingId,
|
||||
visibilityId: visibilityId
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error updating visibility:', err);
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -129,3 +176,9 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
@@ -30,7 +30,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateValue(checked) {
|
||||
this.$emit("input", checked);
|
||||
this.$emit("input", checked || false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<input type="number" :value="formattedValue" :placeholder="$t(labelTr)" :title="$t(tooltipTr)"
|
||||
@input="updateValue($event.target.value)" :step="step" />
|
||||
@change="updateValue($event.target.value)" :step="step" />
|
||||
<span v-if="postfix">{{ postfix }}</span>
|
||||
</label>
|
||||
</template>
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
formattedValue() {
|
||||
return this.value != null && typeof this.value === 'float' ? this.value.toFixed(this.decimals) : '';
|
||||
return this.value != null ? this.value.toFixed(this.decimals) : '';
|
||||
},
|
||||
step() {
|
||||
return Math.pow(10, -this.decimals);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<input type="number" :value="value" :title="$t(tooltipTr)" :min="min" :max="max"
|
||||
@input="updateValue($event.target.value)" />
|
||||
@change="updateValue($event.target.value)" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
@@ -38,7 +38,8 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.$emit("input", parseFloat(value));
|
||||
console.log('changed to ', value)
|
||||
this.$emit("input", parseInt(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<input type="text" :value="value" :placeholder="$t(labelTr)" :title="$t(tooltipTr)"
|
||||
@input="validateAndUpdate($event.target.value)" />
|
||||
@change="validateAndUpdate($event.target.value)" />
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
||||
139
frontend/src/components/form/MultiselectWidget.vue
Normal file
139
frontend/src/components/form/MultiselectWidget.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<label>
|
||||
<span :style="{ width: width + 'em' }">{{ $t(labelTr) }}</span>
|
||||
<Multiselect
|
||||
v-model="selectedOptions"
|
||||
:options="validList"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('select_option')"
|
||||
:track-by="'value'"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<span v-if="option && option.value">Option: {{ getTranslation(option) }}</span>
|
||||
</template>
|
||||
<template #tag="{ option, remove }">
|
||||
<span v-if="option && option.captionTr" class="custom-tag">
|
||||
{{ $t(option.captionTr) }}
|
||||
<span @click="remove(option)">×</span>
|
||||
</span>
|
||||
<span v-else>@e</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
|
||||
export default {
|
||||
name: "MultiselectWidget",
|
||||
components: { Multiselect },
|
||||
props: {
|
||||
labelTr: { type: String, required: true },
|
||||
value: { type: String, required: false, default: '[]' },
|
||||
tooltipTr: { type: String, required: true },
|
||||
width: { type: Number, required: false, default: 10 },
|
||||
list: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [] // Standardwert hinzufügen, um undefined zu vermeiden
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalValues: this.stringToArray(this.value), // Speichert nur die IDs (Werte)
|
||||
selectedOptions: this.getOptionsFromIds(this.stringToArray(this.value)) // Hilfsvariable, speichert die vollständigen Objekte
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
validList() {
|
||||
return this.validatedList(); // Immer ein Array zurückgeben
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
const ids = this.stringToArray(newValue);
|
||||
this.internalValues = ids; // Nur die IDs speichern
|
||||
this.selectedOptions = this.getOptionsFromIds(ids); // Optionen basierend auf IDs setzen
|
||||
},
|
||||
selectedOptions(newOptions) {
|
||||
this.internalValues = newOptions.map(option => option.value); // Nur die IDs extrahieren
|
||||
this.updateValue();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stringToArray(str) {
|
||||
try {
|
||||
const array = JSON.parse(str);
|
||||
return array.filter(item => item !== null && item !== undefined);
|
||||
} catch (error) {
|
||||
console.error('Invalid JSON string in value:', str);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
updateValue() {
|
||||
const stringValue = JSON.stringify(this.internalValues); // In JSON-String umwandeln
|
||||
this.$emit("input", stringValue); // String an das Parent-Element übermitteln
|
||||
},
|
||||
getTranslation(option) {
|
||||
return option.captionTr ? this.$t(option.captionTr) : option.caption;
|
||||
},
|
||||
findOption(optionId) {
|
||||
return this.validatedList().find(opt => opt.value === optionId);
|
||||
},
|
||||
getOptionsFromIds(ids) {
|
||||
return ids.map(id => this.findOption(id)).filter(option => option); // Vollständige Objekte basierend auf IDs abrufen
|
||||
},
|
||||
validatedList() {
|
||||
// Überprüfen, ob die Liste valide ist
|
||||
if (!this.list || !Array.isArray(this.list)) {
|
||||
return [];
|
||||
}
|
||||
return this.list.filter(option => option && option.value !== null && option.value !== undefined && (option.captionTr || option.caption));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
label>span {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.custom-tag {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.custom-tag span {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
display: inline-block;
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
.multiselect__tags {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
110
frontend/src/dialogues/admin/AnswerContact.vue
Normal file
110
frontend/src/dialogues/admin/AnswerContact.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t('admin.editcontactrequest.title')" :show-close="true" :buttons="buttons"
|
||||
@close="closeDialog" name="AnswerContact" :modal="true" :isTitleTranslated="true">
|
||||
<div class="contact-details">
|
||||
<h3>Von: {{ contact.email }}</h3>
|
||||
<p>{{ contact.message }}</p>
|
||||
</div>
|
||||
<div class="editor-container">
|
||||
<Editor v-model="answer" :init="tinymceInitOptions" :api-key="apiKey" />
|
||||
</div>
|
||||
</DialogWidget>
|
||||
|
||||
<DialogWidget ref="errorDialog" :title="$t('error.title')" :show-close="true" :buttons="errorButtons"
|
||||
@close="closeErrorDialog" name="ErrorDialog" :modal="true" :isTitleTranslated="false">
|
||||
<div>
|
||||
<p>{{ errorMessage }}</p>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import Editor from '@tinymce/tinymce-vue'
|
||||
import apiClient from '@/utils/axios.js'
|
||||
import DialogWidget from '@/components/DialogWidget.vue'
|
||||
|
||||
export default {
|
||||
name: 'AnswerContact',
|
||||
components: {
|
||||
DialogWidget,
|
||||
Editor,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
|
||||
dialog: null,
|
||||
errorDialog: null,
|
||||
contact: null,
|
||||
answer: '',
|
||||
errorMessage: '',
|
||||
tinymceInitOptions: {
|
||||
height: 300,
|
||||
menubar: false,
|
||||
plugins: [
|
||||
'advlist autolink lists link image charmap print preview anchor',
|
||||
'searchreplace visualblocks code fullscreen',
|
||||
'insertdatetime media table paste code help wordcount'
|
||||
],
|
||||
toolbar:
|
||||
'undo redo cut copy paste | bold italic forecolor fontfamily fontsize | \
|
||||
alignleft aligncenter alignright alignjustify | \
|
||||
bullist numlist outdent indent | removeformat | help'
|
||||
},
|
||||
buttons: [
|
||||
{ text: 'OK', action: this.sendAnswer },
|
||||
{ text: 'Cancel', action: this.closeDialog }
|
||||
],
|
||||
errorButtons: [
|
||||
{ text: 'OK', action: this.closeErrorDialog }
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(contactData) {
|
||||
this.contact = contactData;
|
||||
this.dialog.open();
|
||||
this.answer = '';
|
||||
},
|
||||
closeDialog() {
|
||||
this.dialog.close();
|
||||
this.answer = '';
|
||||
},
|
||||
closeErrorDialog() {
|
||||
this.errorDialog.close();
|
||||
},
|
||||
async sendAnswer() {
|
||||
try {
|
||||
await apiClient.post('/api/admin/contacts/answer', {
|
||||
id: this.contact.id,
|
||||
answer: this.answer,
|
||||
});
|
||||
this.dialog.close();
|
||||
this.$emit('refresh');
|
||||
this.answer = '';
|
||||
} catch (error) {
|
||||
const errorText = error.response?.data?.error || 'An unexpected error occurred.';
|
||||
this.errorMessage = errorText;
|
||||
this.errorDialog.open();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.dialog = this.$refs.dialog;
|
||||
this.errorDialog = this.$refs.errorDialog;
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Aufräumarbeiten falls nötig
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.contact-details {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
149
frontend/src/dialogues/socialnetwork/UserProfileDialog.vue
Normal file
149
frontend/src/dialogues/socialnetwork/UserProfileDialog.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t('socialnetwork.profile.pretitle')" :isTitleTranslated="isTitleTranslated"
|
||||
:show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog">
|
||||
<div class="dialog-body">
|
||||
<div>
|
||||
<ul class="tab-list">
|
||||
<li v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }"
|
||||
@click="selectTab(tab.name)">
|
||||
{{ tab.label }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" v-if="activeTab === 'general'">
|
||||
<table>
|
||||
<tr v-for="(value, key) in userProfile.params" :key="key">
|
||||
<td>{{ $t(`socialnetwork.profile.${key}`) }}</td>
|
||||
<td>{{ generateValue(key, value) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogWidget>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DialogWidget from '@/components/DialogWidget.vue';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
|
||||
export default {
|
||||
name: 'UserProfileDialog',
|
||||
components: {
|
||||
DialogWidget
|
||||
},
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isTitleTranslated: true,
|
||||
userProfile: {},
|
||||
activeTab: 'general',
|
||||
userId: '',
|
||||
tabs: [
|
||||
{ name: 'general', label: this.$t('socialnetwork.profile.tab.general') },
|
||||
{ name: 'images', label: this.$t('socialnetwork.profile.tab.images') },
|
||||
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.$refs.dialog.open();
|
||||
this.loadUserProfile();
|
||||
},
|
||||
async loadUserProfile() {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/socialnetwork/profile/${this.userId}`);
|
||||
this.userProfile = response.data;
|
||||
const newTitle = this.$t('socialnetwork.profile.title').replace('<username>', this.userProfile.username);
|
||||
this.$refs.dialog.updateTitle(newTitle, false);
|
||||
} catch (error) {
|
||||
this.$refs.dialog.updateTitle('socialnetwork.profile.error_title', true);
|
||||
console.error('Fehler beim Laden des Benutzerprofils:', error);
|
||||
}
|
||||
},
|
||||
closeDialog() {
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
selectTab(tabName) {
|
||||
this.activeTab = tabName;
|
||||
},
|
||||
generateValue(key, value) {
|
||||
if (Array.isArray(value.value)) {
|
||||
const strings = [];
|
||||
for (const val of value.value) {
|
||||
strings.push(this.generateValue(key, {type: value.type, value: val}));
|
||||
}
|
||||
return strings.join(', ');
|
||||
}
|
||||
switch (value.type) {
|
||||
case 'bool':
|
||||
return this.$t(`socialnetwork.profile.values.bool.${value.value}`);
|
||||
case 'multiselect':
|
||||
case 'singleselect':
|
||||
return this.$t(`socialnetwork.profile.values.${key}.${value.value}`);
|
||||
case 'date':
|
||||
const date = new Date(value.value);
|
||||
return date.toLocaleDateString();
|
||||
case 'string':
|
||||
case 'int':
|
||||
return value.value;
|
||||
case 'float':
|
||||
return new Intl.NumberFormat(navigator.language, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(parseFloat(value.value));
|
||||
default:
|
||||
return value.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
||||
.tab-list li {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
margin-right: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom: none;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.tab-list li.active {
|
||||
background: #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
background: #ffffff;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dialog-body,
|
||||
.dialog-body > div {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dialog-body > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,7 @@ import enError from './locales/en/error.json';
|
||||
import enActivate from './locales/en/activate.json';
|
||||
import enSettings from './locales/en/settings.json';
|
||||
import enAdmin from './locales/en/admin.json';
|
||||
import enSocialNetwork from './locales/en/socialnetwork.json';
|
||||
|
||||
import deGeneral from './locales/de/general.json';
|
||||
import deHeader from './locales/de/header.json';
|
||||
@@ -22,6 +23,7 @@ import deError from './locales/de/error.json';
|
||||
import deActivate from './locales/de/activate.json';
|
||||
import deSettings from './locales/de/settings.json';
|
||||
import deAdmin from './locales/de/admin.json';
|
||||
import deSocialNetwork from './locales/de/socialnetwork.json';
|
||||
|
||||
const messages = {
|
||||
en: {
|
||||
@@ -35,8 +37,10 @@ const messages = {
|
||||
...enActivate,
|
||||
...enSettings,
|
||||
...enAdmin,
|
||||
...enSocialNetwork,
|
||||
},
|
||||
de: {
|
||||
'Ok': 'Ok',
|
||||
...deGeneral,
|
||||
...deHeader,
|
||||
...deNavigation,
|
||||
@@ -47,6 +51,7 @@ const messages = {
|
||||
...deActivate,
|
||||
...deSettings,
|
||||
...deAdmin,
|
||||
...deSocialNetwork,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
"actions": "Aktionen",
|
||||
"open": "Bearbeiten",
|
||||
"finished": "Abschließen"
|
||||
},
|
||||
"editcontactrequest": {
|
||||
"title": "[Admin] - Kontaktanfrage bearbeiten"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,5 +27,7 @@
|
||||
},
|
||||
"general": {
|
||||
"datetimelong": "dd.MM.yyyy HH:mm:ss"
|
||||
}
|
||||
},
|
||||
"OK": "Ok",
|
||||
"Cancel": "Abbrechen"
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
"account": "Account",
|
||||
"personal": "Persönliches",
|
||||
"view": "Aussehen",
|
||||
"flirt": "Flirt",
|
||||
"interests": "Interessen",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"sexuality": "Sexualität"
|
||||
|
||||
@@ -19,8 +19,13 @@
|
||||
"tattoos": "Tattoos",
|
||||
"sexualpreference": "Ausrichtung",
|
||||
"pubichair": "Schamhaare",
|
||||
"penislenght": "Penislänge",
|
||||
"brasize": "BH-Größe"
|
||||
"penislength": "Penislänge",
|
||||
"brasize": "BH-Größe",
|
||||
"willChildren": "Ich möchte Kinder",
|
||||
"smokes": "Rauchen",
|
||||
"drinks": "Ich trinke Alkohol",
|
||||
"hasChildren": "Ich habe Kinder",
|
||||
"interestedInGender": "Interessiert an"
|
||||
},
|
||||
"tooltip": {
|
||||
"language": "Sprache",
|
||||
@@ -39,7 +44,7 @@
|
||||
"tattoos": "Tattoos",
|
||||
"sexualpreference": "Ausrichtung",
|
||||
"pubichair": "Schamhaare",
|
||||
"penislenght": "Penislänge",
|
||||
"penislength": "Penislänge",
|
||||
"brasize": "BH-Größe"
|
||||
},
|
||||
"gender": {
|
||||
@@ -109,6 +114,22 @@
|
||||
"landingstrip": "Landebahn",
|
||||
"bikinizone": "Nur Bikinizone",
|
||||
"other": "Andere"
|
||||
},
|
||||
"interestedInGender": {
|
||||
"male": "Männer",
|
||||
"female": "Frauen"
|
||||
},
|
||||
"smokes": {
|
||||
"often": "Oft",
|
||||
"socially": "In Gesellschaft",
|
||||
"daily": "Täglich",
|
||||
"never": "Nie"
|
||||
},
|
||||
"drinks": {
|
||||
"often": "Oft",
|
||||
"socially": "In Gesellschaft",
|
||||
"daily": "Täglich",
|
||||
"never": "Nie"
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
@@ -136,6 +157,16 @@
|
||||
"added": "Das neue Interesse wurde hinzugefügt und wird bearbeitet. Bis zum Abschluss ist es nicht in der Liste der Interessen sichtbar.",
|
||||
"adderror": "Beim hinzufügen des Interesses ist ein Fehler aufgetreten.",
|
||||
"errorsetinterest": "Das Interest konnte für Dich nicht gebucht werden."
|
||||
},
|
||||
"visibility": {
|
||||
"Invisible": "Nicht anzeigen",
|
||||
"OnlyFriends": "Nur Freunden anzeigen",
|
||||
"FriendsAndAdults": "Freunden und Erwachsenen anzeigen",
|
||||
"AdultsOnly": "Nur Erwachsenen anzeigen",
|
||||
"All": "Jedem zeigen"
|
||||
},
|
||||
"flirt": {
|
||||
"title": "Flirt"
|
||||
}
|
||||
}
|
||||
}
|
||||
143
frontend/src/i18n/locales/de/socialnetwork.json
Normal file
143
frontend/src/i18n/locales/de/socialnetwork.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"socialnetwork": {
|
||||
"usersearch": {
|
||||
"title": "Benutzersuche",
|
||||
"username": "Benutzername",
|
||||
"age_from": "Alter von",
|
||||
"age_to": "bis",
|
||||
"gender": "Geschlecht",
|
||||
"search_button": "Suchen",
|
||||
"no_results": "Keine Ergebnisse gefunden",
|
||||
"results_title": "Suchergebnisse:",
|
||||
"result": {
|
||||
"nick": "Spitzname",
|
||||
"gender": "Geschlecht",
|
||||
"age": "Alter"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"pretitle": "Lade Daten. Bitte warten...",
|
||||
"error_title": "User nicht gefunden",
|
||||
"title": "Profil von <username>",
|
||||
"tab": {
|
||||
"general": "Allgemeines",
|
||||
"sexuality": "Sexualität",
|
||||
"images": "Bilder",
|
||||
"guestbook": "Gästebuch"
|
||||
},
|
||||
"values": {
|
||||
"bool": {
|
||||
"true": "Ja",
|
||||
"false": "Nein"
|
||||
},
|
||||
"smokes": {
|
||||
"never": "Nie",
|
||||
"socially": "In Gesellschaft",
|
||||
"often": "Oft",
|
||||
"daily": "Täglich"
|
||||
},
|
||||
"drinks": {
|
||||
"never": "Nie",
|
||||
"socially": "In Gesellschaft",
|
||||
"often": "Oft",
|
||||
"daily": "Täglich"
|
||||
},
|
||||
"interestedInGender": {
|
||||
"male": "Männern",
|
||||
"female": "Frauen"
|
||||
},
|
||||
"sexualpreference": {
|
||||
"straight": "Heterosexuell",
|
||||
"gay": "Homosexuell",
|
||||
"bi": "Bisexuell",
|
||||
"pan": "Pansexuell",
|
||||
"asexual": "Asexuell"
|
||||
},
|
||||
"pubichair": {
|
||||
"none": "Keine",
|
||||
"short": "Kurz",
|
||||
"medium": "Mittel",
|
||||
"long": "Lang",
|
||||
"hairy": "Unrasiert",
|
||||
"waxed": "Gewachst",
|
||||
"landingstrip": "Landebahn",
|
||||
"other": "Anderes",
|
||||
"bikinizone": "Bikinizone"
|
||||
},
|
||||
"gender": {
|
||||
"male": "Männlich",
|
||||
"female": "Weiblich",
|
||||
"transmale": "Trans-Frau",
|
||||
"transfemale": "Trans-Mann",
|
||||
"nonbinary": "Nonbinär"
|
||||
},
|
||||
"language": {
|
||||
"de": "Deutsch",
|
||||
"en": "Englisch"
|
||||
},
|
||||
"eyecolor": {
|
||||
"blue": "Blau",
|
||||
"green": "Grün",
|
||||
"brown": "Braun",
|
||||
"black": "Schwarz",
|
||||
"grey": "Grau",
|
||||
"hazel": "Haselnuss",
|
||||
"amber": "Bernstein",
|
||||
"red": "Rot",
|
||||
"other": "Andere"
|
||||
},
|
||||
"haircolor": {
|
||||
"black": "Schwarz",
|
||||
"brown": "Braun",
|
||||
"blonde": "Blond",
|
||||
"red": "Rot",
|
||||
"grey": "Grau",
|
||||
"white": "Weiß",
|
||||
"other": "Andere"
|
||||
},
|
||||
"hairlength": {
|
||||
"short": "Kurz",
|
||||
"medium": "Mittel",
|
||||
"long": "Lang",
|
||||
"bald": "Glatze",
|
||||
"other": "Andere"
|
||||
},
|
||||
"skincolor": {
|
||||
"light": "Hell",
|
||||
"medium": "Mittel",
|
||||
"dark": "Dunkel",
|
||||
"other": "Andere"
|
||||
},
|
||||
"freckles": {
|
||||
"much": "Viele",
|
||||
"medium": "Mittel",
|
||||
"less": "Wenige",
|
||||
"none": "Keine"
|
||||
}
|
||||
},
|
||||
"interestedInGender": "Interessiert an",
|
||||
"hasChildren": "Hat Kinder",
|
||||
"smokes": "Rauchen",
|
||||
"drinks": "Alkohol",
|
||||
"willChildren": "Will Kinder",
|
||||
"sexualpreference": "Sexuelle Ausrichtung",
|
||||
"pubichair": "Schamhaare",
|
||||
"penislength": "Penislänge",
|
||||
"brasize": "BH-Größe",
|
||||
"piercings": "Piercings",
|
||||
"tattoos": "Tattoos",
|
||||
"language": "Sprache",
|
||||
"gender": "Geschlecht",
|
||||
"eyecolor": "Augenfarbe",
|
||||
"haircolor": "Haarfarbe",
|
||||
"hairlength": "Haarlänge",
|
||||
"freckles": "Sommersprossen",
|
||||
"skincolor": "Hautfarbe",
|
||||
"birthdate": "Geburtsdatum",
|
||||
"age": "Alter",
|
||||
"town": "Stadt",
|
||||
"bodyheight": "Größe",
|
||||
"weight": "Gewicht"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
frontend/src/i18n/locales/en/socialnetwork.json
Normal file
3
frontend/src/i18n/locales/en/socialnetwork.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
@@ -4,11 +4,13 @@ import HomeView from '../views/HomeView.vue';
|
||||
import ActivateView from '../views/auth/ActivateView.vue';
|
||||
import PeronalSettingsView from '../views/settings/PersonalView.vue';
|
||||
import ViewSettingsView from '../views/settings/ViewView.vue';
|
||||
import FlirtSettingsView from '../views/settings/FlirtView.vue';
|
||||
import SexualitySettingsView from '../views/settings/SexualityView.vue';
|
||||
import AccountSettingsView from '../views/settings/AccountView.vue';
|
||||
import InterestsView from '../views/settings/InterestsView.vue';
|
||||
import AdminInterestsView from '../views/admin/InterestsView.vue';
|
||||
import AdminContactsView from '../views/admin/ContactsView.vue';
|
||||
import SearchView from '../views/social/SearchView.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -21,6 +23,12 @@ const routes = [
|
||||
name: 'Activate page',
|
||||
component: ActivateView
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/search',
|
||||
name: 'Search users',
|
||||
component: SearchView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/settings/personal',
|
||||
name: 'Personal settings',
|
||||
@@ -39,6 +47,12 @@ const routes = [
|
||||
component: SexualitySettingsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/settings/flirt',
|
||||
name: 'Flirt settings',
|
||||
component: FlirtSettingsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/settings/account',
|
||||
name: 'Account settings',
|
||||
@@ -67,7 +81,7 @@ const routes = [
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,23 @@ const mutations = {
|
||||
dialog.dialog.toggleMinimize();
|
||||
}
|
||||
minimizing = false;
|
||||
},
|
||||
updateDialogTitle(state, { name, newTitle, isTitleTranslated }) {
|
||||
const dialogIndex = state.openDialogs.findIndex((d) => d.dialog.name === name);
|
||||
if (dialogIndex !== -1) {
|
||||
// Update dialog object reactively
|
||||
const updatedDialog = {
|
||||
...state.openDialogs[dialogIndex],
|
||||
dialog: {
|
||||
...state.openDialogs[dialogIndex].dialog,
|
||||
localTitle: newTitle,
|
||||
isTitleTranslated: isTitleTranslated
|
||||
}
|
||||
};
|
||||
|
||||
// Replace the old dialog with the updated one
|
||||
state.openDialogs.splice(dialogIndex, 1, updatedDialog);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,7 +55,10 @@ const actions = {
|
||||
},
|
||||
toggleDialogMinimize({ commit }, dialogName) {
|
||||
commit('toggleDialogMinimize', dialogName);
|
||||
}
|
||||
},
|
||||
updateDialogTitle({ commit }, { name, newTitle, isTitleTranslated }) {
|
||||
commit('updateDialogTitle', { name, newTitle, isTitleTranslated });
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -2,7 +2,7 @@ import axios from 'axios';
|
||||
import store from '../store';
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:3001',
|
||||
baseURL: import.meta.env.VUE_APP_API_BASE_URL || 'http://localhost:3001',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
<td>{{ formatDateTimeLong(contact.createdAt) }}</td>
|
||||
<td>{{ contact.email }}</td>
|
||||
<td>
|
||||
<button @clicked="openRequest(contact)">{{ $t('admin.contacts.open') }}</button>
|
||||
<button @clicked="finishRequest(contact)">{{ $t('admin.contacts.finished') }}</button>
|
||||
<button @click="openRequest(contact)">{{ $t('admin.contacts.open') }}</button>
|
||||
<button @click="finishRequest(contact)">{{ $t('admin.contacts.finished') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ErrorDialog ref="errorDialog" />
|
||||
<AnswerContact ref="answerContactDialog" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -29,11 +30,13 @@ import apiClient from '@/utils/axios.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
|
||||
import { formatDateTimeLong } from '@/utils/datetime.js';
|
||||
import AnswerContact from '../../dialogues/admin/AnswerContact.vue';
|
||||
|
||||
export default {
|
||||
name: 'AdminContactsView',
|
||||
components: {
|
||||
ErrorDialog
|
||||
ErrorDialog,
|
||||
AnswerContact,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -54,10 +57,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async openRequest(contact) {
|
||||
|
||||
this.$refs.answerContactDialog.open(contact);
|
||||
},
|
||||
async finishRequest(contact) {
|
||||
|
||||
await apiClient.get('/api/admin/opencontacts/finish/${contact.id}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
frontend/src/views/settings/FlirtView.vue
Normal file
17
frontend/src/views/settings/FlirtView.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>{{ $t("settings.flirt.title") }}</h2>
|
||||
<SettingsWidget :settingsType="'flirt'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsWidget from '@/components/SettingsWidget.vue';
|
||||
|
||||
export default {
|
||||
name: 'FlirtSettingsView',
|
||||
components: {
|
||||
SettingsWidget,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
206
frontend/src/views/social/SearchView.vue
Normal file
206
frontend/src/views/social/SearchView.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div class="search-view">
|
||||
<h2>{{ $t('socialnetwork.usersearch.title') }}</h2>
|
||||
<form @submit.prevent="performSearch">
|
||||
<div class="form-group">
|
||||
<label for="username">{{ $t('socialnetwork.usersearch.username') }}:</label>
|
||||
<input type="text" id="username" v-model="searchCriteria.username"
|
||||
:placeholder="$t('socialnetwork.usersearch.username')" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ageFrom">{{ $t('socialnetwork.usersearch.age_from') }}:</label>
|
||||
<input type="number" id="ageFrom" v-model="searchCriteria.ageFrom" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_from')" class="age-input" />
|
||||
<label for="ageTo">{{ $t('socialnetwork.usersearch.age_to') }}:</label>
|
||||
<input type="number" id="ageTo" v-model="searchCriteria.ageTo" :min="14" :max="150"
|
||||
:placeholder="$t('socialnetwork.usersearch.age_to')" class="age-input" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="gender">{{ $t('socialnetwork.usersearch.gender') }}:</label>
|
||||
<multiselect v-model="searchCriteria.gender" :options="genderOptions" :multiple="true"
|
||||
:close-on-select="false" :placeholder="$t('socialnetwork.usersearch.gender')" label="name"
|
||||
track-by="name" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="search-button">{{ $t('socialnetwork.usersearch.search_button') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="search-results" v-if="searchResults.length">
|
||||
<h3>{{ $t('socialnetwork.usersearch.results_title') }}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.nick") }}</th>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.gender") }}</th>
|
||||
<th>{{ $t("socialnetwork.usersearch.result.age") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="result in searchResults" :key="result.id">
|
||||
<td><span @click.prevent="openUserProfile(result.id)" :class="'clickable g-' + result.gender">{{ result.username }}</span></td>
|
||||
<td>{{ result.gender }}</td>
|
||||
<td>{{ result.age }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="no-results">
|
||||
{{ $t('socialnetwork.usersearch.no_results') }}
|
||||
</div>
|
||||
</div>
|
||||
<UserProfileDialog ref="userProfileDialog" :username="selectedUsername" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import UserProfileDialog from '@/dialogues/socialnetwork/UserProfileDialog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Multiselect,
|
||||
UserProfileDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchCriteria: {
|
||||
username: '',
|
||||
ageFrom: 14,
|
||||
ageTo: 150,
|
||||
gender: []
|
||||
},
|
||||
genderOptions: [],
|
||||
searchResults: []
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadGenderOptions();
|
||||
},
|
||||
methods: {
|
||||
async loadGenderOptions() {
|
||||
try {
|
||||
const response = await apiClient.post('/api/settings/getparamvalues', {
|
||||
type: 'gender'
|
||||
});
|
||||
this.genderOptions = response.data.map(g => ({ name: g.name, value: g.value }));
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Geschlechtsoptionen:', error);
|
||||
}
|
||||
},
|
||||
async performSearch() {
|
||||
const searchCriteria = {
|
||||
username: this.searchCriteria.username,
|
||||
ageFrom: this.searchCriteria.ageFrom,
|
||||
ageTo: this.searchCriteria.ageTo,
|
||||
gender: this.searchCriteria.gender.map(g => g.value)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/socialnetwork/usersearch', searchCriteria);
|
||||
this.searchResults = response.data;
|
||||
} catch (error) {
|
||||
console.error('Fehler bei der Suche:', error);
|
||||
}
|
||||
},
|
||||
openUserProfile(id) {
|
||||
this.$refs.userProfileDialog.userId = id;
|
||||
this.$refs.userProfileDialog.open();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-view {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 120px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
input,
|
||||
.multiselect__input {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.age-input {
|
||||
width: 70px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.search-results ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-results li {
|
||||
padding: 8px;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0.5em 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
thead {
|
||||
color: #7BBE55;
|
||||
}
|
||||
th, td {
|
||||
padding-right: 1em;
|
||||
|
||||
}
|
||||
|
||||
th, td:not:last-child {
|
||||
border-bottom: 1px solid #7E471B;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.g-male {
|
||||
color: #3377ff;
|
||||
}
|
||||
|
||||
.g-female {
|
||||
color: #ff3377;
|
||||
}
|
||||
</style>
|
||||
27
frontend/vite.config.js
Normal file
27
frontend/vite.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
|
||||
import { NodeModulesPolyfillPlugin } from '@esbuild-plugins/node-modules-polyfill';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
plugins: [
|
||||
NodeGlobalsPolyfillPlugin({
|
||||
buffer: true
|
||||
}),
|
||||
NodeModulesPolyfillPlugin()
|
||||
]
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
stream: 'stream-browserify',
|
||||
util: 'util',
|
||||
assert: 'assert',
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
webSocketURL: 'ws://localhost:8080/ws',
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -7,7 +7,7 @@
|
||||
"start:backend": "cd backend && node server.js",
|
||||
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
||||
"dev:backend": "cd backend && nodemon server.js",
|
||||
"dev:frontend": "cd frontend && npm run serve"
|
||||
"dev:frontend": "cd frontend && npm run dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^7.0.0",
|
||||
|
||||
Reference in New Issue
Block a user