Files
yourpart3/frontend/src/views/admin/EroticModerationView.vue
Torsten Schulz (local) cf097772f4 style(admin): update button styles in AdultVerificationView and EroticModerationView
- Added new styles for buttons in the adult verification filters to enhance appearance and usability.
- Set minimum height, padding, and font size for consistency across both views.
2026-03-27 11:25:56 +01:00

256 lines
8.7 KiB
Vue

<template>
<div class="contenthidden">
<div class="contentscroll">
<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>
</div>
</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;
padding-bottom: 24px;
}
.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__filters button {
min-height: 34px;
padding: 0 14px;
font-size: 0.92rem;
}
.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>