This PR breaks the large web and player bootstrap files into smaller modules with clearer ownership.
Web changes: Split shared helpers, bootstrap logic, route groups, and upload-sync behavior out of web.js. Kept web.js focused on wiring and server startup. Fixed screen playlist reassignment so changing a screen’s playlist now triggers a refresh. Fixed single-slide playlist refresh behavior so updates do not get stuck behind the current slide. Player changes: Split websocket/runtime handling into runtime.js. Split playlist assembly and revision hashing into playlist.js. Split onboarding and player HTTP routes into dedicated modules. Split render utilities and template loading into render-helpers.js. Kept player.js mostly as startup/orchestration. Validation: Rebuilt both services with Docker Compose. Smoke-checked web and player routes after the refactor. Verified get_errors was clean on the touched modules.
This commit is contained in:
@@ -5,35 +5,57 @@
|
||||
</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 class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Display name</h3>
|
||||
</div>
|
||||
<form id="account-name-form" method="post" action="/admin/account/name" data-async-save data-async-save-close-url="{{returnUrl}}">
|
||||
<input type="hidden" name="return_url" value="{{returnUrl}}" />
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<input id="account-name" type="text" name="name" class="form-control" value="{{name}}" autocomplete="name" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Display name actions">
|
||||
{{{saveActionButtons formId="account-name-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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 class="col-12 col-xl-6">
|
||||
<div class="card card-outline card-warning admin-form-card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Password</h3>
|
||||
</div>
|
||||
<form id="account-password-form" method="post" action="/admin/account/password" data-async-save data-async-save-close-url="{{returnUrl}}">
|
||||
<input type="hidden" name="return_url" value="{{returnUrl}}" />
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="account-current-password" class="form-label">Current password</label>
|
||||
<input id="account-current-password" type="password" name="current_password" class="form-control" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="account-new-password" class="form-label">New password</label>
|
||||
<input id="account-new-password" type="password" name="new_password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="account-confirm-password" class="form-label">Confirm new password</label>
|
||||
<input id="account-confirm-password" type="password" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Password actions">
|
||||
{{{saveActionButtons formId="account-password-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,22 +3,34 @@
|
||||
<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 class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Canvas size details</h3>
|
||||
</div>
|
||||
<form id="canvas-size-form" method="post" action="/admin/canvas-sizes">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="canvas-size-name" class="form-label">Name</label>
|
||||
<input id="canvas-size-name" name="name" class="form-control" value="{{canvasSize.name}}" required />
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="canvas-size-width" class="form-label">Width</label>
|
||||
<input id="canvas-size-width" name="width" type="number" min="1" class="form-control" value="{{canvasSize.width}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="canvas-size-height" class="form-label">Height</label>
|
||||
<input id="canvas-size-height" name="height" type="number" min="1" class="form-control" value="{{canvasSize.height}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Canvas size actions">
|
||||
{{{saveActionButtons formId="canvas-size-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin/canvas-sizes" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Create size</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit canvas size</h2>
|
||||
<h2>Edit canvas - {{canvasSize.name}}</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 class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Canvas size details</h3>
|
||||
</div>
|
||||
<form id="canvas-size-form" method="post" action="/admin/canvas-sizes/{{canvasSize.id}}" data-async-save data-async-save-close-url="/admin/canvas-sizes" data-async-save-new-url="/admin/canvas-sizes/new">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="canvas-size-name" class="form-label">Name</label>
|
||||
<input id="canvas-size-name" name="name" class="form-control" value="{{canvasSize.name}}" required />
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="canvas-size-width" class="form-label">Width</label>
|
||||
<input id="canvas-size-width" name="width" type="number" min="1" class="form-control" value="{{canvasSize.width}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="canvas-size-height" class="form-label">Height</label>
|
||||
<input id="canvas-size-height" name="height" type="number" min="1" class="form-control" value="{{canvasSize.height}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Canvas size actions">
|
||||
{{{saveActionButtons formId="canvas-size-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/admin/canvas-sizes" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger" form="delete-canvas-size-form">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form id="delete-canvas-size-form" method="post" action="/admin/canvas-sizes/{{canvasSize.id}}/delete" data-confirm-message="Delete this canvas size?"></form>
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
<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">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Saved sizes</h3>
|
||||
<div class="card-tools">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/canvas-sizes/new">Add canvas size</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Name</th><th>Dimensions</th><th>Templates</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if canvasSizes.length}}
|
||||
@@ -19,9 +24,9 @@
|
||||
<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>
|
||||
<a class="btn btn-sm btn-primary" 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>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -5,35 +5,31 @@
|
||||
</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>
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Active connections</h3>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table id="dashboard-clients-table" class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Client</th><th>Current Screen</th><th>Current Slide</th><th>IP</th><th>Viewport</th><th>Connected/Updated</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>
|
||||
<tr data-client-key="{{id}}" data-client-id="{{clientId}}" data-client-device-id="{{deviceId}}" data-client-screen-slug="{{screen_slug}}">
|
||||
<td data-label="Client" class="client-rename-cell" title="Double-click to rename">
|
||||
<div>{{#if client_name}}{{client_name}}{{else}}Unknown{{/if}}</div>
|
||||
</td>
|
||||
<td data-label="Current Screen">
|
||||
<div>{{#if screen_name}}{{screen_name}}{{else}}Unknown{{/if}}</div>
|
||||
</td>
|
||||
<td data-label="Current Slide">
|
||||
{{#if currentSlideTitle}}
|
||||
<div>{{currentSlideTitle}}</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}}
|
||||
<span class="empty">No slide currently showing</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="IP">{{#if clientIp}}{{clientIp}}{{else}}<span class="empty">Unknown</span>{{/if}}</td>
|
||||
<td data-label="Viewport">
|
||||
{{#if viewport}}
|
||||
{{viewport.width}}x{{viewport.height}}
|
||||
@@ -41,58 +37,51 @@
|
||||
<span class="empty">Unknown</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td data-label="Connected">
|
||||
<td data-label="Connected/Updated">
|
||||
{{#if connectedAt}}
|
||||
<div>{{connectedAtLabel}}</div>
|
||||
{{#if lastSeenAt}}
|
||||
<div class="subtle">{{lastSeenAtLabel}}</div>
|
||||
<div class="subtle"><i>{{lastSeenAtLabel}}</i></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">
|
||||
<td data-label="Actions" class="text-end">
|
||||
<div class="actions justify-content-end">
|
||||
<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>
|
||||
<button type="submit" class="btn btn-sm btn-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>
|
||||
<button type="submit" class="btn btn-sm btn-warning" 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>
|
||||
<button type="submit" class="btn btn-sm btn-warning" 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>
|
||||
<button type="submit" class="btn btn-sm btn-info">{{#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>
|
||||
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/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>
|
||||
<tr><td colspan="7" class="empty">No connected clients yet.</td></tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,51 +5,112 @@
|
||||
</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>
|
||||
<section class="dashboard-hero card card-outline card-primary mb-4">
|
||||
<div class="card-body dashboard-hero-body">
|
||||
<div>
|
||||
<span class="dashboard-hero-kicker">Live overview</span>
|
||||
<h3 class="dashboard-hero-title">Keep the control surface focused on live state and actions.</h3>
|
||||
<p class="dashboard-hero-copy">Use the cards below for the current totals, then jump to the screens table when you need a deeper look at playback and connection state.</p>
|
||||
</div>
|
||||
<div class="dashboard-hero-stats">
|
||||
<div class="dashboard-hero-stat">
|
||||
<span class="dashboard-hero-stat-value">{{playlists.length}}</span>
|
||||
<span class="dashboard-hero-stat-label">playlists</span>
|
||||
</div>
|
||||
<div class="dashboard-hero-stat">
|
||||
<span class="dashboard-hero-stat-value">{{screens.length}}</span>
|
||||
<span class="dashboard-hero-stat-label">screens</span>
|
||||
</div>
|
||||
<div class="dashboard-hero-stat">
|
||||
<span class="dashboard-hero-stat-value">{{connectedClientsCount}}</span>
|
||||
<span class="dashboard-hero-stat-label">clients</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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 class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-4 card-outline card-primary dashboard-actions-card">
|
||||
<div class="card-header">
|
||||
<div class="dashboard-card-heading">
|
||||
<h3 class="card-title">Quick actions</h3>
|
||||
<p class="dashboard-card-subtitle">Send global commands without leaving the overview.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="dashboard-action-grid">
|
||||
<form
|
||||
method="post"
|
||||
action="/admin/commands"
|
||||
class="dashboard-action-form"
|
||||
data-confirm-message="Reload all connected clients?"
|
||||
data-async-command
|
||||
>
|
||||
<input type="hidden" name="command" value="reload" />
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-danger dashboard-action-button"
|
||||
id="dashboard-reload-all-button"
|
||||
><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload all clients</button>
|
||||
<span class="dashboard-action-help">Restart every connected player page.</span>
|
||||
</form>
|
||||
<form
|
||||
method="post"
|
||||
action="/admin/commands"
|
||||
class="dashboard-action-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="btn btn-secondary dashboard-action-button"
|
||||
id="dashboard-blackout-all-button"
|
||||
><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout all clients</button>
|
||||
<span class="dashboard-action-help">Blank active players immediately.</span>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 class="row">
|
||||
<div class="col-12">
|
||||
<div class="card card-outline card-secondary dashboard-table-card">
|
||||
<div class="card-header">
|
||||
<div class="dashboard-card-heading">
|
||||
<h3 class="card-title">Current screens</h3>
|
||||
<p class="dashboard-card-subtitle">Player URL, playlist assignment, and live connection count.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table id="dashboard-screens-table" class="table table-striped w-100 mb-0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,39 +6,85 @@
|
||||
<title>{{title}} - Pulse</title>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/admin.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/bootstrap-icons/css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/css/adminlte.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.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>
|
||||
<body class="hold-transition bg-body-tertiary {{frameBodyClass}} {{#if framePopupCard}}frame-popup-card-body{{/if}}">
|
||||
{{#if framePopupCard}}
|
||||
{{{body}}}
|
||||
{{else}}
|
||||
<div class="app-wrapper">
|
||||
<div class="sidebar-overlay" aria-hidden="true"></div>
|
||||
{{#unless hideFrameHeader}}
|
||||
<nav class="app-header navbar navbar-expand bg-body">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-text fw-semibold">
|
||||
{{title}}
|
||||
</span>
|
||||
{{#unless hideThemeToggle}}
|
||||
<li class="nav-item dropdown ms-auto list-unstyled">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="#"
|
||||
id="bd-theme"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-label="Toggle color scheme"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="theme-icon-active">
|
||||
<i class="bi bi-sun-fill" data-lte-theme-icon="light"></i>
|
||||
<i class="bi bi-moon-fill d-none" data-lte-theme-icon="dark"></i>
|
||||
<i class="bi bi-circle-half d-none" data-lte-theme-icon="auto"></i>
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme" style="--bs-dropdown-min-width: 8rem;">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
|
||||
<i class="bi bi-sun-fill me-2"></i>
|
||||
Light
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
|
||||
<i class="bi bi-moon-fill me-2"></i>
|
||||
Dark
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
|
||||
<i class="bi bi-circle-half me-2"></i>
|
||||
Auto
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{{/unless}}
|
||||
</div>
|
||||
</nav>
|
||||
{{/unless}}
|
||||
<main class="app-main">
|
||||
<div class="app-content">
|
||||
<div class="container-fluid py-3">
|
||||
{{{body}}}
|
||||
</div>
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
{{/if}}
|
||||
<script src="/assets/adminlte/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/adminlte/js/adminlte.min.js"></script>
|
||||
<script src="/assets/js/toast.js"></script>
|
||||
<script src="/assets/js/admin-page.js"></script>
|
||||
<script src="/assets/js/system-status.js"></script>
|
||||
{{#if scripts.length}}
|
||||
{{#each scripts}}
|
||||
<script src="/assets/{{this}}"></script>
|
||||
|
||||
+179
-74
@@ -6,103 +6,206 @@
|
||||
<title>{{title}} - Pulse</title>
|
||||
<link rel="icon" type="image/png" href="/assets/favicon.png" />
|
||||
<script src="/assets/js/theme-init.js"></script>
|
||||
<link rel="stylesheet" href="/assets/css/admin.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/bootstrap-icons/css/bootstrap-icons.min.css" />
|
||||
<link rel="stylesheet" href="/assets/adminlte/css/adminlte.min.css" />
|
||||
<link rel="stylesheet" href="/assets/css/theme-custom.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}}">
|
||||
<body class="hold-transition layout-fixed sidebar-expand-lg bg-body-tertiary {{#if authShell}}login-page{{/if}} {{bodyClass}}">
|
||||
{{#if authShell}}
|
||||
<main class="auth-page">
|
||||
{{{body}}}
|
||||
<main class="login-page d-flex align-items-center justify-content-center min-vh-100 p-3">
|
||||
<div class="login-box">
|
||||
{{{body}}}
|
||||
</div>
|
||||
</main>
|
||||
<script src="/assets/js/admin-page.js"></script>
|
||||
<script src="/assets/js/system-status.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 id="app-toast-container" class="toast-container position-fixed top-0 end-0 p-3">
|
||||
{{#if message}}
|
||||
<div class="toast align-items-center text-bg-primary border-0" id="app-toast" role="status" aria-live="polite" aria-atomic="true" data-bs-autohide="true" data-bs-delay="4000">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">{{message}}</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Dismiss notification"></button>
|
||||
</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>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="app-wrapper">
|
||||
<div class="sidebar-overlay" aria-hidden="true"></div>
|
||||
<nav class="app-header navbar navbar-expand bg-body">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link btn btn-link text-reset p-0" data-lte-toggle="sidebar" aria-label="Toggle navigation">
|
||||
<i class="bi bi-list fs-4"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item d-none d-md-flex align-items-center text-secondary small ms-3">
|
||||
<span class="status-dot status-dot--unknown me-2" id="sidebar-status-dot"></span>
|
||||
<span id="sidebar-status-text">Player service</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
<li class="nav-item dropdown">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="#"
|
||||
id="bd-theme"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-label="Toggle color scheme"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="theme-icon-active">
|
||||
<i class="bi bi-sun-fill" data-lte-theme-icon="light"></i>
|
||||
<i class="bi bi-moon-fill d-none" data-lte-theme-icon="dark"></i>
|
||||
<i class="bi bi-circle-half d-none" data-lte-theme-icon="auto"></i>
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme" style="--bs-dropdown-min-width: 8rem;">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
|
||||
<i class="bi bi-sun-fill me-2"></i>
|
||||
Light
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
|
||||
<i class="bi bi-moon-fill me-2"></i>
|
||||
Dark
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
|
||||
<i class="bi bi-circle-half me-2"></i>
|
||||
Auto
|
||||
<i class="bi bi-check-lg ms-auto d-none"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown user-menu">
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="user-menu-avatar" aria-hidden="true">{{userInitial currentUser.name currentUser.username}}</span>
|
||||
<span class="d-none d-md-inline user-menu-name">
|
||||
{{#if currentUser}}{{#if currentUser.name}}{{currentUser.name}}{{else}}{{currentUser.username}}{{/if}}{{/if}}
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-lg dropdown-menu-end user-menu-dropdown">
|
||||
{{#if currentUser}}
|
||||
<li class="user-header text-bg-primary">
|
||||
<span class="user-menu-avatar user-menu-avatar--lg" aria-hidden="true">{{userInitial currentUser.name currentUser.username}}</span>
|
||||
<p>
|
||||
{{#if currentUser.name}}{{currentUser.name}}{{else}}{{currentUser.username}}{{/if}}
|
||||
{{!-- <small>Member since today</small> --}}
|
||||
</p>
|
||||
</li>
|
||||
{{!-- <li class="user-body">
|
||||
<div class="row">
|
||||
<div class="col-4 text-center"><a href="/admin/account">Account</a></div>
|
||||
<div class="col-4 text-center"><a href="/admin/account">Settings</a></div>
|
||||
<div class="col-4 text-center"><a href="/logout">Logout</a></div>
|
||||
</div>
|
||||
</li> --}}
|
||||
<li class="user-footer">
|
||||
<a class="btn btn-outline-secondary" href="/admin/account">Profile</a>
|
||||
<a class="btn btn-outline-danger float-end" href="/logout">Sign out</a>
|
||||
</li>
|
||||
{{else}}
|
||||
<li><a class="dropdown-item" href="/login">Sign in</a></li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</nav>
|
||||
|
||||
<aside class="app-sidebar bg-body-secondary shadow" data-bs-theme="dark">
|
||||
<div class="sidebar-brand">
|
||||
<a href="/admin" class="brand-link">
|
||||
<span class="brand-text fw-light">Pulse Signage</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-footer sidebar-footer--bottom" aria-label="Sidebar controls">
|
||||
<span class="sidebar-version">v{{appVersion}}</span>
|
||||
<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>
|
||||
<div class="sidebar-wrapper">
|
||||
<nav class="mt-2" aria-label="Main navigation">
|
||||
<ul class="nav sidebar-menu flex-column" data-lte-toggle="treeview" data-accordion="false" role="menu" id="navigation">
|
||||
<li class="nav-header">MAIN NAVIGATION</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'dashboard')}}active{{/if}}" href="/admin">
|
||||
<i class="nav-icon bi bi-speedometer2"></i>
|
||||
<p>Dashboard</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'clients')}}active{{/if}}" href="/admin/clients">
|
||||
<i class="nav-icon bi bi-broadcast"></i>
|
||||
<p>Connected clients</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'screens')}}active{{/if}}" href="/admin/screens">
|
||||
<i class="nav-icon bi bi-display"></i>
|
||||
<p>Screens</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'playlists')}}active{{/if}}" href="/admin/playlists">
|
||||
<i class="nav-icon bi bi-collection-play"></i>
|
||||
<p>Playlists</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'slides')}}active{{/if}}" href="/admin/slides">
|
||||
<i class="nav-icon bi bi-file-earmark-richtext"></i>
|
||||
<p>Slides</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'templates')}}active{{/if}}" href="/admin/templates">
|
||||
<i class="nav-icon bi bi-layout-text-window-reverse"></i>
|
||||
<p>Slide templates</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'canvas-sizes')}}active{{/if}}" href="/admin/canvas-sizes">
|
||||
<i class="nav-icon bi bi-aspect-ratio"></i>
|
||||
<p>Canvas sizes</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-header">SETTINGS</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{#if (eq active 'users')}}active{{/if}}" href="/admin/users">
|
||||
<i class="nav-icon bi bi-people"></i>
|
||||
<p>Users</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</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">
|
||||
<main class="app-main">
|
||||
<div class="app-content">
|
||||
<div class="container-fluid py-3">
|
||||
{{{body}}}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/assets/js/theme-sidebar.js"></script>
|
||||
<dialog id="slide-schedule-dialog" class="slide-schedule-dialog">
|
||||
<div id="slide-schedule-content" class="slide-schedule-content"></div>
|
||||
</dialog>
|
||||
<script src="/assets/adminlte/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/adminlte/js/adminlte.min.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}}
|
||||
{{#if (isExternalUrl this)}}
|
||||
@@ -112,5 +215,7 @@
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
<script src="/assets/js/system-status.js"></script>
|
||||
{{/if}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+21
-21
@@ -1,28 +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>Pulse Signage</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-logo mb-3">
|
||||
<a href="/login" class="text-decoration-none fw-bold text-body">Pulse Signage</a>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header text-center">
|
||||
<h1 class="h4 mb-0">Sign in</h1>
|
||||
</div>
|
||||
<div class="card-body login-card-body">
|
||||
<p class="login-box-msg">Use your admin credentials to manage screens, playlists, and slides.</p>
|
||||
|
||||
{{#if message}}
|
||||
<div class="auth-message">{{message}}</div>
|
||||
<div class="alert alert-danger py-2">{{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 method="post" action="/login" class="d-grid gap-3">
|
||||
<div>
|
||||
<label for="login-username" class="form-label">Username</label>
|
||||
<input id="login-username" class="form-control form-control-lg" type="text" name="username" autocomplete="username" required autofocus />
|
||||
</div>
|
||||
<div>
|
||||
<label for="login-password" class="form-label">Password</label>
|
||||
<input id="login-password" class="form-control form-control-lg" type="password" name="password" autocomplete="current-password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-lg">Sign in</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,14 +3,24 @@
|
||||
<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>
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist details</h3>
|
||||
</div>
|
||||
<form id="playlist-edit-form" method="post" action="/admin/playlists">
|
||||
<div class="card-body">
|
||||
<div class="mb-0">
|
||||
<label for="playlist-name" class="form-label">Name</label>
|
||||
<input id="playlist-name" name="name" class="form-control" placeholder="Lobby loop" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Playlist actions">
|
||||
{{{saveActionButtons formId="playlist-edit-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin/playlists" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+132
-105
@@ -1,119 +1,146 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit playlist</h2>
|
||||
<h2>Edit playlist - {{playlist.name}}</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 class="row g-4 mb-4">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card card-outline card-primary mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="playlist-edit-form" method="post" action="/admin/playlists/{{playlist.id}}" data-async-save data-async-save-close-url="/admin/playlists" data-async-save-new-url="/admin/playlists/new">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-8">
|
||||
<label for="playlist-name" class="form-label">Name</label>
|
||||
<input id="playlist-name" name="name" class="form-control" value="{{playlist.name}}" required />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row g-3 mt-1 align-items-start">
|
||||
<div class="col-12">
|
||||
<h4 class="h6 mb-2 text-uppercase text-muted">Add slide to playlist</h4>
|
||||
{{#if hasSlides}}
|
||||
{{#if hasSelectableSlides}}
|
||||
<form id="playlist-add-slide-form">
|
||||
<div class="row g-2 align-items-end playlist-add-slide-form">
|
||||
<div class="col-12 col-md-8">
|
||||
<select id="playlist-add-slide-select" class="form-select" required>
|
||||
{{#each selectableSlides}}
|
||||
<option value="{{id}}" data-canvas-signature="{{canvasSignature}}" {{#if isAssigned}}disabled{{/if}}>{{title}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-flex justify-content-md-end">
|
||||
<button type="button" class="btn btn-primary w-100 w-md-auto" id="playlist-add-slide-button">Add slide</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="playlist-add-slide-empty" class="empty mt-2 playlist-add-slide-empty is-hidden">No slides are currently available for this playlist.</div>
|
||||
{{else}}
|
||||
<div class="empty mb-0">
|
||||
{{#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 mb-0">Create slides first.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group playlist-actions-group" role="group" aria-label="Playlist actions">
|
||||
{{{saveActionButtons formId="playlist-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/admin/playlists" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger" form="delete-playlist-form">Delete</button>
|
||||
</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>Schedule</th><th>Duration</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">
|
||||
<div class="playlist-order-cell-inner">
|
||||
<button type="button" class="playlist-drag-handle" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"></button>
|
||||
<span class="playlist-order-number">{{displayOrder}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Slide">
|
||||
{{title}}
|
||||
<input type="hidden" name="slide_id[]" value="{{slide_id}}" />
|
||||
</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="Duration">
|
||||
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required />
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions playlist-item-actions">
|
||||
<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>
|
||||
</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 class="col-12 col-lg-4">
|
||||
<div class="card card-outline card-light h-100 mb-0">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">Options</h4>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="empty">Create slides first.</div>
|
||||
{{/if}}
|
||||
<div class="card-body py-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="playlist-fade-between-slides" name="fade_between_slides" value="1" form="playlist-edit-form" {{#if playlistFadeBetweenSlides}}checked{{/if}} />
|
||||
<label class="form-check-label" for="playlist-fade-between-slides">Fade between slides</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card card-outline card-secondary mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Playlist items</h3>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Order</th><th>Slide</th><th>Schedule</th><th>Duration</th><th>Actions</th></tr></thead>
|
||||
<tbody id="playlist-items-body" data-playlist-id="{{playlist.id}}">
|
||||
{{#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">
|
||||
<div class="playlist-order-cell-inner">
|
||||
<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder">
|
||||
<span class="playlist-drag-handle-icon" aria-hidden="true">
|
||||
<svg class="playlist-drag-handle-svg" viewBox="0 0 24 32" focusable="false" aria-hidden="true">
|
||||
<polygon points="12,2 20,9 4,9"></polygon>
|
||||
<rect x="4" y="14" width="16" height="4" rx="2"></rect>
|
||||
<polygon points="4,23 20,23 12,30"></polygon>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<span class="playlist-order-number">{{displayOrder}}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Slide">
|
||||
{{title}}
|
||||
<input type="hidden" name="slide_id[]" value="{{slide_id}}" form="playlist-edit-form" />
|
||||
</td>
|
||||
<td data-label="Schedule">
|
||||
<div class="playlist-schedule-summary">{{scheduleSummary}}</div>
|
||||
<input type="hidden" name="schedule_mode[]" value="{{schedule_mode}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_start_datetime[]" value="{{schedule_start_datetime}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_end_datetime[]" value="{{schedule_end_datetime}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_start_time[]" value="{{schedule_start_time}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_end_time[]" value="{{schedule_end_time}}" form="playlist-edit-form" />
|
||||
<input type="hidden" name="schedule_days_json[]" value="{{schedule_days_json}}" form="playlist-edit-form" />
|
||||
</td>
|
||||
<td data-label="Duration">
|
||||
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required form="playlist-edit-form" />
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions playlist-item-actions">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-schedule-config="/admin/playlists/{{../playlist.id}}/slides/{{id}}/config" data-schedule-config-row="existing-{{id}}">Schedule</button>
|
||||
<button type="button" class="btn btn-sm btn-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>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
<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">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Existing playlists</h3>
|
||||
<div class="card-tools">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/playlists/new">Create playlist</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Name</th><th>Slides</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if playlists.length}}
|
||||
@@ -18,9 +23,9 @@
|
||||
<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>
|
||||
<a class="btn btn-sm btn-primary" 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>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
@@ -31,4 +36,5 @@
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,68 @@
|
||||
<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 class="card card-outline card-primary mb-0 slide-schedule-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Slide schedule</h3>
|
||||
</div>
|
||||
<form method="post" action="/admin/playlists/{{playlist.id}}/slides/{{playlistSlide.id}}/config">
|
||||
<div class="card-body">
|
||||
<input type="hidden" name="playlist_slide_id" value="{{playlistSlide.id}}" />
|
||||
<input type="hidden" name="row_key" value="{{rowKey}}" />
|
||||
|
||||
<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="mb-3">
|
||||
<label for="schedule-mode-select" class="form-label">Schedule mode</label>
|
||||
<select name="schedule_mode" id="schedule-mode-select" class="form-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>
|
||||
</div>
|
||||
|
||||
<div class="schedule-save-actions">
|
||||
<button type="submit">Save schedule</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="card card-outline card-primary mb-3 schedule-panel {{#unless isDatesMode}}is-hidden{{/unless}}" id="schedule-dates-panel">
|
||||
<div class="card-header py-2">
|
||||
<h4 class="h6 mb-0">Date range</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-start-datetime" class="form-label">Start datetime</label>
|
||||
<input id="schedule-start-datetime" type="datetime-local" name="schedule_start_datetime" class="form-control" value="{{startDateTimeValue}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-end-datetime" class="form-label">End datetime</label>
|
||||
<input id="schedule-end-datetime" type="datetime-local" name="schedule_end_datetime" class="form-control" value="{{endDateTimeValue}}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-outline card-secondary mb-0 schedule-panel {{#unless isTimesMode}}is-hidden{{/unless}}" id="schedule-times-panel">
|
||||
<div class="card-header py-2">
|
||||
<h4 class="h6 mb-0">Time range</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-start-time" class="form-label">Start time</label>
|
||||
<input id="schedule-start-time" type="time" name="schedule_start_time" class="form-control" value="{{startTimeValue}}" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="schedule-end-time" class="form-label">End time</label>
|
||||
<input id="schedule-end-time" type="time" name="schedule_end_time" class="form-control" value="{{endTimeValue}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm w-100 schedule-day-grid mt-3" role="group" aria-label="Schedule days">
|
||||
{{#each dayOptions}}
|
||||
<input class="btn-check" type="checkbox" id="schedule-day-{{value}}" name="schedule_days" value="{{value}}" autocomplete="off" {{#if checked}}checked{{/if}} />
|
||||
<label class="btn btn-outline-primary flex-fill schedule-day-button" for="schedule-day-{{value}}">{{label}}</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Slide schedule actions">
|
||||
<button type="submit" class="btn btn-primary">Save schedule</button>
|
||||
<button type="button" class="btn btn-warning" data-confirm-unsaved="You have unsaved changes. Leave this page?" onclick="window.closeScheduleModal && window.closeScheduleModal();">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -3,25 +3,39 @@
|
||||
<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>
|
||||
<div class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Screen details</h3>
|
||||
</div>
|
||||
<form id="screen-edit-form" method="post" action="/admin/screens">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-name" class="form-label">Name</label>
|
||||
<input id="screen-name" name="name" class="form-control" placeholder="Front desk TV" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-slug" class="form-label">Slug</label>
|
||||
<input id="screen-slug" name="slug" class="form-control" placeholder="front-desk-tv" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="screen-playlist" class="form-label">Playlist</label>
|
||||
<select id="screen-playlist" name="playlist_id" class="form-select">
|
||||
<option value="">-- not assigned --</option>
|
||||
{{#each playlists}}
|
||||
<option value="{{id}}">{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Screen actions">
|
||||
{{{saveActionButtons formId="screen-edit-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin/screens" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -3,38 +3,55 @@
|
||||
<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 class="row g-4">
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-outline card-primary admin-form-card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Screen details</h3>
|
||||
</div>
|
||||
<form id="screen-edit-form" method="post" action="/admin/screens/{{screen.id}}" data-async-save data-async-save-close-url="/admin/screens" data-async-save-new-url="/admin/screens/new">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-name" class="form-label">Name</label>
|
||||
<input id="screen-name" name="name" class="form-control" value="{{screen.name}}" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="screen-slug" class="form-label">Slug</label>
|
||||
<input id="screen-slug" name="slug" class="form-control" value="{{screen.slug}}" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="screen-playlist" class="form-label">Playlist</label>
|
||||
<select id="screen-playlist" name="playlist_id" class="form-select">
|
||||
<option value="">-- not assigned --</option>
|
||||
{{#each playlists}}
|
||||
<option value="{{id}}" {{#if (eq ../screen.playlist_id id)}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group screen-actions-group" role="group" aria-label="Screen actions">
|
||||
{{{saveActionButtons formId="screen-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/admin/screens" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger" form="screen-delete-form">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Player URL</h3>
|
||||
<p><a href="{{playerUrl screen.slug}}" target="_blank">{{playerUrl screen.slug}}</a></p>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-secondary admin-form-card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Player URL</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0"><a href="{{playerUrl screen.slug}}" target="_blank">{{playerUrl screen.slug}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,12 +3,17 @@
|
||||
<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">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Existing screens</h3>
|
||||
<div class="card-tools">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/screens/new">Add screen</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<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}}
|
||||
@@ -26,9 +31,9 @@
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="actions">
|
||||
<a class="button-link list-action" href="/admin/screens/{{id}}/edit">Edit</a>
|
||||
<a class="btn btn-sm btn-primary" 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>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
@@ -39,4 +44,5 @@
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,46 +1,75 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>{{#if isEdit}}Edit slide{{else}}Create slide{{/if}}</h2>
|
||||
<h2>{{#if isEdit}}Edit slide - {{slide.title}}{{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 class="slide-editor-shell">
|
||||
<form method="post" action="{{action}}" enctype="multipart/form-data" id="slide-form" class="slide-form-stack" data-async-save-close-url="/admin/slides" data-async-save-new-url="/admin/slides/new">
|
||||
<div class="card card-outline card-primary admin-form-card slide-template-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Slide details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<label for="slide-title" class="form-label">Title</label>
|
||||
<input id="slide-title" name="title" class="form-control" value="{{slide.title}}" required />
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="template-select" class="form-label">Template</label>
|
||||
<select name="template_id" id="template-select" class="form-select">
|
||||
<option value="">-- none --</option>
|
||||
{{#each templates}}
|
||||
<option value="{{id}}" {{#if (eq ../slide.template_id id)}}selected{{/if}}>{{name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</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 class="slide-editor-sidebar">
|
||||
<div class="card card-outline card-primary admin-form-card mb-3">
|
||||
<div class="card-body d-grid gap-2">
|
||||
<div class="btn-group template-actions-group slide-actions-group" role="group" aria-label="Slide actions">
|
||||
{{{saveActionButtons formId="slide-form" saveLabel=saveLabel saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/admin/slides" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
{{#if isEdit}}
|
||||
<button type="submit" class="btn btn-danger" form="delete-slide-form">Delete</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="card card-outline card-secondary admin-form-card slide-preview-card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between gap-2">
|
||||
<div class="slide-preview-header">
|
||||
<h3>Preview</h3>
|
||||
</div>
|
||||
<span class="chip slide-preview-dimensions-chip" id="slide-preview-meta">--</span>
|
||||
</div>
|
||||
<div class="card-body slide-preview-scroll">
|
||||
<div class="slide-preview-stage" id="slide-preview-stage">
|
||||
<div class="slide-preview-canvas" id="slide-preview-canvas">
|
||||
<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>
|
||||
<button type="button" class="btn btn-outline-secondary w-100 mt-3" id="open-preview-popup">Open popup preview</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link rel="stylesheet" href="/assets/js/ckeditor5/ckeditor5.css" />
|
||||
{{#if isEdit}}
|
||||
<form id="delete-slide-form" method="post" action="/admin/slides/{{slide.id}}/delete" data-confirm-message="Delete this slide?"></form>
|
||||
{{/if}}
|
||||
|
||||
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
|
||||
<script type="module" src="/assets/js/slide-form.js"></script>
|
||||
<script type="module" src="/assets/js/slides/slide-form.js"></script>
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
<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">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Existing slides</h3>
|
||||
<div class="card-tools">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/slides/new">Create slide</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Title</th><th>Template</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if slides.length}}
|
||||
@@ -18,9 +23,9 @@
|
||||
<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>
|
||||
<a class="btn btn-sm btn-primary" 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>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
@@ -31,4 +36,5 @@
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+127
-45
@@ -3,36 +3,9 @@
|
||||
<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}}" />
|
||||
@@ -41,32 +14,141 @@
|
||||
<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 class="card card-outline card-secondary admin-form-card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Designer</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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 class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-body d-grid gap-2">
|
||||
<div class="btn-group template-actions-group" role="group" aria-label="Template actions">
|
||||
{{{saveActionButtons formId="template-form" saveLabel="Save" saveButtonClass="btn btn-success template-save-button" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin/templates" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-outline card-secondary admin-form-card region-info-card mb-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Region information</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-outline-secondary w-100" id="add-region-button" data-bs-toggle="modal" data-bs-target="#region-add-modal">Add region</button>
|
||||
<div class="region-selector-block mt-4">
|
||||
<label for="region-select" class="form-label">Select region</label>
|
||||
<select id="region-select" class="form-select region-select"></select>
|
||||
</div>
|
||||
<div id="region-list" class="region-list mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4 align-items-stretch">
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 template-details-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Template details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 template-meta-row">
|
||||
<div class="col-12">
|
||||
<label for="template-name" class="form-label">Name</label>
|
||||
<input name="name" id="template-name" class="form-control" value="{{template.name}}" required />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="canvas-size-select" class="form-label">Canvas size</label>
|
||||
<select id="canvas-size-select" class="form-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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 template-options-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Options</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-image" class="form-label">Background image</label>
|
||||
<input type="file" name="background_image" id="background-image" class="form-control" accept="image/*" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-color" class="form-label">Background colour</label>
|
||||
<input type="color" name="background_color" id="background-color" class="form-control form-control-color w-100" value="{{#if template.background_color}}{{template.background_color}}{{else}}#111111{{/if}}" title="Choose a background colour" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<div class="modal fade" id="region-add-modal" tabindex="-1" aria-labelledby="region-add-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-body-secondary mb-3">Choose the type of region you want to add to this template.</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="text">Text region</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="html">HTML region</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="image">Image region</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="webpage">Webpage region</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="region-card-template">
|
||||
<div class="card card-outline card-secondary admin-form-card region-item mb-3">
|
||||
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
|
||||
<div class="card-body p-3 d-grid gap-3">
|
||||
<div class="region-field-grid region-field-grid--identity">
|
||||
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
|
||||
</div>
|
||||
<input type="hidden" name="font_family[]" value="Arial" />
|
||||
<input type="hidden" name="region_type[]" value="text" />
|
||||
<input type="hidden" name="region_key[]" value="" />
|
||||
<input type="hidden" name="region_label[]" value="" />
|
||||
<div class="region-field-grid region-field-grid--position">
|
||||
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
|
||||
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
|
||||
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label>
|
||||
</div>
|
||||
<div class="region-field-grid region-field-grid--size">
|
||||
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
|
||||
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end"><button type="button" class="btn btn-sm btn-outline-danger remove-region">Remove</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
|
||||
@@ -1,42 +1,11 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit template</h2>
|
||||
<h2>Edit template - {{template.name}}</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>
|
||||
|
||||
<form method="post" action="/admin/templates/{{template.id}}" enctype="multipart/form-data" id="template-form" data-async-save data-async-save-close-url="/admin/templates" data-async-save-new-url="/admin/templates/new">
|
||||
<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}}" />
|
||||
@@ -45,32 +14,148 @@
|
||||
<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 class="card card-outline card-secondary admin-form-card mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Designer</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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 class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-body d-grid gap-2">
|
||||
<div class="btn-group template-actions-group" role="group" aria-label="Template actions">
|
||||
{{{saveActionButtons formId="template-form" saveLabel="Save" saveButtonClass="btn btn-success template-save-button" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
|
||||
<a class="btn btn-warning" href="/admin/templates" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger" form="delete-template-form">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-outline card-secondary admin-form-card region-info-card mb-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">Region information</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-outline-secondary w-100" id="add-region-button" data-bs-toggle="modal" data-bs-target="#region-add-modal">Add region</button>
|
||||
<div class="region-selector-block mt-4">
|
||||
<label for="region-select" class="form-label">Select region</label>
|
||||
<select id="region-select" class="form-select region-select"></select>
|
||||
</div>
|
||||
<div id="region-list" class="region-list mt-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4 align-items-stretch">
|
||||
<div class="col-12 col-xl-4">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 template-details-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Template details</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 template-meta-row">
|
||||
<div class="col-12">
|
||||
<label for="template-name" class="form-label">Name</label>
|
||||
<input name="name" id="template-name" class="form-control" value="{{template.name}}" required />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="canvas-size-select" class="form-label">Canvas size</label>
|
||||
<select id="canvas-size-select" class="form-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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-8">
|
||||
<div class="card card-outline card-primary admin-form-card h-100 template-options-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Options</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-image" class="form-label">Background image</label>
|
||||
<div class="template-background-row d-flex gap-2">
|
||||
<input type="file" name="background_image" id="background-image" class="form-control" accept="image/*" />
|
||||
<button type="button" class="btn btn-outline-secondary" id="remove-background-image">Remove</button>
|
||||
</div>
|
||||
<input type="checkbox" name="remove_background_image" id="remove-background-image-flag" value="1" hidden />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="background-color" class="form-label">Background colour</label>
|
||||
<input type="color" name="background_color" id="background-color" class="form-control form-control-color w-100" value="{{#if template.background_color}}{{template.background_color}}{{else}}#111111{{/if}}" title="Choose a background colour" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<form id="delete-template-form" method="post" action="/admin/templates/{{template.id}}/delete" data-confirm-message="Delete this template?"></form>
|
||||
|
||||
<div class="modal fade" id="region-add-modal" tabindex="-1" aria-labelledby="region-add-modal-label" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-body-secondary mb-3">Choose the type of region you want to add to this template.</p>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="text">Text</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="html">HTML</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="image">Image</button>
|
||||
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="webpage">Webpage</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="region-card-template">
|
||||
<div class="card card-outline card-secondary admin-form-card region-item mb-3">
|
||||
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
|
||||
<div class="card-body p-3 d-grid gap-3">
|
||||
<div class="region-field-grid region-field-grid--identity">
|
||||
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
|
||||
</div>
|
||||
<input type="hidden" name="font_family[]" value="Arial" />
|
||||
<input type="hidden" name="region_type[]" value="text" />
|
||||
<input type="hidden" name="region_key[]" value="" />
|
||||
<input type="hidden" name="region_label[]" value="" />
|
||||
<div class="region-field-grid region-field-grid--position">
|
||||
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
|
||||
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
|
||||
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label>
|
||||
</div>
|
||||
<div class="region-field-grid region-field-grid--size">
|
||||
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
|
||||
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end"><button type="button" class="btn btn-sm btn-outline-danger remove-region">Remove</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
<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">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Existing templates</h3>
|
||||
<div class="card-tools">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/templates/new">Create template</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0">
|
||||
<thead><tr><th>Name</th><th>Canvas</th><th>Regions</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{{#if templates.length}}
|
||||
@@ -19,9 +24,9 @@
|
||||
<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>
|
||||
<a class="btn btn-sm btn-primary" 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>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
@@ -32,4 +37,5 @@
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+28
-31
@@ -5,39 +5,36 @@
|
||||
</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 class="card card-outline card-primary admin-form-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">User details</h3>
|
||||
</div>
|
||||
<form id="user-form" method="post" action="/admin/users">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-username" class="form-label">Username</label>
|
||||
<input id="user-username" type="text" name="username" class="form-control" autocomplete="username" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-name" class="form-label">Name</label>
|
||||
<input id="user-name" type="text" name="name" class="form-control" autocomplete="name" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-password" class="form-label">Password</label>
|
||||
<input id="user-password" type="password" name="password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label for="user-confirm-password" class="form-label">Confirm password</label>
|
||||
<input id="user-confirm-password" type="password" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
</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>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="User actions">
|
||||
{{{saveActionButtons formId="user-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin/users" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
</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>
|
||||
@@ -1,49 +1,67 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>Edit user</h2>
|
||||
<h2>Edit user - {{user.name}}</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 class="card card-outline card-primary admin-form-card">
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card card-outline card-secondary admin-form-card mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Profile</h3>
|
||||
</div>
|
||||
<form id="user-profile-form" method="post" action="/admin/users/{{user.id}}/username" data-async-save data-async-save-close-url="/admin/users">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="edit-user-username" class="form-label">Username</label>
|
||||
<input id="edit-user-username" type="text" name="username" value="{{user.username}}" class="form-control" autocomplete="username" required disabled />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="edit-user-name" class="form-label">Name</label>
|
||||
<input id="edit-user-name" type="text" name="name" value="{{user.name}}" class="form-control" autocomplete="name" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Profile actions">
|
||||
{{{saveActionButtons formId="user-profile-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
<a class="btn btn-warning" href="/admin/users" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
|
||||
<button type="submit" class="btn btn-danger" form="delete-user-form">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card card-outline card-warning admin-form-card h-100 mb-0">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Reset password</h3>
|
||||
</div>
|
||||
<form id="user-password-form" method="post" action="/admin/users/{{user.id}}/password" data-async-save data-async-save-close-url="/admin/users">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="edit-user-password" class="form-label">New password</label>
|
||||
<input id="edit-user-password" type="password" name="password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label for="edit-user-confirm-password" class="form-label">Confirm password</label>
|
||||
<input id="edit-user-confirm-password" type="password" name="confirm_password" class="form-control" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-end">
|
||||
<div class="btn-group" role="group" aria-label="Password actions">
|
||||
{{{saveActionButtons formId="user-password-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndCloseValue="close" showSaveAndNew=false}}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="button-link secondary" href="/admin/users">Back to users</a>
|
||||
</div>
|
||||
</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>
|
||||
<form id="delete-user-form" method="post" action="/admin/users/{{user.id}}/delete" data-confirm-message="Delete {{user.username}}?"></form>
|
||||
@@ -3,14 +3,17 @@
|
||||
<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">
|
||||
<div class="card card-outline card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Existing users</h3>
|
||||
<div class="card-tools">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/users/new">Add user</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-striped w-100 mb-0 users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
@@ -24,7 +27,6 @@
|
||||
<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}}
|
||||
@@ -43,9 +45,9 @@
|
||||
<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>
|
||||
<a class="btn btn-sm btn-primary" 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>
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<span class="empty">Protected</span>
|
||||
@@ -59,4 +61,5 @@
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user