Feat : tag system
This commit is contained in:
@@ -8,9 +8,12 @@ import {
|
||||
previewSrc,
|
||||
recordingContext,
|
||||
stateLabel,
|
||||
tagStyle,
|
||||
usedTags,
|
||||
type RecordingContext,
|
||||
} from '../streamers';
|
||||
import { StreamerDetail } from './StreamerDetail';
|
||||
import { TagChip, TagManager } from './Tags';
|
||||
|
||||
interface Props {
|
||||
targets: WatchTarget[];
|
||||
@@ -39,6 +42,22 @@ export function StreamersPage({
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [filter, setFilter] = useState<FilterId>('all');
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
const [selectedTags, setSelectedTags] = useState<Set<string>>(new Set());
|
||||
const [managingTags, setManagingTags] = useState(false);
|
||||
|
||||
// Tirées des profils plutôt que du serveur : la liste arrive déjà entière par
|
||||
// le flux temps réel, donc toujours à jour, et une étiquette que personne ne
|
||||
// porte n'aurait de toute façon rien à filtrer.
|
||||
const tags = useMemo(() => usedTags(targets), [targets]);
|
||||
|
||||
function toggleTag(id: string) {
|
||||
setSelectedTags((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
async function add(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
@@ -76,6 +95,13 @@ export function StreamersPage({
|
||||
);
|
||||
|
||||
const visible = ordered.filter((target) => {
|
||||
// Les deux filtres se cumulent — « en direct » ET portant l'une des
|
||||
// étiquettes retenues. Entre étiquettes en revanche, c'est une union : en
|
||||
// cocher une seconde élargit la sélection, ce qui est le geste attendu
|
||||
// quand on empile des critères de même nature.
|
||||
if (selectedTags.size > 0 && !target.tags.some((tag) => selectedTags.has(tag.id))) {
|
||||
return false;
|
||||
}
|
||||
if (filter === 'favorites') return target.favorite;
|
||||
if (filter === 'live') return target.state === 'public';
|
||||
return true;
|
||||
@@ -120,6 +146,37 @@ export function StreamersPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{targets.length > 0 && (
|
||||
<div className="row filter-row tag-filters">
|
||||
{tags.map((tag) => (
|
||||
<button
|
||||
key={tag.id}
|
||||
className={selectedTags.has(tag.id) ? 'tag tag-filter active' : 'tag tag-filter'}
|
||||
style={tagStyle(tag.name)}
|
||||
onClick={() => toggleTag(tag.id)}
|
||||
title={`Ne garder que les profils portant « ${tag.name} »`}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
))}
|
||||
{tags.length === 0 && (
|
||||
<span className="muted small">
|
||||
Aucune étiquette — pose-en une depuis la fiche d'un streamer pour trier la liste
|
||||
d'ici.
|
||||
</span>
|
||||
)}
|
||||
{selectedTags.size > 0 && (
|
||||
<button className="link-button" onClick={() => setSelectedTags(new Set())}>
|
||||
tout afficher
|
||||
</button>
|
||||
)}
|
||||
<div className="spacer" />
|
||||
<button className="chip" onClick={() => setManagingTags(true)} title="Renommer, supprimer">
|
||||
⚙ Étiquettes
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{targets.length === 0 ? (
|
||||
<div className="empty">
|
||||
<h2>Aucun streamer suivi</h2>
|
||||
@@ -154,6 +211,16 @@ export function StreamersPage({
|
||||
onClose={() => setOpenId(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{managingTags && (
|
||||
<TagManager
|
||||
onClose={() => setManagingTags(false)}
|
||||
// Une étiquette renommée ou supprimée touche tous les profils : le
|
||||
// serveur les republie, et le filtre en cours peut désigner une
|
||||
// étiquette qui n'existe plus.
|
||||
onChanged={() => setSelectedTags(new Set())}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -284,6 +351,15 @@ function StreamerCard({
|
||||
</span>
|
||||
</header>
|
||||
|
||||
{/* Sans retrait possible ici : la vignette sert à voir, la fiche à régler. */}
|
||||
{target.tags.length > 0 && (
|
||||
<div className="tag-list">
|
||||
{target.tags.map((tag) => (
|
||||
<TagChip key={tag.id} name={tag.name} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<dl className="streamer-facts">
|
||||
<div>
|
||||
<dt>{isLive ? 'En direct depuis' : 'Statut depuis'}</dt>
|
||||
|
||||
Reference in New Issue
Block a user