diff --git a/app/routes/company.py b/app/routes/company.py index 673673e..774871d 100644 --- a/app/routes/company.py +++ b/app/routes/company.py @@ -496,9 +496,10 @@ def assign_playlist(display_id: int): @bp.post("/displays/") @login_required def update_display(display_id: int): - """Update display metadata (description + assigned playlist). + """Update display metadata. - Company users should be able to set a short description per display and assign a playlist. + Supports both form POST (full update) and JSON/AJAX (partial update). + Company users can set a short description per display and assign a playlist. """ company_user_required() @@ -507,26 +508,77 @@ def update_display(display_id: int): if not display or display.company_id != current_user.company_id: abort(404) + wants_json = ( + (request.headers.get("X-Requested-With") == "XMLHttpRequest") + or ("application/json" in (request.headers.get("Accept") or "")) + or request.is_json + ) + + def _json_error(message: str, status: int = 400): + return jsonify({"ok": False, "error": message}), status + + # Inputs from either form or JSON + payload = request.get_json(silent=True) if request.is_json else None + # Description (short, optional) - desc = (request.form.get("description") or "").strip() or None - if desc is not None: - desc = desc[:200] - display.description = desc + if request.is_json: + if payload is None: + return _json_error("Invalid JSON") + if "description" in payload: + desc = (payload.get("description") or "").strip() or None + if desc is not None: + desc = desc[:200] + display.description = desc + else: + # form POST implies full update + desc = (request.form.get("description") or "").strip() or None + if desc is not None: + desc = desc[:200] + display.description = desc # Playlist assignment - playlist_id = (request.form.get("playlist_id") or "").strip() - if not playlist_id: - display.assigned_playlist_id = None + if request.is_json: + if "playlist_id" in payload: + playlist_id_val = payload.get("playlist_id") + if playlist_id_val in (None, ""): + display.assigned_playlist_id = None + else: + try: + playlist_id_int = int(playlist_id_val) + except (TypeError, ValueError): + return _json_error("Invalid playlist_id") + playlist = db.session.get(Playlist, playlist_id_int) + if not playlist or playlist.company_id != current_user.company_id: + return _json_error("Invalid playlist") + display.assigned_playlist_id = playlist.id else: - try: - playlist_id_int = int(playlist_id) - except ValueError: - abort(400) - playlist = db.session.get(Playlist, playlist_id_int) - if not playlist or playlist.company_id != current_user.company_id: - abort(400) - display.assigned_playlist_id = playlist.id + playlist_id = (request.form.get("playlist_id") or "").strip() + if not playlist_id: + display.assigned_playlist_id = None + else: + try: + playlist_id_int = int(playlist_id) + except ValueError: + abort(400) + playlist = db.session.get(Playlist, playlist_id_int) + if not playlist or playlist.company_id != current_user.company_id: + abort(400) + display.assigned_playlist_id = playlist.id db.session.commit() + + if wants_json: + return jsonify( + { + "ok": True, + "display": { + "id": display.id, + "name": display.name, + "description": display.description, + "assigned_playlist_id": display.assigned_playlist_id, + }, + } + ) + flash("Display updated", "success") return redirect(url_for("company.dashboard")) diff --git a/app/static/styles.css b/app/static/styles.css new file mode 100644 index 0000000..8514db1 --- /dev/null +++ b/app/static/styles.css @@ -0,0 +1,286 @@ +:root { + --brand: #f2d20b; /* warm yellow */ + --brand-hover: #f6dd3d; + --ink: #111111; + --muted: #6b7280; + --border: #e5e7eb; + --surface: #ffffff; + --surface-alt: #f7f7f7; + --shadow: 0 10px 30px rgba(17, 17, 17, 0.08); + --radius: 14px; +} + +html, body { + height: 100%; +} + +body { + background: var(--surface); + color: var(--ink); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Layout */ +.app-navbar { + background: var(--surface) !important; + border-bottom: 1px solid var(--border) !important; +} + +.app-main { + padding-top: 6.25rem; /* fixed navbar offset */ + padding-bottom: 4rem; +} + +@media (min-width: 992px) { + .app-main { + padding-top: 6.75rem; + } +} + +.container { + max-width: 1120px; +} + +/* Branding */ +.brand-mark { + width: 34px; + height: 34px; + border-radius: 999px; + background: var(--brand); + color: var(--ink); + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 800; + letter-spacing: -0.02em; +} + +.navbar-brand { + font-weight: 700; +} + +.navbar .nav-link { + color: var(--ink) !important; + font-weight: 500; +} + +.navbar .nav-link:hover { + text-decoration: underline; + text-underline-offset: 4px; +} + +/* Typography */ +h1, h2, h3, .display-1, .display-2, .display-3 { + font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; + letter-spacing: -0.02em; +} + +.page-title { + font-size: clamp(2rem, 4vw, 3rem); + margin-bottom: 1.25rem; +} + +/* Cards / surfaces */ +.card { + border-radius: var(--radius); + border-color: var(--border); + box-shadow: 0 1px 0 rgba(0,0,0,0.02); +} + +.card.card-elevated { + box-shadow: var(--shadow); +} + +.card-header { + background: var(--surface); + border-bottom: 1px solid var(--border); +} + +/* Tables */ +.table { + border-color: var(--border); +} + +.table thead th { + background: var(--surface-alt); + color: var(--muted); + font-weight: 600; + border-bottom: 1px solid var(--border); +} + +.table td, .table th { + vertical-align: middle; +} + +/* Prevent tables from breaking layouts on long tokens/URLs */ +.table td, +.table th { + overflow-wrap: anywhere; + word-break: break-word; +} + +.table-responsive { + border-radius: var(--radius); +} + +/* Forms */ +.form-control, +.form-select { + border-radius: 12px; + border-color: var(--border); + padding: .75rem .9rem; +} + +.form-control:focus, +.form-select:focus { + border-color: rgba(242, 210, 11, 0.9); + box-shadow: 0 0 0 .25rem rgba(242, 210, 11, 0.18); +} + +/* Buttons */ +.btn { + border-radius: 12px; + font-weight: 600; +} + +.btn-brand { + background: var(--brand); + border-color: var(--brand); + color: var(--ink); +} + +.btn-brand:hover, +.btn-brand:focus { + background: var(--brand-hover); + border-color: var(--brand-hover); + color: var(--ink); +} + +.btn-ink { + background: var(--ink); + border-color: var(--ink); + color: #fff; +} + +.btn-ink:hover, +.btn-ink:focus { + background: #000; + border-color: #000; + color: #fff; +} + +.btn-outline-ink { + border-color: var(--ink); + color: var(--ink); +} + +.btn-outline-ink:hover, +.btn-outline-ink:focus { + background: var(--ink); + color: #fff; +} + +/* Bootstrap outline-primary -> match brand */ +.btn-outline-primary { + border-color: var(--ink); + color: var(--ink); +} + +.btn-check:checked + .btn-outline-primary, +.btn-outline-primary:hover, +.btn-outline-primary:focus { + background: var(--brand); + border-color: var(--brand); + color: var(--ink); +} + +.btn-outline-secondary { + border-color: var(--border); + color: var(--ink); +} + +.btn-outline-secondary:hover, +.btn-outline-secondary:focus { + background: var(--surface-alt); + border-color: var(--border); + color: var(--ink); +} + +/* Alerts */ +.alert { + border-radius: var(--radius); +} + +/* Auth pages */ +.auth-shell { + max-width: 520px; + margin: 0 auto; +} + +.auth-card { + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +/* Playlist gallery cards (used in company/playlist_detail.html) */ +.playlist-card { + border-radius: var(--radius) !important; + border-color: var(--border) !important; +} + +.playlist-card .card-top { + border-bottom-color: var(--border) !important; +} + +.playlist-card .drag-handle { + color: var(--muted) !important; +} + +/* Nice monospace blocks */ +.monospace { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +/* Company display gallery */ +.display-gallery-card { + border-radius: var(--radius); + border-color: var(--border); +} + +.display-gallery-grid { + display: grid; + grid-template-columns: 1.4fr 0.8fr 1.1fr 0.9fr; + gap: 12px; + align-items: center; +} + +.display-preview { + width: 100%; + aspect-ratio: 16 / 9; + border-top-left-radius: var(--radius); + border-top-right-radius: var(--radius); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + overflow: hidden; + border-bottom: 1px solid var(--border); + background: #000; +} + +.display-preview iframe { + width: 100%; + height: 100%; + border: 0; + background: #000; +} + +@media (max-width: 768px) { + .display-gallery-grid { + grid-template-columns: 1fr; + gap: 10px; + } +} + +.toast { + border-radius: var(--radius); +} diff --git a/app/templates/admin/company_detail.html b/app/templates/admin/company_detail.html index 74e8538..3b8dd54 100644 --- a/app/templates/admin/company_detail.html +++ b/app/templates/admin/company_detail.html @@ -1,13 +1,112 @@ {% extends "base.html" %} {% block content %}
-

Company: {{ company.name }}

+

Company: {{ company.name }}

- Back + Back
-
+
+
+
+

Displays

+
+ + +
+
+
+ + + + + + + + + + + {% for d in company.displays %} + + + + + + + {% else %} + + + + {% endfor %} + +
NameTokenAssignedPlayer
+
+ + +
+
{{ d.token }}{{ d.assigned_playlist.name if d.assigned_playlist else "(none)" }} + Open +
No displays.
+
+
+
+ +
+
+
+
+

Add user

+
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+
+

Users

+
+
+
+ {% for u in company.users %} +
+
+ {{ u.email or "(no email)" }} +
+
+ +
+
+ {% else %} +
No users.
+ {% endfor %} +
+
+
+
+
+ +

Danger zone

@@ -22,80 +121,4 @@

- -
-
-

Users

-
-
- - -
-
- - -
- -
- -
- {% for u in company.users %} -
-
- {{ u.email or "(no email)" }} -
{{ u.email or "(no email set)" }}
-
-
-
- - -
-
- -
-
-
- {% else %} -
No users.
- {% endfor %} -
-
- -
-

Displays

-
-
- - -
-
- -
- {% for d in company.displays %} -
-
-
-
- - -
-
Token: {{ d.token }}
- -
-
Assigned: {{ d.assigned_playlist.name if d.assigned_playlist else "(none)" }}
-
-
- {% else %} -
No displays.
- {% endfor %} -
-
-
{% endblock %} diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html index 8137d2d..aede741 100644 --- a/app/templates/admin/dashboard.html +++ b/app/templates/admin/dashboard.html @@ -1,28 +1,49 @@ {% extends "base.html" %} {% block content %}
-

Admin dashboard

+

Admin

-

Companies

-
-
- - +
+
+

Companies

+ + + + +
+
+ + + + + + + + + + + + {% for c in companies %} + + + + + + + + {% else %} + + + + {% endfor %} + +
NameUsersDisplaysPlaylistsAction
{{ c.name }}{{ c.users|length }}{{ c.displays|length }}{{ c.playlists|length }} + Open +
No companies yet.
- - -
diff --git a/app/templates/auth_change_password.html b/app/templates/auth_change_password.html index a7b8334..e2070f5 100644 --- a/app/templates/auth_change_password.html +++ b/app/templates/auth_change_password.html @@ -3,7 +3,7 @@ {% block content %}
-
+

Change password

@@ -26,8 +26,8 @@
- - Cancel + + Cancel
diff --git a/app/templates/auth_forgot_password.html b/app/templates/auth_forgot_password.html index 02089ee..52fa9bb 100644 --- a/app/templates/auth_forgot_password.html +++ b/app/templates/auth_forgot_password.html @@ -1,20 +1,18 @@ {% extends "base.html" %} {% block content %} -
-
-

Forgot password

-
+
+

Forgot password

+

Enter your email address and we’ll send you a password reset link.

- - Back to login + + Back to login
- -
+
{% endblock %} diff --git a/app/templates/auth_login.html b/app/templates/auth_login.html index d631679..cde937f 100644 --- a/app/templates/auth_login.html +++ b/app/templates/auth_login.html @@ -1,9 +1,8 @@ {% extends "base.html" %} {% block content %} -
-
-

Login

-
+
+

Login

+
@@ -12,11 +11,10 @@
- - +
{% endblock %} diff --git a/app/templates/auth_reset_password.html b/app/templates/auth_reset_password.html index e4dc043..906bdf4 100644 --- a/app/templates/auth_reset_password.html +++ b/app/templates/auth_reset_password.html @@ -1,15 +1,14 @@ {% extends "base.html" %} {% block content %} -
-
-

Reset password

+
+

Reset password

{% if token_error %}
{{ token_error }}
- Request a new reset link + Request a new reset link {% else %} -
+
@@ -19,9 +18,8 @@
- +
{% endif %} -
{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 0e7e40c..a62fa9f 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -5,40 +5,46 @@ {{ title or "Signage" }} - + -
-
+
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
@@ -50,5 +56,8 @@ {% endwith %} {% block content %}{% endblock %}
+ + + {% block page_scripts %}{% endblock %} diff --git a/app/templates/company/dashboard.html b/app/templates/company/dashboard.html index 84ecc34..edea3fa 100644 --- a/app/templates/company/dashboard.html +++ b/app/templates/company/dashboard.html @@ -1,74 +1,254 @@ {% extends "base.html" %} {% block content %} -

Company dashboard

+

Welcome{% if current_user and current_user.email %}, {{ current_user.email }}{% endif %}!

-
-

Playlists

-
-
- - +
+
+
+

Playlists

+ + + + +
+
+ + + + + + + + + + {% for p in playlists %} + + + + + + {% else %} + + + + {% endfor %} + +
NameItemsActions
{{ p.name }}{{ p.items|length }} +
+ Open +
+ +
+
+
No playlists yet.
- - -
- {% for p in playlists %} - - {% else %} -
No playlists yet.
- {% endfor %}
-
-

Displays

-
- {% for d in displays %} -
-
-
-
{{ d.name }}
- {% if d.description %} -
{{ d.description }}
- {% endif %} -
-
-
- - -
- - +
+
+
+

Displays

+
+
+
+ {% for d in displays %} +
+
-
+ {% else %} +
+
No displays. Ask admin to add displays.
+
+ {% endfor %}
- {% else %} -
No displays. Ask admin to add displays.
- {% endfor %} +
+
+
+
+ + +
+ +
+ + + {% endblock %} + +{% block page_scripts %} + +{% endblock %} diff --git a/app/templates/company/playlist_detail.html b/app/templates/company/playlist_detail.html index e64c4f8..3404a3c 100644 --- a/app/templates/company/playlist_detail.html +++ b/app/templates/company/playlist_detail.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block content %} {# Cropper.js (used for image cropping) #} - +