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:
@@ -1,7 +1,7 @@
|
||||
import apiClient from "@/utils/axios.js";
|
||||
|
||||
export const fetchPublicRooms = async () => {
|
||||
const response = await apiClient.get("/api/chat/rooms");
|
||||
export const fetchPublicRooms = async (options = {}) => {
|
||||
const response = await apiClient.get("/api/chat/rooms", { params: options });
|
||||
return response.data; // expecting array of { id, title, ... }
|
||||
};
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
v-for="(item, key) in menu"
|
||||
:key="key"
|
||||
class="mainmenuitem"
|
||||
:class="{ 'mainmenuitem--active': isItemActive(item), 'mainmenuitem--expanded': isMainExpanded(key) }"
|
||||
:class="{ 'mainmenuitem--active': isItemActive(item), 'mainmenuitem--expanded': isMainExpanded(key), 'mainmenuitem--disabled': item.disabled }"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:title="item.disabled ? $t(item.disabledReasonKey || 'socialnetwork.erotic.lockedShort') : undefined"
|
||||
:aria-haspopup="hasTopLevelSubmenu(item) ? 'menu' : undefined"
|
||||
:aria-expanded="hasTopLevelSubmenu(item) ? String(isMainExpanded(key)) : undefined"
|
||||
@click="handleItem(item, $event, key)"
|
||||
@@ -35,7 +36,8 @@
|
||||
:key="subkey"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
:class="{ 'submenu1__item--expanded': isSubExpanded(`${key}:${subkey}`) }"
|
||||
:class="{ 'submenu1__item--expanded': isSubExpanded(`${key}:${subkey}`), 'submenu-item--disabled': subitem.disabled }"
|
||||
:title="subitem.disabled ? $t(subitem.disabledReasonKey || 'socialnetwork.erotic.lockedShort') : undefined"
|
||||
@click="handleSubItem(subitem, subkey, key, $event)"
|
||||
@keydown.enter.prevent="handleSubItem(subitem, subkey, key, $event)"
|
||||
@keydown.space.prevent="handleSubItem(subitem, subkey, key, $event)"
|
||||
@@ -109,6 +111,8 @@
|
||||
:key="subsubkey"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
:class="{ 'submenu-item--disabled': subsubitem.disabled }"
|
||||
:title="subsubitem.disabled ? $t(subsubitem.disabledReasonKey || 'socialnetwork.erotic.lockedShort') : undefined"
|
||||
@click="handleItem(subsubitem, $event)"
|
||||
@keydown.enter.prevent="handleItem(subsubitem, $event)"
|
||||
@keydown.space.prevent="handleItem(subsubitem, $event)"
|
||||
@@ -357,14 +361,20 @@ export default {
|
||||
},
|
||||
|
||||
openMultiChat() {
|
||||
// Räume können später dynamisch geladen werden, hier als Platzhalter ein Beispiel:
|
||||
const exampleRooms = [
|
||||
{ id: 1, title: 'Allgemein' },
|
||||
{ id: 2, title: 'Rollenspiel' }
|
||||
];
|
||||
const ref = this.$root.$refs.multiChatDialog;
|
||||
if (ref && typeof ref.open === 'function') {
|
||||
ref.open(exampleRooms);
|
||||
ref.open();
|
||||
} else if (ref?.$refs?.dialog && typeof ref.$refs.dialog.open === 'function') {
|
||||
ref.$refs.dialog.open();
|
||||
} else {
|
||||
console.error('MultiChatDialog nicht bereit oder ohne open()');
|
||||
}
|
||||
},
|
||||
|
||||
openEroticChat() {
|
||||
const ref = this.$root.$refs.multiChatDialog;
|
||||
if (ref && typeof ref.open === 'function') {
|
||||
ref.open(null, { adultOnly: true });
|
||||
} else if (ref?.$refs?.dialog && typeof ref.$refs.dialog.open === 'function') {
|
||||
ref.$refs.dialog.open();
|
||||
} else {
|
||||
@@ -452,6 +462,10 @@ export default {
|
||||
handleItem(item, event, key = null) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (item?.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key && this.hasTopLevelSubmenu(item)) {
|
||||
if (this.isMobileNav) {
|
||||
this.toggleMain(key);
|
||||
@@ -603,6 +617,18 @@ ul {
|
||||
border-color: rgba(248, 162, 43, 0.2);
|
||||
}
|
||||
|
||||
.mainmenuitem--disabled,
|
||||
.submenu-item--disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.mainmenuitem--disabled:hover {
|
||||
transform: none;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.mainmenuitem--active {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-color: rgba(248, 162, 43, 0.22);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<th>{{ $t('falukant.branch.revenue.perMinute') }}</th>
|
||||
<th>{{ $t('falukant.branch.revenue.profitAbsolute') }}</th>
|
||||
<th>{{ $t('falukant.branch.revenue.profitPerMinute') }}</th>
|
||||
<th>Bessere Preise</th>
|
||||
<th>{{ $t('falukant.branch.revenue.betterPrices') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -29,12 +29,20 @@
|
||||
<td>{{ calculateProductProfit(product).perMinute }}</td>
|
||||
<td>
|
||||
<div v-if="getBetterPrices(product.id) && getBetterPrices(product.id).length > 0" class="price-cities">
|
||||
<span v-for="city in getBetterPrices(product.id)" :key="city.regionId"
|
||||
:class="['city-price', getCityPriceClass(city.branchType)]"
|
||||
:title="`${city.regionName}: ${formatPrice(city.price)}`">
|
||||
<span class="city-name">{{ city.regionName }}</span>
|
||||
<span class="city-price-value">({{ formatPrice(city.price) }})</span>
|
||||
</span>
|
||||
<template v-for="(city, idx) in getBetterPrices(product.id)" :key="city.regionId">
|
||||
<span
|
||||
v-if="idx > 0"
|
||||
class="city-price-sep"
|
||||
aria-hidden="true"
|
||||
>, </span>
|
||||
<span
|
||||
:class="['city-price', getCityPriceClass(city.branchType)]"
|
||||
:title="`${city.regionName}: ${formatPrice(city.price)}`"
|
||||
>
|
||||
<span class="city-name">{{ city.regionName }}</span>
|
||||
<span class="city-price-value">({{ formatPrice(city.price) }})</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<span v-else class="no-better-prices">—</span>
|
||||
</td>
|
||||
@@ -188,7 +196,14 @@
|
||||
.price-cities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3em;
|
||||
align-items: baseline;
|
||||
gap: 0.15em 0.35em;
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
}
|
||||
.city-price-sep {
|
||||
color: #666;
|
||||
user-select: none;
|
||||
}
|
||||
.city-price {
|
||||
padding: 0.2em 0.4em;
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
<input type="checkbox" v-model="localRoom.isPublic" />
|
||||
{{ $t('admin.chatrooms.isPublic') }}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="localRoom.isAdultOnly" />
|
||||
{{ $t('admin.chatrooms.isAdultOnly') }}
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" v-model="showGenderRestriction" />
|
||||
{{ $t('admin.chatrooms.genderRestriction.show') }}
|
||||
@@ -84,7 +88,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
dialog: null,
|
||||
localRoom: this.room ? { ...this.room } : { title: '', isPublic: true },
|
||||
localRoom: this.room ? { ...this.room } : { title: '', isPublic: true, isAdultOnly: false },
|
||||
roomTypes: [],
|
||||
genderRestrictions: [],
|
||||
userRights: [],
|
||||
@@ -102,7 +106,7 @@ export default {
|
||||
watch: {
|
||||
room: {
|
||||
handler(newVal) {
|
||||
this.localRoom = newVal ? { ...newVal } : { title: '', isPublic: true };
|
||||
this.localRoom = newVal ? { ...newVal } : { title: '', isPublic: true, isAdultOnly: false };
|
||||
this.showGenderRestriction = !!(newVal && newVal.genderRestrictionId);
|
||||
this.showMinAge = !!(newVal && newVal.minAge);
|
||||
this.showMaxAge = !!(newVal && newVal.maxAge);
|
||||
@@ -137,7 +141,7 @@ export default {
|
||||
this.fetchGenderRestrictions(),
|
||||
this.fetchUserRights()
|
||||
]);
|
||||
this.localRoom = roomData ? { ...roomData } : { title: '', isPublic: true };
|
||||
this.localRoom = roomData ? { ...roomData } : { title: '', isPublic: true, isAdultOnly: false };
|
||||
this.dialog.open();
|
||||
},
|
||||
closeDialog() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<DialogWidget ref="dialog" :title="$t('chat.multichat.title')" :modal="false" :show-close="true"
|
||||
<DialogWidget ref="dialog" :title="dialogTitle" :modal="false" :show-close="true"
|
||||
@close="onDialogClose" width="75vw" height="75vh" name="MultiChatDialog" icon="multichat24.png">
|
||||
<div class="dialog-widget-content">
|
||||
<div class="multi-chat-top">
|
||||
@@ -7,7 +7,7 @@
|
||||
<select v-model="selectedRoom" class="room-select">
|
||||
<option v-for="room in rooms" :key="room.id" :value="room.id">{{ room.title }}</option>
|
||||
</select>
|
||||
<button class="create-room-toggle-btn" type="button" @click="toggleRoomCreatePanel">
|
||||
<button v-if="!adultOnlyMode" class="create-room-toggle-btn" type="button" @click="toggleRoomCreatePanel">
|
||||
{{ showRoomCreatePanel ? $t('chat.multichat.createRoom.toggleShowChat') : $t('chat.multichat.createRoom.toggleCreateRoom') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -246,6 +246,9 @@ export default {
|
||||
components: { DialogWidget },
|
||||
computed: {
|
||||
...mapGetters(['user', 'menu']),
|
||||
dialogTitle() {
|
||||
return this.adultOnlyMode ? this.$t('chat.multichat.eroticTitle') : this.$t('chat.multichat.title');
|
||||
},
|
||||
isAdmin() {
|
||||
// Infer admin via presence of administration section in menu (server filters by rights)
|
||||
try {
|
||||
@@ -318,6 +321,7 @@ export default {
|
||||
announcedRoomEnter: false,
|
||||
showColorPicker: false,
|
||||
showRoomCreatePanel: false,
|
||||
adultOnlyMode: false,
|
||||
selectedColor: '#000000',
|
||||
lastColor: '#000000',
|
||||
hexInput: '#000000',
|
||||
@@ -690,7 +694,8 @@ export default {
|
||||
return map[code] || 'unknown reason';
|
||||
},
|
||||
// Wird extern aufgerufen um den Dialog zu öffnen
|
||||
open(rooms) {
|
||||
open(rooms, options = {}) {
|
||||
this.adultOnlyMode = Boolean(options?.adultOnly);
|
||||
// Falls externe Räume übergeben wurden, nutzen; sonst vom Server laden
|
||||
if (Array.isArray(rooms) && rooms.length) {
|
||||
this.initializeRooms(rooms);
|
||||
@@ -729,7 +734,7 @@ export default {
|
||||
},
|
||||
async loadRooms() {
|
||||
try {
|
||||
const data = await fetchPublicRooms();
|
||||
const data = await fetchPublicRooms(this.adultOnlyMode ? { adultOnly: true } : {});
|
||||
this.initializeRooms(Array.isArray(data) ? data : []);
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Laden der Räume', e);
|
||||
|
||||
@@ -53,9 +53,11 @@ export default {
|
||||
return {
|
||||
folderTitle: '',
|
||||
visibilityOptions: [],
|
||||
allVisibilityOptions: [],
|
||||
selectedVisibility: [],
|
||||
parentFolder: {id: null, name: ''},
|
||||
folderId: 0
|
||||
folderId: 0,
|
||||
eroticMode: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -69,6 +71,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
open(folder = null) {
|
||||
this.visibilityOptions = this.eroticMode
|
||||
? this.allVisibilityOptions.filter(option => option.description !== 'everyone')
|
||||
: [...this.allVisibilityOptions];
|
||||
if (folder) {
|
||||
this.folderTitle = folder.name;
|
||||
this.selectedVisibility = this.visibilityOptions.filter(option =>
|
||||
@@ -83,7 +88,10 @@ export default {
|
||||
async loadVisibilityOptions() {
|
||||
try {
|
||||
const response = await apiClient.get('/api/socialnetwork/imagevisibilities');
|
||||
this.visibilityOptions = response.data;
|
||||
this.allVisibilityOptions = response.data;
|
||||
this.visibilityOptions = this.eroticMode
|
||||
? response.data.filter(option => option.description !== 'everyone')
|
||||
: [...response.data];
|
||||
if (this.selectedVisibility.length) {
|
||||
this.selectedVisibility = this.visibilityOptions.filter(option =>
|
||||
this.selectedVisibility.map(v => v.id).includes(option.id)
|
||||
@@ -103,10 +111,11 @@ export default {
|
||||
visibilities: this.selectedVisibility.map(item => item.id),
|
||||
};
|
||||
try {
|
||||
const basePath = this.eroticMode ? '/api/socialnetwork/erotic/folders' : '/api/socialnetwork/folders';
|
||||
if (this.parentFolder.id) {
|
||||
await apiClient.post(`/api/socialnetwork/folders/${this.parentFolder.id}`, payload);
|
||||
await apiClient.post(`${basePath}/${this.parentFolder.id}`, payload);
|
||||
} else {
|
||||
await apiClient.post(`/api/socialnetwork/folders/${this.folderId}`, payload);
|
||||
await apiClient.post(`${basePath}/${this.folderId}`, payload);
|
||||
}
|
||||
EventBus.emit('folderCreated');
|
||||
this.closeDialog();
|
||||
|
||||
@@ -30,6 +30,86 @@
|
||||
"actions": "Aktionen",
|
||||
"search": "Suchen"
|
||||
},
|
||||
"adultVerification": {
|
||||
"title": "[Admin] - Erotik-Freigaben",
|
||||
"intro": "Volljährige Nutzer können den Erotikbereich beantragen. Hier werden Anfragen geprüft und freigegeben oder abgelehnt.",
|
||||
"username": "Benutzer",
|
||||
"age": "Alter",
|
||||
"statusLabel": "Status",
|
||||
"requestLabel": "Nachweis",
|
||||
"actions": "Aktionen",
|
||||
"approve": "Freigeben",
|
||||
"reject": "Ablehnen",
|
||||
"resetPending": "Auf Prüfung setzen",
|
||||
"openDocument": "Dokument ansehen",
|
||||
"empty": "Keine passenden Anfragen gefunden.",
|
||||
"loadError": "Die Freigaben konnten nicht geladen werden.",
|
||||
"updateError": "Der Status konnte nicht geändert werden.",
|
||||
"documentError": "Das Dokument konnte nicht geöffnet werden.",
|
||||
"filters": {
|
||||
"pending": "Offen",
|
||||
"approved": "Freigegeben",
|
||||
"rejected": "Abgelehnt",
|
||||
"all": "Alle"
|
||||
},
|
||||
"status": {
|
||||
"none": "Nicht angefragt",
|
||||
"pending": "In Prüfung",
|
||||
"approved": "Freigegeben",
|
||||
"rejected": "Abgelehnt"
|
||||
},
|
||||
"messages": {
|
||||
"approved": "Freigabe erteilt.",
|
||||
"rejected": "Freigabe abgelehnt.",
|
||||
"pending": "Anfrage wieder auf Prüfung gesetzt."
|
||||
}
|
||||
},
|
||||
"eroticModeration": {
|
||||
"title": "[Admin] - Erotik-Moderation",
|
||||
"intro": "Gemeldete Erotikbilder und -videos können hier geprüft, verborgen, gelöscht oder gegen den Account eskaliert werden.",
|
||||
"empty": "Keine passenden Meldungen gefunden.",
|
||||
"loadError": "Die Meldungen konnten nicht geladen werden.",
|
||||
"actionError": "Die Moderationsaktion konnte nicht ausgeführt werden.",
|
||||
"actionSuccess": "Die Moderationsaktion wurde gespeichert.",
|
||||
"target": "Ziel",
|
||||
"owner": "Besitzer",
|
||||
"reporter": "Meldender",
|
||||
"reason": "Grund",
|
||||
"statusLabel": "Status",
|
||||
"meta": "Zeit / Maßnahme",
|
||||
"actions": "Aktionen",
|
||||
"image": "Bild",
|
||||
"video": "Video",
|
||||
"hidden": "Verborgen",
|
||||
"preview": "Vorschau",
|
||||
"previewError": "Die Vorschau konnte nicht geladen werden.",
|
||||
"dismiss": "Zurückweisen",
|
||||
"hide": "Verbergen",
|
||||
"restore": "Wieder freigeben",
|
||||
"delete": "Löschen",
|
||||
"blockUploads": "Uploads sperren",
|
||||
"revokeAccess": "Erotikzugang entziehen",
|
||||
"notePrompt": "Notiz zur Moderationsentscheidung",
|
||||
"actionLabels": {
|
||||
"dismiss": "Zurückgewiesen",
|
||||
"hide_content": "Verborgen",
|
||||
"restore_content": "Freigegeben",
|
||||
"delete_content": "Gelöscht",
|
||||
"block_uploads": "Uploads gesperrt",
|
||||
"revoke_access": "Zugang entzogen"
|
||||
},
|
||||
"filters": {
|
||||
"open": "Offen",
|
||||
"actioned": "Bearbeitet",
|
||||
"dismissed": "Zurückgewiesen",
|
||||
"all": "Alle"
|
||||
},
|
||||
"status": {
|
||||
"open": "Offen",
|
||||
"actioned": "Bearbeitet",
|
||||
"dismissed": "Zurückgewiesen"
|
||||
}
|
||||
},
|
||||
"rights": {
|
||||
"add": "Recht hinzufügen",
|
||||
"select": "Bitte wählen",
|
||||
@@ -151,6 +231,7 @@
|
||||
"edit": "Chatraum bearbeiten",
|
||||
"type": "Typ",
|
||||
"isPublic": "Öffentlich sichtbar",
|
||||
"isAdultOnly": "Nur Erotikbereich",
|
||||
"actions": "Aktionen",
|
||||
"genderRestriction": {
|
||||
"show": "Geschlechtsbeschränkung aktivieren",
|
||||
@@ -346,4 +427,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"chat": {
|
||||
"multichat": {
|
||||
"title": "Multi-Chat",
|
||||
"eroticTitle": "Erotikchat",
|
||||
"autoscroll": "Automatisch scrollen",
|
||||
"options": "Optionen",
|
||||
"send": "Senden",
|
||||
@@ -155,4 +156,4 @@
|
||||
"selfstopped": "Du hast das Gespräch verlassen."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,7 +415,8 @@
|
||||
"collapse": "Erträge ausblenden",
|
||||
"knowledge": "Produktwissen",
|
||||
"profitAbsolute": "Gesamtgewinn",
|
||||
"profitPerMinute": "Gewinn pro Minute"
|
||||
"profitPerMinute": "Gewinn pro Minute",
|
||||
"betterPrices": "Bessere Preise"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Lager",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"userrights": "Benutzerrechte",
|
||||
"m-users": {
|
||||
"userlist": "Benutzerliste",
|
||||
"adultverification": "Erotik-Freigaben",
|
||||
"eroticmoderation": "Erotik-Moderation",
|
||||
"userstatistics": "Benutzerstatistiken",
|
||||
"userrights": "Benutzerrechte"
|
||||
},
|
||||
@@ -115,4 +117,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -148,7 +148,34 @@
|
||||
"language": "Sprache",
|
||||
"showinsearch": "In Usersuchen anzeigen",
|
||||
"changeaction": "Benutzerdaten ändern",
|
||||
"oldpassword": "Altes Passwort (benötigt)"
|
||||
"oldpassword": "Altes Passwort (benötigt)",
|
||||
"adultAccessTitle": "Erotikbereich",
|
||||
"adultAccessIntro": "Der Erotikbereich ist nur für volljährige Nutzer gedacht und wird zusätzlich durch Moderatoren freigeschaltet.",
|
||||
"requestAdultVerification": "Freischaltung anfragen",
|
||||
"requestAdultVerificationSuccess": "Die Freischaltung wurde angefragt.",
|
||||
"requestAdultVerificationError": "Die Freischaltung konnte nicht angefragt werden.",
|
||||
"adultStatus": {
|
||||
"ineligible": {
|
||||
"title": "Nicht verfügbar",
|
||||
"body": "Der Erotikbereich ist nur für volljährige Nutzer sichtbar."
|
||||
},
|
||||
"none": {
|
||||
"title": "Noch nicht freigeschaltet",
|
||||
"body": "Der Bereich ist sichtbar, aber bis zur Prüfung durch einen Moderator gesperrt."
|
||||
},
|
||||
"pending": {
|
||||
"title": "Prüfung läuft",
|
||||
"body": "Deine Anfrage liegt zur Moderationsprüfung vor. Bis zur Freigabe bleibt der Bereich gesperrt."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Freigeschaltet",
|
||||
"body": "Der Erotikbereich ist für deinen Account freigeschaltet."
|
||||
},
|
||||
"rejected": {
|
||||
"title": "Freischaltung abgelehnt",
|
||||
"body": "Die letzte Anfrage wurde nicht freigegeben. Du kannst eine neue Anfrage stellen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"languageAssistant": {
|
||||
"eyebrow": "Einstellungen",
|
||||
|
||||
@@ -248,8 +248,72 @@
|
||||
"withdrawn": "Du hast Deine Freundschaftsanfrage zurückgezogen.",
|
||||
"denied": "Du hast die Freundschaftsanfrage abgelehnt.",
|
||||
"accepted": "Die Freundschaft wurde geschlossen."
|
||||
}
|
||||
,
|
||||
},
|
||||
"erotic": {
|
||||
"eyebrow": "Erotik",
|
||||
"accessTitle": "Freischaltung für den Erotikbereich",
|
||||
"accessIntro": "Bilder, Videos und später auch Chatbereiche werden ab 18 angezeigt, aber erst nach Moderationsfreigabe nutzbar.",
|
||||
"lockedShort": "Dieser Bereich wird erst nach Moderatorfreigabe nutzbar.",
|
||||
"requestVerification": "Freischaltung anfragen",
|
||||
"requestSent": "Die Freischaltung wurde angefragt.",
|
||||
"requestError": "Die Freischaltung konnte nicht angefragt werden.",
|
||||
"requestInfoTitle": "Eingereichter Nachweis",
|
||||
"documentLabel": "Nachweisdatei",
|
||||
"noteLabel": "Kurze Notiz für die Moderation",
|
||||
"settingsLink": "Account-Einstellungen öffnen",
|
||||
"picturesTitle": "Erotikbilder",
|
||||
"picturesIntro": "Eigene Inhalte bleiben strikt vom normalen Galeriebereich getrennt. Hier verwaltest du nur Bilder für den freigeschalteten Erotikbereich.",
|
||||
"uploadTitle": "Erotikbild hochladen",
|
||||
"noimages": "In diesem Erotikordner befinden sich zur Zeit keine Bilder.",
|
||||
"videosTitle": "Erotikvideos",
|
||||
"videosIntro": "Eigene Videos werden getrennt vom normalen Social-Bereich verwaltet. Diese erste Version konzentriert sich auf Upload, Liste und Wiedergabe.",
|
||||
"videoUploadTitle": "Erotikvideo hochladen",
|
||||
"videoUploadHint": "Die erste Ausbaustufe speichert Videos direkt und verzichtet noch auf Transcoding oder Streamingprofile.",
|
||||
"videoDescription": "Beschreibung",
|
||||
"videoFile": "Videodatei",
|
||||
"myVideos": "Meine Videos",
|
||||
"noVideos": "Du hast noch keine Erotikvideos hochgeladen.",
|
||||
"reportAction": "Melden",
|
||||
"reportNote": "Kurze Notiz für die Moderation",
|
||||
"submitReport": "Meldung absenden",
|
||||
"reportSubmitted": "Die Meldung wurde aufgenommen.",
|
||||
"reportError": "Die Meldung konnte nicht gespeichert werden.",
|
||||
"moderationHidden": "Von Moderation verborgen",
|
||||
"hiddenByModeration": "Dieser Inhalt wurde vorläufig durch die Moderation verborgen.",
|
||||
"reportReasons": {
|
||||
"suspected_minor": "Verdacht auf Minderjährigkeit",
|
||||
"non_consensual": "Nicht einvernehmlicher Inhalt",
|
||||
"violence": "Gewalt oder Missbrauch",
|
||||
"harassment": "Belästigung oder Druck",
|
||||
"spam": "Spam oder Scam",
|
||||
"other": "Sonstiges"
|
||||
},
|
||||
"intro": "Der Bereich ist freigeschaltet. Die eigentlichen Bilder- und Videomodule folgen im nächsten Schritt.",
|
||||
"enabledTitle": "Zugang freigeschaltet",
|
||||
"enabledBody": "Dein Account ist für den Erotikbereich freigegeben. Hier entsteht jetzt die getrennte Bilder- und Videoansicht.",
|
||||
"roadmapTitle": "Als Nächstes",
|
||||
"roadmapModeration": "getrennte Moderation und Meldewege",
|
||||
"roadmapUpload": "eigene Upload- und Verwaltungsansichten",
|
||||
"roadmapSeparation": "saubere Trennung von normaler Galerie und Erotikbereich",
|
||||
"status": {
|
||||
"none": {
|
||||
"title": "Noch nicht freigeschaltet",
|
||||
"body": "Der Bereich ist sichtbar, bleibt aber bis zur Moderatorfreigabe gesperrt."
|
||||
},
|
||||
"pending": {
|
||||
"title": "Prüfung läuft",
|
||||
"body": "Deine Anfrage liegt zur Moderationsprüfung vor."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Freigeschaltet",
|
||||
"body": "Der Erotikbereich ist für deinen Account bereits freigeschaltet."
|
||||
},
|
||||
"rejected": {
|
||||
"title": "Freischaltung abgelehnt",
|
||||
"body": "Die letzte Anfrage wurde abgelehnt. Du kannst eine neue Anfrage stellen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"vocab": {
|
||||
"title": "Vokabeltrainer",
|
||||
"description": "Lege Sprachen an (oder abonniere sie) und teile sie mit Freunden.",
|
||||
|
||||
@@ -30,6 +30,86 @@
|
||||
"actions": "Actions",
|
||||
"search": "Search"
|
||||
},
|
||||
"adultVerification": {
|
||||
"title": "[Admin] - Erotic approvals",
|
||||
"intro": "Adult users can request access to the erotic area. Requests can be reviewed, approved or rejected here.",
|
||||
"username": "User",
|
||||
"age": "Age",
|
||||
"statusLabel": "Status",
|
||||
"requestLabel": "Proof",
|
||||
"actions": "Actions",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"resetPending": "Set pending",
|
||||
"openDocument": "Open document",
|
||||
"empty": "No matching requests found.",
|
||||
"loadError": "Could not load approvals.",
|
||||
"updateError": "Could not update the status.",
|
||||
"documentError": "Could not open the document.",
|
||||
"filters": {
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"all": "All"
|
||||
},
|
||||
"status": {
|
||||
"none": "Not requested",
|
||||
"pending": "Pending",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected"
|
||||
},
|
||||
"messages": {
|
||||
"approved": "Approval granted.",
|
||||
"rejected": "Approval rejected.",
|
||||
"pending": "Request set back to pending."
|
||||
}
|
||||
},
|
||||
"eroticModeration": {
|
||||
"title": "[Admin] - Erotic moderation",
|
||||
"intro": "Reported erotic pictures and videos can be reviewed, hidden, deleted, or escalated against the account here.",
|
||||
"empty": "No matching reports found.",
|
||||
"loadError": "The reports could not be loaded.",
|
||||
"actionError": "The moderation action could not be completed.",
|
||||
"actionSuccess": "The moderation action was saved.",
|
||||
"target": "Target",
|
||||
"owner": "Owner",
|
||||
"reporter": "Reporter",
|
||||
"reason": "Reason",
|
||||
"statusLabel": "Status",
|
||||
"meta": "Time / action",
|
||||
"actions": "Actions",
|
||||
"image": "Image",
|
||||
"video": "Video",
|
||||
"hidden": "Hidden",
|
||||
"preview": "Preview",
|
||||
"previewError": "The preview could not be loaded.",
|
||||
"dismiss": "Dismiss",
|
||||
"hide": "Hide",
|
||||
"restore": "Restore",
|
||||
"delete": "Delete",
|
||||
"blockUploads": "Block uploads",
|
||||
"revokeAccess": "Revoke erotic access",
|
||||
"notePrompt": "Note for this moderation action",
|
||||
"actionLabels": {
|
||||
"dismiss": "Dismissed",
|
||||
"hide_content": "Hidden",
|
||||
"restore_content": "Restored",
|
||||
"delete_content": "Deleted",
|
||||
"block_uploads": "Uploads blocked",
|
||||
"revoke_access": "Access revoked"
|
||||
},
|
||||
"filters": {
|
||||
"open": "Open",
|
||||
"actioned": "Actioned",
|
||||
"dismissed": "Dismissed",
|
||||
"all": "All"
|
||||
},
|
||||
"status": {
|
||||
"open": "Open",
|
||||
"actioned": "Actioned",
|
||||
"dismissed": "Dismissed"
|
||||
}
|
||||
},
|
||||
"rights": {
|
||||
"add": "Add right",
|
||||
"select": "Please select",
|
||||
@@ -178,6 +258,7 @@
|
||||
"edit": "Edit Chat Room",
|
||||
"type": "Type",
|
||||
"isPublic": "Publicly Visible",
|
||||
"isAdultOnly": "Erotic area only",
|
||||
"actions": "Actions",
|
||||
"genderRestriction": {
|
||||
"show": "Enable Gender Restriction",
|
||||
@@ -318,4 +399,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"chat": {
|
||||
"multichat": {
|
||||
"title": "Multi Chat",
|
||||
"eroticTitle": "Erotic chat",
|
||||
"autoscroll": "Auto scroll",
|
||||
"options": "Options",
|
||||
"send": "Send",
|
||||
@@ -155,4 +156,4 @@
|
||||
"selfstopped": "You left the conversation."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +374,27 @@
|
||||
"active": "Active",
|
||||
"noProductions": "No running productions."
|
||||
},
|
||||
"columns": {
|
||||
"city": "City",
|
||||
"type": "Type"
|
||||
},
|
||||
"types": {
|
||||
"production": "Production",
|
||||
"store": "Sales",
|
||||
"fullstack": "Production with sales"
|
||||
},
|
||||
"revenue": {
|
||||
"title": "Product revenue",
|
||||
"product": "Product",
|
||||
"absolute": "Revenue (absolute)",
|
||||
"perMinute": "Revenue per minute",
|
||||
"expand": "Show revenue",
|
||||
"collapse": "Hide revenue",
|
||||
"knowledge": "Product knowledge",
|
||||
"profitAbsolute": "Total profit",
|
||||
"profitPerMinute": "Profit per minute",
|
||||
"betterPrices": "Better prices elsewhere"
|
||||
},
|
||||
"vehicles": {
|
||||
"cargo_cart": "Cargo cart",
|
||||
"ox_cart": "Ox cart",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"userrights": "User rights",
|
||||
"m-users": {
|
||||
"userlist": "User list",
|
||||
"adultverification": "Erotic approvals",
|
||||
"eroticmoderation": "Erotic moderation",
|
||||
"userstatistics": "User statistics",
|
||||
"userrights": "User rights"
|
||||
},
|
||||
@@ -114,4 +116,4 @@
|
||||
"church": "Church"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,34 @@
|
||||
"language": "Language",
|
||||
"showinsearch": "Show in User Search",
|
||||
"changeaction": "Change User Data",
|
||||
"oldpassword": "Old Password (required)"
|
||||
"oldpassword": "Old Password (required)",
|
||||
"adultAccessTitle": "Erotic area",
|
||||
"adultAccessIntro": "The erotic area is intended only for adult users and also requires moderator approval.",
|
||||
"requestAdultVerification": "Request access",
|
||||
"requestAdultVerificationSuccess": "Access request submitted.",
|
||||
"requestAdultVerificationError": "Access request could not be submitted.",
|
||||
"adultStatus": {
|
||||
"ineligible": {
|
||||
"title": "Not available",
|
||||
"body": "The erotic area is only visible to adult users."
|
||||
},
|
||||
"none": {
|
||||
"title": "Not unlocked yet",
|
||||
"body": "The area is visible, but it stays locked until a moderator approves it."
|
||||
},
|
||||
"pending": {
|
||||
"title": "Review pending",
|
||||
"body": "Your request is waiting for moderation review. The area stays locked until then."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Unlocked",
|
||||
"body": "The erotic area is enabled for your account."
|
||||
},
|
||||
"rejected": {
|
||||
"title": "Request denied",
|
||||
"body": "The last request was not approved. You can submit a new request."
|
||||
}
|
||||
}
|
||||
},
|
||||
"languageAssistant": {
|
||||
"eyebrow": "Settings",
|
||||
|
||||
@@ -248,8 +248,72 @@
|
||||
"withdrawn": "You have withdrawn your friendship request.",
|
||||
"denied": "You have denied the friendship request.",
|
||||
"accepted": "The friendship has been established."
|
||||
}
|
||||
,
|
||||
},
|
||||
"erotic": {
|
||||
"eyebrow": "Erotic",
|
||||
"accessTitle": "Unlock the erotic area",
|
||||
"accessIntro": "Pictures, videos and later chat areas are visible from age 18, but only usable after moderator approval.",
|
||||
"lockedShort": "This area becomes usable only after moderator approval.",
|
||||
"requestVerification": "Request access",
|
||||
"requestSent": "The access request has been submitted.",
|
||||
"requestError": "The access request could not be submitted.",
|
||||
"requestInfoTitle": "Submitted proof",
|
||||
"documentLabel": "Verification document",
|
||||
"noteLabel": "Short note for moderation",
|
||||
"settingsLink": "Open account settings",
|
||||
"picturesTitle": "Erotic pictures",
|
||||
"picturesIntro": "Your content stays strictly separate from the normal gallery. Manage only images for the unlocked erotic area here.",
|
||||
"uploadTitle": "Upload erotic picture",
|
||||
"noimages": "There are currently no pictures in this erotic folder.",
|
||||
"videosTitle": "Erotic videos",
|
||||
"videosIntro": "Your own videos are managed separately from the normal social area. This first version focuses on upload, list and playback.",
|
||||
"videoUploadTitle": "Upload erotic video",
|
||||
"videoUploadHint": "The first rollout stores videos directly and intentionally skips transcoding or streaming profiles for now.",
|
||||
"videoDescription": "Description",
|
||||
"videoFile": "Video file",
|
||||
"myVideos": "My videos",
|
||||
"noVideos": "You have not uploaded any erotic videos yet.",
|
||||
"reportAction": "Report",
|
||||
"reportNote": "Short note for moderation",
|
||||
"submitReport": "Submit report",
|
||||
"reportSubmitted": "The report has been submitted.",
|
||||
"reportError": "The report could not be saved.",
|
||||
"moderationHidden": "Hidden by moderation",
|
||||
"hiddenByModeration": "This content has been temporarily hidden by moderation.",
|
||||
"reportReasons": {
|
||||
"suspected_minor": "Suspected minor",
|
||||
"non_consensual": "Non-consensual content",
|
||||
"violence": "Violence or abuse",
|
||||
"harassment": "Harassment or pressure",
|
||||
"spam": "Spam or scam",
|
||||
"other": "Other"
|
||||
},
|
||||
"intro": "The area is unlocked. The actual picture and video modules follow in the next step.",
|
||||
"enabledTitle": "Access unlocked",
|
||||
"enabledBody": "Your account is enabled for the erotic area. The separate picture and video views are built next.",
|
||||
"roadmapTitle": "Next up",
|
||||
"roadmapModeration": "separate moderation and reporting paths",
|
||||
"roadmapUpload": "dedicated upload and management views",
|
||||
"roadmapSeparation": "clear separation from the normal gallery",
|
||||
"status": {
|
||||
"none": {
|
||||
"title": "Not unlocked yet",
|
||||
"body": "The area is visible, but stays locked until moderator approval."
|
||||
},
|
||||
"pending": {
|
||||
"title": "Review pending",
|
||||
"body": "Your request is waiting for moderation review."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Unlocked",
|
||||
"body": "The erotic area is already unlocked for your account."
|
||||
},
|
||||
"rejected": {
|
||||
"title": "Request denied",
|
||||
"body": "The last request was denied. You can submit a new request."
|
||||
}
|
||||
}
|
||||
},
|
||||
"vocab": {
|
||||
"title": "Vocabulary trainer",
|
||||
"description": "Create languages (or subscribe to them) and share them with friends.",
|
||||
|
||||
@@ -30,6 +30,86 @@
|
||||
"actions": "Acciones",
|
||||
"search": "Buscar"
|
||||
},
|
||||
"adultVerification": {
|
||||
"title": "[Admin] - Aprobaciones eróticas",
|
||||
"intro": "Los usuarios adultos pueden solicitar acceso al área erótica. Aquí se revisan, aprueban o rechazan las solicitudes.",
|
||||
"username": "Usuario",
|
||||
"age": "Edad",
|
||||
"statusLabel": "Estado",
|
||||
"requestLabel": "Prueba",
|
||||
"actions": "Acciones",
|
||||
"approve": "Aprobar",
|
||||
"reject": "Rechazar",
|
||||
"resetPending": "Poner en revisión",
|
||||
"openDocument": "Abrir documento",
|
||||
"empty": "No se han encontrado solicitudes.",
|
||||
"loadError": "No se pudieron cargar las aprobaciones.",
|
||||
"updateError": "No se pudo actualizar el estado.",
|
||||
"documentError": "No se pudo abrir el documento.",
|
||||
"filters": {
|
||||
"pending": "Pendientes",
|
||||
"approved": "Aprobadas",
|
||||
"rejected": "Rechazadas",
|
||||
"all": "Todas"
|
||||
},
|
||||
"status": {
|
||||
"none": "No solicitada",
|
||||
"pending": "En revisión",
|
||||
"approved": "Aprobada",
|
||||
"rejected": "Rechazada"
|
||||
},
|
||||
"messages": {
|
||||
"approved": "Aprobación concedida.",
|
||||
"rejected": "Aprobación rechazada.",
|
||||
"pending": "La solicitud se ha vuelto a poner en revisión."
|
||||
}
|
||||
},
|
||||
"eroticModeration": {
|
||||
"title": "[Admin] - Moderación erótica",
|
||||
"intro": "Aquí se pueden revisar, ocultar, eliminar o escalar imágenes y vídeos eróticos denunciados.",
|
||||
"empty": "No se encontraron denuncias.",
|
||||
"loadError": "No se pudieron cargar las denuncias.",
|
||||
"actionError": "No se pudo ejecutar la acción de moderación.",
|
||||
"actionSuccess": "La acción de moderación fue guardada.",
|
||||
"target": "Objetivo",
|
||||
"owner": "Propietario",
|
||||
"reporter": "Denunciante",
|
||||
"reason": "Motivo",
|
||||
"statusLabel": "Estado",
|
||||
"meta": "Hora / acción",
|
||||
"actions": "Acciones",
|
||||
"image": "Imagen",
|
||||
"video": "Vídeo",
|
||||
"hidden": "Oculto",
|
||||
"preview": "Vista previa",
|
||||
"previewError": "No se pudo cargar la vista previa.",
|
||||
"dismiss": "Descartar",
|
||||
"hide": "Ocultar",
|
||||
"restore": "Restaurar",
|
||||
"delete": "Eliminar",
|
||||
"blockUploads": "Bloquear subidas",
|
||||
"revokeAccess": "Retirar acceso erótico",
|
||||
"notePrompt": "Nota para esta decisión de moderación",
|
||||
"actionLabels": {
|
||||
"dismiss": "Descartado",
|
||||
"hide_content": "Oculto",
|
||||
"restore_content": "Restaurado",
|
||||
"delete_content": "Eliminado",
|
||||
"block_uploads": "Subidas bloqueadas",
|
||||
"revoke_access": "Acceso retirado"
|
||||
},
|
||||
"filters": {
|
||||
"open": "Abierto",
|
||||
"actioned": "Procesado",
|
||||
"dismissed": "Descartado",
|
||||
"all": "Todos"
|
||||
},
|
||||
"status": {
|
||||
"open": "Abierto",
|
||||
"actioned": "Procesado",
|
||||
"dismissed": "Descartado"
|
||||
}
|
||||
},
|
||||
"rights": {
|
||||
"add": "Añadir permiso",
|
||||
"select": "Por favor, selecciona",
|
||||
@@ -151,6 +231,7 @@
|
||||
"edit": "Editar sala de chat",
|
||||
"type": "Typ",
|
||||
"isPublic": "Visible públicamente",
|
||||
"isAdultOnly": "Solo área erótica",
|
||||
"actions": "Acciones",
|
||||
"genderRestriction": {
|
||||
"show": "Activar restricción de género",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"chat": {
|
||||
"multichat": {
|
||||
"title": "Multi-Chat",
|
||||
"eroticTitle": "Chat erótico",
|
||||
"autoscroll": "Desplazamiento automático",
|
||||
"options": "Opciones",
|
||||
"send": "Enviar",
|
||||
|
||||
@@ -400,7 +400,8 @@
|
||||
"collapse": "Ocultar ingresos",
|
||||
"knowledge": "Conocimiento del producto",
|
||||
"profitAbsolute": "Beneficio total",
|
||||
"profitPerMinute": "Beneficio por minuto"
|
||||
"profitPerMinute": "Beneficio por minuto",
|
||||
"betterPrices": "Mejores precios"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Almacén",
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
"userrights": "Permisos de usuario",
|
||||
"m-users": {
|
||||
"userlist": "Lista de usuarios",
|
||||
"adultverification": "Aprobaciones eróticas",
|
||||
"eroticmoderation": "Moderación erótica",
|
||||
"userstatistics": "Estadísticas de usuarios",
|
||||
"userrights": "Permisos de usuario"
|
||||
},
|
||||
|
||||
@@ -148,7 +148,34 @@
|
||||
"language": "Idioma",
|
||||
"showinsearch": "Mostrar en búsquedas de usuarios",
|
||||
"changeaction": "Actualizar datos de usuario",
|
||||
"oldpassword": "Contraseña anterior (obligatoria)"
|
||||
"oldpassword": "Contraseña anterior (obligatoria)",
|
||||
"adultAccessTitle": "Área erótica",
|
||||
"adultAccessIntro": "El área erótica está destinada solo a usuarios adultos y además requiere aprobación de moderación.",
|
||||
"requestAdultVerification": "Solicitar acceso",
|
||||
"requestAdultVerificationSuccess": "La solicitud de acceso se ha enviado.",
|
||||
"requestAdultVerificationError": "No se pudo enviar la solicitud de acceso.",
|
||||
"adultStatus": {
|
||||
"ineligible": {
|
||||
"title": "No disponible",
|
||||
"body": "El área erótica solo es visible para usuarios adultos."
|
||||
},
|
||||
"none": {
|
||||
"title": "Aún no desbloqueado",
|
||||
"body": "El área es visible, pero seguirá bloqueada hasta que un moderador la apruebe."
|
||||
},
|
||||
"pending": {
|
||||
"title": "Revisión pendiente",
|
||||
"body": "Tu solicitud está pendiente de revisión. El área seguirá bloqueada hasta entonces."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Desbloqueado",
|
||||
"body": "El área erótica está habilitada para tu cuenta."
|
||||
},
|
||||
"rejected": {
|
||||
"title": "Solicitud rechazada",
|
||||
"body": "La última solicitud no fue aprobada. Puedes enviar una nueva."
|
||||
}
|
||||
}
|
||||
},
|
||||
"languageAssistant": {
|
||||
"eyebrow": "Ajustes",
|
||||
|
||||
@@ -249,6 +249,71 @@
|
||||
"denied": "Has rechazado la solicitud de amistad.",
|
||||
"accepted": "Se ha aceptado la amistad."
|
||||
},
|
||||
"erotic": {
|
||||
"eyebrow": "Erótico",
|
||||
"accessTitle": "Desbloqueo del área erótica",
|
||||
"accessIntro": "Las imágenes, los vídeos y más adelante los chats se muestran a partir de los 18 años, pero solo se pueden usar tras la aprobación de moderación.",
|
||||
"lockedShort": "Esta área solo estará disponible tras la aprobación de moderación.",
|
||||
"requestVerification": "Solicitar acceso",
|
||||
"requestSent": "La solicitud de acceso se ha enviado.",
|
||||
"requestError": "No se pudo enviar la solicitud de acceso.",
|
||||
"requestInfoTitle": "Prueba enviada",
|
||||
"documentLabel": "Documento de verificación",
|
||||
"noteLabel": "Breve nota para moderación",
|
||||
"settingsLink": "Abrir ajustes de la cuenta",
|
||||
"picturesTitle": "Imágenes eróticas",
|
||||
"picturesIntro": "Tus contenidos permanecen estrictamente separados de la galería normal. Aquí gestionas solo imágenes del área erótica desbloqueada.",
|
||||
"uploadTitle": "Subir imagen erótica",
|
||||
"noimages": "Actualmente no hay imágenes en esta carpeta erótica.",
|
||||
"videosTitle": "Vídeos eróticos",
|
||||
"videosIntro": "Tus propios vídeos se gestionan por separado del área social normal. Esta primera versión se centra en subida, lista y reproducción.",
|
||||
"videoUploadTitle": "Subir vídeo erótico",
|
||||
"videoUploadHint": "La primera fase guarda los vídeos directamente y por ahora evita transcodificación o perfiles de streaming.",
|
||||
"videoDescription": "Descripción",
|
||||
"videoFile": "Archivo de vídeo",
|
||||
"myVideos": "Mis vídeos",
|
||||
"noVideos": "Todavía no has subido vídeos eróticos.",
|
||||
"reportAction": "Denunciar",
|
||||
"reportNote": "Nota breve para moderación",
|
||||
"submitReport": "Enviar denuncia",
|
||||
"reportSubmitted": "La denuncia fue enviada.",
|
||||
"reportError": "No se pudo guardar la denuncia.",
|
||||
"moderationHidden": "Oculto por moderación",
|
||||
"hiddenByModeration": "Este contenido fue ocultado temporalmente por la moderación.",
|
||||
"reportReasons": {
|
||||
"suspected_minor": "Sospecha de minoría de edad",
|
||||
"non_consensual": "Contenido no consentido",
|
||||
"violence": "Violencia o abuso",
|
||||
"harassment": "Acoso o presión",
|
||||
"spam": "Spam o estafa",
|
||||
"other": "Otro"
|
||||
},
|
||||
"intro": "El área está desbloqueada. Los módulos reales de imágenes y vídeos llegarán en el siguiente paso.",
|
||||
"enabledTitle": "Acceso desbloqueado",
|
||||
"enabledBody": "Tu cuenta está habilitada para el área erótica. Las vistas separadas de imágenes y vídeos se construirán a continuación.",
|
||||
"roadmapTitle": "Próximamente",
|
||||
"roadmapModeration": "moderación y vías de reporte separadas",
|
||||
"roadmapUpload": "vistas propias para subir y gestionar contenido",
|
||||
"roadmapSeparation": "separación clara de la galería normal",
|
||||
"status": {
|
||||
"none": {
|
||||
"title": "Aún no desbloqueado",
|
||||
"body": "El área es visible, pero seguirá bloqueada hasta la aprobación de moderación."
|
||||
},
|
||||
"pending": {
|
||||
"title": "Revisión pendiente",
|
||||
"body": "Tu solicitud está pendiente de revisión por moderación."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Desbloqueado",
|
||||
"body": "El área erótica ya está desbloqueada para tu cuenta."
|
||||
},
|
||||
"rejected": {
|
||||
"title": "Solicitud rechazada",
|
||||
"body": "La última solicitud fue rechazada. Puedes enviar una nueva."
|
||||
}
|
||||
}
|
||||
},
|
||||
"vocab": {
|
||||
"title": "Entrenador de vocabulario",
|
||||
"description": "Crea idiomas (o suscríbete) y compártelos con tus amigos.",
|
||||
|
||||
@@ -9,6 +9,8 @@ const AdminFalukantCreateNPCView = () => import('../views/admin/falukant/CreateN
|
||||
const AdminMinigamesView = () => import('../views/admin/MinigamesView.vue');
|
||||
const AdminTaxiToolsView = () => import('../views/admin/TaxiToolsView.vue');
|
||||
const AdminUsersView = () => import('../views/admin/UsersView.vue');
|
||||
const AdminAdultVerificationView = () => import('../views/admin/AdultVerificationView.vue');
|
||||
const AdminEroticModerationView = () => import('../views/admin/EroticModerationView.vue');
|
||||
const UserStatisticsView = () => import('../views/admin/UserStatisticsView.vue');
|
||||
const ServicesStatusView = () => import('../views/admin/ServicesStatusView.vue');
|
||||
|
||||
@@ -31,6 +33,18 @@ const adminRoutes = [
|
||||
component: UserStatisticsView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/users/adult-verification',
|
||||
name: 'AdminAdultVerification',
|
||||
component: AdminAdultVerificationView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/users/erotic-moderation',
|
||||
name: 'AdminEroticModeration',
|
||||
component: AdminEroticModerationView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/contacts',
|
||||
name: 'AdminContacts',
|
||||
|
||||
@@ -10,6 +10,7 @@ import minigamesRoutes from './minigamesRoutes';
|
||||
import personalRoutes from './personalRoutes';
|
||||
import marketingRoutes from './marketingRoutes';
|
||||
import { applyRouteSeo, buildAbsoluteUrl } from '../utils/seo';
|
||||
import apiClient from '../utils/axios';
|
||||
|
||||
const HomeView = () => import('../views/HomeView.vue');
|
||||
|
||||
@@ -58,18 +59,30 @@ const router = createRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
if (!store.getters.isLoggedIn) {
|
||||
next('/');
|
||||
return next('/');
|
||||
} else if (!store.getters.user.active) {
|
||||
next('/activate');
|
||||
} else {
|
||||
next();
|
||||
return next('/activate');
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
if (to.matched.some(record => record.meta.requiresAdultArea)) {
|
||||
try {
|
||||
const response = await apiClient.post('/api/settings/account', { userId: store.getters.user.id });
|
||||
if (!response.data?.isAdult) {
|
||||
return next('/');
|
||||
}
|
||||
if (!response.data?.adultAccessEnabled) {
|
||||
return next('/socialnetwork/erotic/access');
|
||||
}
|
||||
} catch (error) {
|
||||
return next('/socialnetwork/erotic/access');
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.afterEach((to) => {
|
||||
|
||||
@@ -13,6 +13,9 @@ const VocabChapterView = () => import('../views/social/VocabChapterView.vue');
|
||||
const VocabCourseListView = () => import('../views/social/VocabCourseListView.vue');
|
||||
const VocabCourseView = () => import('../views/social/VocabCourseView.vue');
|
||||
const VocabLessonView = () => import('../views/social/VocabLessonView.vue');
|
||||
const EroticAccessView = () => import('../views/social/EroticAccessView.vue');
|
||||
const EroticPicturesView = () => import('../views/social/EroticPicturesView.vue');
|
||||
const EroticVideosView = () => import('../views/social/EroticVideosView.vue');
|
||||
|
||||
const socialRoutes = [
|
||||
{
|
||||
@@ -39,6 +42,24 @@ const socialRoutes = [
|
||||
component: GalleryView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/erotic/access',
|
||||
name: 'EroticAccess',
|
||||
component: EroticAccessView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/erotic/pictures',
|
||||
name: 'EroticPictures',
|
||||
component: EroticPicturesView,
|
||||
meta: { requiresAuth: true, requiresAdultArea: true }
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/erotic/videos',
|
||||
name: 'EroticVideos',
|
||||
component: EroticVideosView,
|
||||
meta: { requiresAuth: true, requiresAdultArea: true }
|
||||
},
|
||||
{
|
||||
path: '/socialnetwork/forum/:id',
|
||||
name: 'Forum',
|
||||
|
||||
258
frontend/src/views/admin/AdultVerificationView.vue
Normal file
258
frontend/src/views/admin/AdultVerificationView.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<div class="adult-verification">
|
||||
<section class="adult-verification__hero surface-card">
|
||||
<span class="adult-verification__eyebrow">Administration</span>
|
||||
<h1>{{ $t('admin.adultVerification.title') }}</h1>
|
||||
<p>{{ $t('admin.adultVerification.intro') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="adult-verification__filters surface-card">
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:class="{ active: statusFilter === option.value }"
|
||||
@click="changeFilter(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="adult-verification__list surface-card">
|
||||
<div v-if="loading" class="adult-verification__state">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="rows.length === 0" class="adult-verification__state">{{ $t('admin.adultVerification.empty') }}</div>
|
||||
<table v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('admin.adultVerification.username') }}</th>
|
||||
<th>{{ $t('admin.adultVerification.age') }}</th>
|
||||
<th>{{ $t('admin.adultVerification.statusLabel') }}</th>
|
||||
<th>{{ $t('admin.adultVerification.requestLabel') }}</th>
|
||||
<th>{{ $t('admin.adultVerification.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.id">
|
||||
<td>{{ row.username }}</td>
|
||||
<td>{{ row.age }}</td>
|
||||
<td>
|
||||
<span class="adult-verification__badge" :class="`adult-verification__badge--${row.adultVerificationStatus}`">
|
||||
{{ $t(`admin.adultVerification.status.${row.adultVerificationStatus}`) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="adult-verification__request">
|
||||
<template v-if="row.adultVerificationRequest">
|
||||
<strong>{{ row.adultVerificationRequest.originalName }}</strong>
|
||||
<span v-if="row.adultVerificationRequest.note">{{ row.adultVerificationRequest.note }}</span>
|
||||
<button type="button" class="secondary" @click="openDocument(row)">
|
||||
{{ $t('admin.adultVerification.openDocument') }}
|
||||
</button>
|
||||
</template>
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td class="adult-verification__actions">
|
||||
<button type="button" @click="setStatus(row, 'approved')">{{ $t('admin.adultVerification.approve') }}</button>
|
||||
<button type="button" class="secondary" @click="setStatus(row, 'rejected')">{{ $t('admin.adultVerification.reject') }}</button>
|
||||
<button
|
||||
v-if="row.adultVerificationStatus !== 'pending'"
|
||||
type="button"
|
||||
class="secondary"
|
||||
@click="setStatus(row, 'pending')"
|
||||
>
|
||||
{{ $t('admin.adultVerification.resetPending') }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'AdminAdultVerificationView',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
statusFilter: 'pending',
|
||||
rows: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filterOptions() {
|
||||
return [
|
||||
{ value: 'pending', label: this.$t('admin.adultVerification.filters.pending') },
|
||||
{ value: 'approved', label: this.$t('admin.adultVerification.filters.approved') },
|
||||
{ value: 'rejected', label: this.$t('admin.adultVerification.filters.rejected') },
|
||||
{ value: 'all', label: this.$t('admin.adultVerification.filters.all') }
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/api/admin/users/adult-verification', {
|
||||
params: { status: this.statusFilter }
|
||||
});
|
||||
this.rows = response.data || [];
|
||||
} catch (error) {
|
||||
this.rows = [];
|
||||
showApiError(this, error, this.$t('admin.adultVerification.loadError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async changeFilter(filter) {
|
||||
this.statusFilter = filter;
|
||||
await this.load();
|
||||
},
|
||||
async setStatus(row, status) {
|
||||
try {
|
||||
await apiClient.put(`/api/admin/users/${row.id}/adult-verification`, { status });
|
||||
showSuccess(this, this.$t(`admin.adultVerification.messages.${status}`));
|
||||
await this.load();
|
||||
} catch (error) {
|
||||
showApiError(this, error, this.$t('admin.adultVerification.updateError'));
|
||||
}
|
||||
},
|
||||
async openDocument(row) {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/admin/users/${row.id}/adult-verification/document`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
const url = window.URL.createObjectURL(response.data);
|
||||
window.open(url, '_blank', 'noopener');
|
||||
window.setTimeout(() => window.URL.revokeObjectURL(url), 10000);
|
||||
} catch (error) {
|
||||
showApiError(this, error, this.$t('admin.adultVerification.documentError'));
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.adult-verification {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.adult-verification__hero,
|
||||
.adult-verification__filters,
|
||||
.adult-verification__list {
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.adult-verification__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-secondary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.adult-verification__hero p,
|
||||
.adult-verification__state {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.adult-verification__filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.adult-verification__filters button.active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.adult-verification table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.adult-verification th,
|
||||
.adult-verification td {
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid rgba(93, 64, 55, 0.08);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.adult-verification__badge {
|
||||
display: inline-flex;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.adult-verification__badge--pending {
|
||||
background: rgba(216, 167, 65, 0.16);
|
||||
}
|
||||
|
||||
.adult-verification__badge--approved {
|
||||
background: rgba(92, 156, 106, 0.18);
|
||||
}
|
||||
|
||||
.adult-verification__badge--rejected {
|
||||
background: rgba(176, 88, 88, 0.16);
|
||||
}
|
||||
|
||||
.adult-verification__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.adult-verification__request {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.adult-verification__actions .secondary {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.adult-verification__hero,
|
||||
.adult-verification__filters,
|
||||
.adult-verification__list {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.adult-verification table,
|
||||
.adult-verification thead,
|
||||
.adult-verification tbody,
|
||||
.adult-verification tr,
|
||||
.adult-verification td {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.adult-verification thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.adult-verification td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
<th>{{ $t('admin.chatrooms.roomName') }}</th>
|
||||
<th>{{ $t('admin.chatrooms.type') }}</th>
|
||||
<th>{{ $t('admin.chatrooms.isPublic') }}</th>
|
||||
<th>{{ $t('admin.chatrooms.isAdultOnly') }}</th>
|
||||
<th>{{ $t('admin.chatrooms.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -17,6 +18,7 @@
|
||||
<td>{{ room.title }}</td>
|
||||
<td>{{ getRoomTypeLabel(room) }}</td>
|
||||
<td>{{ room.isPublic ? $t('common.yes') : $t('common.no') }}</td>
|
||||
<td>{{ room.isAdultOnly ? $t('common.yes') : $t('common.no') }}</td>
|
||||
<td>
|
||||
<button @click="editRoom(room)">{{ $t('common.edit') }}</button>
|
||||
<button @click="deleteRoom(room)">{{ $t('common.delete') }}</button>
|
||||
|
||||
244
frontend/src/views/admin/EroticModerationView.vue
Normal file
244
frontend/src/views/admin/EroticModerationView.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div class="adult-verification">
|
||||
<section class="adult-verification__hero surface-card">
|
||||
<span class="adult-verification__eyebrow">Administration</span>
|
||||
<h1>{{ $t('admin.eroticModeration.title') }}</h1>
|
||||
<p>{{ $t('admin.eroticModeration.intro') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="adult-verification__filters surface-card">
|
||||
<button
|
||||
v-for="option in filterOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
:class="{ active: statusFilter === option.value }"
|
||||
@click="changeFilter(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="adult-verification__list surface-card">
|
||||
<div v-if="loading" class="adult-verification__state">{{ $t('general.loading') }}</div>
|
||||
<div v-else-if="rows.length === 0" class="adult-verification__state">{{ $t('admin.eroticModeration.empty') }}</div>
|
||||
<table v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('admin.eroticModeration.target') }}</th>
|
||||
<th>{{ $t('admin.eroticModeration.owner') }}</th>
|
||||
<th>{{ $t('admin.eroticModeration.reporter') }}</th>
|
||||
<th>{{ $t('admin.eroticModeration.reason') }}</th>
|
||||
<th>{{ $t('admin.eroticModeration.statusLabel') }}</th>
|
||||
<th>{{ $t('admin.eroticModeration.meta') }}</th>
|
||||
<th>{{ $t('admin.eroticModeration.actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in rows" :key="row.id">
|
||||
<td class="adult-verification__request">
|
||||
<strong>{{ row.targetType === 'image' ? $t('admin.eroticModeration.image') : $t('admin.eroticModeration.video') }}</strong>
|
||||
<span>{{ row.target?.title || '—' }}</span>
|
||||
<span v-if="row.target?.isModeratedHidden" class="adult-verification__badge adult-verification__badge--rejected">
|
||||
{{ $t('admin.eroticModeration.hidden') }}
|
||||
</span>
|
||||
<button v-if="row.target" type="button" class="secondary" @click="previewTarget(row)">
|
||||
{{ $t('admin.eroticModeration.preview') }}
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ row.owner?.username || '—' }}</td>
|
||||
<td>{{ row.reporter?.username || '—' }}</td>
|
||||
<td class="adult-verification__request">
|
||||
<strong>{{ $t(`socialnetwork.erotic.reportReasons.${row.reason}`) }}</strong>
|
||||
<span v-if="row.note">{{ row.note }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="adult-verification__badge" :class="`adult-verification__badge--${row.status}`">
|
||||
{{ $t(`admin.eroticModeration.status.${row.status}`) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="adult-verification__request">
|
||||
<span>{{ formatDate(row.createdAt) }}</span>
|
||||
<span v-if="row.actionTaken">{{ $t(`admin.eroticModeration.actionLabels.${row.actionTaken}`) }}</span>
|
||||
<span v-if="row.handledAt">{{ formatDate(row.handledAt) }}</span>
|
||||
</td>
|
||||
<td class="adult-verification__actions">
|
||||
<button type="button" @click="applyAction(row, 'dismiss')">{{ $t('admin.eroticModeration.dismiss') }}</button>
|
||||
<button type="button" class="secondary" @click="applyAction(row, 'hide_content')">{{ $t('admin.eroticModeration.hide') }}</button>
|
||||
<button type="button" class="secondary" @click="applyAction(row, 'restore_content')">{{ $t('admin.eroticModeration.restore') }}</button>
|
||||
<button type="button" class="secondary" @click="applyAction(row, 'delete_content')">{{ $t('admin.eroticModeration.delete') }}</button>
|
||||
<button type="button" class="secondary" @click="applyAction(row, 'block_uploads')">{{ $t('admin.eroticModeration.blockUploads') }}</button>
|
||||
<button type="button" class="secondary" @click="applyAction(row, 'revoke_access')">{{ $t('admin.eroticModeration.revokeAccess') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'EroticModerationView',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
statusFilter: 'open',
|
||||
rows: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filterOptions() {
|
||||
return [
|
||||
{ value: 'open', label: this.$t('admin.eroticModeration.filters.open') },
|
||||
{ value: 'actioned', label: this.$t('admin.eroticModeration.filters.actioned') },
|
||||
{ value: 'dismissed', label: this.$t('admin.eroticModeration.filters.dismissed') },
|
||||
{ value: 'all', label: this.$t('admin.eroticModeration.filters.all') }
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/api/admin/users/erotic-moderation', {
|
||||
params: { status: this.statusFilter }
|
||||
});
|
||||
this.rows = response.data || [];
|
||||
} catch (error) {
|
||||
this.rows = [];
|
||||
showApiError(this, error, this.$t('admin.eroticModeration.loadError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async changeFilter(filter) {
|
||||
this.statusFilter = filter;
|
||||
await this.load();
|
||||
},
|
||||
async applyAction(row, action) {
|
||||
const note = window.prompt(this.$t('admin.eroticModeration.notePrompt'), row.note || '') || '';
|
||||
try {
|
||||
await apiClient.put(`/api/admin/users/erotic-moderation/${row.id}`, { action, note });
|
||||
showSuccess(this, this.$t('admin.eroticModeration.actionSuccess'));
|
||||
await this.load();
|
||||
} catch (error) {
|
||||
showApiError(this, error, this.$t('admin.eroticModeration.actionError'));
|
||||
}
|
||||
},
|
||||
async previewTarget(row) {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/admin/users/erotic-moderation/preview/${row.targetType}/${row.targetId}`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
const url = window.URL.createObjectURL(response.data);
|
||||
window.open(url, '_blank', 'noopener');
|
||||
window.setTimeout(() => window.URL.revokeObjectURL(url), 10000);
|
||||
} catch (error) {
|
||||
showApiError(this, error, this.$t('admin.eroticModeration.previewError'));
|
||||
}
|
||||
},
|
||||
formatDate(value) {
|
||||
if (!value) return '—';
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
}).format(new Date(value));
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.adult-verification {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.adult-verification__hero,
|
||||
.adult-verification__filters,
|
||||
.adult-verification__list {
|
||||
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.adult-verification__eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-secondary-soft);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.adult-verification__hero p,
|
||||
.adult-verification__state {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.adult-verification__filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.adult-verification__filters button.active {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.adult-verification table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.adult-verification th,
|
||||
.adult-verification td {
|
||||
padding: 12px 10px;
|
||||
border-bottom: 1px solid rgba(93, 64, 55, 0.08);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.adult-verification__badge {
|
||||
display: inline-flex;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.adult-verification__badge--open {
|
||||
background: rgba(216, 167, 65, 0.16);
|
||||
}
|
||||
|
||||
.adult-verification__badge--actioned {
|
||||
background: rgba(92, 156, 106, 0.18);
|
||||
}
|
||||
|
||||
.adult-verification__badge--dismissed {
|
||||
background: rgba(176, 88, 88, 0.16);
|
||||
}
|
||||
|
||||
.adult-verification__actions,
|
||||
.adult-verification__request {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.adult-verification__actions .secondary {
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -394,6 +394,7 @@ const CERTIFICATE_PRODUCT_LEVELS = [
|
||||
{ level: 5, products: ['horse', 'ox'] },
|
||||
];
|
||||
|
||||
// Stückkosten wie backend/utils/falukant/falukantProductEconomy.js (bei Änderungen dort mitziehen).
|
||||
const PRODUCTION_COST_BASE = 6.0;
|
||||
const PRODUCTION_COST_PER_PRODUCT_CATEGORY = 1.0;
|
||||
const PRODUCTION_HEADROOM_DISCOUNT_PER_STEP = 0.035;
|
||||
@@ -761,7 +762,7 @@ export default {
|
||||
// Fallback auf Standard-Berechnung
|
||||
const knowledgeFactor = product.knowledges?.[0]?.knowledge || 0;
|
||||
const maxPrice = product.sellCost;
|
||||
const minPrice = maxPrice * 0.6;
|
||||
const minPrice = maxPrice * 0.7;
|
||||
prices[product.id] = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
||||
}
|
||||
}
|
||||
@@ -845,7 +846,7 @@ export default {
|
||||
// Fallback auf Standard-Berechnung
|
||||
const knowledgeFactor = product.knowledges[0].knowledge || 0;
|
||||
const maxPrice = product.sellCost;
|
||||
const minPrice = maxPrice * 0.6;
|
||||
const minPrice = maxPrice * 0.7;
|
||||
revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,19 @@
|
||||
<span>{{ $t("settings.account.showinsearch") }}</span>
|
||||
</label>
|
||||
|
||||
<section class="account-settings__adult surface-card">
|
||||
<h3>{{ $t("settings.account.adultAccessTitle") }}</h3>
|
||||
<p>{{ $t("settings.account.adultAccessIntro") }}</p>
|
||||
<div class="account-settings__adult-status">
|
||||
<strong>{{ adultStatusTitle }}</strong>
|
||||
<span>{{ adultStatusText }}</span>
|
||||
<span v-if="adultVerificationRequest?.originalName">{{ adultVerificationRequest.originalName }}</span>
|
||||
</div>
|
||||
<div class="account-settings__adult-actions" v-if="canRequestAdultVerification">
|
||||
<router-link to="/socialnetwork/erotic/access">{{ $t("settings.account.requestAdultVerification") }}</router-link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="account-settings__actions">
|
||||
<button @click="changeAccount">{{ $t("settings.account.changeaction") }}</button>
|
||||
</div>
|
||||
@@ -69,6 +82,9 @@ export default {
|
||||
newpasswordretype: "",
|
||||
showInSearch: false,
|
||||
oldpassword: "",
|
||||
isAdult: false,
|
||||
adultVerificationStatus: "none",
|
||||
adultVerificationRequest: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -81,6 +97,21 @@ export default {
|
||||
},
|
||||
passwordsMatch() {
|
||||
return this.newpassword === this.newpasswordretype;
|
||||
},
|
||||
canRequestAdultVerification() {
|
||||
return this.isAdult && ['none', 'rejected'].includes(this.adultVerificationStatus);
|
||||
},
|
||||
adultStatusTitle() {
|
||||
if (!this.isAdult) {
|
||||
return this.$t('settings.account.adultStatus.ineligible.title');
|
||||
}
|
||||
return this.$t(`settings.account.adultStatus.${this.adultVerificationStatus}.title`);
|
||||
},
|
||||
adultStatusText() {
|
||||
if (!this.isAdult) {
|
||||
return this.$t('settings.account.adultStatus.ineligible.body');
|
||||
}
|
||||
return this.$t(`settings.account.adultStatus.${this.adultVerificationStatus}.body`);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -137,13 +168,17 @@ export default {
|
||||
console.error('Fehler beim Speichern der Account-Einstellungen:', error);
|
||||
showApiError(this, error, 'Ein Fehler ist beim Speichern der Account-Einstellungen aufgetreten.');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
async mounted() {
|
||||
const response = await apiClient.post('/api/settings/account', { userId: this.user.id });
|
||||
this.username = response.data.username;
|
||||
this.showInSearch = response.data.showinsearch;
|
||||
this.email = response.data.email;
|
||||
this.isAdult = !!response.data.isAdult;
|
||||
this.adultVerificationStatus = response.data.adultVerificationStatus || 'none';
|
||||
this.adultVerificationRequest = response.data.adultVerificationRequest || null;
|
||||
|
||||
// Stelle sicher, dass Passwort-Felder leer sind
|
||||
this.newpassword = '';
|
||||
@@ -228,6 +263,29 @@ export default {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.account-settings__adult {
|
||||
margin-top: 18px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(250, 244, 235, 0.72);
|
||||
}
|
||||
|
||||
.account-settings__adult p,
|
||||
.account-settings__adult-status {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.account-settings__adult-status {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.account-settings__adult-actions {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.account-settings__grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
201
frontend/src/views/social/EroticAccessView.vue
Normal file
201
frontend/src/views/social/EroticAccessView.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="erotic-access-page">
|
||||
<section class="erotic-access-hero surface-card">
|
||||
<span class="erotic-access-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
|
||||
<h2>{{ $t('socialnetwork.erotic.accessTitle') }}</h2>
|
||||
<p>{{ $t('socialnetwork.erotic.accessIntro') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="erotic-access-panel surface-card">
|
||||
<div class="erotic-access-status">
|
||||
<strong>{{ statusTitle }}</strong>
|
||||
<span>{{ statusText }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="account?.adultVerificationRequest" class="erotic-access-request">
|
||||
<strong>{{ $t('socialnetwork.erotic.requestInfoTitle') }}</strong>
|
||||
<span>{{ account.adultVerificationRequest.originalName }}</span>
|
||||
<span v-if="account.adultVerificationRequest.submittedAt">{{ formattedSubmittedAt }}</span>
|
||||
<span v-if="account.adultVerificationRequest.note">{{ account.adultVerificationRequest.note }}</span>
|
||||
</div>
|
||||
|
||||
<div class="erotic-access-actions">
|
||||
<router-link to="/settings/account" class="erotic-access-link">
|
||||
{{ $t('socialnetwork.erotic.settingsLink') }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<form v-if="canRequestVerification" class="erotic-access-form" @submit.prevent="requestVerification">
|
||||
<label>
|
||||
<span>{{ $t('socialnetwork.erotic.documentLabel') }}</span>
|
||||
<input type="file" accept=".jpg,.jpeg,.png,.webp,.pdf" @change="handleFileChange" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ $t('socialnetwork.erotic.noteLabel') }}</span>
|
||||
<textarea v-model="note" rows="4"></textarea>
|
||||
</label>
|
||||
<button type="submit">{{ $t('socialnetwork.erotic.requestVerification') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'EroticAccessView',
|
||||
data() {
|
||||
return {
|
||||
account: null,
|
||||
note: '',
|
||||
documentFile: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['user']),
|
||||
status() {
|
||||
return this.account?.adultVerificationStatus || 'none';
|
||||
},
|
||||
canRequestVerification() {
|
||||
return this.account?.isAdult && ['none', 'rejected'].includes(this.status);
|
||||
},
|
||||
statusTitle() {
|
||||
if (!this.account?.isAdult) {
|
||||
return this.$t('settings.account.adultStatus.ineligible.title');
|
||||
}
|
||||
return this.$t(`socialnetwork.erotic.status.${this.status}.title`);
|
||||
},
|
||||
statusText() {
|
||||
if (!this.account?.isAdult) {
|
||||
return this.$t('settings.account.adultStatus.ineligible.body');
|
||||
}
|
||||
return this.$t(`socialnetwork.erotic.status.${this.status}.body`);
|
||||
},
|
||||
formattedSubmittedAt() {
|
||||
const value = this.account?.adultVerificationRequest?.submittedAt;
|
||||
if (!value) return '';
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadAccount() {
|
||||
const response = await apiClient.post('/api/settings/account', { userId: this.user.id });
|
||||
this.account = response.data;
|
||||
},
|
||||
handleFileChange(event) {
|
||||
this.documentFile = event.target.files?.[0] || null;
|
||||
},
|
||||
async requestVerification() {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
if (this.documentFile) {
|
||||
formData.append('document', this.documentFile);
|
||||
}
|
||||
formData.append('note', this.note || '');
|
||||
await apiClient.post('/api/settings/adult-verification/request', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
await this.loadAccount();
|
||||
this.note = '';
|
||||
this.documentFile = null;
|
||||
showSuccess(this, this.$t('socialnetwork.erotic.requestSent'));
|
||||
} catch (error) {
|
||||
showApiError(this, error, this.$t('socialnetwork.erotic.requestError'));
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadAccount();
|
||||
if (this.account?.adultAccessEnabled) {
|
||||
this.$router.replace('/socialnetwork/erotic/pictures');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.erotic-access-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.erotic-access-hero,
|
||||
.erotic-access-panel {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(180deg, rgba(255, 249, 244, 0.98), rgba(245, 237, 229, 0.96));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.erotic-access-hero {
|
||||
padding: 26px 28px;
|
||||
}
|
||||
|
||||
.erotic-access-eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(164, 98, 72, 0.14);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.erotic-access-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.erotic-access-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.erotic-access-status {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(196, 162, 108, 0.14);
|
||||
}
|
||||
|
||||
.erotic-access-request,
|
||||
.erotic-access-form {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.erotic-access-request {
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.erotic-access-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.erotic-access-link {
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.erotic-access-form label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
116
frontend/src/views/social/EroticMediaPlaceholderView.vue
Normal file
116
frontend/src/views/social/EroticMediaPlaceholderView.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="adult-media-page">
|
||||
<section class="adult-media-hero surface-card">
|
||||
<span class="adult-media-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ $t('socialnetwork.erotic.intro') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="adult-media-panel surface-card">
|
||||
<div class="adult-media-status">
|
||||
<strong>{{ $t('socialnetwork.erotic.enabledTitle') }}</strong>
|
||||
<span>{{ $t('socialnetwork.erotic.enabledBody') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="adult-media-roadmap">
|
||||
<h3>{{ $t('socialnetwork.erotic.roadmapTitle') }}</h3>
|
||||
<ul>
|
||||
<li>{{ $t('socialnetwork.erotic.roadmapModeration') }}</li>
|
||||
<li>{{ $t('socialnetwork.erotic.roadmapUpload') }}</li>
|
||||
<li>{{ $t('socialnetwork.erotic.roadmapSeparation') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<router-link to="/settings/account" class="adult-media-link">
|
||||
{{ $t('socialnetwork.erotic.settingsLink') }}
|
||||
</router-link>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EroticMediaPlaceholderView',
|
||||
props: {
|
||||
mediaType: {
|
||||
type: String,
|
||||
default: 'pictures'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.mediaType === 'videos'
|
||||
? this.$t('socialnetwork.erotic.videosTitle')
|
||||
: this.$t('socialnetwork.erotic.picturesTitle');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.adult-media-page {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.adult-media-hero,
|
||||
.adult-media-panel {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(180deg, rgba(255, 249, 244, 0.98), rgba(245, 237, 229, 0.96));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.adult-media-hero {
|
||||
padding: 26px 28px;
|
||||
}
|
||||
|
||||
.adult-media-eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(164, 98, 72, 0.14);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.adult-media-hero p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.adult-media-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.adult-media-status {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(196, 162, 108, 0.14);
|
||||
}
|
||||
|
||||
.adult-media-roadmap h3 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adult-media-roadmap ul {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.adult-media-link {
|
||||
width: fit-content;
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
269
frontend/src/views/social/EroticVideosView.vue
Normal file
269
frontend/src/views/social/EroticVideosView.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div class="erotic-videos-page">
|
||||
<section class="erotic-videos-hero surface-card">
|
||||
<div>
|
||||
<span class="erotic-videos-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
|
||||
<h2>{{ $t('socialnetwork.erotic.videosTitle') }}</h2>
|
||||
<p>{{ $t('socialnetwork.erotic.videosIntro') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="erotic-videos-upload surface-card">
|
||||
<div class="erotic-videos-upload__header">
|
||||
<h3>{{ $t('socialnetwork.erotic.videoUploadTitle') }}</h3>
|
||||
<p>{{ $t('socialnetwork.erotic.videoUploadHint') }}</p>
|
||||
</div>
|
||||
|
||||
<form class="erotic-videos-form" @submit.prevent="handleUpload">
|
||||
<label>
|
||||
<span>{{ $t('socialnetwork.gallery.upload.image_title') }}</span>
|
||||
<input v-model="title" type="text" :placeholder="$t('socialnetwork.gallery.upload.image_title')" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ $t('socialnetwork.erotic.videoDescription') }}</span>
|
||||
<textarea v-model="description" rows="3" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ $t('socialnetwork.erotic.videoFile') }}</span>
|
||||
<input type="file" accept="video/mp4,video/webm,video/ogg,video/quicktime" required @change="onFileChange" />
|
||||
</label>
|
||||
<button type="submit">{{ $t('socialnetwork.gallery.upload.upload_button') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="erotic-videos-list surface-card">
|
||||
<h3>{{ $t('socialnetwork.erotic.myVideos') }}</h3>
|
||||
<div v-if="videos.length === 0" class="erotic-videos-empty">
|
||||
{{ $t('socialnetwork.erotic.noVideos') }}
|
||||
</div>
|
||||
<ul v-else class="erotic-videos-grid">
|
||||
<li v-for="video in videos" :key="video.id" class="erotic-videos-card">
|
||||
<video v-if="!video.isModeratedHidden" :src="video.url" controls preload="metadata" />
|
||||
<div v-else class="erotic-videos-card__hidden">
|
||||
{{ $t('socialnetwork.erotic.hiddenByModeration') }}
|
||||
</div>
|
||||
<strong>{{ video.title }}</strong>
|
||||
<span v-if="video.isModeratedHidden" class="erotic-videos-card__badge">
|
||||
{{ $t('socialnetwork.erotic.moderationHidden') }}
|
||||
</span>
|
||||
<p v-if="video.description">{{ video.description }}</p>
|
||||
<div class="erotic-videos-card__actions">
|
||||
<button type="button" class="secondary" @click="startReport('video', video.id)">
|
||||
{{ $t('socialnetwork.erotic.reportAction') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="reportTarget.type === 'video' && reportTarget.id === video.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>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '@/utils/axios.js';
|
||||
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
||||
|
||||
export default {
|
||||
name: 'EroticVideosView',
|
||||
data() {
|
||||
return {
|
||||
videos: [],
|
||||
fileToUpload: null,
|
||||
title: '',
|
||||
description: '',
|
||||
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.loadVideos();
|
||||
},
|
||||
methods: {
|
||||
async loadVideos() {
|
||||
const response = await apiClient.get('/api/socialnetwork/erotic/videos');
|
||||
this.videos = await Promise.all(response.data.map(async (video) => ({
|
||||
...video,
|
||||
url: video.isModeratedHidden ? null : await this.fetchVideoUrl(video.hash),
|
||||
})));
|
||||
},
|
||||
async fetchVideoUrl(hash) {
|
||||
const response = await apiClient.get(`/api/socialnetwork/erotic/video/${hash}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
return URL.createObjectURL(response.data);
|
||||
},
|
||||
onFileChange(event) {
|
||||
this.fileToUpload = event.target.files[0];
|
||||
},
|
||||
async handleUpload() {
|
||||
if (!this.fileToUpload) return;
|
||||
const formData = new FormData();
|
||||
formData.append('video', this.fileToUpload);
|
||||
formData.append('title', this.title);
|
||||
formData.append('description', this.description);
|
||||
await apiClient.post('/api/socialnetwork/erotic/videos', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
this.fileToUpload = null;
|
||||
this.title = '';
|
||||
this.description = '';
|
||||
await this.loadVideos();
|
||||
},
|
||||
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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.erotic-videos-page {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.erotic-videos-hero,
|
||||
.erotic-videos-upload,
|
||||
.erotic-videos-list {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.erotic-videos-hero {
|
||||
padding: 1.5rem;
|
||||
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-videos-eyebrow {
|
||||
display: inline-flex;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 241, 245, 0.12);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.erotic-videos-hero p {
|
||||
color: rgba(255, 241, 245, 0.84);
|
||||
}
|
||||
|
||||
.erotic-videos-upload,
|
||||
.erotic-videos-list {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.erotic-videos-upload__header,
|
||||
.erotic-videos-form {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.erotic-videos-form label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.erotic-videos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.erotic-videos-card {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding: 0.85rem;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(112, 60, 80, 0.08);
|
||||
}
|
||||
|
||||
.erotic-videos-card__hidden {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 160px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(96, 32, 48, 0.18);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.erotic-videos-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-videos-card video {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-sm);
|
||||
background: #120b0f;
|
||||
}
|
||||
|
||||
.erotic-videos-card__actions,
|
||||
.erotic-report-form,
|
||||
.erotic-report-form__actions {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.erotic-videos-empty {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -166,6 +166,7 @@ export default {
|
||||
Object.assign(this.$root.$refs.createFolderDialog, {
|
||||
parentFolder: parentFolder,
|
||||
folderId: 0,
|
||||
eroticMode: false,
|
||||
});
|
||||
this.$root.$refs.createFolderDialog.open();
|
||||
},
|
||||
@@ -246,6 +247,7 @@ export default {
|
||||
Object.assign(this.$root.$refs.createFolderDialog, {
|
||||
parentFolder: parentFolder,
|
||||
folderId: folder.id,
|
||||
eroticMode: false,
|
||||
});
|
||||
this.$root.$refs.createFolderDialog.open(folder);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user