mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Initial moderator view
This commit is contained in:
@@ -46,7 +46,7 @@ spring:
|
||||
client-authentication-method: client_secret_post
|
||||
redirect-uri: "${TWITCH_REDIRECT_URI:{baseUrl}/login/oauth2/code/twitch}"
|
||||
authorization-grant-type: authorization_code
|
||||
scope: ["user:read:email"]
|
||||
scope: ["user:read:email", "moderation:read"]
|
||||
provider:
|
||||
twitch:
|
||||
authorization-uri: https://id.twitch.tv/oauth2/authorize
|
||||
|
||||
@@ -556,6 +556,13 @@ body {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.card-section {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 40px auto;
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
function buildIdentity(admin) {
|
||||
const identity = document.createElement('div');
|
||||
identity.className = 'identity-row';
|
||||
|
||||
const avatar = document.createElement(admin.avatarUrl ? 'img' : 'div');
|
||||
avatar.className = 'avatar';
|
||||
if (admin.avatarUrl) {
|
||||
avatar.src = admin.avatarUrl;
|
||||
avatar.alt = `${admin.displayName || admin.login} avatar`;
|
||||
} else {
|
||||
avatar.classList.add('avatar-fallback');
|
||||
avatar.textContent = (admin.displayName || admin.login || '?').charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
const details = document.createElement('div');
|
||||
details.className = 'identity-text';
|
||||
const title = document.createElement('p');
|
||||
title.className = 'list-title';
|
||||
title.textContent = admin.displayName || admin.login;
|
||||
const subtitle = document.createElement('p');
|
||||
subtitle.className = 'muted';
|
||||
subtitle.textContent = `@${admin.login}`;
|
||||
|
||||
details.appendChild(title);
|
||||
details.appendChild(subtitle);
|
||||
identity.appendChild(avatar);
|
||||
identity.appendChild(details);
|
||||
return identity;
|
||||
}
|
||||
|
||||
function renderAdmins(list) {
|
||||
const adminList = document.getElementById('admin-list');
|
||||
if (!adminList) return;
|
||||
adminList.innerHTML = '';
|
||||
if (!list || list.length === 0) {
|
||||
const empty = document.createElement('li');
|
||||
@@ -12,33 +43,7 @@ function renderAdmins(list) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'stacked-list-item';
|
||||
|
||||
const identity = document.createElement('div');
|
||||
identity.className = 'identity-row';
|
||||
|
||||
const avatar = document.createElement(admin.avatarUrl ? 'img' : 'div');
|
||||
avatar.className = 'avatar';
|
||||
if (admin.avatarUrl) {
|
||||
avatar.src = admin.avatarUrl;
|
||||
avatar.alt = `${admin.displayName || admin.login} avatar`;
|
||||
} else {
|
||||
avatar.classList.add('avatar-fallback');
|
||||
avatar.textContent = (admin.displayName || admin.login || '?').charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
const details = document.createElement('div');
|
||||
details.className = 'identity-text';
|
||||
const title = document.createElement('p');
|
||||
title.className = 'list-title';
|
||||
title.textContent = admin.displayName || admin.login;
|
||||
const subtitle = document.createElement('p');
|
||||
subtitle.className = 'muted';
|
||||
subtitle.textContent = `@${admin.login}`;
|
||||
|
||||
details.appendChild(title);
|
||||
details.appendChild(subtitle);
|
||||
identity.appendChild(avatar);
|
||||
identity.appendChild(details);
|
||||
li.appendChild(identity);
|
||||
li.appendChild(buildIdentity(admin));
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'actions';
|
||||
@@ -55,6 +60,54 @@ function renderAdmins(list) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderSuggestedAdmins(list) {
|
||||
const suggestionList = document.getElementById('admin-suggestions');
|
||||
if (!suggestionList) return;
|
||||
|
||||
suggestionList.innerHTML = '';
|
||||
if (!list || list.length === 0) {
|
||||
const empty = document.createElement('li');
|
||||
empty.className = 'stacked-list-item';
|
||||
empty.textContent = 'No moderator suggestions right now';
|
||||
suggestionList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
list.forEach((admin) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'stacked-list-item';
|
||||
|
||||
li.appendChild(buildIdentity(admin));
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'actions';
|
||||
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.type = 'button';
|
||||
addBtn.className = 'ghost';
|
||||
addBtn.textContent = 'Add as admin';
|
||||
addBtn.addEventListener('click', () => addAdmin(admin.login));
|
||||
|
||||
actions.appendChild(addBtn);
|
||||
li.appendChild(actions);
|
||||
suggestionList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchSuggestedAdmins() {
|
||||
fetch(`/api/channels/${broadcaster}/admins/suggestions`)
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to load admin suggestions');
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(renderSuggestedAdmins)
|
||||
.catch(() => {
|
||||
renderSuggestedAdmins([]);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAdmins() {
|
||||
fetch(`/api/channels/${broadcaster}/admins`)
|
||||
.then((r) => {
|
||||
@@ -81,6 +134,7 @@ function removeAdmin(username) {
|
||||
showToast('Failed to remove admin. Please retry.', 'error');
|
||||
}
|
||||
fetchAdmins();
|
||||
fetchSuggestedAdmins();
|
||||
}).catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Failed to remove admin. Please retry.', 'error');
|
||||
@@ -88,9 +142,9 @@ function removeAdmin(username) {
|
||||
});
|
||||
}
|
||||
|
||||
function addAdmin() {
|
||||
function addAdmin(usernameFromAction) {
|
||||
const input = document.getElementById('new-admin');
|
||||
const username = input.value.trim();
|
||||
const username = (usernameFromAction || input?.value || '').trim();
|
||||
if (!username) {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('Enter a Twitch username to add as an admin.', 'info');
|
||||
@@ -107,11 +161,14 @@ function addAdmin() {
|
||||
if (!response.ok) {
|
||||
throw new Error('Add admin failed');
|
||||
}
|
||||
input.value = '';
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(`Added @${username} as an admin.`, 'success');
|
||||
}
|
||||
fetchAdmins();
|
||||
fetchSuggestedAdmins();
|
||||
})
|
||||
.catch(() => {
|
||||
if (typeof showToast === 'function') {
|
||||
@@ -187,4 +244,5 @@ function saveCanvasSettings() {
|
||||
}
|
||||
|
||||
fetchAdmins();
|
||||
fetchSuggestedAdmins();
|
||||
fetchCanvasSettings();
|
||||
|
||||
@@ -76,7 +76,21 @@
|
||||
<input id="new-admin" placeholder="Twitch username" />
|
||||
<button type="button" onclick="addAdmin()">Add admin</button>
|
||||
</div>
|
||||
<ul id="admin-list" class="stacked-list"></ul>
|
||||
<div class="card-section">
|
||||
<div class="section-header">
|
||||
<p class="eyebrow subtle">Current</p>
|
||||
<h4 class="list-title">Admins</h4>
|
||||
</div>
|
||||
<ul id="admin-list" class="stacked-list"></ul>
|
||||
</div>
|
||||
<div class="card-section">
|
||||
<div class="section-header">
|
||||
<p class="eyebrow subtle">Suggested</p>
|
||||
<h4 class="list-title">Your Twitch moderators</h4>
|
||||
<p class="muted">Add moderators who already help run your channel.</p>
|
||||
</div>
|
||||
<ul id="admin-suggestions" class="stacked-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -22,28 +22,11 @@
|
||||
<div class="hero-text">
|
||||
<p class="eyebrow">Overlay toolkit</p>
|
||||
<h1>Keep your Twitch overlays tidy.</h1>
|
||||
<p class="lead">Upload artwork, drop it into a shared dashboard, and stay in sync with your mods.</p>
|
||||
<p class="lead">Upload artwork, drop it into a shared dashboard, and stay in sync with your mods without clutter.</p>
|
||||
<div class="cta-row">
|
||||
<a class="button" href="/oauth2/authorization/twitch">Login with Twitch</a>
|
||||
<a class="button ghost" href="/channels">Browse channels</a>
|
||||
</div>
|
||||
<ul class="pill-list minimal">
|
||||
<li>Instant overlay updates</li>
|
||||
<li>Shared access for teammates</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="hero-panel hero-preview">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<p class="eyebrow subtle">Ready to go live</p>
|
||||
<h3>Preview & publish quickly</h3>
|
||||
</div>
|
||||
<span class="badge">Secure</span>
|
||||
</div>
|
||||
<div class="preview-summary">
|
||||
<p class="muted">Spot check your canvas, push assets live, and keep everything aligned with your stream.</p>
|
||||
</div>
|
||||
<a class="button block" href="/oauth2/authorization/twitch">Open dashboard</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user