Add adult verification and erotic moderation features: Implement new routes and controller methods for managing adult verification requests, status updates, and document retrieval. Introduce erotic moderation actions and reports, enhancing administrative capabilities. Update chat and navigation controllers to support adult content filtering and access control. Enhance user parameter handling for adult verification status and requests, improving overall user experience and compliance.
This commit is contained in:
361
frontend/src/views/social/EroticPicturesView.vue
Normal file
361
frontend/src/views/social/EroticPicturesView.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="gallery-page erotic-gallery-page">
|
||||
<section class="gallery-hero erotic-gallery-hero surface-card">
|
||||
<div>
|
||||
<span class="gallery-kicker">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
|
||||
<h2>{{ $t('socialnetwork.erotic.picturesTitle') }}</h2>
|
||||
<p>{{ $t('socialnetwork.erotic.picturesIntro') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="gallery-view">
|
||||
<div class="sidebar surface-card">
|
||||
<h3>{{ $t('socialnetwork.gallery.folders') }}</h3>
|
||||
<ul 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]"
|
||||
@edit-folder="openEditFolderDialog"
|
||||
@delete-folder="deleteFolder"
|
||||
/>
|
||||
</ul>
|
||||
<button @click="openCreateFolderDialog">{{ $t('socialnetwork.gallery.create_folder') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="upload-section surface-card">
|
||||
<div class="upload-header" @click="toggleUploadSection">
|
||||
<span><i class="icon-upload-toggle">{{ isUploadVisible ? '▲' : '▼' }}</i></span>
|
||||
<h3>{{ $t('socialnetwork.erotic.uploadTitle') }}</h3>
|
||||
</div>
|
||||
<div v-if="isUploadVisible" class="upload-content">
|
||||
<form @submit.prevent="handleUpload">
|
||||
<div class="form-group">
|
||||
<label for="imageTitle">{{ $t('socialnetwork.gallery.upload.image_title') }}</label>
|
||||
<input v-model="imageTitle" type="text" :placeholder="$t('socialnetwork.gallery.upload.image_title')" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="imageFile">{{ $t('socialnetwork.gallery.upload.image_file') }}</label>
|
||||
<input type="file" accept="image/*" required @change="onFileChange" />
|
||||
<div v-if="imagePreview" class="image-preview">
|
||||
<img :src="imagePreview" alt="Image Preview" style="max-width: 150px; max-height: 150px;" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="visibility">{{ $t('socialnetwork.gallery.upload.visibility') }}</label>
|
||||
<multiselect
|
||||
v-model="selectedVisibilities"
|
||||
:options="visibilityOptions"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
label="description"
|
||||
:placeholder="$t('socialnetwork.gallery.upload.selectvisibility')"
|
||||
:track-by="'value'"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<span v-if="option && option.description">
|
||||
{{ $t(`socialnetwork.gallery.visibility.${option.description}`) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #tag="{ option, remove }">
|
||||
<span v-if="option && option.description" class="multiselect__tag">
|
||||
{{ $t(`socialnetwork.gallery.visibility.${option.description}`) }}
|
||||
<span @click="remove(option)">×</span>
|
||||
</span>
|
||||
</template>
|
||||
</multiselect>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="upload-button">
|
||||
{{ $t('socialnetwork.gallery.upload.upload_button') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="image-list surface-card">
|
||||
<h3>{{ $t('socialnetwork.gallery.images') }}</h3>
|
||||
<ul v-if="images.length > 0" class="image-grid">
|
||||
<li v-for="image in images" :key="image.id" class="erotic-image-card">
|
||||
<div class="erotic-image-card__preview" @click="!image.isModeratedHidden && openImageDialog(image)">
|
||||
<img v-if="!image.isModeratedHidden" :src="image.url || image.placeholder" alt="Loading..." />
|
||||
<div v-else class="erotic-image-card__hidden">
|
||||
{{ $t('socialnetwork.erotic.hiddenByModeration') }}
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ image.title }}</p>
|
||||
<span v-if="image.isModeratedHidden" class="erotic-image-card__badge">
|
||||
{{ $t('socialnetwork.erotic.moderationHidden') }}
|
||||
</span>
|
||||
<div class="erotic-image-card__actions">
|
||||
<button type="button" class="secondary" @click="startReport('image', image.id)">
|
||||
{{ $t('socialnetwork.erotic.reportAction') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="reportTarget.type === 'image' && reportTarget.id === image.id" class="erotic-report-form">
|
||||
<select v-model="reportReason">
|
||||
<option v-for="option in reportReasonOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<textarea v-model="reportNote" rows="3" :placeholder="$t('socialnetwork.erotic.reportNote')" />
|
||||
<div class="erotic-report-form__actions">
|
||||
<button type="button" @click="submitReport">{{ $t('socialnetwork.erotic.submitReport') }}</button>
|
||||
<button type="button" class="secondary" @click="resetReport">{{ $t('general.cancel') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<span v-else>{{ $t('socialnetwork.erotic.noimages') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
import FolderItem from '../../components/FolderItem.vue';
|
||||
import 'vue-multiselect/dist/vue-multiselect.min.css';
|
||||
import { EventBus } from '@/utils/eventBus.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FolderItem,
|
||||
Multiselect,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
folders: { children: [] },
|
||||
images: [],
|
||||
selectedFolder: null,
|
||||
imageTitle: '',
|
||||
fileToUpload: null,
|
||||
isUploadVisible: true,
|
||||
visibilityOptions: [],
|
||||
selectedVisibilities: [],
|
||||
imagePreview: null,
|
||||
reportTarget: { type: null, id: null },
|
||||
reportReason: 'other',
|
||||
reportNote: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
reportReasonOptions() {
|
||||
return ['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].map(value => ({
|
||||
value,
|
||||
label: this.$t(`socialnetwork.erotic.reportReasons.${value}`)
|
||||
}));
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadFolders();
|
||||
await this.loadImageVisibilities();
|
||||
if (this.folders) {
|
||||
this.selectFolder(this.folders);
|
||||
}
|
||||
EventBus.on('folderCreated', this.loadFolders);
|
||||
},
|
||||
beforeUnmount() {
|
||||
EventBus.off('folderCreated', this.loadFolders);
|
||||
},
|
||||
methods: {
|
||||
async loadFolders() {
|
||||
const response = await apiClient.get('/api/socialnetwork/erotic/folders');
|
||||
this.folders = response.data;
|
||||
},
|
||||
async loadImageVisibilities() {
|
||||
const response = await apiClient.get('/api/socialnetwork/imagevisibilities');
|
||||
this.visibilityOptions = response.data.filter(option => option.description !== 'everyone');
|
||||
if (!this.selectedVisibilities.length) {
|
||||
this.selectedVisibilities = this.visibilityOptions.filter(option => option.description === 'adults');
|
||||
}
|
||||
},
|
||||
async selectFolder(folder) {
|
||||
this.selectedFolder = folder;
|
||||
await this.loadImages(folder.id);
|
||||
},
|
||||
async loadImages(folderId) {
|
||||
const response = await apiClient.get(`/api/socialnetwork/erotic/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();
|
||||
},
|
||||
async fetchImages() {
|
||||
this.images.forEach((image) => {
|
||||
this.fetchImage(image);
|
||||
});
|
||||
},
|
||||
openCreateFolderDialog() {
|
||||
const parentFolder = this.selectedFolder || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
|
||||
Object.assign(this.$root.$refs.createFolderDialog, {
|
||||
parentFolder,
|
||||
folderId: 0,
|
||||
eroticMode: true,
|
||||
});
|
||||
this.$root.$refs.createFolderDialog.open();
|
||||
},
|
||||
onFileChange(event) {
|
||||
this.fileToUpload = event.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
this.imagePreview = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(this.fileToUpload);
|
||||
},
|
||||
async handleUpload() {
|
||||
if (!this.fileToUpload || !this.selectedFolder?.id) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', this.fileToUpload);
|
||||
formData.append('folderId', this.selectedFolder.id);
|
||||
formData.append('title', this.imageTitle);
|
||||
formData.append('visibility', JSON.stringify(this.selectedVisibilities.map((v) => v.id)));
|
||||
|
||||
await apiClient.post('/api/socialnetwork/erotic/images', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
await this.loadImages(this.selectedFolder.id);
|
||||
this.imageTitle = '';
|
||||
this.fileToUpload = null;
|
||||
this.imagePreview = null;
|
||||
this.selectedVisibilities = this.visibilityOptions.filter(option => option.description === 'adults');
|
||||
},
|
||||
async fetchImage(image) {
|
||||
if (image.isModeratedHidden) {
|
||||
return;
|
||||
}
|
||||
const userId = localStorage.getItem('userid') || sessionStorage.getItem('userid');
|
||||
const response = await apiClient.get(`/api/socialnetwork/erotic/image/${image.hash}`, {
|
||||
headers: { userid: userId },
|
||||
responseType: 'blob',
|
||||
});
|
||||
image.url = URL.createObjectURL(response.data);
|
||||
},
|
||||
toggleUploadSection() {
|
||||
this.isUploadVisible = !this.isUploadVisible;
|
||||
},
|
||||
openImageDialog(image) {
|
||||
this.$root.$refs.editImageDialog.open(image);
|
||||
},
|
||||
startReport(type, id) {
|
||||
this.reportTarget = { type, id };
|
||||
this.reportReason = 'other';
|
||||
this.reportNote = '';
|
||||
},
|
||||
resetReport() {
|
||||
this.reportTarget = { type: null, id: null };
|
||||
this.reportReason = 'other';
|
||||
this.reportNote = '';
|
||||
},
|
||||
async submitReport() {
|
||||
try {
|
||||
await apiClient.post('/api/socialnetwork/erotic/report', {
|
||||
targetType: this.reportTarget.type,
|
||||
targetId: this.reportTarget.id,
|
||||
reason: this.reportReason,
|
||||
note: this.reportNote
|
||||
});
|
||||
showSuccess(this, this.$t('socialnetwork.erotic.reportSubmitted'));
|
||||
this.resetReport();
|
||||
} catch (error) {
|
||||
showApiError(this, error, this.$t('socialnetwork.erotic.reportError'));
|
||||
}
|
||||
},
|
||||
async saveImage(updatedImage) {
|
||||
const response = await apiClient.put(`/api/socialnetwork/erotic/images/${updatedImage.id}`, {
|
||||
title: updatedImage.title,
|
||||
visibilities: updatedImage.visibilities,
|
||||
});
|
||||
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();
|
||||
},
|
||||
openEditFolderDialog(folder) {
|
||||
const parentFolder = folder.parent || { id: null, name: this.$t('socialnetwork.gallery.root_folder') };
|
||||
Object.assign(this.$root.$refs.createFolderDialog, {
|
||||
parentFolder,
|
||||
folderId: folder.id,
|
||||
eroticMode: true,
|
||||
});
|
||||
this.$root.$refs.createFolderDialog.open(folder);
|
||||
},
|
||||
async deleteFolder() {
|
||||
// Separate delete flow for adult folders is intentionally not enabled yet.
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.erotic-gallery-page {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.erotic-gallery-hero {
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(204, 44, 94, 0.18), transparent 38%),
|
||||
linear-gradient(140deg, rgba(37, 25, 33, 0.98), rgba(83, 34, 51, 0.96));
|
||||
color: #fff1f5;
|
||||
}
|
||||
|
||||
.erotic-gallery-hero p {
|
||||
color: rgba(255, 241, 245, 0.85);
|
||||
}
|
||||
|
||||
.erotic-image-card {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.erotic-image-card__preview {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.erotic-image-card__hidden {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 180px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(96, 32, 48, 0.18);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.erotic-image-card__badge {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
padding: 0.2rem 0.65rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(176, 88, 88, 0.14);
|
||||
color: #8b3340;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.erotic-image-card__actions,
|
||||
.erotic-report-form,
|
||||
.erotic-report-form__actions {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user