180 lines
10 KiB
HTML
180 lines
10 KiB
HTML
<div class="container mx-auto p-4 sm:p-6">
|
|
<h2 class="text-3xl font-bold mb-6 text-slate-100 border-l-4 border-red-500 pl-4">{{ pageHeading() }}</h2>
|
|
|
|
@if (notice()) {
|
|
<div class="mb-4 bg-amber-900/40 border border-amber-600 text-amber-200 p-3 rounded">
|
|
{{ notice() }}
|
|
</div>
|
|
}
|
|
|
|
@if (!hasQuery()) {
|
|
<p class="text-slate-400">{{ 'search.hint' | t }}</p>
|
|
}
|
|
|
|
@if (hasQuery() && availableTags().length > 0) {
|
|
<div class="mb-4 flex items-center gap-2 overflow-x-auto pb-1">
|
|
@for (tag of availableTags(); track tag.key) {
|
|
<button
|
|
class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
|
|
[ngClass]="{
|
|
'bg-slate-200 text-slate-900 border-slate-200': filterTag() === tag.key,
|
|
'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700': filterTag() !== tag.key
|
|
}"
|
|
(click)="setFilterTag(tag.key)">
|
|
{{ tag.label }}
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- Unified multi-provider grouped suggestions -->
|
|
@if (showUnified()) {
|
|
<app-search-suggestions [groups]="groups()" [query]="q()"></app-search-suggestions>
|
|
}
|
|
|
|
<input
|
|
#searchInput
|
|
type="text"
|
|
class="flex-1 bg-slate-900 text-slate-100 border border-slate-700 rounded-l-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-red-500"
|
|
placeholder="Rechercher..."
|
|
[value]="q()"
|
|
(input)="onSearchInput($event)"
|
|
(keydown.enter)="onSearchEnter($event)"
|
|
(keydown.escape)="closeSearchResults()"
|
|
/>
|
|
|
|
@if (loading()) {
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
@for (item of [1,2,3,4,5,6,7,8]; track item) {
|
|
<div class="animate-pulse bg-slate-800 rounded-lg overflow-hidden">
|
|
<div class="w-full h-48 bg-slate-700"></div>
|
|
<div class="p-4 space-y-3">
|
|
<div class="h-4 bg-slate-700 rounded w-3/4"></div>
|
|
<div class="h-4 bg-slate-700 rounded w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
} @else if (selectedProviderForView() === 'twitch') {
|
|
<!-- Twitch: two sections -->
|
|
<div class="space-y-10">
|
|
<!-- Live Channels -->
|
|
<section *ngIf="filterTag() === 'twitch_all' || filterTag() === 'twitch_live'">
|
|
<h3 class="text-xl font-semibold text-slate-200 mb-3">Chaînes en direct</h3>
|
|
@if (twitchChannels().length > 0) {
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-8">
|
|
@for (video of twitchChannels(); track video.videoId) {
|
|
<a [routerLink]="['/watch', video.videoId]" [queryParams]="watchQueryParams(video)" [state]="{ video }" class="group flex flex-col bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1">
|
|
<div class="relative">
|
|
<img [src]="video.thumbnail" [alt]="video.title" class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105">
|
|
<div class="absolute top-2 left-2 bg-black/75 text-white text-xs px-2 py-1 rounded">
|
|
{{ formatViews(video.views) }} en direct
|
|
</div>
|
|
<div class="absolute bottom-2 left-2">
|
|
<app-like-button [videoId]="video.videoId" [provider]="'twitch'" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
|
|
<app-add-to-playlist class="ml-2" [provider]="'twitch'" [videoId]="video.videoId" [title]="video.title" [thumbnail]="video.thumbnail"></app-add-to-playlist>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 flex-grow flex flex-col">
|
|
<h4 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 line-clamp-2">{{ video.title }}</h4>
|
|
<div class="mt-2 flex items-center space-x-3 text-sm text-slate-400">
|
|
<img [src]="video.uploaderAvatar" [alt]="video.uploaderName" class="w-8 h-8 rounded-full">
|
|
<span>{{ video.uploaderName }}</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
@if (twitchCursorChannels()) {
|
|
<div class="mt-4 flex justify-center">
|
|
<button class="px-4 py-2 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 disabled:opacity-50" (click)="loadMoreTwitchChannels()" [disabled]="twitchBusyChannels()">Afficher plus</button>
|
|
</div>
|
|
}
|
|
} @else {
|
|
<p class="text-slate-400">Aucune chaîne en direct trouvée.</p>
|
|
}
|
|
</section>
|
|
|
|
<!-- Past Videos (VODs) -->
|
|
<section *ngIf="filterTag() === 'twitch_all' || filterTag() === 'twitch_vod'">
|
|
<h3 class="text-xl font-semibold text-slate-200 mb-3">Vidéos (VOD)</h3>
|
|
@if (twitchVods().length > 0) {
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-8">
|
|
@for (video of twitchVods(); track video.videoId) {
|
|
<a [routerLink]="['/watch', video.videoId]" [queryParams]="watchQueryParams(video)" [state]="{ video }" class="group flex flex-col bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1">
|
|
<div class="relative">
|
|
<img [src]="video.thumbnail" [alt]="video.title" class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105">
|
|
<div class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded">
|
|
{{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }}
|
|
</div>
|
|
<div class="absolute bottom-2 left-2">
|
|
<app-like-button [videoId]="video.videoId" [provider]="'twitch'" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
|
|
<app-add-to-playlist class="ml-2" [provider]="'twitch'" [videoId]="video.videoId" [title]="video.title" [thumbnail]="video.thumbnail"></app-add-to-playlist>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 flex-grow flex flex-col">
|
|
<h4 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 line-clamp-2">{{ video.title }}</h4>
|
|
<div class="mt-2 flex items-center space-x-3 text-sm text-slate-400">
|
|
<img [src]="video.uploaderAvatar" [alt]="video.uploaderName" class="w-8 h-8 rounded-full">
|
|
<span>{{ video.uploaderName }}</span>
|
|
</div>
|
|
<div class="mt-auto pt-2 text-sm text-slate-400">
|
|
<span>{{ formatViews(video.views) }} visionnements</span> • <span>{{ formatRelative(video.uploadedDate) }}</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
@if (twitchCursorVods()) {
|
|
<div class="mt-4 flex justify-center">
|
|
<button class="px-4 py-2 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 disabled:opacity-50" (click)="loadMoreTwitchVods()" [disabled]="twitchBusyVods()">Afficher plus</button>
|
|
</div>
|
|
}
|
|
} @else {
|
|
<p class="text-slate-400">Aucune vidéo VOD trouvée.</p>
|
|
}
|
|
</section>
|
|
</div>
|
|
} @else if (results().length > 0) {
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-8">
|
|
@for (video of filteredResults(); track video.videoId) {
|
|
<a [routerLink]="['/watch', video.videoId]" [queryParams]="watchQueryParams(video)" [state]="{ video }" class="group flex flex-col bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1">
|
|
<div class="relative">
|
|
<img [src]="video.thumbnail" [alt]="video.title" class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105">
|
|
<div class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded">
|
|
{{ video.duration / 60 | number:'1.0-0' }}:{{ (video.duration % 60) | number:'2.0-0' }}
|
|
</div>
|
|
<div class="absolute bottom-2 left-2">
|
|
<app-like-button [videoId]="video.videoId" [provider]="selectedProviderForView()" [title]="video.title" [thumbnail]="video.thumbnail"></app-like-button>
|
|
<app-add-to-playlist class="ml-2" [provider]="selectedProviderForView()" [videoId]="video.videoId" [title]="video.title" [thumbnail]="video.thumbnail"></app-add-to-playlist>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 flex-grow flex flex-col">
|
|
<h3 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 line-clamp-2">{{ video.title }}</h3>
|
|
<div class="mt-2 flex items-center space-x-3 text-sm text-slate-400">
|
|
<img [src]="video.uploaderAvatar" [alt]="video.uploaderName" class="w-8 h-8 rounded-full">
|
|
<span>{{ video.uploaderName }}</span>
|
|
</div>
|
|
<div class="mt-auto pt-2 text-sm text-slate-400">
|
|
<span>{{ formatViews(video.views) }} visionnements</span> • <span>{{ formatRelative(video.uploadedDate) }}</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
}
|
|
</div>
|
|
<!-- Infinite scroll anchor -->
|
|
<app-infinite-anchor class="mt-6"
|
|
[disabled]="!nextCursor()"
|
|
[busy]="busyMore()"
|
|
(loadMore)="fetchNextPage()"></app-infinite-anchor>
|
|
@if (busyMore()) {
|
|
<div class="flex items-center justify-center py-4 text-slate-400">
|
|
<svg class="animate-spin h-5 w-5 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
|
|
{{ 'loading.more' | t }}
|
|
</div>
|
|
}
|
|
} @else if (hasQuery()) {
|
|
<p class="text-slate-400">{{ 'search.noResults' | t }}</p>
|
|
}
|
|
</div>
|