web-player: split player, navigation and accent state into focused modules
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { appendUnique, shuffle } from './collections';
|
||||
|
||||
describe('appendUnique', () => {
|
||||
const keyOf = (item: { id: number }) => item.id;
|
||||
it('appends only unseen keys', () => {
|
||||
const result = appendUnique([{ id: 1 }], [{ id: 1 }, { id: 2 }, { id: 2 }, { id: 3 }], keyOf);
|
||||
expect(result).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]);
|
||||
});
|
||||
it('returns the same array when nothing is new', () => {
|
||||
const current = [{ id: 1 }];
|
||||
expect(appendUnique(current, [{ id: 1 }], keyOf)).toBe(current);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shuffle', () => {
|
||||
it('keeps the same elements and does not mutate the input', () => {
|
||||
const input = [1, 2, 3, 4, 5, 6];
|
||||
const result = shuffle(input);
|
||||
expect([...result].sort()).toEqual(input);
|
||||
expect(input).toEqual([1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
export function shuffle<T>(items: T[]): T[] {
|
||||
const copy = [...items];
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
export function appendUnique<T>(
|
||||
current: T[],
|
||||
incoming: T[],
|
||||
keyOf: (item: T) => string | number
|
||||
): T[] {
|
||||
const seen = new Set(current.map(keyOf));
|
||||
const added: T[] = [];
|
||||
for (const item of incoming) {
|
||||
const key = keyOf(item);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
added.push(item);
|
||||
}
|
||||
return added.length > 0 ? [...current, ...added] : current;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { findTopResult } from './search';
|
||||
import { findTopResult, searchChips } from './search';
|
||||
|
||||
const empty = { tracks: [], albums: [], playlists: [], users: [] };
|
||||
|
||||
@@ -26,3 +26,17 @@ describe('findTopResult', () => {
|
||||
expect(findTopResult({ ...empty, query: 'x' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchChips', () => {
|
||||
it('adds an "all" chip with the total', () => {
|
||||
const chips = searchChips({ tracks: 3, albums: 2, playlists: 0, users: 1 });
|
||||
expect(chips.map((chip) => [chip.filter, chip.count])).toEqual([
|
||||
['all', 6],
|
||||
['tracks', 3],
|
||||
['albums', 2],
|
||||
['playlists', 0],
|
||||
['users', 1]
|
||||
]);
|
||||
expect(chips[0].label).toBe('Todo');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +1,40 @@
|
||||
export const SEARCH_FILTERS = ['Todo', 'Canciones', 'Álbumes', 'Playlists', 'Usuarios'] as const;
|
||||
export type SearchFilter = (typeof SEARCH_FILTERS)[number];
|
||||
export const SEARCH_KINDS = ['tracks', 'albums', 'playlists', 'users'] as const;
|
||||
export type SearchKind = (typeof SEARCH_KINDS)[number];
|
||||
export type SearchFilter = 'all' | SearchKind;
|
||||
export const SEARCH_GROUP_PREVIEW = 4;
|
||||
|
||||
export function showGroup(filter: SearchFilter, label: Exclude<SearchFilter, 'Todo'>) {
|
||||
return filter === 'Todo' || filter === label;
|
||||
export const SEARCH_LABELS: Record<SearchFilter, string> = {
|
||||
all: 'Todo',
|
||||
tracks: 'Canciones',
|
||||
albums: 'Álbumes',
|
||||
playlists: 'Playlists',
|
||||
users: 'Usuarios'
|
||||
};
|
||||
|
||||
export function showGroup(filter: SearchFilter, kind: SearchKind) {
|
||||
return filter === 'all' || filter === kind;
|
||||
}
|
||||
|
||||
export function capped<T>(filter: SearchFilter, list: T[]) {
|
||||
return filter === 'Todo' ? list.slice(0, SEARCH_GROUP_PREVIEW) : list;
|
||||
return filter === 'all' ? list.slice(0, SEARCH_GROUP_PREVIEW) : list;
|
||||
}
|
||||
|
||||
export function matchPlaylists<T extends { name: string }>(playlists: T[], query: string): T[] {
|
||||
return query ? playlists.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())) : [];
|
||||
}
|
||||
|
||||
export function searchCounts(counts: {
|
||||
tracks: number;
|
||||
albums: number;
|
||||
playlists: number;
|
||||
users: number;
|
||||
}) {
|
||||
return {
|
||||
Canciones: counts.tracks,
|
||||
Álbumes: counts.albums,
|
||||
Playlists: counts.playlists,
|
||||
Usuarios: counts.users
|
||||
};
|
||||
export type SearchCounts = Record<SearchKind, number>;
|
||||
|
||||
export function searchTotal(counts: SearchCounts): number {
|
||||
return SEARCH_KINDS.reduce((sum, kind) => sum + counts[kind], 0);
|
||||
}
|
||||
|
||||
export function searchChips(counts: ReturnType<typeof searchCounts>) {
|
||||
const total = counts.Canciones + counts.Álbumes + counts.Playlists + counts.Usuarios;
|
||||
return SEARCH_FILTERS.map((label) => ({
|
||||
label,
|
||||
count: label === 'Todo' ? total : counts[label]
|
||||
export function searchChips(counts: SearchCounts) {
|
||||
const filters: SearchFilter[] = ['all', ...SEARCH_KINDS];
|
||||
return filters.map((filter) => ({
|
||||
filter,
|
||||
label: SEARCH_LABELS[filter],
|
||||
count: filter === 'all' ? searchTotal(counts) : counts[filter]
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user