mirror of
https://github.com/imgfloat/server.git
synced 2026-05-08 10:19:35 +00:00
refactor: redesign dashboard UI
- Replace large 200x200 icon tiles with compact horizontal nav cards (icon + title + description, grid-wrapped, responsive) - Split single Settings card into separate Overlay and Integrations cards, each with its own Save button and status indicator - Fix canvas save wiring bug: add missing #save-canvas-btn and #canvas-status elements that were referenced in JS but absent from HTML - Remove silent saveCanvasSettings() side-effect from saveScriptSettings() - Replace window.prompt delete confirmation with inline two-step confirm flow (Delete button → confirm panel → cancel/confirm) - Add danger-zone collapsible section with danger-border styling - Add responsive media queries: topbar stacks on narrow viewports, nav cards and dashboard grid collapse to single column below 700px - Remove dead CSS classes (dashboard-action, dashboard-tile, dashboard-toggle-tile, large-dashboard-tiles, etc.) - Merge near-duplicate renderAdmins/renderSuggestedAdmins into single parameterised renderAdminList() helper - Remove unused addVersionAttributes() call from dashboard route
This commit is contained in:
@@ -75,7 +75,6 @@ public class ViewController {
|
|||||||
model.addAttribute("adminChannels", channelDirectoryService.adminChannelsFor(sessionUsername));
|
model.addAttribute("adminChannels", channelDirectoryService.adminChannelsFor(sessionUsername));
|
||||||
model.addAttribute("isSystemAdmin", authorizationService.userIsSystemAdministrator(sessionUsername));
|
model.addAttribute("isSystemAdmin", authorizationService.userIsSystemAdministrator(sessionUsername));
|
||||||
addStagingAttribute(model);
|
addStagingAttribute(model);
|
||||||
addVersionAttributes(model);
|
|
||||||
return "dashboard";
|
return "dashboard";
|
||||||
}
|
}
|
||||||
addStagingAttribute(model);
|
addStagingAttribute(model);
|
||||||
|
|||||||
@@ -1041,6 +1041,7 @@ button:disabled:hover {
|
|||||||
|
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
@@ -1054,7 +1055,7 @@ button:disabled:hover {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 14px 18px;
|
padding: 14px 20px;
|
||||||
background: var(--color-surface-1);
|
background: var(--color-surface-1);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -1092,167 +1093,147 @@ button:disabled:hover {
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-action {
|
/* ── Navigation cards ────────────────────────────────── */
|
||||||
display: inline-flex;
|
.dashboard-nav-cards {
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: var(--color-accent-subtle);
|
|
||||||
border: 1px solid var(--color-accent-border);
|
|
||||||
color: var(--color-text);
|
|
||||||
text-decoration: none;
|
|
||||||
transition:
|
|
||||||
background 120ms ease,
|
|
||||||
border-color 120ms ease,
|
|
||||||
transform 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-action:hover {
|
|
||||||
background: var(--color-accent-subtle-hover);
|
|
||||||
border-color: rgba(124, 58, 237, 0.35);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-action-icon {
|
|
||||||
min-width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
background: rgba(124, 58, 237, 0.18);
|
gap: 12px;
|
||||||
color: var(--color-accent-icon);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-action-copy {
|
.dashboard-nav-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 14px;
|
||||||
}
|
padding: 14px 16px;
|
||||||
|
|
||||||
.large-dashboard-tiles {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly
|
|
||||||
}
|
|
||||||
|
|
||||||
.large-dashboard-tiles a {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
background: var(--color-surface-1);
|
background: var(--color-surface-1);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
min-height: 140px;
|
|
||||||
transition:
|
transition:
|
||||||
border-color 0.2s ease,
|
border-color 0.15s ease,
|
||||||
background-color 0.2s ease,
|
background-color 0.15s ease,
|
||||||
transform 0.2s ease;
|
transform 0.15s ease;
|
||||||
justify-content: space-evenly;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.large-dashboard-tiles a:hover,
|
.dashboard-nav-card:hover,
|
||||||
.large-dashboard-tiles a:focus-visible {
|
.dashboard-nav-card:focus-visible {
|
||||||
border-color: var(--color-border-strong);
|
border-color: var(--color-border-strong);
|
||||||
background: var(--color-surface-4);
|
background: var(--color-surface-4);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.large-dashboard-tiles a i {
|
.dashboard-nav-card-icon {
|
||||||
font-size: 72px;
|
flex-shrink: 0;
|
||||||
}
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
.large-dashboard-tiles a span {
|
border-radius: 10px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(124, 58, 237, 0.18);
|
||||||
|
color: var(--color-accent-icon);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-action-copy strong {
|
.dashboard-nav-card-icon--muted {
|
||||||
font-size: 15px;
|
background: var(--color-surface-3);
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-action-copy small {
|
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tile-grid {
|
.dashboard-nav-card-copy {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-tile {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 3px;
|
||||||
padding: 16px;
|
min-width: 0;
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
background: var(--color-surface-1);
|
|
||||||
color: var(--color-text);
|
|
||||||
text-decoration: none;
|
|
||||||
min-height: 140px;
|
|
||||||
transition:
|
|
||||||
border-color 0.2s ease,
|
|
||||||
background-color 0.2s ease,
|
|
||||||
transform 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tile:hover,
|
.dashboard-nav-card-copy strong {
|
||||||
.dashboard-tile:focus-visible {
|
font-size: 14px;
|
||||||
border-color: var(--color-border-strong);
|
|
||||||
background: var(--color-surface-4);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-tile .tile-icon {
|
|
||||||
min-width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: rgba(124, 58, 237, 0.18);
|
|
||||||
color: var(--color-accent-icon);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-tile .tile-title {
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 15px;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-tile .tile-subtitle {
|
.dashboard-nav-card-copy span {
|
||||||
color: var(--color-text-2);
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
color: var(--color-text-2);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-toggle-tile {
|
/* ── Danger zone ─────────────────────────────────────── */
|
||||||
|
.danger-zone {
|
||||||
|
border-color: var(--color-danger-border, rgba(239, 68, 68, 0.25));
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone > details > summary {
|
||||||
|
list-style: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding: 14px 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--color-danger, #ef4444);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-toggle-tile .tile-row {
|
.danger-zone > details > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone > details > summary:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-summary-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-toggle-tile input[type="checkbox"] {
|
.danger-zone-body {
|
||||||
margin-left: auto;
|
padding: 16px 18px 18px;
|
||||||
|
border-top: 1px solid var(--color-danger-border, rgba(239, 68, 68, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(239, 68, 68, 0.06);
|
||||||
|
border: 1px solid var(--color-danger-border, rgba(239, 68, 68, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-display {
|
.user-display {
|
||||||
@@ -3136,3 +3117,46 @@ button:disabled:hover {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Dashboard responsive ───────────────────────────── */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.dashboard-body {
|
||||||
|
padding: 16px 12px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-topbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-pill {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-pill-copy {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-nav-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone-item {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm {
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-confirm-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// TODO: Code smell Dashboard script uses broad shared state and imperative DOM updates instead of focused components.
|
|
||||||
const elements = {
|
const elements = {
|
||||||
adminList: document.getElementById("admin-list"),
|
adminList: document.getElementById("admin-list"),
|
||||||
suggestionList: document.getElementById("admin-suggestions"),
|
suggestionList: document.getElementById("admin-suggestions"),
|
||||||
@@ -16,6 +15,9 @@ const elements = {
|
|||||||
scriptSettingsStatus: document.getElementById("script-settings-status"),
|
scriptSettingsStatus: document.getElementById("script-settings-status"),
|
||||||
scriptSettingsSaveButton: document.getElementById("save-script-settings-btn"),
|
scriptSettingsSaveButton: document.getElementById("save-script-settings-btn"),
|
||||||
deleteAccountButton: document.getElementById("delete-account-btn"),
|
deleteAccountButton: document.getElementById("delete-account-btn"),
|
||||||
|
deleteConfirmStep: document.getElementById("delete-confirm-step"),
|
||||||
|
deleteConfirmButton: document.getElementById("delete-account-confirm-btn"),
|
||||||
|
deleteCancelButton: document.getElementById("delete-account-cancel-btn"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiBase = `/api/channels/${encodeURIComponent(broadcaster)}`;
|
const apiBase = `/api/channels/${encodeURIComponent(broadcaster)}`;
|
||||||
@@ -50,68 +52,52 @@ function buildIdentity(admin) {
|
|||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAdmins(list) {
|
function renderAdminList(listEl, list, { actionLabel, actionClass, onAction, emptyMessage }) {
|
||||||
if (!elements.adminList) return;
|
if (!listEl) return;
|
||||||
elements.adminList.innerHTML = "";
|
listEl.innerHTML = "";
|
||||||
if (!list || list.length === 0) {
|
if (!list || list.length === 0) {
|
||||||
const empty = document.createElement("li");
|
const empty = document.createElement("li");
|
||||||
empty.className = "stacked-list-item empty";
|
empty.className = "stacked-list-item empty";
|
||||||
empty.textContent = "No channel admins yet.";
|
empty.textContent = emptyMessage;
|
||||||
elements.adminList.appendChild(empty);
|
listEl.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list.forEach((admin) => {
|
list.forEach((admin) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "stacked-list-item";
|
li.className = "stacked-list-item";
|
||||||
|
|
||||||
li.appendChild(buildIdentity(admin));
|
li.appendChild(buildIdentity(admin));
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
actions.className = "actions";
|
actions.className = "actions";
|
||||||
|
|
||||||
const removeBtn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
removeBtn.type = "button";
|
btn.type = "button";
|
||||||
removeBtn.className = "secondary";
|
btn.className = actionClass;
|
||||||
removeBtn.textContent = "Remove";
|
btn.textContent = actionLabel;
|
||||||
removeBtn.addEventListener("click", () => removeAdmin(admin.login));
|
btn.addEventListener("click", () => onAction(admin.login));
|
||||||
|
|
||||||
actions.appendChild(removeBtn);
|
actions.appendChild(btn);
|
||||||
li.appendChild(actions);
|
li.appendChild(actions);
|
||||||
elements.adminList.appendChild(li);
|
listEl.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAdmins(list) {
|
||||||
|
renderAdminList(elements.adminList, list, {
|
||||||
|
actionLabel: "Remove",
|
||||||
|
actionClass: "secondary",
|
||||||
|
onAction: removeAdmin,
|
||||||
|
emptyMessage: "No channel admins yet.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSuggestedAdmins(list) {
|
function renderSuggestedAdmins(list) {
|
||||||
if (!elements.suggestionList) return;
|
renderAdminList(elements.suggestionList, list, {
|
||||||
|
actionLabel: "Add channel admin",
|
||||||
elements.suggestionList.innerHTML = "";
|
actionClass: "ghost",
|
||||||
if (!list || list.length === 0) {
|
onAction: addAdmin,
|
||||||
const empty = document.createElement("li");
|
emptyMessage: "No moderator suggestions right now.",
|
||||||
empty.className = "stacked-list-item empty";
|
|
||||||
empty.textContent = "No moderator suggestions right now.";
|
|
||||||
elements.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 channel admin";
|
|
||||||
addBtn.addEventListener("click", () => addAdmin(admin.login));
|
|
||||||
|
|
||||||
actions.appendChild(addBtn);
|
|
||||||
li.appendChild(actions);
|
|
||||||
elements.suggestionList.appendChild(li);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +387,6 @@ async function fetchScriptSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveScriptSettings() {
|
async function saveScriptSettings() {
|
||||||
saveCanvasSettings()
|
|
||||||
const allowChannelEmotesForAssets = elements.allowChannelEmotes?.checked ?? true;
|
const allowChannelEmotesForAssets = elements.allowChannelEmotes?.checked ?? true;
|
||||||
const allowSevenTvEmotesForAssets = elements.allowSevenTvEmotes?.checked ?? true;
|
const allowSevenTvEmotesForAssets = elements.allowSevenTvEmotes?.checked ?? true;
|
||||||
const allowScriptChatAccess = elements.allowScriptChat?.checked ?? true;
|
const allowScriptChatAccess = elements.allowScriptChat?.checked ?? true;
|
||||||
@@ -423,30 +408,30 @@ async function saveScriptSettings() {
|
|||||||
);
|
);
|
||||||
renderScriptSettings(settings);
|
renderScriptSettings(settings);
|
||||||
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "Saved.";
|
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "Saved.";
|
||||||
showToast("Script settings saved successfully.", "success");
|
showToast("Integration settings saved successfully.", "success");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "";
|
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "";
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "Unable to save right now.";
|
if (elements.scriptSettingsStatus) elements.scriptSettingsStatus.textContent = "Unable to save right now.";
|
||||||
showToast("Unable to save script settings. Please retry.", "error");
|
showToast("Unable to save integration settings. Please retry.", "error");
|
||||||
} finally {
|
} finally {
|
||||||
setButtonBusy(elements.scriptSettingsSaveButton, false, "Saving...");
|
setButtonBusy(elements.scriptSettingsSaveButton, false, "Saving...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAccount() {
|
function showDeleteConfirm() {
|
||||||
const confirmation = window.prompt(
|
if (elements.deleteAccountButton) elements.deleteAccountButton.classList.add("hidden");
|
||||||
"Type DELETE to permanently remove your account, assets, and session.",
|
if (elements.deleteConfirmStep) elements.deleteConfirmStep.classList.remove("hidden");
|
||||||
);
|
}
|
||||||
if (confirmation !== "DELETE") {
|
|
||||||
if (confirmation !== null) {
|
|
||||||
showToast("Account deletion cancelled.", "info");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setButtonBusy(elements.deleteAccountButton, true, "Deleting...");
|
function hideDeleteConfirm() {
|
||||||
|
if (elements.deleteAccountButton) elements.deleteAccountButton.classList.remove("hidden");
|
||||||
|
if (elements.deleteConfirmStep) elements.deleteConfirmStep.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAccount() {
|
||||||
|
setButtonBusy(elements.deleteConfirmButton, true, "Deleting...");
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/account", { method: "DELETE" });
|
const response = await fetch("/api/account", { method: "DELETE" });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -456,7 +441,8 @@ async function deleteAccount() {
|
|||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast("Unable to delete account right now. Please retry.", "error");
|
showToast("Unable to delete account right now. Please retry.", "error");
|
||||||
setButtonBusy(elements.deleteAccountButton, false, "Deleting...");
|
setButtonBusy(elements.deleteConfirmButton, false, "Deleting...");
|
||||||
|
hideDeleteConfirm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,14 +455,20 @@ if (elements.adminInput) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAdmins();
|
|
||||||
fetchSuggestedAdmins();
|
|
||||||
fetchCanvasSettings();
|
|
||||||
fetchScriptSettings();
|
|
||||||
|
|
||||||
if (elements.deleteAccountButton) {
|
if (elements.deleteAccountButton) {
|
||||||
elements.deleteAccountButton.addEventListener("click", deleteAccount);
|
elements.deleteAccountButton.addEventListener("click", showDeleteConfirm);
|
||||||
|
}
|
||||||
|
if (elements.deleteCancelButton) {
|
||||||
|
elements.deleteCancelButton.addEventListener("click", hideDeleteConfirm);
|
||||||
|
}
|
||||||
|
if (elements.deleteConfirmButton) {
|
||||||
|
elements.deleteConfirmButton.addEventListener("click", deleteAccount);
|
||||||
}
|
}
|
||||||
if (elements.maxVolumeDb) {
|
if (elements.maxVolumeDb) {
|
||||||
elements.maxVolumeDb.addEventListener("input", handleVolumeSliderInput);
|
elements.maxVolumeDb.addEventListener("input", handleVolumeSliderInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchAdmins();
|
||||||
|
fetchSuggestedAdmins();
|
||||||
|
fetchCanvasSettings();
|
||||||
|
fetchScriptSettings();
|
||||||
|
|||||||
@@ -54,29 +54,51 @@
|
|||||||
<ul id="copyright-notices-list" class="copyright-notices-list"></ul>
|
<ul id="copyright-notices-list" class="copyright-notices-list"></ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="large-dashboard-tiles">
|
<!-- Navigation cards -->
|
||||||
<a th:href="@{'/view/' + ${channel} + '/broadcast'}">
|
<nav class="dashboard-nav-cards">
|
||||||
<i class="fa-solid fa-tower-broadcast"></i>
|
<a class="dashboard-nav-card" th:href="@{'/view/' + ${channel} + '/broadcast'}">
|
||||||
<span>Broadcast Overlay</span>
|
<div class="dashboard-nav-card-icon">
|
||||||
|
<i class="fa-solid fa-tower-broadcast" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-nav-card-copy">
|
||||||
|
<strong>Broadcast Overlay</strong>
|
||||||
|
<span>Add to OBS as a browser source</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a th:href="@{'/view/' + ${channel} + '/admin'}">
|
<a class="dashboard-nav-card" th:href="@{'/view/' + ${channel} + '/admin'}">
|
||||||
<i class="fa-solid fa-layer-group"></i>
|
<div class="dashboard-nav-card-icon">
|
||||||
<span>Admin Console</span>
|
<i class="fa-solid fa-layer-group" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-nav-card-copy">
|
||||||
|
<strong>Admin Console</strong>
|
||||||
|
<span>Manage assets and scripts</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a th:href="@{'/view/' + ${channel} + '/audit'}">
|
<a class="dashboard-nav-card" th:href="@{'/view/' + ${channel} + '/audit'}">
|
||||||
<i class="fa-solid fa-user-shield"></i>
|
<div class="dashboard-nav-card-icon">
|
||||||
<span>Audit Log</span>
|
<i class="fa-solid fa-user-shield" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-nav-card-copy">
|
||||||
|
<strong>Audit Log</strong>
|
||||||
|
<span>Review channel activity</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a th:if="${isSystemAdmin}" href="/settings">
|
<a th:if="${isSystemAdmin}" class="dashboard-nav-card" href="/settings">
|
||||||
<i class="fa-solid fa-gear"></i>
|
<div class="dashboard-nav-card-icon dashboard-nav-card-icon--muted">
|
||||||
<span>Application Settings</span>
|
<i class="fa-solid fa-gear" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-nav-card-copy">
|
||||||
|
<strong>Application Settings</strong>
|
||||||
|
<span>System-wide configuration</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</nav>
|
||||||
|
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
|
<!-- Overlay settings -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="eyebrow">Settings</p>
|
<p class="eyebrow">Settings</p>
|
||||||
<h3>Overlay dimensions</h3>
|
<h3>Overlay</h3>
|
||||||
<p class="muted">Match these with your OBS resolution.</p>
|
<p class="muted">Match these with your OBS resolution.</p>
|
||||||
<div class="control-grid">
|
<div class="control-grid">
|
||||||
<label>
|
<label>
|
||||||
@@ -94,10 +116,19 @@
|
|||||||
<span class="form-helper">Drag to preview. Left = much quieter, right = louder.</span>
|
<span class="form-helper">Drag to preview. Left = much quieter, right = louder.</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-actions">
|
||||||
|
<button id="save-canvas-btn" type="button" onclick="saveCanvasSettings()">
|
||||||
|
Save overlay settings
|
||||||
|
</button>
|
||||||
|
<span id="canvas-status" class="muted" role="status" aria-live="polite"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Integration settings -->
|
||||||
|
<section class="card">
|
||||||
|
<p class="eyebrow">Settings</p>
|
||||||
<h3>Integrations</h3>
|
<h3>Integrations</h3>
|
||||||
<p class="muted">Set access policy for script assets.</p>
|
<p class="muted">Set access policy for script assets.</p>
|
||||||
|
|
||||||
<div class="control-list">
|
<div class="control-list">
|
||||||
<label class="control">
|
<label class="control">
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
@@ -123,12 +154,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="control-actions">
|
<div class="control-actions">
|
||||||
<button id="save-script-settings-btn" type="button" onclick="saveScriptSettings()">
|
<button id="save-script-settings-btn" type="button" onclick="saveScriptSettings()">
|
||||||
Save settings
|
Save integration settings
|
||||||
</button>
|
</button>
|
||||||
<span id="script-settings-status" class="muted" role="status" aria-live="polite"></span>
|
<span id="script-settings-status" class="muted" role="status" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Channel admins -->
|
||||||
<section class="card-grid two-col dashboard-span-full">
|
<section class="card-grid two-col dashboard-span-full">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -162,6 +194,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Channels you administer -->
|
||||||
<section th:if="${adminChannels != null}" class="card dashboard-span-full">
|
<section th:if="${adminChannels != null}" class="card dashboard-span-full">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -182,21 +215,41 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card dashboard-span-full">
|
<!-- Danger zone -->
|
||||||
<div class="card-header">
|
<section class="card danger-zone dashboard-span-full">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<span class="danger-zone-summary-content">
|
||||||
|
<i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
|
||||||
|
<span>Danger zone</span>
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="danger-zone-body">
|
||||||
|
<div class="danger-zone-item">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Account</p>
|
<p class="eyebrow">Account</p>
|
||||||
<h3>Delete account</h3>
|
<h4>Delete account</h4>
|
||||||
<p class="muted">
|
<p class="muted">Permanently remove your account, assets, and session. This cannot be undone.</p>
|
||||||
Permanently remove your account, assets, and session. This cannot be undone.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="danger-confirm" id="delete-confirm-area">
|
||||||
<div class="control-actions">
|
|
||||||
<button id="delete-account-btn" class="secondary danger" type="button">
|
<button id="delete-account-btn" class="secondary danger" type="button">
|
||||||
Delete my account
|
Delete my account
|
||||||
</button>
|
</button>
|
||||||
|
<div class="danger-confirm-step hidden" id="delete-confirm-step">
|
||||||
|
<p class="muted">Are you sure? This is permanent and cannot be reversed.</p>
|
||||||
|
<div class="danger-confirm-actions">
|
||||||
|
<button id="delete-account-confirm-btn" class="danger" type="button">
|
||||||
|
Yes, delete my account
|
||||||
|
</button>
|
||||||
|
<button id="delete-account-cancel-btn" class="ghost" type="button">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user