feat: add debounced Twitch category picker
This commit is contained in:
+23
-2
@@ -24,6 +24,9 @@
|
|||||||
let streamLanguage = 'en';
|
let streamLanguage = 'en';
|
||||||
let streamTags = '';
|
let streamTags = '';
|
||||||
let streamClassificationLabels = '';
|
let streamClassificationLabels = '';
|
||||||
|
let categorySearch = '';
|
||||||
|
let categoryOptions: Array<{ id: string; name: string; box_art_url: string }> = [];
|
||||||
|
let categorySearchTimer: number | undefined;
|
||||||
let oauthLink = '';
|
let oauthLink = '';
|
||||||
let oauthIdentity: 'streamer' | 'bot' = 'streamer';
|
let oauthIdentity: 'streamer' | 'bot' = 'streamer';
|
||||||
let oauthBusy = false;
|
let oauthBusy = false;
|
||||||
@@ -171,9 +174,10 @@
|
|||||||
headers: { authorization: `Bearer ${sessionToken}` },
|
headers: { authorization: `Bearer ${sessionToken}` },
|
||||||
});
|
});
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
const info: { title: string; game_id: string; language: string; tags: string[]; content_classification_labels: string[] } = await response.json();
|
const info: { title: string; game_id: string; game_name: string; language: string; tags: string[]; content_classification_labels: string[] } = await response.json();
|
||||||
streamTitle = info.title;
|
streamTitle = info.title;
|
||||||
streamGameId = info.game_id;
|
streamGameId = info.game_id;
|
||||||
|
categorySearch = info.game_name;
|
||||||
streamLanguage = info.language;
|
streamLanguage = info.language;
|
||||||
streamTags = info.tags.join(', ');
|
streamTags = info.tags.join(', ');
|
||||||
streamClassificationLabels = info.content_classification_labels.join(', ');
|
streamClassificationLabels = info.content_classification_labels.join(', ');
|
||||||
@@ -182,6 +186,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function searchCategories(value: string) {
|
||||||
|
categorySearch = value;
|
||||||
|
categoryOptions = [];
|
||||||
|
if (categorySearchTimer) window.clearTimeout(categorySearchTimer);
|
||||||
|
if (!value.trim()) return;
|
||||||
|
categorySearchTimer = window.setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${serverAddress}/api/v1/twitch/games?query=${encodeURIComponent(value.trim())}`, {
|
||||||
|
headers: { authorization: `Bearer ${sessionToken}` },
|
||||||
|
});
|
||||||
|
if (response.ok) categoryOptions = await response.json();
|
||||||
|
} catch {
|
||||||
|
categoryOptions = [];
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
async function saveStreamInfo() {
|
async function saveStreamInfo() {
|
||||||
streamBusy = true;
|
streamBusy = true;
|
||||||
streamError = '';
|
streamError = '';
|
||||||
@@ -315,7 +336,7 @@
|
|||||||
<section class="panel preview-panel"><div class="panel-header"><span>STREAM PREVIEW</span><span class="panel-menu">⋮</span></div><div class="preview-stage"><div class="offline-mark">STREAM OFFLINE</div><div class="offline-subtitle">CONNECT TWITCH TO BEGIN</div></div><div class="panel-footer"><span class="micro">VIDEO PLAYER</span><span class="badge">OFFLINE</span></div></section>
|
<section class="panel preview-panel"><div class="panel-header"><span>STREAM PREVIEW</span><span class="panel-menu">⋮</span></div><div class="preview-stage"><div class="offline-mark">STREAM OFFLINE</div><div class="offline-subtitle">CONNECT TWITCH TO BEGIN</div></div><div class="panel-footer"><span class="micro">VIDEO PLAYER</span><span class="badge">OFFLINE</span></div></section>
|
||||||
<section class="panel activity-panel"><div class="panel-header"><span>ACTIVITY FEED</span><span class="panel-menu">⋮</span></div><div class="panel-tabs"><button class="tab active">ALL ACTIVITY</button><button class="tab">MODERATION</button></div>{#if activityEvents.length}<div class="activity-list">{#each activityEvents as event}<div class="activity-row"><span class="activity-type">{event.event_type}</span><span class="activity-time">{new Date(event.occurred_at).toLocaleTimeString()}</span></div>{/each}</div>{:else}<div class="empty-state"><div class="empty-glyph">◌</div><strong>Nothing here yet</strong><span>Follows, subscriptions, raids, and server events will appear here.</span></div>{/if}</section>
|
<section class="panel activity-panel"><div class="panel-header"><span>ACTIVITY FEED</span><span class="panel-menu">⋮</span></div><div class="panel-tabs"><button class="tab active">ALL ACTIVITY</button><button class="tab">MODERATION</button></div>{#if activityEvents.length}<div class="activity-list">{#each activityEvents as event}<div class="activity-row"><span class="activity-type">{event.event_type}</span><span class="activity-time">{new Date(event.occurred_at).toLocaleTimeString()}</span></div>{/each}</div>{:else}<div class="empty-state"><div class="empty-glyph">◌</div><strong>Nothing here yet</strong><span>Follows, subscriptions, raids, and server events will appear here.</span></div>{/if}</section>
|
||||||
<section class="panel chat-panel"><div class="panel-header"><span>MY CHAT</span><span class="panel-menu">⋮</span></div><div class="chat-empty"><div class="empty-glyph">⌁</div><strong>Chat is waiting</strong><span>Connect a streamer or bot account to begin.</span></div><div class="chat-composer"><input class="control" placeholder="Send a message" disabled /><button class="primary" disabled>SEND</button></div></section>
|
<section class="panel chat-panel"><div class="panel-header"><span>MY CHAT</span><span class="panel-menu">⋮</span></div><div class="chat-empty"><div class="empty-glyph">⌁</div><strong>Chat is waiting</strong><span>Connect a streamer or bot account to begin.</span></div><div class="chat-composer"><input class="control" placeholder="Send a message" disabled /><button class="primary" disabled>SEND</button></div></section>
|
||||||
<section class="panel quick-panel"><div class="panel-header"><span>QUICK ACTIONS</span><span class="panel-menu">⋮</span></div><div class="quick-grid"><button class="quick-action accent" onclick={() => (showStreamEditor = !showStreamEditor)}>EDIT STREAM INFO</button><button class="quick-action">MODERATION</button><button class="quick-action">BOT COMMANDS</button><button class="quick-action">OVERLAYS</button></div>{#if showStreamEditor}<div class="stream-editor"><label class="micro" for="stream-title">TITLE</label><input id="stream-title" class="control" bind:value={streamTitle} maxlength="140" /><label class="micro" for="stream-game-id">CATEGORY / GAME ID</label><input id="stream-game-id" class="control" bind:value={streamGameId} maxlength="64" /><label class="micro" for="stream-language">LANGUAGE</label><input id="stream-language" class="control" bind:value={streamLanguage} maxlength="10" /><label class="micro" for="stream-tags">TAGS</label><input id="stream-tags" class="control" bind:value={streamTags} placeholder="comma, separated" /><label class="micro" for="stream-labels">CLASSIFICATION LABELS</label><input id="stream-labels" class="control" bind:value={streamClassificationLabels} placeholder="comma, separated" /><button class="primary control-button" onclick={saveStreamInfo} disabled={streamBusy}>SAVE STREAM INFO</button>{#if streamError}<div class="auth-error">{streamError}</div>{/if}</div>{/if}</section>
|
<section class="panel quick-panel"><div class="panel-header"><span>QUICK ACTIONS</span><span class="panel-menu">⋮</span></div><div class="quick-grid"><button class="quick-action accent" onclick={() => (showStreamEditor = !showStreamEditor)}>EDIT STREAM INFO</button><button class="quick-action">MODERATION</button><button class="quick-action">BOT COMMANDS</button><button class="quick-action">OVERLAYS</button></div>{#if showStreamEditor}<div class="stream-editor"><label class="micro" for="stream-title">TITLE</label><input id="stream-title" class="control" bind:value={streamTitle} maxlength="140" /><label class="micro" for="stream-category">CATEGORY</label><input id="stream-category" class="control" value={categorySearch} oninput={(event) => searchCategories(event.currentTarget.value)} autocomplete="off" placeholder="Search Twitch categories" />{#if categoryOptions.length}<div class="category-options">{#each categoryOptions as category}<button class="category-option" onclick={() => { streamGameId = category.id; categorySearch = category.name; categoryOptions = []; }}><span>{category.name}</span><span class="mono">{category.id}</span></button>{/each}</div>{/if}<label class="micro" for="stream-language">LANGUAGE</label><input id="stream-language" class="control" bind:value={streamLanguage} maxlength="10" /><label class="micro" for="stream-tags">TAGS</label><input id="stream-tags" class="control" bind:value={streamTags} placeholder="comma, separated" /><label class="micro" for="stream-labels">CLASSIFICATION LABELS</label><input id="stream-labels" class="control" bind:value={streamClassificationLabels} placeholder="comma, separated" /><button class="primary control-button" onclick={saveStreamInfo} disabled={streamBusy}>SAVE STREAM INFO</button>{#if streamError}<div class="auth-error">{streamError}</div>{/if}</div>{/if}</section>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -477,6 +477,22 @@ label {
|
|||||||
.stream-editor label {
|
.stream-editor label {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
.category-options {
|
||||||
|
display: grid;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--ui-line);
|
||||||
|
background: var(--ed-bg);
|
||||||
|
}
|
||||||
|
.category-option {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid var(--ui-divider);
|
||||||
|
border-radius: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
.quick-action:hover,
|
.quick-action:hover,
|
||||||
.quick-action.accent {
|
.quick-action.accent {
|
||||||
border-color: var(--ui-line-focus);
|
border-color: var(--ui-line-focus);
|
||||||
|
|||||||
Reference in New Issue
Block a user