Refactor web server and release workflow
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>My account</h2>
|
||||
<p>Update your display name and password.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card account-card">
|
||||
<form method="post" action="/admin/account/name" class="account-form account-form--name">
|
||||
<label>
|
||||
Display name
|
||||
<input type="text" name="name" value="{{name}}" autocomplete="name" required />
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button type="submit">Save name</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card account-card">
|
||||
<form method="post" action="/admin/account/password" class="account-form">
|
||||
<label>
|
||||
Current password
|
||||
<input type="password" name="current_password" autocomplete="current-password" required />
|
||||
</label>
|
||||
<label>
|
||||
New password
|
||||
<input type="password" name="new_password" autocomplete="new-password" minlength="8" required />
|
||||
</label>
|
||||
<label>
|
||||
Confirm new password
|
||||
<input type="password" name="confirm_password" autocomplete="new-password" minlength="8" required />
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button type="submit">Update password</button>
|
||||
<a class="button-link secondary" href="/admin">Back to dashboard</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Create canvas size</h2>
|
||||
<p>Add a reusable canvas dimension for templates.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/canvas-sizes">Back to sizes</a>
|
||||
</div>
|
||||
|
||||
<div class="card canvas-size-card">
|
||||
<form method="post" action="/admin/canvas-sizes" class="canvas-size-form">
|
||||
<label>Name
|
||||
<input name="name" value="{{canvasSize.name}}" required />
|
||||
</label>
|
||||
<div class="row">
|
||||
<label style="flex:1">Width
|
||||
<input name="width" type="number" min="1" value="{{canvasSize.width}}" required />
|
||||
</label>
|
||||
<label style="flex:1">Height
|
||||
<input name="height" type="number" min="1" value="{{canvasSize.height}}" required />
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit">Create size</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit canvas size</h2>
|
||||
<p>Update the stored canvas dimensions.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/canvas-sizes">Back to sizes</a>
|
||||
</div>
|
||||
|
||||
<div class="card canvas-size-card">
|
||||
<form method="post" action="/admin/canvas-sizes/{{canvasSize.id}}" class="canvas-size-form">
|
||||
<label>Name
|
||||
<input name="name" value="{{canvasSize.name}}" required />
|
||||
</label>
|
||||
<div class="row">
|
||||
<label style="flex:1">Width
|
||||
<input name="width" type="number" min="1" value="{{canvasSize.width}}" required />
|
||||
</label>
|
||||
<label style="flex:1">Height
|
||||
<input name="height" type="number" min="1" value="{{canvasSize.height}}" required />
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Canvas sizes</h2>
|
||||
<p>Keep template dimensions standardized across the system.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/canvas-sizes/new">Add canvas size</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Saved sizes</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Name</th><th>Dimensions</th><th>Templates</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if canvasSizes.length}}
|
||||
{{#each canvasSizes}}
|
||||
<tr>
|
||||
<td data-label="Name">{{name}}</td>
|
||||
<td data-label="Dimensions">{{width}}x{{height}}</td>
|
||||
<td data-label="Templates">{{templateCount}}</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions">
|
||||
<a class="button-link list-action" href="/admin/canvas-sizes/{{id}}/edit">Edit</a>
|
||||
<form class="inline-form" method="post" action="/admin/canvas-sizes/{{id}}/delete" data-confirm-message="Delete this canvas size?">
|
||||
<button class="danger list-action" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="4" class="empty">No canvas sizes yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Connected clients</h2>
|
||||
<p>Inspect live player connections and send commands to individual clients.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Active connections</h3>
|
||||
<table id="dashboard-clients-table" class="table-cards">
|
||||
<thead><tr><th>Client/Connection ID</th><th>IP</th><th>Viewport</th><th>Connected</th><th>Screen</th><th>Actions</th></tr></thead>
|
||||
<tbody id="dashboard-clients-table-body">
|
||||
{{#if clients.length}}
|
||||
{{#each clients}}
|
||||
<tr>
|
||||
<td data-label="Client/Connection ID">
|
||||
{{#if clientId}}
|
||||
<div class="connection-count">{{clientId}}</div>
|
||||
{{else}}
|
||||
<span class="empty">Unknown</span>
|
||||
{{/if}}
|
||||
{{#if id}}
|
||||
<div class="connection-id">{{id}}</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="IP">
|
||||
{{#if clientIp}}
|
||||
{{clientIp}}
|
||||
{{else}}
|
||||
{{#if remoteAddress}}
|
||||
{{remoteAddress}}
|
||||
{{else}}
|
||||
<span class="empty">Unknown</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="Viewport">
|
||||
{{#if viewport}}
|
||||
{{viewport.width}}x{{viewport.height}}
|
||||
{{else}}
|
||||
<span class="empty">Unknown</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="Connected">
|
||||
{{#if connectedAt}}
|
||||
<div>{{connectedAtLabel}}</div>
|
||||
{{#if lastSeenAt}}
|
||||
<div class="subtle">{{lastSeenAtLabel}}</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<span class="empty">Unknown</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="Screen">
|
||||
<div>{{screen_name}}</div>
|
||||
{{#if currentSlideTitle}}
|
||||
<div class="subtle">Showing: {{currentSlideTitle}}</div>
|
||||
{{else}}
|
||||
<div class="subtle">No slide currently showing</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions">
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command>
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="button-link list-action is-danger">Reload</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="previous" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="button-link list-action list-action--nav" aria-label="Previous slide">◀</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="next" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="button-link list-action list-action--nav" aria-label="Next slide">▶</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="pause" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="button-link list-action{{#if paused}} is-paused{{/if}}">{{#if paused}}Resume{{else}}Pause{{/if}}</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/screens/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="connectionId" value="{{id}}" />
|
||||
<button type="submit" class="button-link list-action{{#if blackout}} is-blackout{{/if}}">{{#if blackout}}Restore{{else}}Blackout{{/if}}</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="6" class="empty">No connected clients yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,55 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Dashboard</h2>
|
||||
<p>Monitor screens and quick actions from one place.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<section class="stat"><strong id="dashboard-playlist-count">{{playlists.length}}</strong><span>Playlists</span></section>
|
||||
<section class="stat"><strong id="dashboard-slide-count">{{slides.length}}</strong><span>Slides</span></section>
|
||||
<section class="stat"><strong id="dashboard-screen-count">{{screens.length}}</strong><span>Screens</span></section>
|
||||
<section class="stat"><strong id="dashboard-client-count">{{connectedClientsCount}}</strong><span>Clients</span></section>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Quick actions</h3>
|
||||
<div class="actions">
|
||||
<form method="post" action="/admin/commands" class="inline-form" data-confirm-message="Reload all connected clients?" data-async-command>
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<button type="submit" class="button-link list-action is-danger" id="dashboard-reload-all-button">Reload all clients</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/commands" class="inline-form" data-confirm-message="Blackout all connected clients?" data-async-command>
|
||||
<input type="hidden" name="command" value="blackout" />
|
||||
<input type="hidden" name="blackout" value="true" />
|
||||
<button type="submit" class="button-link list-action" id="dashboard-blackout-all-button">Blackout all clients</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Current screens</h3>
|
||||
<table id="dashboard-screens-table" class="table-cards">
|
||||
<thead><tr><th>Name</th><th>Player URL</th><th>Playlist</th><th>Connected clients</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if screens.length}}
|
||||
{{#each screens}}
|
||||
<tr data-client-key="{{id}}">
|
||||
<td data-label="Name">{{name}}</td>
|
||||
<td data-label="Player URL"><a href="{{playerUrl slug}}" target="_blank">{{playerUrl slug}}</a></td>
|
||||
<td data-label="Playlist">{{playlist_name}}</td>
|
||||
<td data-label="Connected clients">
|
||||
{{#if player_connection_count}}
|
||||
<div class="connection-count">{{player_connection_count}} connected</div>
|
||||
{{else}}
|
||||
<span class="empty">No clients connected.</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="4" class="empty">No screens yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{title}} - Signage</title>
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/admin.css" />
|
||||
{{#if stylesheets.length}}
|
||||
{{#each stylesheets}}
|
||||
<link rel="stylesheet" href="/assets/{{this}}" />
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</head>
|
||||
<body class="admin-shell-body admin-shell-body--frame {{frameBodyClass}}">
|
||||
<div class="app-shell app-shell--frame">
|
||||
<div class="shell-main shell-main--frame">
|
||||
{{#unless hideThemeToggle}}
|
||||
<button type="button" class="theme-toggle theme-toggle--floating" data-theme-toggle aria-pressed="false" aria-label="Switch to dark mode">
|
||||
<span class="theme-toggle__icon theme-toggle__icon--moon" aria-hidden="true"></span>
|
||||
</button>
|
||||
{{/unless}}
|
||||
{{#unless hideFrameHeader}}
|
||||
<header class="topbar topbar--frame">
|
||||
<div class="topbar-copy">
|
||||
<span class="eyebrow">Slide workspace</span>
|
||||
<h1>{{title}}</h1>
|
||||
</div>
|
||||
</header>
|
||||
{{/unless}}
|
||||
<main class="content content--frame">
|
||||
<div class="page-shell page-shell--frame">
|
||||
{{{body}}}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/js/theme-sidebar.js"></script>
|
||||
<script src="/assets/js/toast.js"></script>
|
||||
<script src="/assets/js/admin-page.js"></script>
|
||||
{{#if scripts.length}}
|
||||
{{#each scripts}}
|
||||
<script src="/assets/{{this}}"></script>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,111 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>{{title}} - Signage</title>
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/admin.css" />
|
||||
{{#if stylesheets.length}}
|
||||
{{#each stylesheets}}
|
||||
<link rel="stylesheet" href="/assets/{{this}}" />
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</head>
|
||||
<body class="admin-shell-body {{#if authShell}}auth-shell-body{{/if}} {{bodyClass}}">
|
||||
{{#if authShell}}
|
||||
<button type="button" class="theme-toggle theme-toggle--floating" data-theme-toggle aria-pressed="false" aria-label="Switch to dark mode">
|
||||
<span class="theme-toggle__icon theme-toggle__icon--moon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<main class="auth-page">
|
||||
{{{body}}}
|
||||
</main>
|
||||
<script src="/assets/js/admin-page.js"></script>
|
||||
{{else}}
|
||||
{{#if message}}
|
||||
<div class="toast" id="app-toast" role="status" aria-live="polite">
|
||||
<div class="toast-body">{{message}}</div>
|
||||
<button type="button" class="toast-close" id="app-toast-close" aria-label="Dismiss notification">×</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" aria-label="Primary navigation">
|
||||
<div class="brand">
|
||||
<div class="brand-mark">PS</div>
|
||||
<div class="brand-copy">
|
||||
<strong>Pulse Signage</strong>
|
||||
<span>Admin Panel</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<span class="status-dot status-dot--unknown" id="sidebar-status-dot"></span>
|
||||
<div>
|
||||
<strong>System status</strong>
|
||||
<span id="sidebar-status-text">Checking player service</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<span class="nav-label">Manage</span>
|
||||
<a class="nav-item {{#if (eq active 'dashboard')}}active{{/if}}" href="/admin">Dashboard</a>
|
||||
<a class="nav-item {{#if (eq active 'clients')}}active{{/if}}" href="/admin/clients">Connected clients</a>
|
||||
<a class="nav-item {{#if (eq active 'screens')}}active{{/if}}" href="/admin/screens">Screens</a>
|
||||
<a class="nav-item {{#if (eq active 'playlists')}}active{{/if}}" href="/admin/playlists">Playlists</a>
|
||||
<a class="nav-item {{#if (eq active 'slides')}}active{{/if}}" href="/admin/slides">Slides</a>
|
||||
<a class="nav-item {{#if (eq active 'templates')}}active{{/if}}" href="/admin/templates">Slide templates</a>
|
||||
<a class="nav-item {{#if (eq active 'canvas-sizes')}}active{{/if}}" href="/admin/canvas-sizes">Canvas sizes</a>
|
||||
</nav>
|
||||
<div class="sidebar-divider" aria-hidden="true"></div>
|
||||
<nav class="nav nav--sidebar-bottom" aria-label="Secondary navigation">
|
||||
<a class="nav-item {{#if (eq active 'users')}}active{{/if}}" href="/admin/users">Users</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer sidebar-footer--account">
|
||||
{{#if currentUser}}
|
||||
<div class="user-avatar" aria-hidden="true">{{userInitial currentUser.name currentUser.username}}</div>
|
||||
{{else}}
|
||||
<div class="user-avatar user-avatar--fallback" aria-hidden="true">A</div>
|
||||
{{/if}}
|
||||
<div>
|
||||
<strong class="sidebar-account-name">{{#if currentUser}}{{#if currentUser.name}}{{currentUser.name}}{{else}}{{currentUser.username}}{{/if}}{{else}}Account{{/if}}</strong>
|
||||
<span>{{#if currentUser}}{{currentUser.username}}{{else}}Not signed in{{/if}}</span>
|
||||
<div class="sidebar-footer-links">
|
||||
{{#if currentUser}}
|
||||
<a href="/admin/account">Account</a>
|
||||
<a href="/logout">Sign out</a>
|
||||
{{else}}
|
||||
<a href="/login">Sign in</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="theme-toggle theme-toggle--sidebar" data-theme-toggle aria-pressed="false" aria-label="Switch to dark mode">
|
||||
<span class="theme-toggle__icon theme-toggle__icon--moon" aria-hidden="true"></span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<button type="button" class="sidebar-backdrop" data-sidebar-backdrop aria-label="Close navigation"></button>
|
||||
|
||||
<div class="shell-main">
|
||||
<button type="button" class="sidebar-toggle sidebar-toggle--shell" data-sidebar-toggle aria-label="Toggle navigation" aria-expanded="false">
|
||||
<span class="sidebar-toggle-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
<main class="content">
|
||||
<div class="page-shell">
|
||||
{{{body}}}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/js/theme-sidebar.js"></script>
|
||||
<script src="/assets/js/toast.js"></script>
|
||||
<script src="/assets/js/table-sort.js"></script>
|
||||
<script src="/assets/js/admin-page.js"></script>
|
||||
<script src="/assets/js/system-status.js"></script>
|
||||
{{/if}}
|
||||
{{#if scripts.length}}
|
||||
{{#each scripts}}
|
||||
<script src="/assets/{{this}}"></script>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="auth-shell">
|
||||
<div class="auth-card card">
|
||||
<div class="auth-brand">
|
||||
<div class="brand-mark">PS</div>
|
||||
<div>
|
||||
<h1>Sign in</h1>
|
||||
<p>Sign in with your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if message}}
|
||||
<div class="auth-message">{{message}}</div>
|
||||
{{/if}}
|
||||
|
||||
<form method="post" action="/login" class="auth-form">
|
||||
<label>
|
||||
Username
|
||||
<input type="text" name="username" autocomplete="username" required autofocus />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" autocomplete="current-password" required />
|
||||
</label>
|
||||
<button type="submit">Sign in</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Create playlist</h2>
|
||||
<p>Name the playlist and start adding slides.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/playlists">Back to playlists</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="/admin/playlists" data-async-save data-async-save-reload>
|
||||
<label>Name
|
||||
<input name="name" placeholder="Lobby loop" required />
|
||||
</label>
|
||||
<button type="submit">Create playlist</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit playlist</h2>
|
||||
<p>Adjust the playlist details, slide order, and scheduling.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/playlists">Back to playlists</a>
|
||||
</div>
|
||||
|
||||
<form id="playlist-edit-form" method="post" action="/admin/playlists/{{playlist.id}}" data-async-save data-async-save-reload>
|
||||
<div class="card playlist-name-card">
|
||||
<div class="row playlist-edit-header">
|
||||
<label class="playlist-name-field">Name
|
||||
<input name="name" value="{{playlist.name}}" required />
|
||||
</label>
|
||||
<div class="row playlist-edit-actions">
|
||||
<div class="playlist-fade-field switch-field">
|
||||
<span class="switch-label">Fade:</span>
|
||||
<label class="switch-control">
|
||||
<input type="checkbox" name="fade_between_slides" value="1" {{#if playlistFadeBetweenSlides}}checked{{/if}} />
|
||||
<span class="switch-ui" aria-hidden="true"></span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" form="playlist-edit-form">Save changes</button>
|
||||
<button type="submit" class="danger" form="delete-playlist-form">Delete playlist</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Playlist items</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Duration</th><th>Schedule</th><th>Actions</th></tr></thead>
|
||||
<tbody id="playlist-items-body">
|
||||
{{#if playlistSlides.length}}
|
||||
{{#each playlistSlides}}
|
||||
<tr data-playlist-slide-row data-row-key="existing-{{id}}" data-slide-id="{{slide_id}}" data-canvas-signature="{{canvasSignature}}">
|
||||
<td class="playlist-order-cell" data-label="Order">{{displayOrder}}</td>
|
||||
<td data-label="Slide">
|
||||
{{title}}
|
||||
<input type="hidden" name="slide_id[]" value="{{slide_id}}" />
|
||||
</td>
|
||||
<td data-label="Duration">
|
||||
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required />
|
||||
</td>
|
||||
<td data-label="Schedule">
|
||||
<div class="playlist-schedule-summary">{{scheduleSummary}}</div>
|
||||
<input type="hidden" name="schedule_mode[]" value="{{schedule_mode}}" />
|
||||
<input type="hidden" name="schedule_start_datetime[]" value="{{schedule_start_datetime}}" />
|
||||
<input type="hidden" name="schedule_end_datetime[]" value="{{schedule_end_datetime}}" />
|
||||
<input type="hidden" name="schedule_start_time[]" value="{{schedule_start_time}}" />
|
||||
<input type="hidden" name="schedule_end_time[]" value="{{schedule_end_time}}" />
|
||||
<input type="hidden" name="schedule_days_json[]" value="{{schedule_days_json}}" />
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions playlist-item-actions">
|
||||
<button type="button" class="secondary" data-playlist-move="up" {{#if isFirst}}disabled{{/if}}>Up</button>
|
||||
<button type="button" class="secondary" data-playlist-move="down" {{#if isLast}}disabled{{/if}}>Down</button>
|
||||
<button type="button" class="schedule-config-button" data-schedule-config="/admin/playlists/{{../playlist.id}}/slides/{{id}}/config" data-schedule-config-row="existing-{{id}}">Schedule</button>
|
||||
<button type="button" class="danger" data-playlist-remove-row>Remove</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr class="playlist-empty-row"><td colspan="5" class="empty">No slides assigned yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card" id="playlist-add-section">
|
||||
<h3>Add slide to playlist</h3>
|
||||
{{#if hasSlides}}
|
||||
{{#if hasSelectableSlides}}
|
||||
<form id="playlist-add-slide-form">
|
||||
<div class="row">
|
||||
<label style="flex: 2;">Slide
|
||||
<select id="playlist-add-slide-select" required>
|
||||
{{#each selectableSlides}}
|
||||
<option value="{{id}}" data-canvas-signature="{{canvasSignature}}" {{#if isAssigned}}disabled{{/if}}>{{title}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<label style="width: 160px;">Visible for (seconds)
|
||||
<input id="playlist-add-duration" type="number" min="1" value="10" required />
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" id="playlist-add-slide-button">Add slide</button>
|
||||
</form>
|
||||
<div id="playlist-add-slide-empty" class="empty" style="display: none;">No slides are currently available for this playlist.</div>
|
||||
{{else}}
|
||||
<div class="empty">
|
||||
{{#if playlistCanvasMismatch}}
|
||||
This playlist contains slides with different canvas sizes. Remove the mismatched items before adding more.
|
||||
{{else if playlistCanvasSignature}}
|
||||
No slides match this playlist canvas size.
|
||||
{{else}}
|
||||
No slides are currently available for this playlist.
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="empty">Create slides first.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<form id="delete-playlist-form" method="post" action="/admin/playlists/{{playlist.id}}/delete" data-confirm-message="Delete this playlist?"></form>
|
||||
|
||||
<dialog id="slide-schedule-dialog" class="slide-schedule-dialog">
|
||||
<div class="slide-schedule-dialog-head">
|
||||
<div>
|
||||
<strong>Slide schedule</strong>
|
||||
<div class="muted">Configure availability for this playlist item.</div>
|
||||
</div>
|
||||
<button type="button" class="secondary" id="slide-schedule-close">Close</button>
|
||||
</div>
|
||||
<iframe id="slide-schedule-frame" title="Slide schedule configuration"></iframe>
|
||||
</dialog>
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Playlists</h2>
|
||||
<p>Organize slide rotations and manage scheduled playback.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/playlists/new">Create playlist</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Existing playlists</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Name</th><th>Slides</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if playlists.length}}
|
||||
{{#each playlists}}
|
||||
<tr>
|
||||
<td data-label="Name">{{name}}</td>
|
||||
<td data-label="Slides"><span class="chip">{{slideCount}} slide{{#unless slideCountIsOne}}s{{/unless}}</span></td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions">
|
||||
<a class="button-link list-action" href="/admin/playlists/{{id}}/edit">Edit</a>
|
||||
<form class="inline-form" method="post" action="/admin/playlists/{{id}}/delete" data-confirm-message="Delete this playlist?">
|
||||
<button class="danger list-action" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="3" class="empty">No playlists yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<form method="post" action="/admin/playlists/{{playlist.id}}/slides/{{playlistSlide.id}}/config">
|
||||
<input type="hidden" name="playlist_slide_id" value="{{playlistSlide.id}}" />
|
||||
<input type="hidden" name="row_key" value="{{rowKey}}" />
|
||||
<label class="schedule-type-field">
|
||||
<select name="schedule_mode" id="schedule-mode-select">
|
||||
<option value="always" {{#if isAlwaysMode}}selected{{/if}}>Always visible</option>
|
||||
<option value="dates" {{#if isDatesMode}}selected{{/if}}>Between two datetimes</option>
|
||||
<option value="times" {{#if isTimesMode}}selected{{/if}}>Between times on selected days</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="card" id="schedule-dates-panel">
|
||||
<h3>Date range</h3>
|
||||
<div class="row">
|
||||
<label style="flex: 1;">Start datetime
|
||||
<input type="datetime-local" name="schedule_start_datetime" value="{{startDateTimeValue}}" />
|
||||
</label>
|
||||
<label style="flex: 1;">End datetime
|
||||
<input type="datetime-local" name="schedule_end_datetime" value="{{endDateTimeValue}}" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="schedule-times-panel">
|
||||
<h3>Time range</h3>
|
||||
<div class="row">
|
||||
<label style="flex: 1;">Start time
|
||||
<input type="time" name="schedule_start_time" value="{{startTimeValue}}" />
|
||||
</label>
|
||||
<label style="flex: 1;">End time
|
||||
<input type="time" name="schedule_end_time" value="{{endTimeValue}}" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="schedule-day-grid">
|
||||
{{#each dayOptions}}
|
||||
<label class="schedule-day-option switch-control">
|
||||
<input type="checkbox" name="schedule_days" value="{{value}}" {{#if checked}}checked{{/if}} />
|
||||
<span class="switch-ui" aria-hidden="true"></span>
|
||||
<span class="schedule-day-label">{{label}}</span>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schedule-save-actions">
|
||||
<button type="submit">Save schedule</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Add screen</h2>
|
||||
<p>Register a player endpoint and connect it to a playlist.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/screens">Back to screens</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="/admin/screens" data-async-save data-async-save-reload>
|
||||
<label>Name
|
||||
<input name="name" placeholder="Front desk TV" required />
|
||||
</label>
|
||||
<label>Slug
|
||||
<input name="slug" placeholder="front-desk-tv" />
|
||||
</label>
|
||||
<label>Playlist
|
||||
<select name="playlist_id">
|
||||
<option value="">-- not assigned --</option>
|
||||
{{#each playlists}}
|
||||
<option value="{{id}}">{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Create screen</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit screen</h2>
|
||||
<p>Update the display name and assigned playlist.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/screens">Back to screens</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Edit screen</h3>
|
||||
<form id="screen-edit-form" class="screen-edit-form" method="post" action="/admin/screens/{{screen.id}}" data-async-save data-async-save-reload>
|
||||
<label>Name
|
||||
<input name="name" value="{{screen.name}}" required />
|
||||
</label>
|
||||
<label>Slug
|
||||
<input name="slug" value="{{screen.slug}}" />
|
||||
</label>
|
||||
<label>Playlist
|
||||
<select name="playlist_id">
|
||||
<option value="">-- not assigned --</option>
|
||||
{{#each playlists}}
|
||||
<option value="{{id}}" {{#if (eq ../screen.playlist_id id)}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<div class="screen-edit-actions">
|
||||
<button type="submit" form="screen-edit-form">Save changes</button>
|
||||
<form class="inline-form" method="post" action="/admin/screens/{{screen.id}}/delete" data-confirm-message="Delete this screen?">
|
||||
<button class="danger" type="submit">Delete screen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Player URL</h3>
|
||||
<p><a href="{{playerUrl screen.slug}}" target="_blank">{{playerUrl screen.slug}}</a></p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Screens</h2>
|
||||
<p>Register display endpoints and keep playlist assignments in sync.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/screens/new">Add screen</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Existing screens</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Name</th><th>Player URL</th><th>Playlist</th><th>Connected clients</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if screens.length}}
|
||||
{{#each screens}}
|
||||
<tr>
|
||||
<td data-label="Name">{{name}}</td>
|
||||
<td data-label="Player URL"><a href="{{playerUrl slug}}" target="_blank">{{playerUrl slug}}</a></td>
|
||||
<td data-label="Playlist">{{playlist_name}}</td>
|
||||
<td data-label="Connected clients">
|
||||
{{#if player_connection_count}}
|
||||
<div class="connection-count">{{player_connection_count}} connected</div>
|
||||
{{else}}
|
||||
<span class="empty">No clients connected.</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions">
|
||||
<a class="button-link list-action" href="/admin/screens/{{id}}/edit">Edit</a>
|
||||
<form class="inline-form" method="post" action="/admin/screens/{{id}}/delete" data-confirm-message="Delete this screen?">
|
||||
<button class="danger list-action" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="5" class="empty">No screens yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{#if isEdit}}Edit slide{{else}}Create slide{{/if}}</h2>
|
||||
<p>Build slide content and preview it against the selected template.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/slides">Back to slides</a>
|
||||
</div>
|
||||
|
||||
<div class="slide-editor-top">
|
||||
<form method="post" action="{{action}}" enctype="multipart/form-data" id="slide-form">
|
||||
<div class="card slide-template-card">
|
||||
<div class="row">
|
||||
<label style="flex: 2;">Title
|
||||
<input name="title" value="{{slide.title}}" required />
|
||||
</label>
|
||||
<label style="flex: 3;">Template
|
||||
<select name="template_id" id="template-select">
|
||||
<option value="">-- none --</option>
|
||||
{{#each templates}}
|
||||
<option value="{{id}}" {{#if (eq ../slide.template_id id)}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="template-fields"></div>
|
||||
</form>
|
||||
<div class="card slide-preview-card">
|
||||
<div class="slide-preview-header">
|
||||
<h3>Preview</h3>
|
||||
<div class="muted" id="slide-preview-meta">Select a template to preview the slide.</div>
|
||||
</div>
|
||||
<div class="slide-preview-scroll">
|
||||
<button type="submit" form="slide-form">{{#if isEdit}}Save changes{{else}}Create slide{{/if}}</button>
|
||||
<div class="slide-preview-stage" id="slide-preview-stage">
|
||||
<img id="slide-preview-background" class="slide-preview-background" alt="" style="display:none;" />
|
||||
<div class="slide-preview-empty" id="slide-preview-empty">Select a template to preview the slide.</div>
|
||||
<div class="slide-preview-overlay" id="slide-preview-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/compact/editor.header.min.js"></script>
|
||||
<script src="/assets/js/compact/editor.list.min.js"></script>
|
||||
<script src="/assets/js/compact/editor.marker.min.js"></script>
|
||||
<script src="/assets/js/compact/editor.table.min.js"></script>
|
||||
<script src="/assets/js/compact/editor.min.js"></script>
|
||||
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
|
||||
<script src="/assets/js/slide-form.js"></script>
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Slides</h2>
|
||||
<p>Manage individual slide content and template assignments.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/slides/new">Create slide</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Existing slides</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Title</th><th>Template</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if slides.length}}
|
||||
{{#each slides}}
|
||||
<tr>
|
||||
<td data-label="Title">{{title}}</td>
|
||||
<td data-label="Template">{{template_name}}</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions">
|
||||
<a class="button-link list-action" href="/admin/slides/{{id}}/edit">Edit</a>
|
||||
<form class="inline-form" method="post" action="/admin/slides/{{id}}/delete" data-confirm-message="Delete this slide?">
|
||||
<button class="danger list-action" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="3" class="empty">No slides yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,72 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Create template</h2>
|
||||
<p>Define a canvas, upload a background, and place content regions.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/templates">Back to templates</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/templates" enctype="multipart/form-data" id="template-form">
|
||||
<div class="card template-meta-card">
|
||||
<div class="template-meta-row">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" id="template-name" value="{{template.name}}" required />
|
||||
</label>
|
||||
<label>
|
||||
Canvas size
|
||||
<select id="canvas-size-select" data-lock-on-existing-template="false">
|
||||
{{#unless template.canvas_size_id}}
|
||||
<option value="" data-width="{{template.canvas_size_width}}" data-height="{{template.canvas_size_height}}" selected>{{template.canvas_size_label}}</option>
|
||||
{{/unless}}
|
||||
{{#if canvasSizes.length}}
|
||||
{{#each canvasSizes}}
|
||||
<option value="{{id}}" data-width="{{width}}" data-height="{{height}}" {{#if (eq ../template.canvas_size_id id)}}selected{{/if}}>{{name}} ({{width}}x{{height}})</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Background image
|
||||
<input type="file" name="background_image" id="background-image" accept="image/*" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="canvas_size_id" id="canvas-size-id" value="{{template.canvas_size_id}}" />
|
||||
<input type="hidden" name="canvas_width" id="canvas-width" value="{{template.canvas_size_width}}" />
|
||||
<input type="hidden" name="canvas_height" id="canvas-height" value="{{template.canvas_size_height}}" />
|
||||
<input type="hidden" id="canvas-size-summary" />
|
||||
<input type="hidden" name="existing_background_image_path" value="{{template.background_image_path}}" />
|
||||
<input type="hidden" name="regions_json" id="regions-json" value="" />
|
||||
|
||||
<div class="template-designer-layout">
|
||||
<div class="card">
|
||||
<div class="template-designer-main">
|
||||
<div class="designer-stage" id="designer-stage" style="aspect-ratio: {{template.canvas_size_width}} / {{template.canvas_size_height}};">
|
||||
<img id="background-preview" src="{{template.background_image_path}}" alt="" style="{{#if template.background_image_path}}{{else}}display:none;{{/if}}" />
|
||||
<div class="empty-bg" id="background-empty" style="{{#if template.background_image_path}}display:none;{{/if}}">No background image selected</div>
|
||||
<div class="designer-overlay" id="designer-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-designer-sidebar">
|
||||
<button type="submit" class="template-save-button">Create template</button>
|
||||
<div class="card">
|
||||
<div class="row template-designer-actions">
|
||||
<button type="button" class="secondary" id="add-text-region">Add text region</button>
|
||||
<button type="button" class="secondary" id="add-html-region">Add HTML region</button>
|
||||
<button type="button" class="secondary" id="add-image-region">Add image region</button>
|
||||
<button type="button" class="secondary" id="add-webpage-region">Add webpage region</button>
|
||||
</div>
|
||||
<h3>Region information</h3>
|
||||
<label>Select region
|
||||
<select id="region-select" class="region-select"></select>
|
||||
</label>
|
||||
<div id="region-list" class="region-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
@@ -0,0 +1,76 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit template</h2>
|
||||
<p>Adjust the canvas and region layout for this template.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/templates">Back to templates</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/templates/{{template.id}}" enctype="multipart/form-data" id="template-form">
|
||||
<div class="card template-meta-card">
|
||||
<div class="template-meta-row">
|
||||
<label>
|
||||
Name
|
||||
<input name="name" id="template-name" value="{{template.name}}" required />
|
||||
</label>
|
||||
<label>
|
||||
Canvas size
|
||||
<select id="canvas-size-select" data-lock-on-existing-template="true">
|
||||
{{#unless template.canvas_size_id}}
|
||||
<option value="" data-width="{{template.canvas_size_width}}" data-height="{{template.canvas_size_height}}" selected>{{template.canvas_size_label}}</option>
|
||||
{{/unless}}
|
||||
{{#if canvasSizes.length}}
|
||||
{{#each canvasSizes}}
|
||||
<option value="{{id}}" data-width="{{width}}" data-height="{{height}}" {{#if (eq ../template.canvas_size_id id)}}selected{{/if}}>{{name}} ({{width}}x{{height}})</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Background image
|
||||
<div class="template-background-row">
|
||||
<input type="file" name="background_image" id="background-image" accept="image/*" />
|
||||
<button type="button" class="secondary" id="remove-background-image">Remove background</button>
|
||||
</div>
|
||||
<input type="checkbox" name="remove_background_image" id="remove-background-image-flag" value="1" hidden />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="canvas_size_id" id="canvas-size-id" value="{{template.canvas_size_id}}" />
|
||||
<input type="hidden" name="canvas_width" id="canvas-width" value="{{template.canvas_size_width}}" />
|
||||
<input type="hidden" name="canvas_height" id="canvas-height" value="{{template.canvas_size_height}}" />
|
||||
<input type="hidden" id="canvas-size-summary" />
|
||||
<input type="hidden" name="existing_background_image_path" value="{{template.background_image_path}}" />
|
||||
<input type="hidden" name="regions_json" id="regions-json" value="" />
|
||||
|
||||
<div class="template-designer-layout">
|
||||
<div class="card">
|
||||
<div class="template-designer-main">
|
||||
<div class="designer-stage" id="designer-stage" style="aspect-ratio: {{template.canvas_size_width}} / {{template.canvas_size_height}};">
|
||||
<img id="background-preview" src="{{template.background_image_path}}" alt="" style="{{#if template.background_image_path}}{{else}}display:none;{{/if}}" />
|
||||
<div class="empty-bg" id="background-empty" style="{{#if template.background_image_path}}display:none;{{/if}}">No background image selected</div>
|
||||
<div class="designer-overlay" id="designer-overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-designer-sidebar">
|
||||
<button type="submit" class="template-save-button">Save changes</button>
|
||||
<div class="card">
|
||||
<div class="row template-designer-actions">
|
||||
<button type="button" class="secondary" id="add-text-region">Add text</button>
|
||||
<button type="button" class="secondary" id="add-html-region">Add HTML</button>
|
||||
<button type="button" class="secondary" id="add-image-region">Add image</button>
|
||||
<button type="button" class="secondary" id="add-webpage-region">Add webpage</button>
|
||||
</div>
|
||||
<h3>Region information</h3>
|
||||
<label>Select region
|
||||
<select id="region-select" class="region-select"></select>
|
||||
</label>
|
||||
<div id="region-list" class="region-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Slide templates</h2>
|
||||
<p>Design reusable canvas layouts and region definitions.</p>
|
||||
</div>
|
||||
<a class="button-link" href="/admin/templates/new">Create Template</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Existing templates</h3>
|
||||
<table class="table-cards">
|
||||
<thead><tr><th>Name</th><th>Canvas</th><th>Regions</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if templates.length}}
|
||||
{{#each templates}}
|
||||
<tr>
|
||||
<td data-label="Name">{{name}}</td>
|
||||
<td data-label="Canvas">{{canvas_size_width}}x{{canvas_size_height}}</td>
|
||||
<td data-label="Regions">{{regionCount}}</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions">
|
||||
<a class="button-link list-action" href="/admin/templates/{{id}}/edit">Edit</a>
|
||||
<form class="inline-form" method="post" action="/admin/templates/{{id}}/delete" data-confirm-message="Delete this template?">
|
||||
<button class="danger list-action" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="4" class="empty">No templates yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Add user</h2>
|
||||
<p>Create a new login for the web UI.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card user-create-card">
|
||||
<form method="post" action="/admin/users" class="user-create-form">
|
||||
<section class="user-create-section">
|
||||
<h3>Account details</h3>
|
||||
<div class="user-create-fields">
|
||||
<label>
|
||||
Username
|
||||
<input type="text" name="username" autocomplete="username" required />
|
||||
</label>
|
||||
<label>
|
||||
Name
|
||||
<input type="text" name="name" autocomplete="name" required />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="user-create-section">
|
||||
<h3>Set password</h3>
|
||||
<div class="user-create-fields">
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" autocomplete="new-password" minlength="8" required />
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
<input type="password" name="confirm_password" autocomplete="new-password" minlength="8" required />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="actions user-create-actions">
|
||||
<button type="submit" class="user-create-button">Create user</button>
|
||||
<a class="button-link secondary" href="/admin/users">Back to users</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit user</h2>
|
||||
<p>Update the name or password for {{user.username}}.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card user-edit-card">
|
||||
<div class="user-edit-header">
|
||||
<div class="user-cell">
|
||||
<div class="user-avatar" aria-hidden="true">{{usernameInitial user.username}}</div>
|
||||
<div>
|
||||
<strong>{{user.username}}</strong>
|
||||
{{#if user.isCurrentUser}}
|
||||
<div class="subtle">Current signed-in user</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="button-link secondary" href="/admin/users">Back to users</a>
|
||||
</div>
|
||||
|
||||
<div class="user-edit-grid">
|
||||
<form method="post" action="/admin/users/{{user.id}}/username" class="card user-edit-panel">
|
||||
<h3>Edit user</h3>
|
||||
<label>
|
||||
Username
|
||||
<input type="text" name="username" value="{{user.username}}" autocomplete="username" required disabled />
|
||||
</label>
|
||||
<label>
|
||||
Name
|
||||
<input type="text" name="name" value="{{user.name}}" autocomplete="name" required />
|
||||
</label>
|
||||
<button type="submit" class="user-edit-button">Save changes</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/admin/users/{{user.id}}/password" class="card user-edit-panel">
|
||||
<h3>Reset password</h3>
|
||||
<label>
|
||||
New password
|
||||
<input type="password" name="password" autocomplete="new-password" minlength="8" required />
|
||||
</label>
|
||||
<label>
|
||||
Confirm password
|
||||
<input type="password" name="confirm_password" autocomplete="new-password" minlength="8" required />
|
||||
</label>
|
||||
<button type="submit" class="user-edit-button">Save password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Users</h2>
|
||||
<p>Create accounts, rename users, reset passwords, and remove unused logins.</p>
|
||||
</div>
|
||||
<div class="page-toolbar">
|
||||
<a class="button-link" href="/admin/users/new">Add user</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Existing users</h3>
|
||||
<table class="table-cards users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if users.length}}
|
||||
{{#each users}}
|
||||
<tr>
|
||||
<td data-label="User">
|
||||
<div class="user-cell">
|
||||
<div class="user-avatar user-avatar--small" aria-hidden="true">{{usernameInitial name}}</div>
|
||||
<div>
|
||||
<strong>{{username}}</strong>
|
||||
{{#if isCurrentUser}}
|
||||
<div class="subtle">Current signed-in user</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Name">
|
||||
<div class="user-cell">
|
||||
<div>
|
||||
<strong>{{#if name}}{{name}}{{else}}-{{/if}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions users-row-actions">
|
||||
{{#unless isCurrentUser}}
|
||||
<a class="button-link list-action" href="/admin/users/{{id}}/edit">Edit</a>
|
||||
<form method="post" action="/admin/users/{{id}}/delete" class="inline-form" data-confirm-message="Delete {{username}}?">
|
||||
<button type="submit" class="button-link list-action is-danger">Delete</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<span class="empty">Protected</span>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr><td colspan="3" class="empty">No users found.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
Reference in New Issue
Block a user