refactor(EroticVideosView): restructure layout and enhance video statistics display

- Updated the layout of the EroticVideosView component to improve organization and user experience.
- Introduced a sidebar for video upload and management, separating it from the video list.
- Added statistics for total, visible, and hidden videos to provide users with better insights.
- Enhanced form elements and labels for clarity and usability during video uploads.
This commit is contained in:
Torsten Schulz (local)
2026-03-27 15:36:00 +01:00
parent 6cbcf9d95f
commit e76be33743

View File

@@ -1,72 +1,139 @@
<template> <template>
<div class="erotic-videos-page"> <div class="contenthidden">
<section class="erotic-videos-hero surface-card"> <div class="contentscroll">
<div> <div class="erotic-videos-page">
<span class="erotic-videos-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span> <section class="erotic-videos-hero surface-card">
<h2>{{ $t('socialnetwork.erotic.videosTitle') }}</h2> <div>
<p>{{ $t('socialnetwork.erotic.videosIntro') }}</p> <span class="erotic-videos-eyebrow">{{ $t('socialnetwork.erotic.eyebrow') }}</span>
</div> <h2>{{ $t('socialnetwork.erotic.videosTitle') }}</h2>
</section> <p>{{ $t('socialnetwork.erotic.videosIntro') }}</p>
<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> </div>
<strong>{{ video.title }}</strong> <div class="erotic-videos-hero__stats">
<span v-if="video.isModeratedHidden" class="erotic-videos-card__badge"> <div class="erotic-videos-stat">
{{ $t('socialnetwork.erotic.moderationHidden') }} <strong>{{ videos.length }}</strong>
</span> <span>{{ $t('socialnetwork.erotic.myVideos') }}</span>
<p v-if="video.description">{{ video.description }}</p> </div>
<div class="erotic-videos-card__actions"> <div class="erotic-videos-stat">
<button type="button" class="secondary" @click="startReport('video', video.id)"> <strong>{{ visibleVideosCount }}</strong>
{{ $t('socialnetwork.erotic.reportAction') }} <span>Online</span>
</button> </div>
</div> <div class="erotic-videos-stat">
<div v-if="reportTarget.type === 'video' && reportTarget.id === video.id" class="erotic-report-form"> <strong>{{ hiddenVideosCount }}</strong>
<select v-model="reportReason"> <span>{{ $t('socialnetwork.erotic.moderationHidden') }}</span>
<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>
</div> </div>
</li> </section>
</ul>
</section> <div class="erotic-videos-workspace">
<aside class="erotic-videos-sidebar">
<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="4" />
</label>
<label>
<span>{{ $t('socialnetwork.erotic.videoFile') }}</span>
<input type="file" accept="video/mp4,video/webm,video/ogg,video/quicktime" required @change="onFileChange" />
</label>
<div class="erotic-videos-upload__meta">
<span>MP4, WEBM, OGG, MOV</span>
<span v-if="fileToUpload">{{ fileToUpload.name }}</span>
</div>
<button type="submit">{{ $t('socialnetwork.gallery.upload.upload_button') }}</button>
</form>
</section>
<section class="erotic-videos-panel surface-card">
<h3>Bibliothek</h3>
<div class="erotic-videos-panel__list">
<div class="erotic-videos-panel__item">
<span>Letzter Upload</span>
<strong>{{ latestVideoTitle }}</strong>
</div>
<div class="erotic-videos-panel__item">
<span>Sichtbare Videos</span>
<strong>{{ visibleVideosCount }}</strong>
</div>
<div class="erotic-videos-panel__item">
<span>Moderationsfälle</span>
<strong>{{ hiddenVideosCount }}</strong>
</div>
</div>
</section>
<section class="erotic-videos-panel surface-card">
<h3>Hinweise</h3>
<ul class="erotic-videos-checklist">
<li>{{ $t('socialnetwork.erotic.videoUploadHint') }}</li>
<li>{{ $t('socialnetwork.erotic.reportAction') }} bei problematischen Inhalten direkt am Eintrag.</li>
<li>Die Bibliothek rechts ist eigenstaendig scrollbar, auch wenn viele Videos vorhanden sind.</li>
</ul>
</section>
</aside>
<section class="erotic-videos-library surface-card">
<div class="erotic-videos-library__header">
<div>
<h3>{{ $t('socialnetwork.erotic.myVideos') }}</h3>
<p>Eigene Uploads, Moderationsstatus und Meldungen an einem Ort.</p>
</div>
<span class="erotic-videos-library__count">{{ videos.length }}</span>
</div>
<div v-if="videos.length === 0" class="erotic-videos-empty">
<strong>{{ $t('socialnetwork.erotic.noVideos') }}</strong>
<span>Nutze links den Upload-Bereich, um deine erste Videokarte anzulegen.</span>
</div>
<div v-else class="erotic-videos-library__scroll">
<ul 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>
<div class="erotic-videos-card__meta">
<strong>{{ video.title || 'Ohne Titel' }}</strong>
<span v-if="video.createdAtLabel">{{ video.createdAtLabel }}</span>
</div>
<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>
</div>
</section>
</div>
</div>
</div>
</div> </div>
</template> </template>
@@ -93,19 +160,40 @@ export default {
value, value,
label: this.$t(`socialnetwork.erotic.reportReasons.${value}`) label: this.$t(`socialnetwork.erotic.reportReasons.${value}`)
})); }));
},
visibleVideosCount() {
return this.videos.filter((video) => !video.isModeratedHidden).length;
},
hiddenVideosCount() {
return this.videos.filter((video) => video.isModeratedHidden).length;
},
latestVideoTitle() {
return this.videos[0]?.title || 'Noch kein Upload';
} }
}, },
async mounted() { async mounted() {
await this.loadVideos(); await this.loadVideos();
}, },
beforeUnmount() {
this.releaseVideoUrls();
},
methods: { methods: {
async loadVideos() { async loadVideos() {
this.releaseVideoUrls();
const response = await apiClient.get('/api/socialnetwork/erotic/videos'); const response = await apiClient.get('/api/socialnetwork/erotic/videos');
this.videos = await Promise.all(response.data.map(async (video) => ({ this.videos = await Promise.all(response.data.map(async (video) => ({
...video, ...video,
url: video.isModeratedHidden ? null : await this.fetchVideoUrl(video.hash), url: video.isModeratedHidden ? null : await this.fetchVideoUrl(video.hash),
createdAtLabel: video.createdAt ? new Date(video.createdAt).toLocaleDateString() : '',
}))); })));
}, },
releaseVideoUrls() {
this.videos.forEach((video) => {
if (video?.url) {
URL.revokeObjectURL(video.url);
}
});
},
async fetchVideoUrl(hash) { async fetchVideoUrl(hash) {
const response = await apiClient.get(`/api/socialnetwork/erotic/video/${hash}`, { const response = await apiClient.get(`/api/socialnetwork/erotic/video/${hash}`, {
responseType: 'blob', responseType: 'blob',
@@ -161,17 +249,23 @@ export default {
.erotic-videos-page { .erotic-videos-page {
display: grid; display: grid;
gap: 1.25rem; gap: 1.25rem;
padding-bottom: 1.25rem;
} }
.erotic-videos-hero, .erotic-videos-hero,
.erotic-videos-upload, .erotic-videos-upload,
.erotic-videos-list { .erotic-videos-library,
.erotic-videos-panel {
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft); box-shadow: var(--shadow-soft);
} }
.erotic-videos-hero { .erotic-videos-hero {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 1rem;
align-items: end;
padding: 1.5rem; padding: 1.5rem;
background: background:
radial-gradient(circle at top right, rgba(204, 44, 94, 0.18), transparent 38%), radial-gradient(circle at top right, rgba(204, 44, 94, 0.18), transparent 38%),
@@ -195,9 +289,49 @@ export default {
color: rgba(255, 241, 245, 0.84); color: rgba(255, 241, 245, 0.84);
} }
.erotic-videos-hero__stats {
display: grid;
grid-template-columns: repeat(3, minmax(90px, 1fr));
gap: 0.75rem;
}
.erotic-videos-stat {
display: grid;
gap: 0.15rem;
padding: 0.8rem 0.9rem;
border-radius: var(--radius-md);
background: rgba(255, 241, 245, 0.1);
}
.erotic-videos-stat strong {
font-size: 1.3rem;
line-height: 1;
}
.erotic-videos-stat span {
color: rgba(255, 241, 245, 0.84);
font-size: 0.78rem;
}
.erotic-videos-workspace {
display: grid;
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
gap: 1.25rem;
min-height: min(72vh, 860px);
}
.erotic-videos-sidebar {
display: grid;
gap: 1rem;
align-content: start;
min-height: 0;
}
.erotic-videos-upload, .erotic-videos-upload,
.erotic-videos-list { .erotic-videos-library,
.erotic-videos-panel {
padding: 1.25rem; padding: 1.25rem;
overflow: hidden;
} }
.erotic-videos-upload__header, .erotic-videos-upload__header,
@@ -211,6 +345,78 @@ export default {
gap: 0.35rem; gap: 0.35rem;
} }
.erotic-videos-upload__meta {
display: grid;
gap: 0.25rem;
color: var(--color-text-secondary);
font-size: 0.82rem;
}
.erotic-videos-panel {
display: grid;
gap: 0.9rem;
}
.erotic-videos-panel__list,
.erotic-videos-checklist {
display: grid;
gap: 0.7rem;
}
.erotic-videos-panel__item {
display: grid;
gap: 0.15rem;
padding: 0.75rem 0.85rem;
border-radius: var(--radius-md);
background: rgba(112, 60, 80, 0.06);
}
.erotic-videos-panel__item span,
.erotic-videos-checklist {
color: var(--color-text-secondary);
}
.erotic-videos-checklist {
margin: 0;
padding-left: 1.1rem;
}
.erotic-videos-library {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 1rem;
min-height: 0;
}
.erotic-videos-library__header {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: start;
}
.erotic-videos-library__header p {
margin: 0.3rem 0 0;
color: var(--color-text-secondary);
}
.erotic-videos-library__count {
display: inline-flex;
min-width: 2.2rem;
min-height: 2.2rem;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(112, 60, 80, 0.08);
font-weight: 800;
}
.erotic-videos-library__scroll {
min-height: 0;
overflow: auto;
padding-right: 0.2rem;
}
.erotic-videos-grid { .erotic-videos-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
@@ -228,6 +434,16 @@ export default {
background: rgba(112, 60, 80, 0.08); background: rgba(112, 60, 80, 0.08);
} }
.erotic-videos-card__meta {
display: grid;
gap: 0.2rem;
}
.erotic-videos-card__meta span {
color: var(--color-text-secondary);
font-size: 0.82rem;
}
.erotic-videos-card__hidden { .erotic-videos-card__hidden {
display: grid; display: grid;
place-items: center; place-items: center;
@@ -264,6 +480,33 @@ export default {
} }
.erotic-videos-empty { .erotic-videos-empty {
display: grid;
gap: 0.35rem;
place-items: center;
min-height: 240px;
padding: 1.5rem;
text-align: center;
color: var(--color-text-secondary); color: var(--color-text-secondary);
background: rgba(112, 60, 80, 0.05);
border-radius: var(--radius-md);
}
@media (max-width: 980px) {
.erotic-videos-hero,
.erotic-videos-workspace {
grid-template-columns: 1fr;
}
.erotic-videos-workspace {
min-height: auto;
}
.erotic-videos-library {
grid-template-rows: auto auto;
}
.erotic-videos-library__scroll {
overflow: visible;
}
} }
</style> </style>