feat(channel): page details chaine multi-providers avec onglets et infinite scroll
CI / build-and-test (push) Successful in 13m34s
CI / build-and-test (push) Successful in 13m34s
- Strategy Pattern front: ChannelProviderInterface + 6 providers (yt/dm/tw/pt/od/ru) + factory (ids longs/courts) - ChannelContentService: cache par onglet/tri/recherche (TTL 5min), pagination nextPage - ChannelPage: header (banniere/avatar/subs/description ...plus), tabs dynamiques par capabilities, grilles videos/shorts 9:16/playlists/live, recherche debounced, tri recent/populaire, infinite scroll IntersectionObserver, skeletons/empty/error - Backend: GET /api/channels/:provider/:id/content (videos/shorts/playlists/live) natif par provider
This commit is contained in:
@@ -1,6 +1,146 @@
|
||||
<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">Abonnements</h2>
|
||||
<div class="bg-slate-800/50 rounded-lg p-6 border border-slate-700">
|
||||
<p class="text-slate-300">Section pour suivre vos abonnements (à implémenter). Vous verrez ici les dernières vidéos des chaînes suivies sur chaque provider.</p>
|
||||
<div class="mb-6 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold text-slate-100">Abonnements</h2>
|
||||
<p class="text-sm text-slate-400">Gérez vos chaînes suivies multi-fournisseurs</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="rounded-md border border-slate-700 bg-slate-800 px-3 py-1.5 text-sm text-slate-200 transition hover:border-slate-500 hover:text-white"
|
||||
(click)="refresh()"
|
||||
type="button"
|
||||
>
|
||||
Rafraîchir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
|
||||
<div class="bg-slate-900/60 rounded-xl border border-slate-800 p-4 shadow-lg shadow-black/30">
|
||||
<div class="flex flex-wrap items-center gap-2 border-b border-slate-800 pb-4">
|
||||
<span class="text-xs uppercase tracking-wide text-slate-400">Filtres</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
*ngFor="let filter of filters"
|
||||
type="button"
|
||||
(click)="setFilter(filter.id)"
|
||||
class="rounded-full border px-3 py-1 text-xs font-semibold transition"
|
||||
[class.bg-red-500/20]="activeFilter() === filter.id"
|
||||
[class.text-red-200]="activeFilter() === filter.id"
|
||||
[class.border-red-500/40]="activeFilter() === filter.id"
|
||||
[class.border-slate-700]="activeFilter() !== filter.id"
|
||||
[class.text-slate-300]="activeFilter() !== filter.id"
|
||||
[class.bg-slate-800/70]="activeFilter() !== filter.id"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between py-4">
|
||||
<div class="flex items-center gap-2 text-sm text-slate-400">
|
||||
<span>Trier par :</span>
|
||||
<button
|
||||
*ngFor="let sort of sorts"
|
||||
type="button"
|
||||
(click)="setSort(sort.key)"
|
||||
class="rounded-md px-3 py-1 text-xs transition"
|
||||
[class.bg-emerald-500/20]="activeSort() === sort.key"
|
||||
[class.text-emerald-200]="activeSort() === sort.key"
|
||||
[class.bg-slate-800/60]="activeSort() !== sort.key"
|
||||
[class.text-slate-300]="activeSort() !== sort.key"
|
||||
>
|
||||
{{ sort.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="filteredItems().length > 0; else emptyState">
|
||||
<div class="divide-y divide-slate-800">
|
||||
<div
|
||||
*ngFor="let item of filteredItems()"
|
||||
class="flex flex-col gap-4 py-4 md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<ng-container *ngIf="item.channel as channel; else subscriptionFallback">
|
||||
<div class="flex items-center gap-4">
|
||||
<img
|
||||
[src]="channel.avatarUrl || 'https://placehold.co/80x80/png'"
|
||||
[alt]="channel.title || channel.handle || 'Avatar'"
|
||||
class="h-16 w-16 rounded-full border border-slate-800 object-cover"
|
||||
/>
|
||||
<div class="space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="text-lg font-semibold text-slate-100">
|
||||
{{ channel.title || channel.handle || 'Chaîne inconnue' }}
|
||||
</h3>
|
||||
<app-provider-badge [provider]="channel.provider"></app-provider-badge>
|
||||
<span *ngIf="channel.verified" class="text-xs text-emerald-300">✓ Vérifiée</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 text-sm text-slate-400">
|
||||
<span *ngIf="channel.handle">{{ channel.handle }}</span>
|
||||
<span *ngIf="channel.subsCount != null">
|
||||
{{ channel.subsCount | number }} abonnés
|
||||
</span>
|
||||
<span class="text-slate-500">
|
||||
Abonné depuis {{ item.createdAt | date:'mediumDate' }}
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
*ngIf="channel.url"
|
||||
class="text-xs text-blue-300 hover:text-blue-200"
|
||||
[href]="channel.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Voir la chaîne externe
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-stretch gap-2 text-sm md:text-right">
|
||||
<app-subscribe-button
|
||||
[provider]="channel.provider"
|
||||
[externalId]="channel.externalId"
|
||||
[channelMeta]="channel"
|
||||
(unsubscribed)="refresh()"
|
||||
(subscribed)="refresh()"
|
||||
></app-subscribe-button>
|
||||
<button
|
||||
class="rounded-md border border-slate-700 bg-slate-800/60 px-3 py-1 text-sm text-slate-300 transition hover:border-slate-500 hover:text-white"
|
||||
type="button"
|
||||
(click)="openChannel(item)"
|
||||
>
|
||||
Voir la chaîne
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #subscriptionFallback>
|
||||
<div class="flex-1 text-sm text-slate-400">
|
||||
<p class="font-medium text-slate-200">Chaîne non disponible</p>
|
||||
<p class="text-xs">Essayez de rafraîchir l'abonnement plus tard.</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #emptyState>
|
||||
<div class="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-slate-700 bg-slate-900/60 p-10 text-center">
|
||||
<h3 class="text-lg font-semibold text-slate-100">Aucune chaîne suivie</h3>
|
||||
<p class="text-sm text-slate-400">Explorez la page Trending ou effectuez une recherche pour commencer à vous abonner.</p>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<aside class="rounded-xl border border-slate-800 bg-slate-900/50 p-4 shadow-lg shadow-black/30">
|
||||
<h3 class="text-lg font-semibold text-slate-100">Astuce</h3>
|
||||
<p class="mt-2 text-sm text-slate-400">
|
||||
Utilisez le raccourci <code class="rounded bg-slate-800 px-2 py-0.5 text-xs">S</code> sur une carte vidéo pour activer
|
||||
rapidement le bouton d'abonnement. Les abonnements sont synchronisés sur tous vos appareils.
|
||||
</p>
|
||||
<p class="mt-4 text-sm text-slate-500">
|
||||
Cache TTL: {{ hasCache() ? '6h' : '—' }}.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user