Finished guestbook and gallery. started diary

This commit is contained in:
Torsten Schulz
2024-09-27 07:40:06 +02:00
parent a2ee66c9de
commit c31be3f879
34 changed files with 2298 additions and 185 deletions

View File

@@ -13,6 +13,16 @@ class SocialNetworkController {
this.getFolderImageList = this.getFolderImageList.bind(this); this.getFolderImageList = this.getFolderImageList.bind(this);
this.getImageByHash = this.getImageByHash.bind(this); this.getImageByHash = this.getImageByHash.bind(this);
this.changeImage = this.changeImage.bind(this); this.changeImage = this.changeImage.bind(this);
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
this.deleteFolder = this.deleteFolder.bind(this);
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
this.getGuestbookEntries = this.getGuestbookEntries.bind(this);
this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this);
this.getGuestbookImage = this.getGuestbookImage.bind(this);
this.createDiaryEntry = this.createDiaryEntry.bind(this);
this.updateDiaryEntry = this.updateDiaryEntry.bind(this);
this.deleteDiaryEntry = this.deleteDiaryEntry.bind(this);
this.getDiaryEntries = this.getDiaryEntries.bind(this);
} }
async userSearch(req, res) { async userSearch(req, res) {
@@ -45,7 +55,8 @@ class SocialNetworkController {
try { try {
const userId = req.headers.userid; const userId = req.headers.userid;
const folderData = req.body; const folderData = req.body;
const folder = await this.socialNetworkService.createFolder(userId, folderData); const { folderId } = req.params;
const folder = await this.socialNetworkService.createFolder(userId, folderData, folderId);
res.status(201).json(folder); res.status(201).json(folder);
} catch (error) { } catch (error) {
console.error('Error in createFolder:', error); console.error('Error in createFolder:', error);
@@ -115,7 +126,6 @@ class SocialNetworkController {
const userId = req.headers.userid; const userId = req.headers.userid;
const { hash } = req.params; const { hash } = req.params;
const filePath = await this.socialNetworkService.getImageFilePath(userId, hash); const filePath = await this.socialNetworkService.getImageFilePath(userId, hash);
console.log(filePath);
res.sendFile(filePath, err => { res.sendFile(filePath, err => {
if (err) { if (err) {
console.error('Error sending file:', err); console.error('Error sending file:', err);
@@ -141,6 +151,141 @@ class SocialNetworkController {
res.status(403).json({ error: error.message || 'Access denied or image not found' }); res.status(403).json({ error: error.message || 'Access denied or image not found' });
} }
} }
async getFoldersByUsername(req, res) {
try {
const { username } = req.params;
const requestingUserId = req.headers.userid;
if (!username || !requestingUserId) {
return res.status(400).json({ error: 'Invalid username or requesting user ID.' });
}
const folders = await this.socialNetworkService.getFoldersByUsername(username, requestingUserId);
if (!folders) {
return res.status(404).json({ error: 'No folders found or access denied.' });
}
res.status(200).json(folders);
} catch (error) {
console.error('Error in getFoldersByUsername:', error);
res.status(500).json({ error: error.message });
}
}
async deleteFolder(req, res) {
try {
const userId = req.headers.userid;
const { folderId } = req.params;
await this.socialNetworkService.deleteFolder(userId, folderId);
res.status(204).send();
} catch (error) {
console.error('Error in deleteFolder:', error);
res.status(500).json({ error: error.message });
}
}
async createGuestbookEntry(req, res) {
try {
const { htmlContent, recipientName } = req.body;
const hashedSenderId = req.headers.userid;
const image = req.file ? req.file : null;
const entry = await this.socialNetworkService.createGuestbookEntry(
hashedSenderId,
recipientName,
htmlContent,
image
);
res.status(201).json(entry);
} catch (error) {
console.error('Error in createGuestbookEntry:', error);
res.status(500).json({ error: error.message });
}
}
async getGuestbookEntries(req, res) {
try {
const hashedUserId = req.headers.userid;
const { username, page = 1 } = req.params;
const entries = await this.socialNetworkService.getGuestbookEntries(hashedUserId, username, page);
res.status(200).json(entries);
} catch (error) {
console.error('Error in getGuestbookEntries:', error);
res.status(500).json({ error: error.message });
}
}
async deleteGuestbookEntry(req, res) {
try {
const hashedUserId = req.headers.userid;
const { entryId } = req.params;
await this.socialNetworkService.deleteGuestbookEntry(hashedUserId, entryId);
res.status(200).json({ message: 'Entry deleted successfully' });
} catch (error) {
console.error('Error in deleteGuestbookEntry:', error);
res.status(500).json({ error: error.message });
}
}
async getGuestbookImage(req, res) {
try {
const userId = req.headers.userid;
const { guestbookUserName, entryId } = req.params;
const filePath = await this.socialNetworkService.getGuestbookImageFilePath(userId, guestbookUserName, entryId);
res.sendFile(filePath, err => {
if (err) {
console.error('Error sending file:', err);
res.status(500).json({ error: 'Error sending file' });
}
});
} catch (error) {
console.error('Error in getImageByHash:', error);
res.status(403).json({ error: error.message || 'Access denied or image not found' });
}
}
async createDiaryEntry(req, res) {
try {
const { userId, text } = req.body;
const entry = await this.socialNetworkService.createDiaryEntry(userId, text);
res.status(201).json(entry);
} catch (error) {
console.error('Error in createDiaryEntry:', error);
res.status(500).json({ error: error.message });
}
}
async updateDiaryEntry(req, res) {
try {
const { diaryId } = req.params;
const { userId, text } = req.body;
const updatedEntry = await this.socialNetworkService.updateDiaryEntry(diaryId, userId, text);
res.status(200).json(updatedEntry);
} catch (error) {
console.error('Error in updateDiaryEntry:', error);
res.status(500).json({ error: error.message });
}
}
async deleteDiaryEntry(req, res) {
try {
const { diaryId } = req.params;
const { userId } = req.body;
const result = await this.socialNetworkService.deleteDiaryEntry(diaryId, userId);
res.status(200).json({ message: 'Entry deleted successfully', result });
} catch (error) {
console.error('Error in deleteDiaryEntry:', error);
res.status(500).json({ error: error.message });
}
}
async getDiaryEntries(req, res) {
try {
const { userId } = req.params;
const entries = await this.socialNetworkService.getDiaryEntries(userId);
res.status(200).json(entries);
} catch (error) {
console.error('Error in getDiaryEntries:', error);
res.status(500).json({ error: error.message });
}
}
} }
export default SocialNetworkController; export default SocialNetworkController;

View File

@@ -17,6 +17,7 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js'; import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.js'; import ImageImageVisibility from './community/image_image_visibility.js';
import FolderVisibilityUser from './community/folder_visibility_user.js'; import FolderVisibilityUser from './community/folder_visibility_user.js';
import GuestbookEntry from './community/guestbook.js';
export default function setupAssociations() { export default function setupAssociations() {
SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' }); SettingsType.hasMany(UserParamType, { foreignKey: 'settingsId', as: 'user_param_types' });
@@ -95,4 +96,25 @@ export default function setupAssociations() {
foreignKey: 'visibilityUserId', foreignKey: 'visibilityUserId',
otherKey: 'folderId' otherKey: 'folderId'
}); });
User.hasMany(GuestbookEntry, {
foreignKey: 'recipientId',
as: 'receivedEntries'
});
User.hasMany(GuestbookEntry, {
foreignKey: 'senderId',
as: 'sentEntries'
});
GuestbookEntry.belongsTo(User, {
foreignKey: 'recipientId',
as: 'recipient'
});
GuestbookEntry.belongsTo(User, {
foreignKey: 'senderId',
as: 'sender'
});
} }

View File

@@ -0,0 +1,31 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class Diary extends Model { }
Diary.init({
userId: {
type: DataTypes.INTEGER,
allowNull: false,
},
text: {
type: DataTypes.TEXT,
allowNull: false,
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
updatedAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
}
}, {
sequelize,
modelName: 'Diary',
tableName: 'diary',
schema: 'community',
timestamps: true,
});
export default Diary;

View File

@@ -0,0 +1,35 @@
import { Model, DataTypes } from 'sequelize';
import { sequelize } from '../../utils/sequelize.js';
class DiaryHistory extends Model { }
DiaryHistory.init({
diaryId: {
type: DataTypes.INTEGER,
allowNull: false,
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
},
oldText: {
type: DataTypes.TEXT,
allowNull: false,
},
oldCreatedAt: {
type: DataTypes.DATE,
allowNull: false,
},
oldUpdatedAt: {
type: DataTypes.DATE,
allowNull: false,
},
}, {
sequelize,
modelName: 'DiaryHistory',
tableName: 'diary_history',
schema: 'community',
timestamps: false,
});
export default DiaryHistory;

View File

@@ -0,0 +1,47 @@
import { sequelize } from '../../utils/sequelize.js';
import { DataTypes } from 'sequelize';
import User from './user.js';
const GuestbookEntry = sequelize.define('guestbook_entry', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
recipientId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: User,
key: 'id'
}
},
senderId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: User,
key: 'id'
}
},
senderUsername: {
type: DataTypes.STRING,
allowNull: true,
},
contentHtml: {
type: DataTypes.TEXT,
allowNull: false,
},
imageUrl: {
type: DataTypes.STRING,
allowNull: true,
},
}, {
tableName: 'guestbook_entry',
schema: 'community',
timestamps: true,
underscored: true,
});
export default GuestbookEntry;

View File

@@ -19,6 +19,9 @@ import ImageVisibilityUser from './community/image_visibility_user.js';
import FolderImageVisibility from './community/folder_image_visibility.js'; import FolderImageVisibility from './community/folder_image_visibility.js';
import ImageImageVisibility from './community/image_image_visibility.js'; import ImageImageVisibility from './community/image_image_visibility.js';
import FolderVisibilityUser from './community/folder_visibility_user.js'; import FolderVisibilityUser from './community/folder_visibility_user.js';
import GuestbookEntry from './community/guestbook.js';
import DiaryHistory from './community/diary_history.js';
import Diary from './community/diary.js';
const models = { const models = {
SettingsType, SettingsType,
@@ -42,6 +45,9 @@ const models = {
FolderImageVisibility, FolderImageVisibility,
ImageImageVisibility, ImageImageVisibility,
FolderVisibilityUser, FolderVisibilityUser,
GuestbookEntry,
DiaryHistory,
Diary,
}; };
export default models; export default models;

View File

@@ -22,23 +22,43 @@ export async function createTriggers() {
`; `;
const createInsertTrigger = ` const createInsertTrigger = `
CREATE TRIGGER trigger_create_user_param_visibility CREATE OR REPLACE TRIGGER trigger_create_user_param_visibility
AFTER INSERT ON community.user_param AFTER INSERT ON community.user_param
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION create_user_param_visibility_trigger(); EXECUTE FUNCTION create_user_param_visibility_trigger();
`; `;
const createUpdateTrigger = ` const createUpdateTrigger = `
CREATE TRIGGER trigger_update_user_param_visibility CREATE OR REPLACE TRIGGER trigger_update_user_param_visibility
AFTER UPDATE ON community.user_param AFTER UPDATE ON community.user_param
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION create_user_param_visibility_trigger(); EXECUTE FUNCTION create_user_param_visibility_trigger();
`; `;
const createDiaryHistoryTriggerFunction = `
CREATE OR REPLACE FUNCTION insert_diary_history()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO community.diary_history (diaryId, userId, oldText, oldCreatedAt, oldUpdatedAt)
VALUES (OLD.id, OLD.userId, OLD.text, OLD.createdAt, OLD.updatedAt);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
`;
const createDiaryHistoryTrigger = `
CREATE OR REPLACE TRIGGER diary_update_trigger
BEFORE UPDATE ON community.diary
FOR EACH ROW
EXECUTE FUNCTION insert_diary_history();
`;
try { try {
await sequelize.query(createTriggerFunction); await sequelize.query(createTriggerFunction);
await sequelize.query(createInsertTrigger); await sequelize.query(createInsertTrigger);
await sequelize.query(createUpdateTrigger); await sequelize.query(createUpdateTrigger);
await sequelize.query(createDiaryHistoryTriggerFunction);
await sequelize.query(createDiaryHistoryTrigger);
console.log('Triggers created successfully'); console.log('Triggers created successfully');
} catch (error) { } catch (error) {

File diff suppressed because it is too large Load Diff

View File

@@ -14,16 +14,19 @@
"amqplib": "^0.10.4", "amqplib": "^0.10.4",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dompurify": "^3.1.7",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"joi": "^17.13.3", "joi": "^17.13.3",
"jsdom": "^25.0.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.10.3", "mysql2": "^3.10.3",
"nodemailer": "^6.9.14", "nodemailer": "^6.9.14",
"pg": "^8.12.0", "pg": "^8.12.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",
"sharp": "^0.33.5",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"uuid": "^10.0.0" "uuid": "^10.0.0"
}, },

View File

@@ -8,8 +8,8 @@ const router = express.Router();
const socialNetworkController = new SocialNetworkController(); const socialNetworkController = new SocialNetworkController();
router.post('/usersearch', authenticate, socialNetworkController.userSearch); router.post('/usersearch', authenticate, socialNetworkController.userSearch);
router.get('/profile/:userId', authenticate, socialNetworkController.profile); router.get('/profile/main/:userId', authenticate, socialNetworkController.profile);
router.post('/folders', authenticate, socialNetworkController.createFolder); router.post('/folders/:folderId', authenticate, socialNetworkController.createFolder);
router.get('/folders', authenticate, socialNetworkController.getFolders); router.get('/folders', authenticate, socialNetworkController.getFolders);
router.get('/folder/:folderId', authenticate, socialNetworkController.getFolderImageList); router.get('/folder/:folderId', authenticate, socialNetworkController.getFolderImageList);
router.post('/images', authenticate, upload.single('image'), socialNetworkController.uploadImage); router.post('/images', authenticate, upload.single('image'), socialNetworkController.uploadImage);
@@ -17,5 +17,15 @@ router.get('/images/:imageId', authenticate, socialNetworkController.getImage);
router.put('/images/:imageId', authenticate, socialNetworkController.changeImage); router.put('/images/:imageId', authenticate, socialNetworkController.changeImage);
router.get('/imagevisibilities', authenticate, socialNetworkController.getImageVisibilityTypes); router.get('/imagevisibilities', authenticate, socialNetworkController.getImageVisibilityTypes);
router.get('/image/:hash', authenticate, socialNetworkController.getImageByHash); router.get('/image/:hash', authenticate, socialNetworkController.getImageByHash);
router.get('/profile/images/folders/:username', authenticate, socialNetworkController.getFoldersByUsername);
router.delete('/folders/:folderId', authenticate, socialNetworkController.deleteFolder);
router.post('/guestbook/entries', authenticate, upload.single('image'), socialNetworkController.createGuestbookEntry);
router.get('/guestbook/entries/:username/:page', authenticate, socialNetworkController.getGuestbookEntries);
router.delete('/guestbook/entries/:entryId', authenticate, socialNetworkController.deleteGuestbookEntry);
router.get('/guestbook/image/:guestbookUserName/:entryId', authenticate, socialNetworkController.getGuestbookImage);
router.post('/diary', authenticate, socialNetworkController.createDiaryEntry);
router.put('/diary/:diaryId', authenticate, socialNetworkController.updateDiaryEntry);
router.delete('/diary/:diaryId', authenticate, socialNetworkController.deleteDiaryEntry);
router.get('/diary/:userId', authenticate, socialNetworkController.getDiaryEntries);
export default router; export default router;

View File

@@ -51,6 +51,18 @@ class BaseService {
const ageDate = new Date(ageDifMs); const ageDate = new Date(ageDifMs);
return Math.abs(ageDate.getUTCFullYear() - 1970); return Math.abs(ageDate.getUTCFullYear() - 1970);
} }
async isUserAdult(userId) {
const birthdateParam = await this.getUserParams(userId, ['birthdate']);
if (!birthdateParam || birthdateParam.length === 0) {
return false;
}
const birthdate = birthdateParam[0].value;
const age = this.calculateAge(birthdate);
return age >= 18;
}
} }
export default BaseService; export default BaseService;

View File

@@ -11,10 +11,17 @@ import Image from '../models/community/image.js';
import ImageVisibilityType from '../models/type/image_visibility.js'; import ImageVisibilityType from '../models/type/image_visibility.js';
import FolderImageVisibility from '../models/community/folder_image_visibility.js'; import FolderImageVisibility from '../models/community/folder_image_visibility.js';
import ImageImageVisibility from '../models/community/image_image_visibility.js'; import ImageImageVisibility from '../models/community/image_image_visibility.js';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import fs from 'fs'; import fs from 'fs';
import fsPromises from 'fs/promises';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import FolderVisibilityUser from '../models/community/folder_visibility_user.js';
import ImageVisibilityUser from '../models/community/image_visibility_user.js';
import GuestbookEntry from '../models/community/guestbook.js';
import { JSDOM } from 'jsdom';
import DOMPurify from 'dompurify';
import sharp from 'sharp';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -33,22 +40,39 @@ class SocialNetworkService extends BaseService {
return this.constructUserProfile(user, requestingUserId); return this.constructUserProfile(user, requestingUserId);
} }
async createFolder(hashedUserId, data) { async createFolder(hashedUserId, data, folderId) {
await this.checkUserAccess(hashedUserId); await this.checkUserAccess(hashedUserId);
const user = await User.findOne({ const user = await User.findOne({
hashedId: hashedUserId where: { hashedId: hashedUserId }
}); });
const parentFolder = Folder.findOne({ if (!user) {
id: data.parentId, throw new Error('User not found');
userId: user.id
});
if (!parentFolder) {
throw new Error('foldernotfound');
} }
const newFolder = await Folder.create({ const parentFolder = data.parentId ? await Folder.findOne({
parentId: data.parentId, where: { id: data.parentId, userId: user.id }
userId: user.id, }) : null;
name: data.name if (data.parentId && !parentFolder) {
throw new Error('Parent folder not found');
}
let newFolder;
if (folderId === 0) {
newFolder = await Folder.create({
parentId: data.parentId || null,
userId: user.id,
name: data.name
});
} else {
newFolder = await Folder.findOne({
where: { id: folderId, userId: user.id }
});
if (!newFolder) {
throw new Error('Folder not found or user does not own the folder');
}
newFolder.name = data.name;
await newFolder.save();
}
await FolderImageVisibility.destroy({
where: { folderId: newFolder.id }
}); });
for (const visibilityId of data.visibilities) { for (const visibilityId of data.visibilities) {
await FolderImageVisibility.create({ await FolderImageVisibility.create({
@@ -61,32 +85,63 @@ class SocialNetworkService extends BaseService {
async getFolders(hashedId) { async getFolders(hashedId) {
const userId = await this.checkUserAccess(hashedId); const userId = await this.checkUserAccess(hashedId);
let rootFolder = await Folder.findOne({ where: { parentId: null, userId } }); let rootFolder = await Folder.findOne({
where: { parentId: null, userId },
include: [{
model: ImageVisibilityType,
through: { model: FolderImageVisibility },
attributes: ['id'],
}]
});
if (!rootFolder) { if (!rootFolder) {
const user = await User.findOne({ where: { id: userId } }); const user = await User.findOne({ where: { id: userId } });
const visibility = await ImageVisibilityType.findOne({ const visibility = await ImageVisibilityType.findOne({
where: { where: { description: 'everyone' }
description: 'everyone'
}
}); });
rootFolder = await Folder.create({ rootFolder = await Folder.create({
name: user.username, name: user.username,
parentId: null, parentId: null,
userId, userId
});
await FolderImageVisibility.create({
folderId: rootFolder.id,
visibilityTypeId: visibility.id visibilityTypeId: visibility.id
}); });
rootFolder = await Folder.findOne({
where: { id: rootFolder.id },
include: [{
model: ImageVisibilityType,
through: { model: FolderImageVisibility },
attributes: ['id'],
}]
});
} }
const children = await this.getSubFolders(rootFolder.id, userId); const children = await this.getSubFolders(rootFolder.id, userId);
rootFolder = rootFolder.get(); rootFolder = rootFolder.get();
rootFolder.visibilityTypeIds = rootFolder.image_visibility_types.map(v => v.id);
delete rootFolder.image_visibility_types;
rootFolder.children = children; rootFolder.children = children;
return rootFolder; return rootFolder;
} }
async getSubFolders(parentId, userId) { async getSubFolders(parentId, userId) {
const folders = await Folder.findAll({ where: { parentId, userId } }); const folders = await Folder.findAll({
where: { parentId, userId },
include: [{
model: ImageVisibilityType,
through: { model: FolderImageVisibility },
attributes: ['id'],
}],
order: [
['name', 'asc']
]
});
for (const folder of folders) { for (const folder of folders) {
const children = await this.getSubFolders(folder.id, userId); const children = await this.getSubFolders(folder.id, userId);
const visibilityTypeIds = folder.image_visibility_types.map(v => v.id);
folder.setDataValue('visibilityTypeIds', visibilityTypeIds);
folder.setDataValue('children', children); folder.setDataValue('children', children);
folder.setDataValue('image_visibility_types', undefined);
} }
return folders.map(folder => folder.get()); return folders.map(folder => folder.get());
} }
@@ -112,32 +167,56 @@ class SocialNetworkService extends BaseService {
async uploadImage(hashedId, file, formData) { async uploadImage(hashedId, file, formData) {
const userId = await this.getUserId(hashedId); const userId = await this.getUserId(hashedId);
const newFileName = this.generateUniqueFileName(file.originalname); const processedImageName = await this.processAndUploadUserImage(file);
const filePath = this.buildFilePath(newFileName); const newImage = await this.createImageRecord(formData, userId, file, processedImageName);
await this.saveFile(file.buffer, filePath);
const newImage = await this.createImageRecord(formData, userId, file, newFileName);
await this.saveImageVisibilities(newImage.id, formData.visibility); await this.saveImageVisibilities(newImage.id, formData.visibility);
return newImage; return newImage;
} }
async processAndUploadUserImage(file) {
try {
const img = sharp(file.buffer);
const metadata = await img.metadata();
if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) {
throw new Error('File is not a valid image');
}
if (metadata.width < 75 || metadata.height < 75) {
throw new Error('Image dimensions are too small. Minimum size is 75x75 pixels.');
}
const resizedImg = img.resize({
width: Math.min(metadata.width, 500),
height: Math.min(metadata.height, 500),
fit: sharp.fit.inside,
withoutEnlargement: true
});
const newFileName = this.generateUniqueFileName(file.originalname);
const filePath = this.buildFilePath(newFileName, 'user');
await resizedImg.toFile(filePath);
return newFileName;
} catch (error) {
throw new Error(`Failed to process image: ${error.message}`);
}
}
async getUserId(hashedId) { async getUserId(hashedId) {
return await this.checkUserAccess(hashedId); return await this.checkUserAccess(hashedId);
} }
generateUniqueFileName(originalFileName) { generateUniqueFileName(originalFileName) {
const uniqueHash = uuidv4(); const uniqueHash = uuidv4();
return `${uniqueHash}`; return uniqueHash;
} }
buildFilePath(fileName) { buildFilePath(fileName, type) {
const userImagesPath = path.join(__dirname, '../images/user'); const basePath = path.join(__dirname, '..', 'images', type);
return path.join(userImagesPath, fileName); return path.join(basePath, fileName);
} }
async saveFile(buffer, filePath) { async saveFile(buffer, filePath) {
try { try {
await fs.mkdir(path.dirname(filePath), { recursive: true }); await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, buffer); await fsPromises.writeFile(filePath, buffer);
} catch (error) { } catch (error) {
throw new Error(`Failed to save file: ${error.message}`); throw new Error(`Failed to save file: ${error.message}`);
} }
@@ -273,9 +352,15 @@ class SocialNetworkService extends BaseService {
}); });
} }
async constructUserProfile(user, requestingUserId) { async constructUserProfile(user, hashedUserId) {
const userParams = {}; const userParams = {};
const requestingUserAge = await this.getUserAge(requestingUserId); const requestingUser = await User.findOne({
where: { hashedId: hashedUserId },
});
if (!requestingUser) {
throw new Error('User not found');
}
const requestingUserAge = await this.getUserAge(requestingUser.id);
for (const param of user.user_params) { for (const param of user.user_params) {
const visibility = param.param_visibilities?.[0]?.visibility_type?.description || 'Invisible'; const visibility = param.param_visibilities?.[0]?.visibility_type?.description || 'Invisible';
if (visibility === 'Invisible') continue; if (visibility === 'Invisible') continue;
@@ -340,9 +425,9 @@ class SocialNetworkService extends BaseService {
if (!hasAccess) { if (!hasAccess) {
throw new Error('Access denied'); throw new Error('Access denied');
} }
const imagePath = this.buildFilePath(image.hash); const imagePath = this.buildFilePath(image.hash, 'user');
if (!fs.existsSync(imagePath)) { if (!fs.existsSync(imagePath)) {
throw new Error('File not found'); throw new Error(`File "${imagePath}" not found`);
} }
return imagePath; return imagePath;
} }
@@ -350,7 +435,7 @@ class SocialNetworkService extends BaseService {
async checkUserImageAccess(userId, imageId) { async checkUserImageAccess(userId, imageId) {
const image = await Image.findByPk(imageId); const image = await Image.findByPk(imageId);
if (image.userId === userId) { if (image.userId === userId) {
return true; return true;
} }
const accessRules = await ImageImageVisibility.findAll({ const accessRules = await ImageImageVisibility.findAll({
where: { imageId } where: { imageId }
@@ -374,6 +459,258 @@ class SocialNetworkService extends BaseService {
} }
return image.folderId; return image.folderId;
} }
async getFoldersByUsername(username, hashedUserId) {
const user = await User.findOne({ where: { username } });
if (!user) {
throw new Error('User not found');
}
const requestingUserId = await this.checkUserAccess(hashedUserId);
let rootFolder = await Folder.findOne({ where: { parentId: null, userId: user.id } });
if (!rootFolder) {
return null;
}
const accessibleFolders = await this.getAccessibleFolders(rootFolder.id, requestingUserId);
rootFolder = rootFolder.get();
rootFolder.children = accessibleFolders;
return rootFolder;
}
async getAccessibleFolders(folderId, requestingUserId) {
const folderIdString = String(folderId);
const requestingUserIdString = String(requestingUserId);
const requestingUser = await User.findOne({ where: { id: requestingUserIdString } });
const isAdult = this.isUserAdult(requestingUser.id);
const accessibleFolders = await Folder.findAll({
where: { parentId: folderIdString },
include: [
{
model: ImageVisibilityType,
through: { model: FolderImageVisibility },
where: {
[Op.or]: [
{ description: 'everyone' },
{ description: 'adults', ...(isAdult ? {} : { [Op.not]: null }) },
{ description: 'friends-and-adults', ...(isAdult ? {} : { [Op.not]: null }) }
]
},
required: false
},
{
model: ImageVisibilityUser,
through: { model: FolderVisibilityUser },
where: { user_id: requestingUserIdString },
required: false
}
]
});
const folderList = [];
for (let folder of accessibleFolders) {
const children = await this.getAccessibleFolders(folder.id, requestingUserIdString);
folder = folder.get();
folder.children = children;
folderList.push(folder);
}
return folderList;
}
async deleteFolder(hashedUserId, folderId) {
const userId = await this.checkUserAccess(hashedUserId);
const folder = await Folder.findOne({
where: { id: folderId, userId }
});
if (!folder) {
throw new Error('Folder not found or access denied');
}
await FolderImageVisibility.destroy({ where: { folderId: folder.id } });
await folder.destroy();
return true;
}
async createGuestbookEntry(hashedSenderId, recipientName, htmlContent, image) {
const sender = await User.findOne({ where: { hashedId: hashedSenderId } });
if (!sender) {
throw new Error('Sender not found');
}
const recipient = await User.findOne({ where: { username: recipientName } });
if (!recipient) {
throw new Error('Recipient not found');
}
const sanitizedContent = this.sanitizeHtml(htmlContent);
let uploadedImage = null;
if (image) {
uploadedImage = await this.processAndUploadGuestbookImage(image);
}
const entry = await GuestbookEntry.create({
senderId: sender.id,
recipientId: recipient.id,
senderUsername: sender.username,
contentHtml: sanitizedContent,
imageUrl: uploadedImage
});
return entry;
}
sanitizeHtml(htmlContent) {
const window = new JSDOM('').window;
const purify = DOMPurify(window);
const cleanHtml = purify.sanitize(htmlContent, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'br'],
ALLOWED_ATTR: ['href', 'title'],
FORBID_TAGS: ['script', 'style', 'iframe', 'img'],
ALLOWED_URI_REGEXP: /^https?:\/\//,
});
return cleanHtml;
}
async processAndUploadGuestbookImage(image) {
try {
const img = sharp(image.buffer);
const metadata = await img.metadata();
if (!metadata || !['jpeg', 'png', 'webp', 'gif'].includes(metadata.format)) {
throw new Error('File is not a valid image');
}
if (metadata.width < 20 || metadata.height < 20) {
throw new Error('Image dimensions are too small. Minimum size is 20x20 pixels.');
}
const resizedImg = img.resize({
width: Math.min(metadata.width, 500),
height: Math.min(metadata.height, 500),
fit: sharp.fit.inside,
withoutEnlargement: true
});
const newFileName = this.generateUniqueFileName(image.originalname);
const filePath = this.buildFilePath(newFileName, 'guestbook');
await resizedImg.toFile(filePath);
return newFileName;
} catch (error) {
throw new Error(`Failed to process image: ${error.message}`);
}
}
async getGuestbookEntries(hashedUserId, username, page = 1) {
const pageSize = 20;
const offset = (page - 1) * pageSize;
this.checkUserAccess(hashedUserId);
const user = await User.findOne({ where: { username: username } });
if (!user) {
throw new Error('User not found');
}
const entries = await GuestbookEntry.findAndCountAll({
where: { recipientId: user.id },
include: [
{ model: User, as: 'sender', attributes: ['username'] },
],
limit: pageSize,
offset: offset,
order: [['createdAt', 'DESC']]
});
const resultList = entries.rows.map(entry => ({
id: entry.id,
sender: entry.sender ? entry.sender.username : entry.senderUsername,
contentHtml: entry.contentHtml,
withImage: entry.imageUrl !== null,
createdAt: entry.createdAt
}));
return {
entries: resultList,
currentPage: page,
totalPages: Math.ceil(entries.count / pageSize),
};
}
async getGuestbookImageFilePath(hashedUserId, guestbookOwnerName, entryId) {
await this.checkUserAccess(hashedUserId);
const guestbookOwner = await User.findOne({
where: { username: guestbookOwnerName },
});
if (!guestbookOwner) {
throw new Error('usernotfound');
}
const entry = await GuestbookEntry.findOne({
where: { id: entryId, recipientId: guestbookOwner.id },
});
if (!entry) {
throw new Error('entrynotfound');
}
if (!entry.imageUrl) {
console.log(entry);
throw new Error('entryhasnoimage');
}
console.log(`Image URL: ${entry.imageUrl}`);
const imagePath = this.buildFilePath(entry.imageUrl, 'guestbook');
if (!fs.existsSync(imagePath)) {
throw new Error(imagenotfound);
}
return imagePath;
}
async deleteGuestbookEntry(hashedUserId, entryId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) {
throw new Error('User not found');
}
const entry = await GuestbookEntry.findOne({ where: { id: entryId } });
if (!entry) {
throw new Error('Entry not found');
}
if (entry.senderId !== user.id && entry.recipientId !== user.id) {
throw new Error('Not authorized to delete this entry');
}
await entry.destroy();
}
async createDiaryEntry(userId, text) {
const newEntry = await Diary.create({
userId: userId,
text: text,
createdAt: new Date(),
updatedAt: new Date(),
});
return newEntry;
}
async updateDiaryEntry(diaryId, userId, newText) {
const existingEntry = await Diary.findOne({
where: { id: diaryId, userId: userId }
});
if (!existingEntry) {
throw new Error('Diary entry not found or unauthorized access');
}
await DiaryHistory.create({
diaryId: existingEntry.id,
userId: existingEntry.userId,
oldText: existingEntry.text,
oldCreatedAt: existingEntry.createdAt,
oldUpdatedAt: existingEntry.updatedAt,
});
existingEntry.text = newText;
existingEntry.updatedAt = new Date();
await existingEntry.save();
return existingEntry;
}
async deleteDiaryEntry(diaryId, userId) {
const entryToDelete = await Diary.findOne({
where: { id: diaryId, userId: userId }
});
if (!entryToDelete) {
throw new Error('Diary entry not found or unauthorized access');
}
await entryToDelete.destroy();
return true;
}
async getDiaryEntries(userId) {
const entries = await Diary.findAll({
where: { userId: userId },
order: [['createdAt', 'DESC']]
});
return entries;
}
} }
export default SocialNetworkService; export default SocialNetworkService;

View File

@@ -30,4 +30,4 @@ const syncModels = async (models) => {
} }
}; };
export { sequelize, initializeDatabase }; export { sequelize, initializeDatabase, syncModels };

View File

@@ -1,4 +1,4 @@
import { initializeDatabase } from './sequelize.js'; import { initializeDatabase, syncModels } from './sequelize.js';
import initializeTypes from './initializeTypes.js'; import initializeTypes from './initializeTypes.js';
import initializeSettings from './initializeSettings.js'; import initializeSettings from './initializeSettings.js';
import initializeUserRights from './initializeUserRights.js'; import initializeUserRights from './initializeUserRights.js';
@@ -10,10 +10,8 @@ import { createTriggers } from '../models/trigger.js';
const syncDatabase = async () => { const syncDatabase = async () => {
try { try {
await initializeDatabase(); await initializeDatabase();
await syncModels(models);
setupAssociations(); setupAssociations();
for (const model of Object.values(models)) {
await model.sync();
}
createTriggers(); createTriggers();
await initializeSettings(); await initializeSettings();

View File

@@ -4,7 +4,17 @@
<AppNavigation v-if="isLoggedIn && user.active" /> <AppNavigation v-if="isLoggedIn && user.active" />
<AppContent /> <AppContent />
<AppFooter /> <AppFooter />
</div> <AnswerContact ref="answerContactDialog" />
<RandomChatDialog ref="randomChatDialog" />
<CreateFolderDialog ref="createFolderDialog" />
<EditImageDialog ref="editImageDialog" />
<UserProfileDialog ref="userProfileDialog" />
<ChooseDialog ref="chooseDialog" />
<ContactDialog ref="contactDialog" />
<DataPrivacyDialog ref="dataPrivacyDialog" />
<ErrorDialog ref="errorDialog" />
<ImprintDialog ref="imprintDialog" />
<ShowImageDialog ref="showImageDialog" /></div>
</template> </template>
<script> <script>
@@ -13,6 +23,17 @@ import AppHeader from './components/AppHeader.vue';
import AppNavigation from './components/AppNavigation.vue'; import AppNavigation from './components/AppNavigation.vue';
import AppContent from './components/AppContent.vue'; import AppContent from './components/AppContent.vue';
import AppFooter from './components/AppFooter.vue'; import AppFooter from './components/AppFooter.vue';
import AnswerContact from './dialogues/admin/AnswerContact.vue';
import RandomChatDialog from './dialogues/chat/RandomChatDialog.vue';
import CreateFolderDialog from './dialogues/socialnetwork/CreateFolderDialog.vue';
import EditImageDialog from './dialogues/socialnetwork/EditImageDialog.vue';
import UserProfileDialog from './dialogues/socialnetwork/UserProfileDialog.vue';
import ChooseDialog from './dialogues/standard/ChooseDialog.vue';
import ContactDialog from './dialogues/standard/ContactDialog.vue';
import DataPrivacyDialog from './dialogues/standard/DataPrivacyDialog.vue';
import ErrorDialog from './dialogues/standard/ErrorDialog.vue';
import ImprintDialog from './dialogues/standard/ImprintDialog.vue';
import ShowImageDialog from './dialogues/socialnetwork/ShowImageDialog.vue';
export default { export default {
name: 'App', name: 'App',
@@ -26,7 +47,18 @@ export default {
AppHeader, AppHeader,
AppNavigation, AppNavigation,
AppContent, AppContent,
AppFooter AppFooter,
AnswerContact,
RandomChatDialog,
CreateFolderDialog,
EditImageDialog,
UserProfileDialog,
ChooseDialog,
ContactDialog,
DataPrivacyDialog,
ErrorDialog,
ImprintDialog,
ShowImageDialog,
}, },
created() { created() {
this.$store.dispatch('loadLoginState'); this.$store.dispatch('loadLoginState');

View File

@@ -14,37 +14,28 @@
<a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a> <a href="#" @click.prevent="openDataPrivacyDialog">{{ $t('dataPrivacy.button') }}</a>
<a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a> <a href="#" @click.prevent="openContactDialog">{{ $t('contact.button') }}</a>
</div> </div>
<ImprintDialog ref="imprintDialog" name="imprintDialog" />
<DataPrivacyDialog ref="dataPrivacyDialog" name="dataPrivacyDialog" />
<ContactDialog ref="contactDialog" name="contactDialog" />
</footer> </footer>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import ImprintDialog from '../dialogues/standard/ImprintDialog.vue';
import DataPrivacyDialog from '../dialogues/standard/DataPrivacyDialog.vue';
import ContactDialog from '../dialogues/standard/ContactDialog.vue';
export default { export default {
name: 'AppFooter', name: 'AppFooter',
components: { components: {
ImprintDialog,
DataPrivacyDialog,
ContactDialog,
}, },
computed: { computed: {
...mapGetters('dialogs', ['openDialogs']) ...mapGetters('dialogs', ['openDialogs'])
}, },
methods: { methods: {
openImprintDialog() { openImprintDialog() {
this.$refs.imprintDialog.open(); this.$root.$refs.imprintDialog.open();
}, },
openDataPrivacyDialog() { openDataPrivacyDialog() {
this.$refs.dataPrivacyDialog.open(); this.$root.$refs.dataPrivacyDialog.open();
}, },
openContactDialog() { openContactDialog() {
this.$refs.contactDialog.open(); this.$root.$refs.contactDialog.open();
}, },
toggleDialogMinimize(dialogName) { toggleDialogMinimize(dialogName) {
this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName); this.$store.dispatch('dialogs/toggleDialogMinimize', dialogName);

View File

@@ -1,5 +1,5 @@
<template> <template>
<div v-if="visible" :class="['dialog-overlay', { 'non-modal': !modal }]" @click.self="handleOverlayClick"> <div v-if="visible" :class="['dialog-overlay', { 'non-modal': !modal, 'is-active': isActive }]" @click.self="handleOverlayClick">
<div class="dialog" :class="{ minimized: minimized }" <div class="dialog" :class="{ minimized: minimized }"
:style="{ width: dialogWidth, height: dialogHeight, top: dialogTop, left: dialogLeft, position: 'absolute' }" :style="{ width: dialogWidth, height: dialogHeight, top: dialogTop, left: dialogLeft, position: 'absolute' }"
v-if="!minimized" ref="dialog"> v-if="!minimized" ref="dialog">
@@ -75,6 +75,7 @@ export default {
dragOffsetY: 0, dragOffsetY: 0,
localTitle: this.title, localTitle: this.title,
localIsTitleTranslated: this.isTitleTranslated, localIsTitleTranslated: this.isTitleTranslated,
isActive: false,
}; };
}, },
computed: { computed: {
@@ -112,7 +113,7 @@ export default {
}, },
buttonClick(action) { buttonClick(action) {
if (typeof action === 'function') { if (typeof action === 'function') {
action(); // Wenn action eine Funktion ist, rufe sie direkt auf action();
} else { } else {
this.$emit(action); this.$emit(action);
if (action === 'close') { if (action === 'close') {
@@ -132,6 +133,9 @@ export default {
this.minimized = !this.minimized; this.minimized = !this.minimized;
this.$store.dispatch('dialogs/toggleDialogMinimize', this.name); this.$store.dispatch('dialogs/toggleDialogMinimize', this.name);
}, },
isMinimized() {
return this.minimized;
},
startDragging(event) { startDragging(event) {
this.isDragging = true; this.isDragging = true;
const dialog = this.$refs.dialog; const dialog = this.$refs.dialog;
@@ -159,6 +163,9 @@ export default {
isTitleTranslated: this.localIsTitleTranslated isTitleTranslated: this.localIsTitleTranslated
}); });
}, },
setActiveState(newActiveState) {
this.isActive = newActiveState;
}
} }
}; };
</script> </script>
@@ -254,4 +261,7 @@ export default {
color: #7E471B; color: #7E471B;
border: 1px solid #7E471B; border: 1px solid #7E471B;
} }
.is-active {
z-index: 990;
}
</style> </style>

View File

@@ -1,20 +1,25 @@
<template> <template>
<div class="folder-item"> <div class="folder-item">
<span @click="selectFolder" class="folder-name" :class="{ selected: folder.id === selectedFolder?.id }"> <div @click="selectFolder" class="folder-name" :class="{ selected: folder.id === selectedFolder?.id }">
<span v-if="!noActionItems" class="action-items">
<span @click="$emit('edit-folder', folder)" class="icon edit-icon" title="Edit folder"></span>
<span @click="$emit('delete-folder', folder)" class="icon delete-icon" title="Delete folder"></span>
</span>
<template v-for="i in depth"> <template v-for="i in depth">
<span v-if="showPipe(i)" class="marker filler">|</span> <span v-if="showPipe(i)" class="marker filler">|</span>
<span v-else class="marker filler">&nbsp;</span> <span v-else class="marker filler">&nbsp;</span>
</template> </template>
<span v-if="isLastItem" class="end-marker marker"></span> <span v-if="isLastItem" class="end-marker marker"></span>
<span v-else class="marker">&#x251C;</span> <span v-else class="marker">&#x251C;</span>
<span>&nbsp;{{ folder.name }}</span> <span class="folder-name-text">&nbsp;{{ folder.name }}</span>
</span> </div>
<template v-if="folder.children && folder.children.length" class="children"> <template v-if="folder.children && folder.children.length" class="children">
<folder-item v-for="(child, index) in folder.children" :key="child.id" :folder="child" <folder-item v-for="(child, index) in folder.children" :key="child.id" :folder="child"
:selected-folder="selectedFolder" @select-folder="forwardSelectFolderEvent" :selected-folder="selectedFolder" @select-folder="forwardSelectFolderEvent"
:depth="depth + 1" @edit-folder="$emit('edit-folder', $event)" @delete-folder="$emit('delete-folder', $event)"
:isLastItem="index === folder.children.length - 1" :depth="depth + 1" :isLastItem="index === folder.children.length - 1"
:parentsWithChildren="getNewParentsWithChildrenList(index)"> :parentsWithChildren="getNewParentsWithChildrenList(index)" :noActionItems="noActionItems">
</folder-item> </folder-item>
</template> </template>
</div> </div>
@@ -36,6 +41,10 @@ export default {
type: Array, type: Array,
default: () => [] default: () => []
}, },
noActionItems: {
type: Boolean,
default: false
},
selectedFolder: Object, selectedFolder: Object,
}, },
methods: { methods: {
@@ -58,10 +67,6 @@ export default {
</script> </script>
<style scoped> <style scoped>
.folder-name {
cursor: pointer;
}
.selected { .selected {
font-weight: bold; font-weight: bold;
} }
@@ -89,4 +94,25 @@ export default {
.folder-item { .folder-item {
margin: -2px 0; margin: -2px 0;
} }
.folder-actions {
display: flex;
gap: 10px;
}
.icon {
cursor: pointer;
}
.edit-icon {
color: green;
}
.delete-icon {
color: red;
}
.folder-name-text {
cursor: pointer;
}
</style> </style>

View File

@@ -18,7 +18,6 @@
<SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language" <SelectDropdownWidget labelTr="settings.personal.label.language" :v-model="language"
tooltipTr="settings.personal.tooltip.language" :list="languages" :value="language" /> tooltipTr="settings.personal.tooltip.language" :list="languages" :value="language" />
</div> </div>
<ErrorDialog ref="errorDialog" />
</DialogWidget> </DialogWidget>
</template> </template>
@@ -26,14 +25,12 @@
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import DialogWidget from '@/components/DialogWidget.vue'; import DialogWidget from '@/components/DialogWidget.vue';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue'; import SelectDropdownWidget from '@/components/form/SelectDropdownWidget.vue';
export default { export default {
name: 'RegisterDialog', name: 'RegisterDialog',
components: { components: {
DialogWidget, DialogWidget,
ErrorDialog,
SelectDropdownWidget, SelectDropdownWidget,
}, },
data() { data() {
@@ -85,7 +82,7 @@ export default {
}, },
async register() { async register() {
if (!this.canRegister) { if (!this.canRegister) {
this.$refs.errorDialog.open('tr:register.passwordMismatch'); this.$root.$refs.errrorDialog.open('tr:register.passwordMismatch');
return; return;
} }
@@ -102,14 +99,14 @@ export default {
this.$refs.dialog.close(); this.$refs.dialog.close();
this.$router.push('/activate'); this.$router.push('/activate');
} else { } else {
this.$refs.errorDialog.open("tr:register.failure"); this.$root.$refs.errrorDialog.open("tr:register.failure");
} }
} catch (error) { } catch (error) {
if (error.response && error.response.status === 409) { if (error.response && error.response.status === 409) {
this.$refs.errorDialog.open('tr:register.' + error.response.data.error); this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
} else { } else {
console.error('Error registering user:', error); console.error('Error registering user:', error);
this.$refs.errorDialog.open('tr:register.' + error.response.data.error); this.$root.$refs.errrorDialog.open('tr:register.' + error.response.data.error);
} }
} }
}, },

View File

@@ -5,36 +5,38 @@
<div> <div>
<div class="form-group"> <div class="form-group">
<label>{{ $t("socialnetwork.gallery.create_folder_dialog.parent_folder") }}</label> <label>{{ $t("socialnetwork.gallery.create_folder_dialog.parent_folder") }}</label>
<!-- Hier wird der übergeordnete Ordner angezeigt, aber nicht bearbeitbar -->
<input type="text" :value="parentFolder.name" disabled /> <input type="text" :value="parentFolder.name" disabled />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="folderTitle">{{ $t("socialnetwork.gallery.create_folder_dialog.folder_title") }}</label> <label for="folderTitle">{{ $t("socialnetwork.gallery.create_folder_dialog.folder_title") }}</label>
<!-- Setze den Titel des Ordners für Bearbeiten -->
<input type="text" v-model="folderTitle" <input type="text" v-model="folderTitle"
:placeholder="$t('socialnetwork.gallery.create_folder_dialog.folder_title')" required /> :placeholder="$t('socialnetwork.gallery.create_folder_dialog.folder_title')" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="visibility">{{ $t("socialnetwork.gallery.create_folder_dialog.visibility") }} <label for="visibility">{{ $t("socialnetwork.gallery.create_folder_dialog.visibility") }}</label>
<multiselect v-model="selectedVisibility" :options="visibilityOptions" :multiple="true" <multiselect v-model="selectedVisibility" :options="visibilityOptions" :multiple="true"
label="description" :track-by="'id'" :close-on-select="false" label="description" :track-by="'id'" :close-on-select="false"
:placeholder="$t('socialnetwork.gallery.create_folder_dialog.select_visibility')"> :placeholder="$t('socialnetwork.gallery.create_folder_dialog.select_visibility')">
<template #option="{ option }"> <template #option="{ option }">
<span v-if="option && option.description">{{ <span v-if="option && option.description">{{
$t(`socialnetwork.gallery.visibility.${option.description}`) }} $t(`socialnetwork.gallery.visibility.${option.description}`) }}
</span> </span>
</template> </template>
<template #tag="{ option, remove }"> <template #tag="{ option, remove }">
<span v-if="option && option.description" class="multiselect__tag"> <span v-if="option && option.description" class="multiselect__tag">
{{ $t(`socialnetwork.gallery.visibility.${option.description}`) }} {{ $t(`socialnetwork.gallery.visibility.${option.description}`) }}
<span @click="remove(option)">×</span> <span @click="remove(option)">×</span>
</span> </span>
</template> </template>
</multiselect> </multiselect>
</label>
</div> </div>
</div> </div>
</DialogWidget> </DialogWidget>
</template> </template>
<script> <script>
import Multiselect from 'vue-multiselect'; import Multiselect from 'vue-multiselect';
import DialogWidget from '@/components/DialogWidget.vue'; import DialogWidget from '@/components/DialogWidget.vue';
@@ -47,20 +49,13 @@ export default {
DialogWidget, DialogWidget,
Multiselect Multiselect
}, },
props: {
parentFolder: {
type: [Object, null],
required: true,
default() {
return { id: null, name: '' };
}
}
},
data() { data() {
return { return {
folderTitle: '', folderTitle: '',
visibilityOptions: [], visibilityOptions: [],
selectedVisibility: [], selectedVisibility: [],
parentFolder: {id: null, name: ''},
folderId: 0
}; };
}, },
computed: { computed: {
@@ -73,44 +68,56 @@ export default {
await this.loadVisibilityOptions(); await this.loadVisibilityOptions();
}, },
methods: { methods: {
async open() { open(folder = null) {
if (!this.parentFolder || !this.parentFolder.id) { if (folder) {
console.error('No parent folder selected'); this.folderTitle = folder.name;
return; this.selectedVisibility = this.visibilityOptions.filter(option =>
folder.visibilityTypeIds.includes(option.id)
);
} else {
this.folderTitle = '';
this.selectedVisibility = [];
} }
this.$refs.dialog.open(); this.$refs.dialog.open();
}, },
closeDialog() {
this.$refs.dialog.close();
},
async loadVisibilityOptions() { async loadVisibilityOptions() {
try { try {
const response = await apiClient.get('/api/socialnetwork/imagevisibilities'); const response = await apiClient.get('/api/socialnetwork/imagevisibilities');
this.visibilityOptions = response.data; this.visibilityOptions = response.data;
if (this.selectedVisibility.length) {
this.selectedVisibility = this.visibilityOptions.filter(option =>
this.selectedVisibility.map(v => v.id).includes(option.id)
);
}
} catch (error) { } catch (error) {
console.error('Error loading visibility options:', error); console.error('Error loading visibility options:', error);
} }
}, },
async createFolder() { async createFolder() {
if (!this.folderTitle || !this.selectedVisibility) { if (!this.folderTitle || !this.selectedVisibility.length) {
alert(this.$t('socialnetwork.gallery.errors.missing_fields')); alert(this.$t('socialnetwork.gallery.errors.missing_fields'));
return; return;
} }
const payload = {
name: this.folderTitle,
parentId: this.parentFolder.id,
visibilities: this.selectedVisibility.map(item => item.id),
};
try { try {
const payload = { if (this.parentFolder.id) {
name: this.folderTitle, await apiClient.put(`/api/socialnetwork/folders/${this.parentFolder.id}`, payload);
parentId: this.parentFolder.id, } else {
visibilities: this.selectedVisibility.map(item => item.id), await apiClient.post(`/api/socialnetwork/folders/${this.folderId}`, payload);
}; }
await apiClient.post('/api/socialnetwork/folders', payload);
this.$emit('created', payload); this.$emit('created', payload);
this.closeDialog(); this.closeDialog();
} catch (error) { } catch (error) {
console.error('Error creating folder:', error); console.error('Fehler beim Erstellen/Bearbeiten des Ordners:', error);
} }
} },
closeDialog() {
this.$refs.dialog.close();
},
} }
}; };

View File

@@ -1,7 +1,7 @@
<template> <template>
<DialogWidget ref="dialog" title="socialnetwork.gallery.edit_image_dialog.title" icon="image16.png" <DialogWidget ref="dialog" title="socialnetwork.gallery.edit_image_dialog.title" icon="image16.png"
:show-close="true" :buttons="buttons" :modal="true" :isTitleTranslated="true" @close="closeDialog" :show-close="true" :buttons="buttons" :modal="true" :isTitleTranslated="true" @close="closeDialog"
name="ImageDialog"> name="EditImageDialog">
<div> <div>
<div class="image-container"> <div class="image-container">
<img :src="image.url" alt="Image" :style="{ maxWidth: '600px', maxHeight: '600px' }" /> <img :src="image.url" alt="Image" :style="{ maxWidth: '600px', maxHeight: '600px' }" />
@@ -37,21 +37,17 @@ import Multiselect from 'vue-multiselect';
import DialogWidget from '@/components/DialogWidget.vue'; import DialogWidget from '@/components/DialogWidget.vue';
export default { export default {
name: "EditImageDialog",
components: { components: {
DialogWidget, DialogWidget,
Multiselect, Multiselect,
}, },
props: {
visibilityOptions: {
type: Array,
required: true,
},
},
data() { data() {
return { return {
image: null, image: null,
imageTitle: '', imageTitle: '',
selectedVisibilities: [], selectedVisibilities: [],
visibilityOptions: [],
}; };
}, },
computed: { computed: {

View File

@@ -0,0 +1,65 @@
<template>
<DialogWidget ref="dialog" title="socialnetwork.gallery.show_image_dialog.title" icon="image16.png"
:show-close="true" :buttons="buttons" :modal="true" :isTitleTranslated="true" @close="closeDialog"
name="ImageDialog">
<div>
<div class="image-container">
<img :src="image.url" alt="Image" :style="{ maxWidth: '600px', maxHeight: '600px' }" />
</div>
<div class="form-group">
<label for="imageTitle">{{ $t('socialnetwork.gallery.imagedialog.image_title') }} <span type="text">{{
imageTitle }}</span></label>
</div>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from '@/components/DialogWidget.vue';
export default {
name: "ShowImageDialog",
components: {
DialogWidget,
},
data() {
return {
image: null,
imageTitle: '',
};
},
computed: {
buttons() {
return [
{ text: this.$t('socialnetwork.gallery.imagedialog.close'), action: this.closeDialog }
];
},
},
methods: {
open(image) {
this.image = image;
this.imageTitle = image.title;
this.$refs.dialog.open();
},
closeDialog() {
this.$refs.dialog.close();
},
},
};
</script>
<style scoped>
.form-group {
margin-bottom: 15px;
}
.image-container {
text-align: center;
margin-bottom: 20px;
}
.multiselect {
display: inline-block;
width: auto;
}
</style>

View File

@@ -1,7 +1,8 @@
<template> <template>
<DialogWidget ref="dialog" :title="$t('socialnetwork.profile.pretitle')" :isTitleTranslated="isTitleTranslated" <DialogWidget ref="dialog" :title="$t('socialnetwork.profile.pretitle')" :isTitleTranslated="isTitleTranslated"
:show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog"> :show-close="true" :buttons="[{ text: 'Ok', action: 'close' }]" :modal="false" @close="closeDialog"
<div class="dialog-body"> height="75%">
<div class="dialog-body">
<div> <div>
<ul class="tab-list"> <ul class="tab-list">
<li v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }" <li v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }"
@@ -9,7 +10,6 @@
{{ tab.label }} {{ tab.label }}
</li> </li>
</ul> </ul>
<div class="tab-content" v-if="activeTab === 'general'"> <div class="tab-content" v-if="activeTab === 'general'">
<table> <table>
<tr v-for="(value, key) in userProfile.params" :key="key"> <tr v-for="(value, key) in userProfile.params" :key="key">
@@ -18,6 +18,67 @@
</tr> </tr>
</table> </table>
</div> </div>
<div class="tab-content images-tab" v-if="activeTab === 'images'">
<div v-if="folders.length === 0">{{ $t('socialnetwork.profile.noFolders') }}</div>
<ul v-else class="tree">
<folder-item v-for="folder in [folders]" :key="folder.id" :folder="folder"
:selected-folder="selectedFolder" @select-folder="selectFolder" :isLastItem="true"
:depth="0" :parentsWithChildren="[false]" :noActionItems="true">
</folder-item>
</ul>
<ul v-if="images.length > 0" class="image-list">
<li v-for="image in images" :key="image.id" @click="openImageDialog(image)">
<img :src="image.url || image.placeholder" alt="Loading..." />
<p>{{ image.title }}</p>
</li>
</ul>
</div>
<div class="tab-content" v-if="activeTab === 'guestbook'">
<div class="guestbook-input-section">
<button @click="toggleInputSection">
{{ showInputSection ? $t('socialnetwork.profile.guestbook.hideInput') :
$t('socialnetwork.profile.guestbook.showInput') }}
</button>
<div v-if="showInputSection">
<div class="form-group">
<label for="guestbookImage">{{ $t('socialnetwork.profile.guestbook.imageUpload')
}}</label>
<input type="file" @change="onFileChange" accept="image/*" />
<div v-if="imagePreview" class="image-preview">
<img :src="imagePreview" alt="Image Preview"
style="max-width: 100px; max-height: 100px;" />
</div>
<editor v-model="newEntryContent" :init="tinymceInitOptions" :api-key="apiKey"></editor>
</div>
<button @click="submitGuestbookEntry">{{ $t('socialnetwork.profile.guestbook.submit')
}}</button>
</div>
</div>
<div v-if="guestbookEntries.length === 0">{{ $t('socialnetwork.profile.guestbook.noEntries') }}
</div>
<div v-else class="guestbook-entries">
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
style="max-width: 400px; max-height: 400px;" />
<p v-html="entry.contentHtml"></p>
<div class="entry-info">
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
<span class="entry-user">
<span @click="openProfile(entry.senderUsername)">{{ entry.sender }}</span>
</span>
</div>
</div>
<div class="pagination">
<button @click="loadGuestbookEntries(currentPage - 1)" :disabled="currentPage === 1">{{
$t('socialnetwork.guestbook.prevPage') }}</button>
<span>{{ $t('socialnetwork.guestbook.page') }} {{ currentPage }} / {{ totalPages }}</span>
<button @click="loadGuestbookEntries(currentPage + 1)"
:disabled="currentPage === totalPages">{{ $t('socialnetwork.guestbook.nextPage')
}}</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
</DialogWidget> </DialogWidget>
@@ -26,11 +87,15 @@
<script> <script>
import DialogWidget from '@/components/DialogWidget.vue'; import DialogWidget from '@/components/DialogWidget.vue';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import FolderItem from '../../components/FolderItem.vue';
import TinyMCEEditor from '@tinymce/tinymce-vue';
export default { export default {
name: 'UserProfileDialog', name: 'UserProfileDialog',
components: { components: {
DialogWidget DialogWidget,
FolderItem,
editor: TinyMCEEditor,
}, },
props: { props: {
userId: { userId: {
@@ -44,11 +109,35 @@ export default {
userProfile: {}, userProfile: {},
activeTab: 'general', activeTab: 'general',
userId: '', userId: '',
folders: [],
images: [],
selectedFolder: null,
newEntryContent: '',
guestbookEntries: [],
showInputSection: false,
imagePreview: null,
selectedImage: null,
currentPage: 1,
totalPages: 1,
tabs: [ tabs: [
{ name: 'general', label: this.$t('socialnetwork.profile.tab.general') }, { name: 'general', label: this.$t('socialnetwork.profile.tab.general') },
{ name: 'images', label: this.$t('socialnetwork.profile.tab.images') }, { name: 'images', label: this.$t('socialnetwork.profile.tab.images') },
{ name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') } { name: 'guestbook', label: this.$t('socialnetwork.profile.tab.guestbook') }
], ],
apiKey: import.meta.env.VITE_TINYMCE_API_KEY,
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'
}
}; };
}, },
methods: { methods: {
@@ -58,26 +147,43 @@ export default {
}, },
async loadUserProfile() { async loadUserProfile() {
try { try {
const response = await apiClient.get(`/api/socialnetwork/profile/${this.userId}`); const response = await apiClient.get(`/api/socialnetwork/profile/main/${this.userId}`);
this.userProfile = response.data; this.userProfile = response.data;
const newTitle = this.$t('socialnetwork.profile.title').replace('<username>', this.userProfile.username); const newTitle = this.$t('socialnetwork.profile.title').replace('<username>', this.userProfile.username);
this.$refs.dialog.updateTitle(newTitle, false); this.$refs.dialog.updateTitle(newTitle, false);
if (this.activeTab === 'images') {
await this.loadUserFolders();
}
} catch (error) { } catch (error) {
this.$refs.dialog.updateTitle('socialnetwork.profile.error_title', true); this.$refs.dialog.updateTitle('socialnetwork.profile.error_title', true);
console.error('Fehler beim Laden des Benutzerprofils:', error); console.error('Fehler beim Laden des Benutzerprofils:', error);
} }
}, },
async loadUserFolders() {
try {
const response = await apiClient.get(`/api/socialnetwork/profile/images/folders/${this.userProfile.username}`);
this.folders = response.data || [];
this.selectFolder(this.folders);
} catch (error) {
console.error('Fehler beim Laden der Ordner:', error);
}
},
closeDialog() { closeDialog() {
this.$refs.dialog.close(); this.$refs.dialog.close();
}, },
selectTab(tabName) { selectTab(tabName) {
this.activeTab = tabName; this.activeTab = tabName;
if (tabName === 'images') {
this.loadUserFolders();
} else if (tabName === 'guestbook') {
this.loadGuestbookEntries(1);
}
}, },
generateValue(key, value) { generateValue(key, value) {
if (Array.isArray(value.value)) { if (Array.isArray(value.value)) {
const strings = []; const strings = [];
for (const val of value.value) { for (const val of value.value) {
strings.push(this.generateValue(key, {type: value.type, value: val})); strings.push(this.generateValue(key, { type: value.type, value: val }));
} }
return strings.join(', '); return strings.join(', ');
} }
@@ -101,7 +207,109 @@ export default {
default: default:
return value.value; return value.value;
} }
} },
async selectFolder(folder) {
this.selectedFolder = folder;
await this.loadImages(folder.id);
},
async loadImages(folderId) {
try {
const response = await apiClient.get(`/api/socialnetwork/folder/${folderId}`);
this.images = response.data.map((image) => ({
...image,
placeholder:
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3C/svg%3E',
url: null,
}));
await this.fetchImages();
} catch (error) {
console.error('Error loading images:', error);
}
},
async fetchImages() {
this.images.forEach((image) => {
this.fetchImage(image);
});
},
async fetchImage(image) {
const userId = localStorage.getItem('userid');
try {
const response = await apiClient.get(`/api/socialnetwork/image/${image.hash}`, {
headers: {
userid: userId,
},
responseType: 'blob',
});
image.url = URL.createObjectURL(response.data);
} catch (error) {
console.error('Error fetching image:', error);
}
},
openImageDialog(image) {
this.$root.$refs.showImageDialog.open(image);
},
toggleInputSection() {
this.showInputSection = !this.showInputSection;
},
onFileChange(event) {
const file = event.target.files[0];
if (file) {
this.selectedImage = file;
const reader = new FileReader();
reader.onload = (e) => {
this.imagePreview = e.target.result;
};
reader.readAsDataURL(file);
}
},
async submitGuestbookEntry() {
if (!this.newEntryContent) return alert(this.$t('socialnetwork.guestbook.emptyContent'));
const formData = new FormData();
formData.append('htmlContent', this.newEntryContent);
formData.append('recipientName', this.userProfile.username);
if (this.selectedImage) {
formData.append('image', this.selectedImage);
}
try {
await apiClient.post('/api/socialnetwork/guestbook/entries', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
this.newEntryContent = '';
this.selectedImage = null;
this.imagePreview = null;
await this.loadGuestbookEntries(1);
} catch (error) {
console.error('Fehler beim Erstellen des Gästebucheintrags:', error);
}
},
async loadGuestbookEntries(page) {
try {
const response = await apiClient.get(`/api/socialnetwork/guestbook/entries/${this.userProfile.username}/${page}`);
this.guestbookEntries = response.data.entries;
this.currentPage = response.data.currentPage;
this.totalPages = response.data.totalPages;
this.guestbookEntries.forEach((entry) => {
if (entry.withImage) {
this.fetchGuestbookImage(this.userProfile.username, entry);
}
});
} catch (error) {
console.error('Fehler beim Laden der Gästebucheinträge:', error);
}
},
async fetchGuestbookImage(guestbookOwnerName, entry) {
try {
console.log(entry, guestbookOwnerName);
const response = await apiClient.get(`/api/socialnetwork/guestbook/image/${guestbookOwnerName}/${entry.id}`, {
responseType: 'blob',
});
entry.image = { url: URL.createObjectURL(response.data) };
} catch (error) {
console.error('Error fetching image:', error);
}
},
} }
}; };
</script> </script>
@@ -138,12 +346,90 @@ export default {
} }
.dialog-body, .dialog-body,
.dialog-body > div { .dialog-body>div {
height: 100%; height: 100%;
} }
.dialog-body > div { .dialog-body>div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.tree {
padding: 0;
}
.images-tab {
display: flex;
}
.image-list {
display: flex;
flex-direction: column;
flex-wrap: wrap;
list-style: none;
}
.image-list li {
display: inline-block;
padding: 2px;
border: 1px solid #F9A22C;
margin: 0 4px 4px 0;
}
.image-list li img {
max-width: 200px;
max-height: 200px;
object-fit: contain;
cursor: pointer;
}
.image-list > li > p {
text-align: center;
}
.folder-name-text {
cursor: pointer;
}
.guestbook-input-section {
margin-bottom: 20px;
}
.form-group {
margin: 10px 0;
}
.image-preview img {
max-width: 100px;
max-height: 100px;
}
.guestbook-entries {
display: flex;
flex-direction: column;
}
.guestbook-entry {
border-bottom: 1px solid #ccc;
margin-bottom: 20px;
padding-bottom: 10px;
}
.entry-info {
display: flex;
justify-content: space-between;
font-size: 0.8em;
color: gray;
}
.pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
.pagination button {
margin: 0 10px;
}
</style> </style>

View File

@@ -0,0 +1,62 @@
<template>
<DialogWidget ref="dialog" :title="title" :icon="icon" :show-close="true" :buttons="dialogButtons" :modal="true"
:isTitleTranslated="false" width="30em" height="15em">
<div class="dialog-body">
<p>{{ message }}</p>
</div>
</DialogWidget>
</template>
<script>
import DialogWidget from "@/components/DialogWidget.vue";
export default {
name: "ChooseDialog",
components: {
DialogWidget,
},
data() {
return {
title: "Bestätigung",
message: "Sind Sie sicher?",
icon: null,
resolve: null,
};
},
methods: {
open(options = {}) {
this.title = options.title || "Bestätigung";
this.message = options.message || "Sind Sie sicher?";
this.icon = options.icon || null;
this.dialogButtons = [
{ text: this.$t("yes"), action: this.confirmYes },
{ text: this.$t("no"), action: this.confirmNo },
];
return new Promise((resolve) => {
this.resolve = resolve;
this.$refs.dialog.open();
});
},
close() {
this.$refs.dialog.close();
},
confirmYes() {
console.log('ja');
this.resolve(true);
this.close();
},
confirmNo() {
console.log('nein');
this.resolve(false);
this.close();
},
},
};
</script>
<style scoped>
.dialog-body {
padding: 20px;
text-align: center;
}
</style>

View File

@@ -29,5 +29,7 @@
"datetimelong": "dd.MM.yyyy HH:mm:ss" "datetimelong": "dd.MM.yyyy HH:mm:ss"
}, },
"OK": "Ok", "OK": "Ok",
"Cancel": "Abbrechen" "Cancel": "Abbrechen",
"yes": "Ja",
"no": "Nein"
} }

View File

@@ -115,6 +115,13 @@
"none": "Keine" "none": "Keine"
} }
}, },
"guestbook": {
"showInput": "Neuer Eintrag anzeigen",
"hideInput": "Neuer Eintrag verbergen",
"imageUpload": "Bild",
"submit": "Eintrag absenden",
"noEntries": "Keine Einträge gefunden"
},
"interestedInGender": "Interessiert an", "interestedInGender": "Interessiert an",
"hasChildren": "Hat Kinder", "hasChildren": "Hat Kinder",
"smokes": "Rauchen", "smokes": "Rauchen",
@@ -174,7 +181,21 @@
"save_changes": "Änderungen speichern", "save_changes": "Änderungen speichern",
"close": "Schließen", "close": "Schließen",
"edit_visibility_placeholder": "Bitte auswählen" "edit_visibility_placeholder": "Bitte auswählen"
},
"delete_folder_confirmation_title": "Ordner löschen",
"delete_folder_confirmation_message": "Soll der Ordner '%%folderName%%' wirklich gelöscht werden?",
"edit_image_dialog": {
"title": "Bilddaten editieren"
},
"show_image_dialog": {
"title": "Bild"
} }
},
"guestbook": {
"title": "Gästebuch",
"prevPage": "Zurück",
"nextPage": "Weiter",
"page": "Seite"
} }
} }
} }

View File

@@ -12,6 +12,7 @@ import AdminInterestsView from '../views/admin/InterestsView.vue';
import AdminContactsView from '../views/admin/ContactsView.vue'; import AdminContactsView from '../views/admin/ContactsView.vue';
import SearchView from '../views/social/SearchView.vue'; import SearchView from '../views/social/SearchView.vue';
import GalleryView from '../views/social/GalleryView.vue'; import GalleryView from '../views/social/GalleryView.vue';
import GuestbookView from '../views/social/GuestbookView.vue';
const routes = [ const routes = [
{ {
@@ -24,6 +25,12 @@ const routes = [
name: 'Activate page', name: 'Activate page',
component: ActivateView component: ActivateView
}, },
{
path: '/socialnetwork/guestbook',
name: 'Guestbook',
component: GuestbookView,
meta: { requiresAuth: true }
},
{ {
path: '/socialnetwork/search', path: '/socialnetwork/search',
name: 'Search users', name: 'Search users',

View File

@@ -12,9 +12,20 @@ const mutations = {
if (!state.openDialogs.find((d) => d.dialog.name === dialog.dialog.name)) { if (!state.openDialogs.find((d) => d.dialog.name === dialog.dialog.name)) {
state.openDialogs.push(dialog); state.openDialogs.push(dialog);
} }
state.openDialogs.forEach((dlg) => {
dlg.dialog.setActiveState(false);
});
dialog.dialog.setActiveState(true);
}, },
removeOpenDialog(state, dialogName) { removeOpenDialog(state, dialogName) {
state.openDialogs = state.openDialogs.filter((dialog) => dialog.dialog.name !== dialogName); state.openDialogs = state.openDialogs.filter((dialog) => dialog.dialog.name !== dialogName);
let activeIsSet = false;
state.openDialogs.forEach((dialog) => {
if (!dialog.dialog.isMinimized() && !activeIsSet) {
dialog.dialog.setActiveState(true);
activeIsSet = true;
}
});
}, },
toggleDialogMinimize(state, dialogName) { toggleDialogMinimize(state, dialogName) {
if (minimizing) { if (minimizing) {
@@ -30,7 +41,6 @@ const mutations = {
updateDialogTitle(state, { name, newTitle, isTitleTranslated }) { updateDialogTitle(state, { name, newTitle, isTitleTranslated }) {
const dialogIndex = state.openDialogs.findIndex((d) => d.dialog.name === name); const dialogIndex = state.openDialogs.findIndex((d) => d.dialog.name === name);
if (dialogIndex !== -1) { if (dialogIndex !== -1) {
// Update dialog object reactively
const updatedDialog = { const updatedDialog = {
...state.openDialogs[dialogIndex], ...state.openDialogs[dialogIndex],
dialog: { dialog: {
@@ -39,8 +49,6 @@ const mutations = {
isTitleTranslated: isTitleTranslated isTitleTranslated: isTitleTranslated
} }
}; };
// Replace the old dialog with the updated one
state.openDialogs.splice(dialogIndex, 1, updatedDialog); state.openDialogs.splice(dialogIndex, 1, updatedDialog);
} }
} }

View File

@@ -21,23 +21,14 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<ErrorDialog ref="errorDialog" />
<AnswerContact ref="answerContactDialog" />
</template> </template>
<script> <script>
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import { mapGetters } from 'vuex';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
import { formatDateTimeLong } from '@/utils/datetime.js'; import { formatDateTimeLong } from '@/utils/datetime.js';
import AnswerContact from '../../dialogues/admin/AnswerContact.vue';
export default { export default {
name: 'AdminContactsView', name: 'AdminContactsView',
components: {
ErrorDialog,
AnswerContact,
},
data() { data() {
return { return {
contacts: [] contacts: []
@@ -53,11 +44,11 @@ export default {
const openContactRequests = await apiClient.get('/api/admin/opencontacts'); const openContactRequests = await apiClient.get('/api/admin/opencontacts');
this.contacts = openContactRequests.data; this.contacts = openContactRequests.data;
} catch (error) { } catch (error) {
this.$refs.errorDialog.open(`tr:error.${error.response.data.error}`); this.$root.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
} }
}, },
async openRequest(contact) { async openRequest(contact) {
this.$refs.answerContactDialog.open(contact); this.$root.$refs.answerContactDialog.open(contact);
}, },
async finishRequest(contact) { async finishRequest(contact) {
await apiClient.get('/api/admin/opencontacts/finish/${contact.id}'); await apiClient.get('/api/admin/opencontacts/finish/${contact.id}');

View File

@@ -11,14 +11,12 @@
<button type="submit">{{ $t('activate.submit') }}</button> <button type="submit">{{ $t('activate.submit') }}</button>
</div> </div>
</form> </form>
<ErrorDialog ref="errorDialog" />
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
export default { export default {
name: 'ActivateView', name: 'ActivateView',
@@ -27,9 +25,6 @@ export default {
token: this.$route.query.token || '' token: this.$route.query.token || ''
}; };
}, },
components: {
ErrorDialog,
},
computed: { computed: {
...mapGetters(['user']) ...mapGetters(['user'])
}, },
@@ -43,7 +38,7 @@ export default {
} }
} catch (error) { } catch (error) {
console.error('Error activating account:', error); console.error('Error activating account:', error);
this.$refs.errorDialog.open(this.$t('activate.failure')); this.$root.$refs.errorDialog.open(this.$t('activate.failure'));
} }
} }
} }

View File

@@ -38,7 +38,6 @@
<RandomChatDialog ref="randomChatDialog" /> <RandomChatDialog ref="randomChatDialog" />
<RegisterDialog ref="registerDialog" /> <RegisterDialog ref="registerDialog" />
<PasswordResetDialog ref="passwordResetDialog" /> <PasswordResetDialog ref="passwordResetDialog" />
<ErrorDialog ref="errorDialog" />
</div> </div>
</template> </template>
@@ -48,7 +47,6 @@ import RegisterDialog from '@/dialogues/auth/RegisterDialog.vue';
import PasswordResetDialog from '@/dialogues/auth/PasswordResetDialog.vue'; import PasswordResetDialog from '@/dialogues/auth/PasswordResetDialog.vue';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
export default { export default {
name: 'HomeNoLoginView', name: 'HomeNoLoginView',
@@ -62,7 +60,6 @@ export default {
RandomChatDialog, RandomChatDialog,
RegisterDialog, RegisterDialog,
PasswordResetDialog, PasswordResetDialog,
ErrorDialog,
}, },
methods: { methods: {
...mapActions(['login']), ...mapActions(['login']),
@@ -80,7 +77,7 @@ export default {
const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password }); const response = await apiClient.post('/api/auth/login', { username: this.username, password: this.password });
this.login(response.data); this.login(response.data);
} catch (error) { } catch (error) {
this.$refs.errorDialog.open(`tr:error.${error.response.data.error}`); this.$root.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
} }
} }
} }

View File

@@ -3,10 +3,11 @@
<div class="gallery-view"> <div class="gallery-view">
<div class="sidebar"> <div class="sidebar">
<h3>{{ $t('socialnetwork.gallery.folders') }}</h3> <h3>{{ $t('socialnetwork.gallery.folders') }}</h3>
<ul> <ul class="tree">
<folder-item v-for="folder in [folders]" :key="folder.id" :folder="folder" <folder-item v-for="folder in [folders]" :key="folder.id" :folder="folder"
:selected-folder="selectedFolder" @select-folder="selectFolder" :isLastItem="true" :depth="0" :selected-folder="selectedFolder" @select-folder="selectFolder" :isLastItem="true" :depth="0"
:parentsWithChildren="[false]"></folder-item> :parentsWithChildren="[false]" @edit-folder="openEditFolderDialog" @delete-folder="deleteFolder">
</folder-item>
</ul> </ul>
<button @click="openCreateFolderDialog">{{ $t('socialnetwork.gallery.create_folder') }}</button> <button @click="openCreateFolderDialog">{{ $t('socialnetwork.gallery.create_folder') }}</button>
</div> </div>
@@ -74,8 +75,6 @@
</div> </div>
</div> </div>
</div> </div>
<CreateFolderDialog ref="createFolderDialog" :parentFolder="selectedFolder" @created="handleFolderCreated" />
<ImageDialog ref="imageDialog" :visibilityOptions="visibilityOptions" @save="saveImage" />
</template> </template>
<script> <script>
@@ -83,15 +82,11 @@ import apiClient from '@/utils/axios.js';
import Multiselect from 'vue-multiselect'; import Multiselect from 'vue-multiselect';
import FolderItem from '../../components/FolderItem.vue'; import FolderItem from '../../components/FolderItem.vue';
import 'vue-multiselect/dist/vue-multiselect.min.css'; import 'vue-multiselect/dist/vue-multiselect.min.css';
import CreateFolderDialog from '../../dialogues/socialnetwork/CreateFolderDialog.vue';
import ImageDialog from '../../dialogues/socialnetwork/ImageDialog.vue';
export default { export default {
components: { components: {
FolderItem, FolderItem,
Multiselect, Multiselect,
CreateFolderDialog,
ImageDialog,
}, },
data() { data() {
return { return {
@@ -154,7 +149,12 @@ export default {
}); });
}, },
openCreateFolderDialog() { openCreateFolderDialog() {
this.$refs.createFolderDialog.open(); const parentFolder = this.selectedFolder || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
Object.assign(this.$root.$refs.createFolderDialog, {
parentFolder: parentFolder,
folderId: 0,
});
this.$root.$refs.createFolderDialog.open();
}, },
async handleFolderCreated() { async handleFolderCreated() {
await this.loadFolders(); await this.loadFolders();
@@ -209,7 +209,7 @@ export default {
this.isUploadVisible = !this.isUploadVisible; this.isUploadVisible = !this.isUploadVisible;
}, },
openImageDialog(image) { openImageDialog(image) {
this.$refs.imageDialog.open(image); this.$root.$refs.editImageDialog.open(image);
}, },
async saveImage(updatedImage) { async saveImage(updatedImage) {
try { try {
@@ -228,7 +228,34 @@ export default {
console.error('Error saving image:', error); console.error('Error saving image:', error);
} }
}, },
}, openEditFolderDialog(folder) {
const parentFolder = folder.parent || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
Object.assign(this.$root.$refs.createFolderDialog, {
parentFolder: parentFolder,
folderId: folder.id,
});
this.$root.$refs.createFolderDialog.open(folder);
},
async deleteFolder(folder) {
const folderName = folder.name;
const confirmed = await this.$root.$refs.chooseDialog.open({
title: this.$t('socialnetwork.gallery.delete_folder_confirmation_title'),
message: this.$t('socialnetwork.gallery.delete_folder_confirmation_message').replace('%%folderName%%', folderName),
});
if (confirmed) {
const deletedFolderIsCurrentFolder = folder.id === this.selectFolder.id;
try {
await apiClient.delete(`/api/socialnetwork/folders/${folder.id}`);
await this.loadFolders();
} catch (error) {
console.error('Fehler beim Löschen des Ordners:', error);
}
if (deletedFolderIsCurrentFolder) {
this.selectFolder = this.folders[0];
}
}
},
},
}; };
</script> </script>
@@ -300,4 +327,21 @@ export default {
object-fit: contain; object-fit: contain;
cursor: pointer; cursor: pointer;
} }
.icon {
cursor: pointer;
margin-left: 10px;
}
.edit-icon {
color: green;
}
.delete-icon {
color: red;
}
.tree {
padding: 0;
}
</style> </style>

View File

@@ -0,0 +1,93 @@
<template>
<h2>{{ $t('socialnetwork.guestbook.title') }}</h2>
<div>
<div v-if="guestbookEntries.length === 0">{{ $t('socialnetwork.profile.guestbook.noEntries') }}
</div>
<div v-else class="guestbook-entries">
<div v-for="entry in guestbookEntries" :key="entry.id" class="guestbook-entry">
<img v-if="entry.image" :src="entry.image.url" alt="Entry Image"
style="max-width: 400px; max-height: 400px;" />
<p v-html="entry.contentHtml"></p>
<div class="entry-info">
<span class="entry-timestamp">{{ new Date(entry.createdAt).toLocaleString() }}</span>
<span class="entry-user">
<span @click="openProfile(entry.senderUsername)">{{ entry.sender }}</span>
</span>
</div>
</div>
</div>
<div class="pagination">
<button @click="loadGuestbookEntries(currentPage - 1)" v-if="currentPage !== 1">{{
$t('socialnetwork.guestbook.prevPage') }}</button>
<span>{{ $t('socialnetwork.guestbook.page') }} {{ currentPage }} / {{ totalPages }}</span>
<button @click="loadGuestbookEntries(currentPage + 1)" v-if="currentPage < totalPages">{{
$t('socialnetwork.guestbook.nextPage')
}}</button>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import apiClient from '@/utils/axios.js';
export default {
name: 'GuestbookView',
data() {
return {
guestbookEntries: [],
currentPage: 1,
totalPages: 1,
};
},
computed: {
...mapGetters(['user']),
},
methods: {
...mapActions('socialnetwork', ['fetchUserProfile']),
openProfile(username) {
this.$router.push({ name: 'profile', params: { username } });
},
async loadGuestbookEntries(page) {
console.log(page);
try {
const response = await apiClient.get(`/api/socialnetwork/guestbook/entries/${this.user.username}/${page}`);
this.guestbookEntries = response.data.entries;
this.currentPage = page;
this.totalPages = response.data.totalPages;
this.guestbookEntries.forEach((entry) => {
if (entry.withImage) {
this.fetchGuestbookImage(this.user.username, entry);
}
});
} catch (error) {
console.error('Fehler beim Laden der Gästebucheinträge:', error);
}
console.log('page changed', this.currentPage);
},
async fetchGuestbookImage(guestbookOwnerName, entry) {
try {
const response = await apiClient.get(`/api/socialnetwork/guestbook/image/${this.user.username}/${entry.id}`, {
responseType: 'blob',
});
entry.image = { url: URL.createObjectURL(response.data) };
} catch (error) {
console.error('Error fetching image:', error);
}
},
},
mounted() {
console.log('get it');
this.loadGuestbookEntries(1);
},
}
</script>
<style lang="css" scoped>
.pagination {
margin-top: 1em;
background-color: #7BBE55;
color: #fff;
padding: 0.5em 0;
}
</style>

View File

@@ -52,19 +52,16 @@
{{ $t('socialnetwork.usersearch.no_results') }} {{ $t('socialnetwork.usersearch.no_results') }}
</div> </div>
</div> </div>
<UserProfileDialog ref="userProfileDialog" :username="selectedUsername" />
</template> </template>
<script> <script>
import Multiselect from 'vue-multiselect'; import Multiselect from 'vue-multiselect';
import 'vue-multiselect/dist/vue-multiselect.min.css'; import 'vue-multiselect/dist/vue-multiselect.min.css';
import apiClient from '@/utils/axios.js'; import apiClient from '@/utils/axios.js';
import UserProfileDialog from '@/dialogues/socialnetwork/UserProfileDialog.vue';
export default { export default {
components: { components: {
Multiselect, Multiselect,
UserProfileDialog
}, },
data() { data() {
return { return {
@@ -108,8 +105,8 @@ export default {
} }
}, },
openUserProfile(id) { openUserProfile(id) {
this.$refs.userProfileDialog.userId = id; this.$root.$refs.userProfileDialog.userId = id;
this.$refs.userProfileDialog.open(); this.$root.$refs.userProfileDialog.open();
} }
} }
}; };