Initial fork from theme-song-booth
This commit is contained in:
commit
99855b1212
33 changed files with 5723 additions and 0 deletions
183
templates/admin/dashboard.html
Normal file
183
templates/admin/dashboard.html
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Dashboard</title>
|
||||
{% if refresh_seconds and refresh_seconds > 0 %}
|
||||
<meta http-equiv="refresh" content="{{ refresh_seconds }}">
|
||||
{% endif %}
|
||||
<style>
|
||||
/*
|
||||
Operator dashboard queue.
|
||||
Shows all requests in a table with status filters, per-row Open/Delete
|
||||
actions, a Settings link, auto-refresh hint, and logout.
|
||||
*/
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
}
|
||||
.container{max-width:1100px;margin:0 auto;}
|
||||
h1{color:#60a5fa;margin-top:0;}
|
||||
.filters{margin-bottom:1rem;display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;}
|
||||
.filters a{
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
padding:.35rem .7rem;
|
||||
border-radius:.4rem;
|
||||
border:1px solid transparent;
|
||||
}
|
||||
.filters a:hover{background:#1f2937;}
|
||||
.filters a.active{
|
||||
font-weight:bold;
|
||||
color:#fff;
|
||||
background:#2563eb;
|
||||
border-color:#2563eb;
|
||||
}
|
||||
table{
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
background:#1f2937;
|
||||
border-radius:.5rem;
|
||||
overflow:hidden;
|
||||
}
|
||||
th,td{padding:.7rem;text-align:left;border-bottom:1px solid #374151;}
|
||||
th{background:#111827;color:#9ca3af;}
|
||||
tr:hover{background:#2d3748;}
|
||||
.status-badge{
|
||||
display:inline-block;
|
||||
padding:.25rem .6rem;
|
||||
border-radius:9999px;
|
||||
font-size:.8rem;
|
||||
font-weight:600;
|
||||
background:#374151;
|
||||
}
|
||||
.awaiting_payment{background:#f59e0b;color:#000;}
|
||||
.paid,.delivered{background:#10b981;color:#000;}
|
||||
.pending,.prompt_ready{background:#60a5fa;color:#000;}
|
||||
.songs_uploaded{background:#a78bfa;color:#000;}
|
||||
.revisions_requested{background:#f87171;color:#000;}
|
||||
.actions a{
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
margin-right:.8rem;
|
||||
}
|
||||
.actions button.delete{
|
||||
background:transparent;
|
||||
color:#f87171;
|
||||
border:none;
|
||||
padding:0;
|
||||
cursor:pointer;
|
||||
font:inherit;
|
||||
text-decoration:none;
|
||||
margin-right:.8rem;
|
||||
}
|
||||
.logout{float:right;color:#f87171;text-decoration:none;}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
|
||||
/* Topbar with Settings and Log out link */
|
||||
.topbar{
|
||||
float:right;
|
||||
display:flex;
|
||||
gap:.75rem;
|
||||
align-items:center;
|
||||
}
|
||||
.topbar form{display:inline;}
|
||||
.topbar a{
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
}
|
||||
.topbar a.settings{
|
||||
color:#10b981;
|
||||
}
|
||||
|
||||
.refresh-hint{
|
||||
color:#6b7280;
|
||||
font-size:.85rem;
|
||||
margin-left:auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Topbar: Kiosk, Pricing, Sales, Settings, Log out -->
|
||||
<div class="topbar">
|
||||
<a href="{{ url_for('kiosk') }}" target="_blank" rel="noopener noreferrer">Kiosk</a>
|
||||
<a href="{{ url_for('admin_pricing') }}">Pricing</a>
|
||||
<a href="{{ url_for('admin_sales') }}">Sales</a>
|
||||
<a href="{{ url_for('admin_settings') }}" class="settings">Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" class="logout">Log out</a>
|
||||
</div>
|
||||
|
||||
<h1>Theme Song Booth — Admin Dashboard</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Status filter links -->
|
||||
<div class="filters">
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="{% if current_status in (None, '') %}active{% endif %}">All</a>
|
||||
<a href="{{ url_for('admin_dashboard', status='pending') }}" class="{% if current_status == 'pending' %}active{% endif %}">Pending</a>
|
||||
<a href="{{ url_for('admin_dashboard', status='prompt_ready') }}" class="{% if current_status == 'prompt_ready' %}active{% endif %}">Needs Upload</a>
|
||||
<a href="{{ url_for('admin_dashboard', status='awaiting_payment') }}" class="{% if current_status == 'awaiting_payment' %}active{% endif %}">Awaiting Payment</a>
|
||||
<a href="{{ url_for('admin_dashboard', status='delivered') }}" class="{% if current_status == 'delivered' %}active{% endif %}">Delivered</a>
|
||||
<span class="refresh-hint">
|
||||
{% if refresh_seconds and refresh_seconds > 0 %}
|
||||
Auto-refresh every {{ refresh_seconds }}s
|
||||
{% else %}
|
||||
Auto-refresh off
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Requests table -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Genre</th>
|
||||
<th>Status</th>
|
||||
<th>Approved</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in requests %}
|
||||
<tr>
|
||||
<td>#{{ r.id }}</td>
|
||||
<td>{{ r.name }}</td>
|
||||
<td>{{ r.email }}</td>
|
||||
<td>{{ r.style_genre or '-' }}</td>
|
||||
<td><span class="status-badge {{ r.status }}">{{ statuses[r.status] }}</span></td>
|
||||
<td>{% if r.customer_approved and r.customer_approved != 'none' %}{{ (r.customer_approved or 'none').upper() }}{% else %}-{% endif %}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ url_for('admin_request', rid=r.id) }}">Open</a>
|
||||
<form method="POST" action="{{ url_for('admin_delete_request', rid=r.id) }}" style="display:inline" onsubmit="return confirm('ARE YOU SURE you want to delete request #{{ r.id }} for {{ r.name }}? This cannot be undone.')">
|
||||
<button type="submit" class="delete">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not requests %}
|
||||
<tr><td colspan="7">No requests found.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
70
templates/admin/login.html
Normal file
70
templates/admin/login.html
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login</title>
|
||||
<style>
|
||||
/*
|
||||
Minimal login page for the operator dashboard.
|
||||
Centered card with dark theme matching the rest of the app.
|
||||
*/
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
min-height:100vh;
|
||||
}
|
||||
form{
|
||||
background:#1f2937;
|
||||
padding:2rem;
|
||||
border-radius:1rem;
|
||||
width:100%;
|
||||
max-width:360px;
|
||||
}
|
||||
h1{margin-top:0;color:#60a5fa;}
|
||||
label{display:block;margin-top:1rem;font-weight:600;}
|
||||
input{
|
||||
width:100%;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
button{
|
||||
margin-top:1.5rem;
|
||||
width:100%;
|
||||
padding:.8rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:#3b82f6;
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
}
|
||||
.flash{margin-top:1rem;color:#f87171;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form method="POST">
|
||||
<h1>Booth Admin</h1>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required autofocus>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="flash">{{ messages[0] }}</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<button type="submit">Log In</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
108
templates/admin/pricing.html
Normal file
108
templates/admin/pricing.html
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pricing — Admin</title>
|
||||
<style>
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
}
|
||||
.container{max-width:800px;margin:0 auto;}
|
||||
h1{color:#60a5fa;margin-top:0;}
|
||||
h2{color:#93c5fd;font-size:1.1rem;margin-top:1.5rem;}
|
||||
a{color:#93c5fd;}
|
||||
.topbar{float:right;display:flex;gap:.75rem;align-items:center;}
|
||||
label{display:block;margin-top:.8rem;font-weight:600;font-size:.9rem;}
|
||||
input[type="text"]{
|
||||
width:100%;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
font:inherit;
|
||||
}
|
||||
.price-row{
|
||||
display:grid;
|
||||
grid-template-columns:1fr 1fr;
|
||||
gap:.75rem;
|
||||
}
|
||||
button{
|
||||
margin-top:1rem;
|
||||
padding:.7rem 1rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:#3b82f6;
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
}
|
||||
button.secondary{background:#4b5563;}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
.hint{font-size:.85rem;color:#9ca3af;margin-top:.25rem;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="topbar">
|
||||
<a href="{{ url_for('admin_dashboard') }}">Dashboard</a>
|
||||
<a href="{{ url_for('admin_sales') }}">Sales</a>
|
||||
<a href="{{ url_for('admin_settings') }}">Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" style="color:#f87171">Log out</a>
|
||||
</div>
|
||||
|
||||
<h1>Pricing</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST">
|
||||
<h2>Standard Items</h2>
|
||||
<p class="hint">Leave blank to hide a price. Enter numbers only or include currency symbols as you prefer.</p>
|
||||
|
||||
<label for="one_song">One Song</label>
|
||||
<input type="text" id="one_song" name="one_song" value="{{ fixed.one_song }}" placeholder="e.g. 25.00">
|
||||
|
||||
<label for="both_songs">Both Songs</label>
|
||||
<input type="text" id="both_songs" name="both_songs" value="{{ fixed.both_songs }}" placeholder="e.g. 40.00">
|
||||
|
||||
<label for="wav_per_song">WAV files / song</label>
|
||||
<input type="text" id="wav_per_song" name="wav_per_song" value="{{ fixed.wav_per_song }}" placeholder="e.g. 10.00">
|
||||
|
||||
<label for="stems_per_song">STEM files / song</label>
|
||||
<input type="text" id="stems_per_song" name="stems_per_song" value="{{ fixed.stems_per_song }}" placeholder="e.g. 25.00">
|
||||
|
||||
<h2>Custom Items</h2>
|
||||
{% for c in customs %}
|
||||
<div class="price-row">
|
||||
<div>
|
||||
<label for="custom_name_{{ loop.index }}">Item {{ loop.index }} name</label>
|
||||
<input type="text" id="custom_name_{{ loop.index }}" name="custom_name_{{ loop.index }}" value="{{ c.name }}" placeholder="e.g. Rush delivery">
|
||||
</div>
|
||||
<div>
|
||||
<label for="custom_price_{{ loop.index }}">Item {{ loop.index }} price</label>
|
||||
<input type="text" id="custom_price_{{ loop.index }}" name="custom_price_{{ loop.index }}" value="{{ c.price }}" placeholder="e.g. 15.00">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit">Save Pricing</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
550
templates/admin/request.html
Normal file
550
templates/admin/request.html
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request #{{ req.id }} — Admin</title>
|
||||
<style>
|
||||
/*
|
||||
Operator detail page for a single request.
|
||||
Two-column layout:
|
||||
left -> customer info + internal operator notes
|
||||
right -> Suno prompt, song upload, customer notification,
|
||||
payment reference, and file delivery.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
}
|
||||
.container{max-width:1200px;margin:0 auto;}
|
||||
h1,h2{color:#60a5fa;margin-top:0;}
|
||||
a{color:#93c5fd;}
|
||||
.section{
|
||||
background:#1f2937;
|
||||
padding:1rem;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
label{display:block;margin-top:.8rem;font-weight:600;}
|
||||
input,textarea{
|
||||
width:100%;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
box-sizing:border-box;
|
||||
font:inherit;
|
||||
}
|
||||
textarea{min-height:100px;}
|
||||
button{
|
||||
padding:.7rem 1rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:#3b82f6;
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
margin-top:.5rem;
|
||||
}
|
||||
button.secondary{background:#4b5563;}
|
||||
button.danger{background:#dc2626;}
|
||||
button.success{background:#10b981;}
|
||||
button:disabled{background:#374151;color:#9ca3af;cursor:not-allowed;}
|
||||
.actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem;}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
.status-badge{
|
||||
display:inline-block;
|
||||
padding:.25rem .6rem;
|
||||
border-radius:9999px;
|
||||
font-size:.8rem;
|
||||
font-weight:600;
|
||||
background:#374151;
|
||||
color:#f3f4f6;
|
||||
margin-right:.3rem;
|
||||
}
|
||||
.status-badge.ok{background:#10b981;color:#000;}
|
||||
.status-badge.missing{background:#ef4444;color:#000;}
|
||||
.status-badge.sent{background:#60a5fa;color:#000;}
|
||||
.copy-hint{font-size:.85rem;color:#9ca3af;margin-top:.3rem;}
|
||||
.revision-note{
|
||||
margin-top:1rem;
|
||||
background:#450a0a;
|
||||
border:1px solid #7f1d1d;
|
||||
border-radius:.5rem;
|
||||
padding:1rem;
|
||||
}
|
||||
.revision-note h3{
|
||||
margin-top:0;
|
||||
color:#f87171;
|
||||
}
|
||||
.file-select{
|
||||
background:#111827;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
margin:.3rem 0;
|
||||
}
|
||||
.file-select input{
|
||||
width:auto;
|
||||
margin-right:.5rem;
|
||||
}
|
||||
.file-select label{
|
||||
display:inline;
|
||||
margin:0;
|
||||
font-weight:400;
|
||||
}
|
||||
.song-row{
|
||||
display:flex;
|
||||
align-items:flex-start;
|
||||
gap:.75rem;
|
||||
margin:.6rem 0;
|
||||
padding:.4rem 0;
|
||||
}
|
||||
.song-row input[type="checkbox"]{
|
||||
width:auto;
|
||||
margin:.2rem 0 0 0;
|
||||
flex-shrink:0;
|
||||
}
|
||||
.song-row label{
|
||||
display:block;
|
||||
margin:0;
|
||||
font-weight:400;
|
||||
line-height:1.4;
|
||||
}
|
||||
.old-rev{
|
||||
font-size:.85rem;
|
||||
color:#9ca3af;
|
||||
margin-left:1.8rem;
|
||||
}
|
||||
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;}
|
||||
.status-row{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;}
|
||||
.history-list{
|
||||
margin:0;
|
||||
padding:0;
|
||||
list-style:none;
|
||||
}
|
||||
.history-list li{
|
||||
border-bottom:1px solid #374151;
|
||||
padding:.6rem 0;
|
||||
}
|
||||
.history-list li:last-child{border-bottom:none;}
|
||||
.history-list .meta{
|
||||
font-size:.8rem;
|
||||
color:#9ca3af;
|
||||
}
|
||||
.history-list .note{
|
||||
margin:.3rem 0 0 0;
|
||||
}
|
||||
|
||||
/* Two-column layout */
|
||||
.two-col{
|
||||
display:grid;
|
||||
grid-template-columns:1fr 1fr;
|
||||
gap:1rem;
|
||||
}
|
||||
@media(max-width:900px){
|
||||
.two-col{grid-template-columns:1fr;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<p><a href="{{ url_for('admin_dashboard') }}">← Dashboard</a></p>
|
||||
<h1>Request #{{ req.id }} — {{ req.name }}</h1>
|
||||
<div class="status-row">
|
||||
<p style="margin:0"><strong>Status:</strong> <span class="status-badge">{{ statuses[req.status] }}</span></p>
|
||||
{% if req.status != 'cancelled' %}
|
||||
<form method="POST" onsubmit="return confirm('Are you sure you want to cancel request #{{ req.id }}?')" style="margin:0">
|
||||
<input type="hidden" name="action" value="cancel_request">
|
||||
<button type="submit" class="danger" style="margin:0">Cancel Request</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="two-col">
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="left">
|
||||
<!-- Customer Info -->
|
||||
<div class="section">
|
||||
<h2>Customer Info</h2>
|
||||
<p class="copy-hint">Operators can correct customer details here. Click Save when done.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="update_customer_info">
|
||||
|
||||
<label for="customer_email">Email address</label>
|
||||
<input type="email" id="customer_email" name="email" value="{{ req.email }}" required>
|
||||
|
||||
<label for="customer_name">Name / persona</label>
|
||||
<input type="text" id="customer_name" name="name" value="{{ req.name }}" required>
|
||||
|
||||
<label for="customer_pronouns">Pronouns <span style="color:#f87171">*</span></label>
|
||||
<select id="customer_pronouns" name="pronouns" required>
|
||||
<option value="" {% if not req.pronouns %}selected{% endif %}>— Select pronouns —</option>
|
||||
<option value="He/Him/His" {% if req.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
|
||||
<option value="She/Her/Hers" {% if req.pronouns == 'She/Her/Hers' %}selected{% endif %}>She/Her/Hers</option>
|
||||
<option value="They/Them/Their" {% if req.pronouns == 'They/Them/Their' %}selected{% endif %}>They/Them/Their</option>
|
||||
</select>
|
||||
|
||||
<label for="customer_hobbies">Hobbies & interests</label>
|
||||
<textarea id="customer_hobbies" name="hobbies" rows="3">{{ req.hobbies or '' }}</textarea>
|
||||
|
||||
<label for="customer_notable_facts">Notable things</label>
|
||||
<textarea id="customer_notable_facts" name="notable_facts" rows="3">{{ req.notable_facts or '' }}</textarea>
|
||||
|
||||
<label for="customer_style_genre_decade">Decade / era <span style="color:#f87171">*</span></label>
|
||||
<select id="customer_style_genre_decade" name="decade" required>
|
||||
<option value="" {% if not style_parts.decade %}selected{% endif %}>— Select decade —</option>
|
||||
{% for d in decades %}
|
||||
<option value="{{ d }}" {% if style_parts.decade == d %}selected{% endif %}>{{ d }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="customer_style_genre_basic">Basic style <span style="color:#f87171">*</span></label>
|
||||
<select id="customer_style_genre_basic" name="basic_style" required>
|
||||
<option value="" {% if not style_parts.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if style_parts.basic_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="customer_style_genre_additional">Additional style (optional)</label>
|
||||
<select id="customer_style_genre_additional" name="additional_style">
|
||||
<option value="" {% if not style_parts.additional_style %}selected{% endif %}>— None —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if style_parts.additional_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Stored style: {{ req.style_genre or '—' }}</p>
|
||||
|
||||
<label for="customer_vocal_gender">Preferred singer voice / gender</label>
|
||||
<select id="customer_vocal_gender" name="vocal_gender">
|
||||
<option value="" {% if not req.vocal_gender %}selected{% endif %}>No preference</option>
|
||||
<option value="female" {% if req.vocal_gender == 'female' %}selected{% endif %}>Female</option>
|
||||
<option value="male" {% if req.vocal_gender == 'male' %}selected{% endif %}>Male</option>
|
||||
<option value="non-binary" {% if req.vocal_gender == 'non-binary' %}selected{% endif %}>Non-binary / Gender-neutral</option>
|
||||
<option value="androgynous" {% if req.vocal_gender == 'androgynous' %}selected{% endif %}>Androgynous</option>
|
||||
<option value="other" {% if req.vocal_gender == 'other' %}selected{% endif %}>Other (mention in Extra Requests)</option>
|
||||
</select>
|
||||
|
||||
<label for="customer_extra_requests">Anything else</label>
|
||||
<textarea id="customer_extra_requests" name="extra_requests" rows="3" maxlength="2000">{{ req.extra_requests or '' }}</textarea>
|
||||
|
||||
<label for="customer_stems_interest" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
|
||||
<input type="checkbox" id="customer_stems_interest" name="stems_interest" value="1" {% if req.stems_interest %}checked{% endif %} style="width:auto;margin:0">
|
||||
Customer is interested in STEMS / multitrack files
|
||||
</label>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="secondary">Save Customer Info</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p style="margin-top:1rem"><strong>Request #:</strong> {{ req.id }}</p>
|
||||
|
||||
{% if req.revision_note %}
|
||||
<div class="revision-note">
|
||||
<h3>📝 Revisions Requested{% if req.revision_count %}<span style="float:right">Revision #{{ req.revision_count }}</span>{% endif %}</h3>
|
||||
<p>{{ req.revision_note }}</p>
|
||||
<button type="button" onclick="copyRevisionPromptForHermes()">📋 Copy revision prompt for Hermes</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Revision History -->
|
||||
<div class="section">
|
||||
<h2>Revision History</h2>
|
||||
{% if revision_history %}
|
||||
<ul class="history-list">
|
||||
{% for h in revision_history %}
|
||||
<li>
|
||||
<div class="meta">Revision #{{ h.revision_count }} — {{ h.created_at }}
|
||||
{% if h.old_song_a_path or h.old_song_b_path %}
|
||||
<br>Archived:
|
||||
{% if h.old_song_a_path %}<br>A: {{ basename(h.old_song_a_path) }}{% endif %}
|
||||
{% if h.old_song_b_path %}<br>B: {{ basename(h.old_song_b_path) }}{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="note">{{ h.note or 'No note provided.' }}</p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="copy-hint">No revisions recorded yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Operator Notes -->
|
||||
<div class="section">
|
||||
<h2>Operator Notes</h2>
|
||||
<p class="copy-hint">Internal notes for the booth team. Customers cannot see this.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_operator_notes">
|
||||
<label for="operator_notes">Notes</label>
|
||||
<textarea id="operator_notes" name="operator_notes" rows="4" placeholder="e.g. paid cash, VIP friend, follow up...">{{ req.operator_notes or '' }}</textarea>
|
||||
<div class="actions">
|
||||
<button type="submit" class="secondary">Save Notes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Stems / Extras Link -->
|
||||
<div class="section">
|
||||
<h2>Stems / Extras Link</h2>
|
||||
<p class="copy-hint">Paste a self-hosted file share link (e.g. from Pingvin Share). It will appear on the customer player page once delivered.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_stems_link">
|
||||
<label for="stems_link">Share link</label>
|
||||
<input type="url" id="stems_link" name="stems_link" value="{{ req.stems_link or '' }}" placeholder="https://files.dionysismedia.ca/share/...">
|
||||
<div class="actions">
|
||||
<button type="submit" class="secondary">Save Link</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="right">
|
||||
<!-- Generate Suno Prompt -->
|
||||
<div class="section">
|
||||
<h2>1. Generate & Save Suno Prompt</h2>
|
||||
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
|
||||
<p class="copy-hint">Paste Hermes' Title, Style, and Lyrics directly into the fields below, then save.</p>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_prompt">
|
||||
<label for="suno_title">Title</label>
|
||||
<input type="text" id="suno_title" name="suno_title" value="{{ req.suno_title or '' }}">
|
||||
<label for="suno_style">Style</label>
|
||||
<textarea id="suno_style" name="suno_style">{{ req.suno_style or '' }}</textarea>
|
||||
<label for="suno_lyrics">Lyrics (with metatags)</label>
|
||||
<textarea id="suno_lyrics" name="suno_lyrics" rows="10">{{ req.suno_lyrics or '' }}</textarea>
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
|
||||
<button type="submit">Save Prompt</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Upload Songs -->
|
||||
<div class="section">
|
||||
<h2>2. Upload Songs</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="delete_songs">
|
||||
<div class="song-row">
|
||||
<input type="checkbox" id="delete_a" name="delete_song" value="a"
|
||||
{% if not (req.song_a_path and file_exists(req.song_a_path)) %}disabled{% endif %}>
|
||||
<label for="delete_a">Version A:
|
||||
{% if req.song_a_path and file_exists(req.song_a_path) %}
|
||||
<span class="status-badge ok">✅ Uploaded</span> {{ basename(req.song_a_path) }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not uploaded</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="song-row">
|
||||
<input type="checkbox" id="delete_b" name="delete_song" value="b"
|
||||
{% if not (req.song_b_path and file_exists(req.song_b_path)) %}disabled{% endif %}>
|
||||
<label for="delete_b">Version B:
|
||||
{% if req.song_b_path and file_exists(req.song_b_path) %}
|
||||
<span class="status-badge ok">✅ Uploaded</span> {{ basename(req.song_b_path) }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not uploaded</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" class="danger" onclick="return confirm('Delete selected song files? This will reset the request to Prompt Ready.')">Delete Selected Songs</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr style="border-color:#374151;margin:1rem 0">
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" value="upload_songs">
|
||||
<label for="song_a">Version A MP3</label>
|
||||
<input type="file" id="song_a" name="song_a" accept="audio/mpeg,.mp3">
|
||||
<label for="song_b">Version B MP3</label>
|
||||
<input type="file" id="song_b" name="song_b" accept="audio/mpeg,.mp3">
|
||||
<div class="actions">
|
||||
<button type="submit">Upload Songs</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Notify Customer -->
|
||||
<div class="section">
|
||||
<h2>3. Notify Customer</h2>
|
||||
<p>
|
||||
Preview email:
|
||||
{% if req.preview_sent_at %}
|
||||
<span class="status-badge sent">✅ Sent</span> {{ req.preview_sent_at }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not sent yet</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>Both songs must be uploaded first.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="notify_customer">
|
||||
<button type="submit" {% if not (req.song_a_path and req.song_b_path) %}disabled{% endif %}>Send Preview Link</button>
|
||||
</form>
|
||||
|
||||
{% if req.song_a_path and req.song_b_path %}
|
||||
<p style="margin-top:.8rem">
|
||||
Operator preview link:
|
||||
<a href="{{ url_for('play', token=req.player_token) }}" target="_blank" rel="noopener noreferrer">Open customer player in new tab →</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Payment & Delivery -->
|
||||
<div class="section">
|
||||
<h2>4. Payment & Delivery</h2>
|
||||
<p>
|
||||
Customer approved:
|
||||
{% if req.customer_approved == 'none' %}
|
||||
<span class="approved-box">Nothing yet</span>
|
||||
{% else %}
|
||||
<span class="approved-box">{{ (req.customer_approved or 'none').upper() }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
Delivery email:
|
||||
{% if req.delivery_sent_at %}
|
||||
<span class="status-badge sent">✅ Sent</span> {{ req.delivery_sent_at }}
|
||||
{% else %}
|
||||
<span class="status-badge missing">❌ Not sent yet</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if req.square_payment_ref %}
|
||||
<p><strong>Square Payment Reference:</strong> {{ req.square_payment_ref }}</p>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<!-- Shared payment reference field -->
|
||||
<label for="square_payment_ref">Square Payment Reference</label>
|
||||
<input type="text" id="square_payment_ref" name="square_payment_ref" value="{{ req.square_payment_ref or '' }}" placeholder="e.g. sq0idp-... or receipt number">
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" name="action" value="update_payment_ref" class="secondary">Update Payment Reference</button>
|
||||
</div>
|
||||
|
||||
<hr style="border-color:#374151;margin:1rem 0">
|
||||
|
||||
<p style="color:#93c5fd;font-weight:600">Select files to deliver:</p>
|
||||
{% set all_files = [] %}
|
||||
{% if req.song_a_path and file_exists(req.song_a_path) %}
|
||||
{% set _ = all_files.append(req.song_a_path) %}
|
||||
{% endif %}
|
||||
{% if req.song_b_path and file_exists(req.song_b_path) %}
|
||||
{% set _ = all_files.append(req.song_b_path) %}
|
||||
{% endif %}
|
||||
|
||||
{% set files_to_show = all_files + extra_files %}
|
||||
{% for fpath in files_to_show %}
|
||||
<div class="file-select">
|
||||
<input type="checkbox" id="file_{{ loop.index }}" name="deliver_file" value="{{ fpath }}"
|
||||
{% if fpath in all_files %}checked{% endif %}
|
||||
{% if req.customer_approved == 'none' %}disabled{% endif %}>
|
||||
<label for="file_{{ loop.index }}"{% if req.customer_approved == 'none' %} style="opacity:.6"{% endif %}>{{ basename(fpath) }}</label>
|
||||
{% if not (fpath == req.song_a_path or fpath == req.song_b_path) %}
|
||||
<div class="old-rev">archived / revision file</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No files available. Upload songs first.</p>
|
||||
{% endfor %}
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" name="action" value="mark_paid_deliver" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
||||
</div>
|
||||
{% if req.customer_approved == 'none' %}
|
||||
<p style="margin-top:.5rem;color:#f87171;font-weight:600">⚠️ Customer must approve a version before you can mark paid or deliver.</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyPromptForHermes() {
|
||||
const callbackUrl = {{ callback_url | tojson }};
|
||||
const styleGenre = {{ req.style_genre | tojson }};
|
||||
const styleParts = styleGenre ? styleGenre.split(', ') : [];
|
||||
const decade = styleParts[0] || '';
|
||||
const basic = styleParts[1] || '';
|
||||
const additional = styleParts.slice(2).join(', ') || '';
|
||||
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
|
||||
const data = {
|
||||
request_id: {{ req.id | tojson }},
|
||||
email: {{ req.email | tojson }},
|
||||
name: {{ req.name | tojson }},
|
||||
pronouns: {{ req.pronouns | tojson }},
|
||||
hobbies: {{ req.hobbies | tojson }},
|
||||
notable_facts: {{ req.notable_facts | tojson }},
|
||||
style_genre: styleGenre,
|
||||
vocal_gender: {{ req.vocal_gender | tojson }},
|
||||
extra_requests: {{ req.extra_requests | tojson }},
|
||||
stems_interest: {{ (req.stems_interest or 0) | tojson }}
|
||||
};
|
||||
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nGenerate a Suno Custom Mode prompt for this customer and POST it back to the Callback URL as JSON.\\n\\nCustomer data:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nExpected JSON response format:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
|
||||
navigator.clipboard.writeText(text).then(() => alert("Copied! Paste into Hermes. Hermes will POST the generated prompt back to the Callback URL."));
|
||||
}
|
||||
|
||||
function copyToClipboard(elementId, label) {
|
||||
const el = document.getElementById(elementId);
|
||||
el.select();
|
||||
el.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(el.value).then(() => alert(label + " copied!"));
|
||||
}
|
||||
|
||||
function copyRevisionPromptForHermes() {
|
||||
const callbackUrl = {{ callback_url | tojson }};
|
||||
const currentTitle = {{ req.suno_title | tojson }};
|
||||
const currentStyle = {{ req.suno_style | tojson }};
|
||||
const currentLyrics = {{ req.suno_lyrics | tojson }};
|
||||
const revisionNote = {{ req.revision_note | tojson }};
|
||||
const revisionCount = {{ req.revision_count | tojson }};
|
||||
const styleGenre = {{ req.style_genre | tojson }};
|
||||
const styleParts = styleGenre ? styleGenre.split(', ') : [];
|
||||
const decade = styleParts[0] || '';
|
||||
const basic = styleParts[1] || '';
|
||||
const additional = styleParts.slice(2).join(', ') || '';
|
||||
const styleSentence = [decade && `${decade}-era`, basic, additional && `with ${additional} influences`].filter(Boolean).join(' ');
|
||||
const data = {
|
||||
request_id: {{ req.id | tojson }},
|
||||
email: {{ req.email | tojson }},
|
||||
name: {{ req.name | tojson }},
|
||||
pronouns: {{ req.pronouns | tojson }},
|
||||
hobbies: {{ req.hobbies | tojson }},
|
||||
notable_facts: {{ req.notable_facts | tojson }},
|
||||
style_genre: styleGenre,
|
||||
vocal_gender: {{ req.vocal_gender | tojson }},
|
||||
extra_requests: {{ req.extra_requests | tojson }},
|
||||
stems_interest: {{ (req.stems_interest or 0) | tojson }}
|
||||
};
|
||||
const text = `Booth request: ${data.request_id}\\nCustomer email: ${data.email}\\nCallback URL: ${callbackUrl}\\n\\nThis is REVISION #${revisionCount || 1} for this customer.\\n\\nGenerate a NEW Suno Custom Mode prompt that addresses the following revision request, while keeping the same overall theme/persona and matching the customer's original brief as closely as possible.\\n\\nOriginal brief:\\nName: ${data.name}\\nPronouns: ${data.pronouns || '-'}\\nHobbies: ${data.hobbies || '-'}\\nNotable facts: ${data.notable_facts || '-'}\\nStyle / genre: ${styleSentence || styleGenre || '-'}\\nPreferred singer voice / gender: ${data.vocal_gender || 'No preference'}\\nExtra requests: ${data.extra_requests || '-'}\\nInterested in STEMS: ${data.stems_interest ? 'Yes' : 'No'}\\n\\nPreviously generated prompt (do not copy verbatim; adapt and improve):\\nTitle: ${currentTitle || '-'}\\nStyle: ${currentStyle || '-'}\\nLyrics:\\n${currentLyrics || '-'}\\n\\nRevision request from customer:\\n${revisionNote || '-'}\\n\\nPOST the new prompt back to the Callback URL as JSON with this shape:\\n{\\n \\"request_id\\": ${data.request_id},\\n \\"email\\": \\"${data.email}\\",\\n \\"suno_title\\": \\"...\\",\\n \\"suno_style\\": \\"...\\",\\n \\"suno_lyrics\\": \\"...\\"\\n}`;
|
||||
navigator.clipboard.writeText(text).then(() => alert("Revision prompt copied! Paste into Hermes. Hermes will POST the revised prompt back to the Callback URL."));
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
72
templates/admin/sales.html
Normal file
72
templates/admin/sales.html
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sales Report — Admin</title>
|
||||
<style>
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
}
|
||||
.container{max-width:1100px;margin:0 auto;}
|
||||
h1{color:#60a5fa;margin-top:0;}
|
||||
a{color:#93c5fd;}
|
||||
table{
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
background:#1f2937;
|
||||
border-radius:.5rem;
|
||||
overflow:hidden;
|
||||
}
|
||||
th,td{padding:.7rem;text-align:left;border-bottom:1px solid #374151;}
|
||||
th{background:#111827;color:#9ca3af;}
|
||||
tr:hover{background:#2d3748;}
|
||||
.topbar{float:right;display:flex;gap:.75rem;align-items:center;}
|
||||
.topbar a{color:#93c5fd;text-decoration:none;}
|
||||
.empty{padding:1rem;color:#9ca3af;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="topbar">
|
||||
<a href="{{ url_for('admin_dashboard') }}">Dashboard</a>
|
||||
<a href="{{ url_for('admin_settings') }}">Settings</a>
|
||||
<a href="{{ url_for('admin_logout') }}" style="color:#f87171">Log out</a>
|
||||
</div>
|
||||
|
||||
<h1>Sales Report</h1>
|
||||
|
||||
{% if sales %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Picked</th>
|
||||
<th>Payment Reference</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in sales %}
|
||||
<tr>
|
||||
<td>#{{ s.id }}</td>
|
||||
<td>{{ s.name }}</td>
|
||||
<td>{{ s.email }}</td>
|
||||
<td>{% if s.customer_approved == 'both' %}Both Versions{% else %}Version {{ (s.customer_approved or 'none').upper() }}{% endif %}</td>
|
||||
<td>{{ s.square_payment_ref or '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="empty">No delivered requests yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
470
templates/admin/settings.html
Normal file
470
templates/admin/settings.html
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Settings</title>
|
||||
<style>
|
||||
/*
|
||||
Admin settings / maintenance page.
|
||||
Two-column layout for booth open/closed status, database health,
|
||||
statistics, revision limit, auto-refresh, SMTP config,
|
||||
MP3 metadata defaults, database backup/restore, and system reset.
|
||||
*/
|
||||
:root{
|
||||
--bg:#0b0f19;
|
||||
--panel:#111827;
|
||||
--text:#f3f4f6;
|
||||
--muted:#9ca3af;
|
||||
--accent:#60a5fa;
|
||||
--accent-dark:#2563eb;
|
||||
--danger:#dc2626;
|
||||
--danger-dark:#991b1b;
|
||||
--success:#10b981;
|
||||
--warning:#f59e0b;
|
||||
--border:#374151;
|
||||
}
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
margin:0;
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:var(--bg);
|
||||
color:var(--text);
|
||||
line-height:1.5;
|
||||
}
|
||||
.container{max-width:1200px;margin:0 auto;padding:1.5rem;}
|
||||
.topbar{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
h1{margin:0;font-size:1.5rem;}
|
||||
.nav a{
|
||||
color:var(--accent);
|
||||
text-decoration:none;
|
||||
margin-left:1rem;
|
||||
}
|
||||
.messages p{
|
||||
padding:.75rem 1rem;
|
||||
border-radius:.5rem;
|
||||
margin:0 0 1rem;
|
||||
}
|
||||
.messages .success{background:#064e3b;color:#a7f3d0;}
|
||||
.messages .error{background:#450a0a;color:#fca5a5;}
|
||||
|
||||
/* Two-column grid for settings */
|
||||
.grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(2, 1fr);
|
||||
gap:1.5rem;
|
||||
}
|
||||
@media (max-width:900px){
|
||||
.grid{grid-template-columns:1fr;}
|
||||
}
|
||||
|
||||
.section{
|
||||
background:var(--panel);
|
||||
border:1px solid var(--border);
|
||||
border-radius:.75rem;
|
||||
padding:1.25rem;
|
||||
}
|
||||
.section h2{
|
||||
margin-top:0;
|
||||
font-size:1.15rem;
|
||||
color:var(--accent);
|
||||
}
|
||||
.section p{
|
||||
margin:.5rem 0;
|
||||
color:var(--muted);
|
||||
font-size:.95rem;
|
||||
}
|
||||
label{
|
||||
display:block;
|
||||
margin-top:.75rem;
|
||||
font-size:.9rem;
|
||||
color:var(--muted);
|
||||
}
|
||||
input[type="text"],input[type="number"],input[type="password"],textarea,select{
|
||||
width:100%;
|
||||
padding:.6rem;
|
||||
border-radius:.4rem;
|
||||
border:1px solid var(--border);
|
||||
background:#1f2937;
|
||||
color:var(--text);
|
||||
font:inherit;
|
||||
margin-top:.25rem;
|
||||
}
|
||||
textarea{resize:vertical;}
|
||||
button{
|
||||
margin-top:1rem;
|
||||
padding:.65rem 1.2rem;
|
||||
background:var(--accent-dark);
|
||||
color:#fff;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
cursor:pointer;
|
||||
font-weight:600;
|
||||
}
|
||||
button:hover{background:var(--accent);}
|
||||
|
||||
/* Metadata and email config tables */
|
||||
.meta-table{
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
margin-top:.5rem;
|
||||
}
|
||||
.meta-table td{
|
||||
padding:.5rem;
|
||||
vertical-align:top;
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.meta-table td:first-child{
|
||||
width:30%;
|
||||
font-size:.9rem;
|
||||
color:var(--muted);
|
||||
padding-left:0;
|
||||
}
|
||||
.meta-table td:last-child{padding-right:0;}
|
||||
.meta-table tr:last-child td{border-bottom:none;}
|
||||
.meta-table input,.meta-table textarea,.meta-table select{
|
||||
margin-top:0;
|
||||
}
|
||||
|
||||
.danger-zone{
|
||||
grid-column:1 / -1;
|
||||
background:#450a0a;
|
||||
border:1px solid #7f1d1d;
|
||||
border-radius:.75rem;
|
||||
padding:1.25rem;
|
||||
}
|
||||
.danger-zone h2{margin-top:0;color:#fca5a5;}
|
||||
.danger-zone button{
|
||||
background:var(--danger);
|
||||
}
|
||||
.danger-zone button:hover{background:var(--danger-dark);}
|
||||
|
||||
.status-ok{color:var(--success);font-weight:600;}
|
||||
.status-bad{color:var(--warning);font-weight:600;}
|
||||
.copy-hint{font-size:.85rem;color:var(--muted);margin-top:.25rem;}
|
||||
.path{word-break:break-all;font-family:monospace;font-size:.85rem;}
|
||||
.version-tag{
|
||||
display:inline-block;
|
||||
background:#2563eb;
|
||||
color:#fff;
|
||||
font-size:.85rem;
|
||||
font-weight:700;
|
||||
padding:.25rem .6rem;
|
||||
border-radius:.4rem;
|
||||
margin-left:.75rem;
|
||||
vertical-align:middle;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="topbar">
|
||||
<h1>Admin Settings <span class="version-tag">v{{ version }}</span></h1>
|
||||
<div class="nav">
|
||||
<a href="{{ url_for('admin_dashboard') }}">← Back to Dashboard</a>
|
||||
<a href="{{ url_for('admin_logout') }}">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for category, message in messages %}
|
||||
<p class="{{ category }}">{{ message }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<!-- Database health check -->
|
||||
<div class="section">
|
||||
<h2>Booth Status</h2>
|
||||
<p class="copy-hint">When closed, the public request page shows a "booth closed" message instead of the form.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_booth_open">
|
||||
<label for="booth_open">Booth is currently</label>
|
||||
<select id="booth_open" name="booth_open">
|
||||
<option value="1" {% if booth_open %}selected{% endif %}>Open ✅</option>
|
||||
<option value="0" {% if not booth_open %}selected{% endif %}>Closed ❌</option>
|
||||
</select>
|
||||
<button type="submit">Save Booth Status</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Database health check -->
|
||||
<div class="section">
|
||||
<h2>Database Health</h2>
|
||||
{% if not health.ok %}
|
||||
<p class="status-bad">❌ {{ health.message }}</p>
|
||||
{% if health.missing_columns %}
|
||||
<p>Missing columns: {{ health.missing_columns | join(', ') }}</p>
|
||||
{% endif %}
|
||||
{% if health.missing_tables %}
|
||||
<p>Missing tables: {{ health.missing_tables | join(', ') }}</p>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('admin_settings') }}">
|
||||
<input type="hidden" name="action" value="fix_db">
|
||||
<button type="submit">Fix Database Schema</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="status-ok">✅ {{ health.message }}</p>
|
||||
<form method="POST" action="{{ url_for('admin_settings') }}" style="margin-top:.5rem">
|
||||
<input type="hidden" name="action" value="fix_db">
|
||||
<button type="submit" class="secondary">Recheck / Apply Schema</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Database statistics -->
|
||||
<div class="section">
|
||||
<h2>Database Statistics</h2>
|
||||
<p><strong>Total requests:</strong> {{ total_records }}</p>
|
||||
<p><strong>Database size:</strong> {{ db_size }}</p>
|
||||
<p><strong>Uploaded MP3s:</strong> {{ upload_file_count }} files in {{ upload_dir_count }} request folders</p>
|
||||
<p><strong>Uploads total size:</strong> {{ upload_size }}</p>
|
||||
{% if status_counts %}
|
||||
<p><strong>Status breakdown:</strong></p>
|
||||
<ul>
|
||||
{% for status, count in status_counts.items() %}
|
||||
<li>{{ status }}: {{ count }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p><strong>Database path:</strong><br><span class="path">{{ db_path }}</span></p>
|
||||
<p><strong>Uploads path:</strong><br><span class="path">{{ upload_path }}</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Revision limit -->
|
||||
<div class="section">
|
||||
<h2>Revision Limit</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_max_revisions">
|
||||
<label for="max_revisions">Maximum customer revisions allowed</label>
|
||||
<input type="number" id="max_revisions" name="max_revisions" min="0" value="{{ current_max_revisions }}">
|
||||
<p class="copy-hint">Set to 0 to disable customer-submitted revisions. Operators can still upload new versions manually.</p>
|
||||
<button type="submit">Save Revision Limit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Auto refresh -->
|
||||
<div class="section">
|
||||
<h2>Dashboard Auto-Refresh</h2>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_refresh">
|
||||
<label for="refresh_seconds">Refresh interval</label>
|
||||
<select id="refresh_seconds" name="refresh_seconds">
|
||||
<option value="0" {% if current_refresh_seconds == 0 %}selected{% endif %}>Off</option>
|
||||
<option value="10" {% if current_refresh_seconds == 10 %}selected{% endif %}>10 seconds</option>
|
||||
<option value="20" {% if current_refresh_seconds == 20 %}selected{% endif %}>20 seconds</option>
|
||||
<option value="30" {% if current_refresh_seconds == 30 %}selected{% endif %}>30 seconds</option>
|
||||
</select>
|
||||
<button type="submit">Save Refresh Interval</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Kiosk cycle -->
|
||||
<div class="section">
|
||||
<h2>Kiosk Display Mode</h2>
|
||||
<p class="copy-hint">Set how the public <a href="{{ url_for('kiosk') }}" target="_blank" rel="noopener noreferrer">/kiosk</a> page cycles. Use -1 for QR only, 0 for pricing only, 1 for queue only, or 5+ seconds to cycle between QR, pricing, and queue.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_kiosk_cycle">
|
||||
<label for="kiosk_cycle_seconds">Kiosk cycle seconds</label>
|
||||
<input type="number" id="kiosk_cycle_seconds" name="kiosk_cycle_seconds" value="{{ current_kiosk_cycle_seconds }}" min="-1">
|
||||
<p class="copy-hint">Examples: -1 = QR only, 0 = pricing only, 1 = queue only, 10 = rotate QR → pricing → queue every 10 seconds.</p>
|
||||
<button type="submit">Save Kiosk Mode</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Hermes API Key display (env var only; no regeneration) -->
|
||||
<div class="section">
|
||||
<h2>Hermes API Key</h2>
|
||||
<p class="copy-hint">Set via Portainer env var <code>HERMES_API_KEY</code>. Used for callback auth.</p>
|
||||
<p><strong>Current key:</strong> <code>{{ hermes_key_masked }}</code></p>
|
||||
|
||||
{% if not hermes_key_set %}
|
||||
<p class="status-bad">⚠️ No Hermes API key is configured. Set the HERMES_API_KEY environment variable in Portainer before using the callback workflow.</p>
|
||||
{% endif %}
|
||||
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
|
||||
|
||||
<h3>Callback Link Expiry</h3>
|
||||
<p class="copy-hint">How long the signed Hermes callback URL stays valid (in hours). Default is 168 hours (7 days).</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_callback_expiry">
|
||||
<label for="callback_expiry_hours">Callback expiry (hours)</label>
|
||||
<input type="number" id="callback_expiry_hours" name="callback_expiry_hours" value="{{ current_callback_expiry_hours }}" min="1">
|
||||
<button type="submit">Save Callback Expiry</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- ntfy push notifications -->
|
||||
<div class="section">
|
||||
<h2>ntfy Notifications</h2>
|
||||
<p class="copy-hint">Push notifications for new customer requests. Leave either field blank to disable.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_ntfy">
|
||||
<table class="meta-table">
|
||||
<tr>
|
||||
<td>ntfy Server URL</td>
|
||||
<td><input type="text" id="ntfy_server" name="ntfy_server" value="{{ ntfy.get('ntfy_server', '') }}" placeholder="e.g. https://ntfy.hallsworth.ca"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ntfy Topic</td>
|
||||
<td><input type="text" id="ntfy_topic" name="ntfy_topic" value="{{ ntfy.get('ntfy_topic', '') }}" placeholder="e.g. Troll-AI-ntfy"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ntfy Access Token</td>
|
||||
<td>
|
||||
<input type="password" id="ntfy_token" name="ntfy_token" placeholder="{% if ntfy.get('ntfy_token') %}Stored encrypted — type to replace{% else %}Enter token (optional){% endif %}">
|
||||
<p class="copy-hint">Required if the topic is access-controlled. Leave blank to keep the existing stored token.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="submit">Save ntfy Settings</button>
|
||||
</form>
|
||||
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
|
||||
|
||||
<h3>Send Test Notification</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="send_test_ntfy">
|
||||
<button type="submit">Send Test ntfy</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Email / SMTP configuration -->
|
||||
<div class="section">
|
||||
<h2>Email (SMTP) Settings</h2>
|
||||
<p class="copy-hint">These settings are used to send confirmation, preview, and delivery emails to customers. The password is stored encrypted using the Flask secret key.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_email_config">
|
||||
<table class="meta-table">
|
||||
<tr>
|
||||
<td>SMTP Host</td>
|
||||
<td><input type="text" id="smtp_host" name="smtp_host" value="{{ email_form.smtp_host }}" placeholder="e.g. mailroot8.namespro.ca"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SMTP Port</td>
|
||||
<td><input type="text" id="smtp_port" name="smtp_port" value="{{ email_form.smtp_port }}" placeholder="e.g. 465"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SMTP Username</td>
|
||||
<td><input type="text" id="smtp_user" name="smtp_user" value="{{ email_form.smtp_user }}" placeholder="e.g. ai@hallsworth.ca"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>From Address</td>
|
||||
<td><input type="text" id="smtp_from" name="smtp_from" value="{{ email_form.smtp_from }}" placeholder="e.g. ai@hallsworth.ca"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SMTP Password</td>
|
||||
<td>
|
||||
<input type="password" id="smtp_pass" name="smtp_pass" placeholder="{% if email_form.smtp_pass_set %}Stored encrypted — type to replace{% else %}Enter password{% endif %}">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<button type="submit">Save Email Settings</button>
|
||||
</form>
|
||||
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:1.5rem 0;">
|
||||
|
||||
<h3>Send Test Email</h3>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="send_test_email">
|
||||
<label for="test_email_address">Test recipient address</label>
|
||||
<input type="email" id="test_email_address" name="test_email_address" placeholder="you@example.com" required>
|
||||
<button type="submit">Send Test Email</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- MP3 metadata defaults -->
|
||||
<div class="section">
|
||||
<h2>MP3 Metadata Tags</h2>
|
||||
<p class="copy-hint">These values are written into every uploaded MP3 file. The song title from the prompt is written to the Title tag automatically. Blank fields are skipped.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_metadata">
|
||||
<table class="meta-table">
|
||||
<tr>
|
||||
<td>Artist</td>
|
||||
<td><input type="text" id="artist" name="artist" value="{{ metadata.get('artist', '') or '' }}" placeholder="e.g. Trollgorithm"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Album</td>
|
||||
<td><input type="text" id="album" name="album" value="{{ metadata.get('album', '') or '' }}" placeholder="e.g. Theme Song Booth 2026"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Year</td>
|
||||
<td><input type="text" id="year" name="year" value="{{ metadata.get('year', '') or '' }}" placeholder="e.g. 2026"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Comment</td>
|
||||
<td><textarea id="comment" name="comment" rows="3" placeholder="e.g. Custom theme song generated at the booth">{{ metadata.get('comment', '') or '' }}</textarea></td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="submit">Save Metadata Defaults</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Database backup / restore -->
|
||||
<div class="section">
|
||||
<h2>Database Backup / Restore</h2>
|
||||
<p class="copy-hint">Download a copy of the SQLite database before the event. Upload a previous backup to restore it; the current database will be renamed as a timestamped backup.</p>
|
||||
|
||||
<form method="POST" style="margin-bottom:1rem">
|
||||
<input type="hidden" name="action" value="download_db">
|
||||
<button type="submit">Download Database Backup</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" value="restore_db">
|
||||
<label for="db_backup">Restore from backup (.db / .sqlite)</label>
|
||||
<input type="file" id="db_backup" name="db_backup" accept=".db,.sqlite,.sqlite3">
|
||||
<button type="submit" onclick="return confirm('This will replace the current database. The old one will be kept as a timestamped backup. Continue?')">Restore Database</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Uploads backup -->
|
||||
<div class="section">
|
||||
<h2>Music Files Backup</h2>
|
||||
<p class="copy-hint">Download all uploaded MP3 and archived revision files as a single ZIP.</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="download_uploads_zip">
|
||||
<button type="submit">Download Music Files ZIP</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- System reset spans both columns -->
|
||||
<div class="danger-zone">
|
||||
<h2>⚠️ Reset System</h2>
|
||||
<p>Use this only at the start of a new event. It will delete every request and every uploaded MP3 file. This cannot be undone.</p>
|
||||
<form method="POST" action="{{ url_for('admin_settings') }}" onsubmit="return confirm('ARE YOU SURE? This will delete ALL requests and ALL uploaded files. This cannot be undone.')">
|
||||
<input type="hidden" name="action" value="reset_system">
|
||||
<button type="submit">Reset System</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyHermesKey() {
|
||||
const el = document.getElementById('new-hermes-key');
|
||||
if (!el) return;
|
||||
const range = document.createRange();
|
||||
range.selectNode(el);
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
navigator.clipboard.writeText(el.textContent).then(() => {
|
||||
alert('Hermes API key copied!');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
49
templates/closed.html
Normal file
49
templates/closed.html
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trollgorithm Theme Song Booth — Closed</title>
|
||||
<style>
|
||||
/*
|
||||
Closed booth page.
|
||||
Friendly message shown when the booth is temporarily not taking requests.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#111827 0%,#1e3a8a 50%,#111827 100%);
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.6;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{max-width:680px;margin:0 auto;text-align:center;}
|
||||
.banner{
|
||||
width:100%;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
.card{
|
||||
background:rgba(31,41,55,.9);
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.3);
|
||||
border:1px solid rgba(96,165,250,.2);
|
||||
}
|
||||
h1{margin-top:0;color:#60a5fa;font-size:1.7rem;}
|
||||
p{font-size:1.1rem;color:#d1d5db;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="{{ url_for('static', filename='Booth_closed.png') }}" alt="Trollgorithm booth is closed" class="banner">
|
||||
<div class="card">
|
||||
<h1>Trollgorithm and the goblin engineers are on a break.</h1>
|
||||
<p>Please come back in a while and they should be back to work!</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
175
templates/faq.html
Normal file
175
templates/faq.html
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FAQ — Trollgorithm Theme Song Booth</title>
|
||||
<style>
|
||||
/*
|
||||
Customer FAQ page.
|
||||
Simple, readable dark layout matching the request page style.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#111827 0%,#1e3a8a 50%,#111827 100%);
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.6;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{max-width:760px;margin:0 auto;}
|
||||
.card{
|
||||
background:rgba(31,41,55,.95);
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.3);
|
||||
border:1px solid rgba(96,165,250,.2);
|
||||
}
|
||||
h1{
|
||||
margin-top:0;
|
||||
color:#60a5fa;
|
||||
text-align:center;
|
||||
font-size:1.9rem;
|
||||
}
|
||||
h2{
|
||||
color:#93c5fd;
|
||||
margin-top:2rem;
|
||||
font-size:1.25rem;
|
||||
border-bottom:1px solid #374151;
|
||||
padding-bottom:.4rem;
|
||||
}
|
||||
h2:first-of-type{margin-top:0;}
|
||||
p{margin:.6rem 0;}
|
||||
strong{color:#fbbf24;}
|
||||
a{color:#93c5fd;text-decoration:none;}
|
||||
a:hover{text-decoration:underline;}
|
||||
.back{
|
||||
display:block;
|
||||
text-align:center;
|
||||
margin-top:1.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>❓ Frequently Asked Questions</h1>
|
||||
|
||||
<h2>The basics</h2>
|
||||
<p><strong>What is the Trollgorithm Song Booth?</strong></p>
|
||||
<p>We create a custom, AI-generated theme song just for you based on your personality, hobbies, and style preferences.</p>
|
||||
|
||||
<p><strong>How long does it take to get my song?</strong></p>
|
||||
<p>Most songs are ready to preview within a few minutes to an hour, depending on how busy the booth is!</p>
|
||||
|
||||
<p><strong>How much does it cost?</strong></p>
|
||||
<p>Pricing is displayed at the booth; you pay after listening to and choosing the version you want.</p>
|
||||
|
||||
<p><strong>What do I actually receive?</strong></p>
|
||||
<p>You get an MP3 file of the version you selected, delivered straight to your email. WAV format is available for a slight additional charge.</p>
|
||||
|
||||
<h2>How it works</h2>
|
||||
<p><strong>What info do I need to give you?</strong></p>
|
||||
<p>Your name, email, hobbies, notable facts about you, a preferred style or genre, and any extra requests you want included.</p>
|
||||
|
||||
<p><strong>Can I pick the style or genre?</strong></p>
|
||||
<p>Yes — tell us anything from “80s power ballad” to “cinematic orchestral” and we will aim for that vibe.</p>
|
||||
|
||||
<p><strong>What if I want a specific singer voice?</strong></p>
|
||||
<p>You can select a vocal preference such as female, male, androgynous, or non-binary on the request form. We are unable to create specific singers due to copyright laws.</p>
|
||||
|
||||
<p><strong>Can I request specific lyrics or topics?</strong></p>
|
||||
<p>Absolutely — put any specific lyrics, themes, or things to avoid in the “Extra requests” field.</p>
|
||||
|
||||
<p><strong>What happens after I submit the form?</strong></p>
|
||||
<p>We generate a custom prompt, produce two versions of your song, and email you a private link to listen.</p>
|
||||
|
||||
<p><strong>How do I hear my song when it's ready?</strong></p>
|
||||
<p>Click the private link in your email — it works on any phone, tablet, or computer.</p>
|
||||
|
||||
<h2>Versions & revisions</h2>
|
||||
<p><strong>What's the difference between Version A and Version B?</strong></p>
|
||||
<p>They are two different takes or arrangements of your theme song, so you can choose the one you like best.</p>
|
||||
|
||||
<p><strong>Can I get both versions?</strong></p>
|
||||
<p>Yes — just select “I want both” on the preview page.</p>
|
||||
|
||||
<p><strong>What if I don't like either version?</strong></p>
|
||||
<p>You can request a limited number of changes through the preview page, or speak to the booth operator.</p>
|
||||
|
||||
<p><strong>Can I ask for changes? How many times?</strong></p>
|
||||
<p>Yes, a small number of revisions are allowed; the exact limit is shown on your preview page.</p>
|
||||
|
||||
<p><strong>What if I change my mind after picking a version?</strong></p>
|
||||
<p>Let the booth operator know right away; if payment has not been finalized, we can usually fix it.</p>
|
||||
|
||||
<h2>Limitations</h2>
|
||||
<p><strong>What limitations are there for topics or lyrics?</strong></p>
|
||||
<p>Trollgorithm is a friendly band and will not produce lyrics that contain any of the following:</p>
|
||||
<ul>
|
||||
<li>Specific acts of violence, self-harm, terrorism, or serious crime against identifiable people.</li>
|
||||
<li>Help create, or refine CSAM or sexual content involving minors.</li>
|
||||
<li>Anything hate-crime related.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Can I use explicit lyrics?</strong></p>
|
||||
<p>Yes! Trollgorithm believes the occasional use of explicit or crude language is fine, but the booth operator is a stick in the mud and won't allow minors to make music filled with swear words!</p>
|
||||
|
||||
<p><strong>Can you make a song using just lyrics I provide?</strong></p>
|
||||
<p>Yes! Trollgorithm loves to collaborate with song writers! Ask the booth operator for help!</p>
|
||||
|
||||
<p><strong>What if I want to sing and they just play the music?</strong></p>
|
||||
<p>While Trollgorithm is talented, they are also still just Trolls. You would probably get eaten if you were in the recording booth with them.</p>
|
||||
|
||||
<p><strong>What languages can Trollgorithm sing in?</strong></p>
|
||||
<p>Trollgorithm knows English, Spanish, and French best. They can attempt German, Italian, Portuguese, Japanese, Mandarin Chinese, Korean, Russian, Arabic, and Hindi — but remember, they're just trolls and goblins, not terribly smart. Have a fluent speaker listen and check the result to make sure it sounds right.</p>
|
||||
|
||||
<h2>Payment & delivery</h2>
|
||||
<p><strong>How do I pay?</strong></p>
|
||||
<p>We accept payment at the booth through Square, including card, tap, and cash where available.</p>
|
||||
|
||||
<p><strong>Do you pay before or after hearing the song?</strong></p>
|
||||
<p>You hear the preview first, pick a version, and then pay before the final MP3 is emailed to you.</p>
|
||||
|
||||
<p><strong>Where is my song delivered?</strong></p>
|
||||
<p>To the email address you gave us, so double-check it for typos before submitting.</p>
|
||||
|
||||
<p><strong>What file format is it?</strong></p>
|
||||
<p>The delivered song is a standard MP3 file that plays on virtually any device. WAV files can be provided upon request. We can also get you STEMS. If you know, you know, otherwise these aren't for everybody.</p>
|
||||
|
||||
<p><strong>Can you send it to someone else's email?</strong></p>
|
||||
<p>At the booth we can update the email address if needed, but the original request needs a valid email to start.</p>
|
||||
|
||||
<h2>Ownership & usage</h2>
|
||||
<p><strong>Do I own my theme song?</strong></p>
|
||||
<p>Once you have paid, you do! You can use it for whatever you want, commercially or privately. Trollgorithm does reserve the right to use any music it makes as part of its demonstration playlist.</p>
|
||||
|
||||
<p><strong>Can I post it online?</strong></p>
|
||||
<p>Yes, for personal, or commercial use — feel free to share it on social media.</p>
|
||||
|
||||
<p><strong>Is this AI-generated music? Is that legal?</strong></p>
|
||||
<p>Trollgorithm is a privately built AI music Writer, Producer, Sound Engineer, and Band. It does each step (with human input, of course!) and produces finished songs. Since the music and lyrics are generated from scratch, no copyright rules are broken.</p>
|
||||
|
||||
<h2>Privacy & support</h2>
|
||||
<p><strong>Is my personal information kept private?</strong></p>
|
||||
<p>Yes, your info is only used to create and deliver your song and is not shared or sold. Your information is only stored for a few days, to ensure you have received the end product you want. After that it is deleted.</p>
|
||||
|
||||
<p><strong>What if I made a typo in my email?</strong></p>
|
||||
<p>Tell the booth operator; they can edit your email address so your song reaches you.</p>
|
||||
|
||||
<p><strong>What if I don't get the email?</strong></p>
|
||||
<p>Check your spam or junk folder first, then ask the operator to resend or verify your address.</p>
|
||||
|
||||
<p><strong>What if there's a technical problem?</strong></p>
|
||||
<p>Let the booth operator know and we will do our best to fix it or retry delivery.</p>
|
||||
|
||||
<p><strong>Who do I talk to for help?</strong></p>
|
||||
<p>The booth operator is your best point of contact for any questions or issues.</p>
|
||||
|
||||
<a class="back" href="{{ url_for('request_form') }}" target="_blank" rel="noopener noreferrer">← Back to the request form</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
322
templates/kiosk.html
Normal file
322
templates/kiosk.html
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trollgorithm Booth — Kiosk</title>
|
||||
{% if refresh_seconds and refresh_seconds > 0 %}
|
||||
<meta http-equiv="refresh" content="{{ refresh_seconds }}">
|
||||
{% endif %}
|
||||
<style>
|
||||
*{box-sizing:border-box;}
|
||||
html,body{
|
||||
margin:0;
|
||||
padding:0;
|
||||
height:100%;
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#0f172a 0%,#1e3a8a 50%,#0f172a 100%);
|
||||
color:#f3f4f6;
|
||||
overflow:hidden;
|
||||
}
|
||||
.kiosk{
|
||||
height:100%;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
justify-content:space-between;
|
||||
padding:2vh 3vw;
|
||||
text-align:center;
|
||||
}
|
||||
.banner{
|
||||
width:100%;
|
||||
max-height:22vh;
|
||||
object-fit:contain;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
}
|
||||
.status-pill{
|
||||
display:inline-block;
|
||||
margin-top:1.5vh;
|
||||
padding:.6rem 1.4rem;
|
||||
border-radius:9999px;
|
||||
font-weight:700;
|
||||
font-size:1.2rem;
|
||||
text-transform:uppercase;
|
||||
letter-spacing:.05em;
|
||||
}
|
||||
.status-open{background:#10b981;color:#000;}
|
||||
.status-closed{background:#ef4444;color:#000;}
|
||||
.stage{
|
||||
flex:1;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
width:100%;
|
||||
max-width:900px;
|
||||
margin:2vh 0;
|
||||
}
|
||||
.slide{
|
||||
width:100%;
|
||||
animation:fadeIn .8s ease;
|
||||
}
|
||||
@keyframes fadeIn{
|
||||
from{opacity:0;transform:translateY(20px);}
|
||||
to{opacity:1;transform:translateY(0);}
|
||||
}
|
||||
.slide.hidden{display:none;}
|
||||
h1{
|
||||
margin:0 0 1.5vh 0;
|
||||
font-size:clamp(2rem,5vw,3.5rem);
|
||||
color:#60a5fa;
|
||||
}
|
||||
.qr-code{
|
||||
max-height:44vh;
|
||||
max-width:44vh;
|
||||
width:auto;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
background:#fff;
|
||||
padding:.6rem;
|
||||
}
|
||||
.qr-caption{
|
||||
margin-top:1.5vh;
|
||||
font-size:clamp(1.1rem,2.5vw,1.6rem);
|
||||
color:#93c5fd;
|
||||
}
|
||||
.price-card{
|
||||
background:rgba(31,41,55,.85);
|
||||
border:1px solid rgba(96,165,250,.25);
|
||||
border-radius:1.2rem;
|
||||
padding:3vh 4vw;
|
||||
box-shadow:0 15px 50px rgba(0,0,0,.35);
|
||||
width:100%;
|
||||
}
|
||||
.price-card h2{
|
||||
margin:0 0 2vh 0;
|
||||
font-size:clamp(1.6rem,4vw,2.6rem);
|
||||
color:#fbbf24;
|
||||
}
|
||||
.price-list{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align:left;
|
||||
}
|
||||
.price-list li{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
border-bottom:1px solid rgba(147,197,253,.2);
|
||||
padding:1.2vh 0;
|
||||
font-size:clamp(1.2rem,3vw,1.8rem);
|
||||
}
|
||||
.price-list li:last-child{border-bottom:none;}
|
||||
.price-list .label{color:#e5e7eb;}
|
||||
.price-list .amount{font-weight:700;color:#fbbf24;}
|
||||
.price-empty{
|
||||
font-size:1.3rem;
|
||||
color:#9ca3af;
|
||||
}
|
||||
.queue-card{
|
||||
background:rgba(31,41,55,.85);
|
||||
border:1px solid rgba(96,165,250,.25);
|
||||
border-radius:1.2rem;
|
||||
padding:3vh 4vw;
|
||||
box-shadow:0 15px 50px rgba(0,0,0,.35);
|
||||
width:100%;
|
||||
max-height:62vh;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
.queue-card h2{
|
||||
margin:0 0 2vh 0;
|
||||
font-size:clamp(1.6rem,4vw,2.6rem);
|
||||
color:#fbbf24;
|
||||
}
|
||||
.queue-list{
|
||||
list-style:none;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align:left;
|
||||
overflow-y:auto;
|
||||
flex:1;
|
||||
}
|
||||
.queue-list li{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
border-bottom:1px solid rgba(147,197,253,.2);
|
||||
padding:1.4vh 0;
|
||||
font-size:clamp(1.2rem,2.8vw,1.7rem);
|
||||
}
|
||||
.queue-list li:last-child{border-bottom:none;}
|
||||
.queue-list .customer{color:#e5e7eb;font-weight:600;}
|
||||
.queue-list .status{font-weight:700;color:#60a5fa;text-align:right;}
|
||||
.queue-empty{
|
||||
font-size:1.4rem;
|
||||
color:#9ca3af;
|
||||
text-align:center;
|
||||
margin:auto 0;
|
||||
}
|
||||
.queue-note{
|
||||
font-size:1rem;
|
||||
color:#9ca3af;
|
||||
margin-top:1.5vh;
|
||||
text-align:center;
|
||||
}
|
||||
.footer{
|
||||
font-size:1rem;
|
||||
color:#9ca3af;
|
||||
}
|
||||
.dots{
|
||||
display:flex;
|
||||
gap:.6rem;
|
||||
justify-content:center;
|
||||
margin-top:1vh;
|
||||
}
|
||||
.dot{
|
||||
width:12px;
|
||||
height:12px;
|
||||
border-radius:50%;
|
||||
background:#374151;
|
||||
transition:background .3s;
|
||||
}
|
||||
.dot.active{background:#60a5fa;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="kiosk">
|
||||
<div class="header">
|
||||
{% if booth_open %}
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="banner">
|
||||
<div class="status-pill status-open">Booth is Open ✅</div>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='Booth_closed.png') }}" alt="Booth Closed" class="banner">
|
||||
<div class="status-pill status-closed">Booth is Closed ❌</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="stage" id="stage">
|
||||
{% if mode == 'qr' %}
|
||||
<div class="slide">
|
||||
<h1>Scan to Order</h1>
|
||||
<img src="{{ url_for('static', filename='qr-code.png') }}" alt="Scan to order" class="qr-code">
|
||||
<p class="qr-caption">Point your camera at the code to start your request.</p>
|
||||
</div>
|
||||
{% elif mode == 'prices' %}
|
||||
<div class="slide">
|
||||
<div class="price-card">
|
||||
<h2>Pricing</h2>
|
||||
{% if price_items %}
|
||||
<ul class="price-list">
|
||||
{% for item in price_items %}
|
||||
<li>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<span class="amount">{{ item.price }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="price-empty">Pricing coming soon. Ask the booth operator!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif mode == 'queue' %}
|
||||
<div class="slide">
|
||||
<div class="queue-card">
|
||||
<h2>Live Queue</h2>
|
||||
{% if queue %}
|
||||
<ul class="queue-list">
|
||||
{% for item in queue %}
|
||||
<li>
|
||||
<span class="customer">#{{ item.id }} {{ item.name }}</span>
|
||||
<span class="status">{{ item.status }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="queue-note">If your request isn't shown, visit the request page and use the status link to check your place.</p>
|
||||
{% else %}
|
||||
<p class="queue-empty">No active requests right now. Be the first!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="slide" id="slide-qr">
|
||||
<h1>Scan to Order</h1>
|
||||
<img src="{{ url_for('static', filename='qr-code.png') }}" alt="Scan to order" class="qr-code">
|
||||
<p class="qr-caption">Point your camera at the code to start your request.</p>
|
||||
</div>
|
||||
<div class="slide hidden" id="slide-prices">
|
||||
<div class="price-card">
|
||||
<h2>Pricing</h2>
|
||||
{% if price_items %}
|
||||
<ul class="price-list">
|
||||
{% for item in price_items %}
|
||||
<li>
|
||||
<span class="label">{{ item.label }}</span>
|
||||
<span class="amount">{{ item.price }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="price-empty">Pricing coming soon. Ask the booth operator!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="slide hidden" id="slide-queue">
|
||||
<div class="queue-card">
|
||||
<h2>Live Queue</h2>
|
||||
{% if queue %}
|
||||
<ul class="queue-list">
|
||||
{% for item in queue %}
|
||||
<li>
|
||||
<span class="customer">#{{ item.id }} {{ item.name }}</span>
|
||||
<span class="status">{{ item.status }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="queue-note">If your request isn't shown, visit the request page and use the status link to check your place.</p>
|
||||
{% else %}
|
||||
<p class="queue-empty">No active requests right now. Be the first!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if mode == 'cycle' %}
|
||||
<div class="footer">
|
||||
<div class="dots">
|
||||
<div class="dot active" id="dot-qr"></div>
|
||||
<div class="dot" id="dot-prices"></div>
|
||||
<div class="dot" id="dot-queue"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const qr = document.getElementById('slide-qr');
|
||||
const prices = document.getElementById('slide-prices');
|
||||
const queue = document.getElementById('slide-queue');
|
||||
const dotQr = document.getElementById('dot-qr');
|
||||
const dotPrices = document.getElementById('dot-prices');
|
||||
const dotQueue = document.getElementById('dot-queue');
|
||||
const seconds = {{ cycle_seconds }};
|
||||
const slides = [qr, prices, queue];
|
||||
const dots = [dotQr, dotPrices, dotQueue];
|
||||
if (seconds > 0) {
|
||||
let idx = 0;
|
||||
setInterval(function(){
|
||||
slides[idx].classList.add('hidden');
|
||||
dots[idx].classList.remove('active');
|
||||
idx = (idx + 1) % slides.length;
|
||||
slides[idx].classList.remove('hidden');
|
||||
dots[idx].classList.add('active');
|
||||
}, seconds * 1000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
207
templates/player.html
Normal file
207
templates/player.html
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Your Theme Song</title>
|
||||
<style>
|
||||
/*
|
||||
Private customer player page.
|
||||
Shows two audio players for Version A and Version B, plus approval
|
||||
buttons or a revision note form depending on request status.
|
||||
After the customer makes a choice, the controls are hidden and a
|
||||
confirmation/waiting message is shown. The number of remaining
|
||||
customer revisions is displayed when revisions are enabled.
|
||||
*/
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
}
|
||||
.container{
|
||||
max-width:640px;
|
||||
margin:0 auto;
|
||||
background:#1f2937;
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
}
|
||||
h1{color:#60a5fa;}
|
||||
.player{
|
||||
background:#111827;
|
||||
padding:1rem;
|
||||
border-radius:.5rem;
|
||||
margin:1rem 0;
|
||||
}
|
||||
audio{width:100%;margin-top:.5rem;}
|
||||
.actions{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:1rem;}
|
||||
button{
|
||||
flex:1;
|
||||
min-width:120px;
|
||||
padding:.8rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:#3b82f6;
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
cursor:pointer;
|
||||
}
|
||||
button:disabled{background:#374151;color:#9ca3af;cursor:not-allowed;}
|
||||
button.selected{background:#10b981;}
|
||||
button.both{background:#8b5cf6;}
|
||||
button.revision{background:#f59e0b;color:#000;}
|
||||
form{margin-top:1rem;}
|
||||
textarea{
|
||||
width:100%;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
box-sizing:border-box;
|
||||
min-height:80px;
|
||||
}
|
||||
.status{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-top:1rem;
|
||||
}
|
||||
.status.waiting{background:#3f3f46;}
|
||||
.locked{
|
||||
margin-top:1rem;
|
||||
padding:1rem;
|
||||
background:#111827;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
}
|
||||
.locked h2{margin-top:0;color:#fbbf24;}
|
||||
.locked p{margin:.3rem 0;}
|
||||
.lyrics{
|
||||
background:#111827;
|
||||
border:1px solid #374151;
|
||||
border-radius:.5rem;
|
||||
padding:1rem 1.25rem;
|
||||
margin:1rem 0;
|
||||
line-height:1.3;
|
||||
font-size:1rem;
|
||||
}
|
||||
.lyrics h2{
|
||||
margin-top:0;
|
||||
margin-bottom:.5rem;
|
||||
color:#fbbf24;
|
||||
font-size:1.2rem;
|
||||
}
|
||||
.lyrics .section-label{
|
||||
color:#60a5fa;
|
||||
font-weight:700;
|
||||
display:block;
|
||||
margin-top:.5rem;
|
||||
margin-bottom:.1rem;
|
||||
}
|
||||
.lyrics .lyric-line{
|
||||
display:block;
|
||||
line-height:1.3;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎧 Your Custom Theme Song</h1>
|
||||
<p>Hi {{ req.name }}! Listen to both versions and pick the one you want.</p>
|
||||
|
||||
{% if req.suno_lyrics %}
|
||||
<div class="lyrics">
|
||||
<h2>🎵 Song Lyrics</h2>
|
||||
{% for line in req.suno_lyrics.splitlines() %}
|
||||
{% set line = line.strip() %}
|
||||
{% if line and not (line.startswith('[') and line.endswith(']')) %}
|
||||
<span class="lyric-line">{{ line }}</span>
|
||||
{% elif not line %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Version A player -->
|
||||
<div class="player">
|
||||
<h3>Version A</h3>
|
||||
<audio controls controlsList="nodownload" id="player-a"></audio>
|
||||
</div>
|
||||
|
||||
<!-- Version B player -->
|
||||
<div class="player">
|
||||
<h3>Version B</h3>
|
||||
<audio controls controlsList="nodownload" id="player-b"></audio>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// Audio sources are assigned via JS so they do not appear in static HTML source.
|
||||
document.getElementById('player-a').src = "{{ url_for('stream_audio', token=req.player_token, version='a') }}";
|
||||
document.getElementById('player-b').src = "{{ url_for('stream_audio', token=req.player_token, version='b') }}";
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% if req.status == 'revisions_requested' %}
|
||||
<!-- Customer asked for changes; lock the page and tell them to wait -->
|
||||
<div class="locked waiting">
|
||||
<h2>📝 Revision Requested</h2>
|
||||
<p>You asked for changes. We will generate a new version and update this page.</p>
|
||||
<p><strong>Your note:</strong> {{ req.revision_note }}</p>
|
||||
</div>
|
||||
|
||||
{% elif req.status in ['awaiting_payment','paid','delivered'] %}
|
||||
<!-- Customer already picked a version; show their choice and payment instruction -->
|
||||
<div class="locked">
|
||||
<h2>✅ Choice Received</h2>
|
||||
<p>You selected: <strong>{% if req.customer_approved == 'both' %}Both Versions{% else %}Version {{ (req.customer_approved or 'none').upper() }}{% endif %}</strong></p>
|
||||
{% if req.status == 'awaiting_payment' %}
|
||||
<p>Please return to the booth to finalize payment and collect your files.</p>
|
||||
{% elif req.status == 'paid' %}
|
||||
<p>Payment recorded. Your files are being prepared.</p>
|
||||
{% elif req.status == 'delivered' %}
|
||||
<p>Delivered! Check your email for the MP3 attachment(s).</p>
|
||||
{% if req.stems_link %}
|
||||
<p style="margin-top:.8rem">
|
||||
<a href="{{ req.stems_link }}" target="_blank" rel="noopener noreferrer" style="display:inline-block;padding:.7rem 1rem;background:#10b981;color:#000;border-radius:.5rem;text-decoration:none;font-weight:700;">Download stems / extras →</a>
|
||||
</p>
|
||||
<p style="font-size:.9rem;color:#9ca3af;">This link expires 3 months after delivery. Download soon.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- Approval form: customer picks Version A, B, or both -->
|
||||
<form method="POST" action="{{ url_for('approve', token=req.player_token) }}">
|
||||
<input type="hidden" name="choice" id="choice">
|
||||
<div class="actions">
|
||||
<button type="submit" onclick="document.getElementById('choice').value='a'">I want Version A</button>
|
||||
<button type="submit" onclick="document.getElementById('choice').value='b'">I want Version B</button>
|
||||
<button type="submit" class="both" onclick="document.getElementById('choice').value='both'">I want both</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if revisions_left > 0 %}
|
||||
<!-- Revision form: customer asks for changes -->
|
||||
<form method="POST" action="{{ url_for('revise', token=req.player_token) }}">
|
||||
<p style="color:#93c5fd;font-weight:600">Revisions remaining: {{ revisions_left }}</p>
|
||||
<label for="revision_note">Or ask for changes:</label>
|
||||
<textarea id="revision_note" name="revision_note" placeholder="e.g. make the chorus louder, swap a lyric..."></textarea>
|
||||
<div class="actions">
|
||||
<button type="submit" class="revision">Request Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<p style="margin-top:1rem;color:#f87171;font-weight:600">No revisions remaining. Please speak to the booth operator if you need further changes.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
224
templates/request.html
Normal file
224
templates/request.html
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request Your Theme Song</title>
|
||||
<style>
|
||||
/*
|
||||
Customer-facing request page.
|
||||
Designed for convention booth tablets/phones. Shows the banner image and
|
||||
a styled card form for name, email, hobbies, facts, style, vocal gender,
|
||||
and extra requests. Hidden when the booth is marked closed.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#111827 0%,#1e3a8a 50%,#111827 100%);
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{max-width:680px;margin:0 auto;}
|
||||
.banner{
|
||||
width:100%;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
.card{
|
||||
background:rgba(31,41,55,.9);
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.3);
|
||||
border:1px solid rgba(96,165,250,.2);
|
||||
}
|
||||
h1{
|
||||
margin-top:0;
|
||||
color:#60a5fa;
|
||||
text-align:center;
|
||||
font-size:1.7rem;
|
||||
}
|
||||
.subtitle{
|
||||
text-align:center;
|
||||
color:#9ca3af;
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
label{
|
||||
display:block;
|
||||
margin-top:1rem;
|
||||
font-weight:600;
|
||||
color:#93c5fd;
|
||||
}
|
||||
input,textarea{
|
||||
width:100%;
|
||||
padding:.7rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
font:inherit;
|
||||
margin-top:.25rem;
|
||||
}
|
||||
select{
|
||||
width:100%;
|
||||
padding:.7rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
font:inherit;
|
||||
margin-top:.25rem;
|
||||
}
|
||||
input:focus,textarea:focus,select:focus{
|
||||
outline:none;
|
||||
border-color:#60a5fa;
|
||||
box-shadow:0 0 0 3px rgba(96,165,250,.2);
|
||||
}
|
||||
textarea{min-height:90px;resize:vertical;}
|
||||
button{
|
||||
margin-top:1.5rem;
|
||||
width:100%;
|
||||
padding:1rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:linear-gradient(90deg,#3b82f6,#8b5cf6);
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
font-size:1.1rem;
|
||||
cursor:pointer;
|
||||
box-shadow:0 4px 15px rgba(59,130,246,.4);
|
||||
transition:transform .1s,box-shadow .1s;
|
||||
}
|
||||
button:hover{
|
||||
transform:translateY(-2px);
|
||||
box-shadow:0 6px 20px rgba(139,92,246,.5);
|
||||
}
|
||||
.note{
|
||||
margin-top:1rem;
|
||||
font-size:.9rem;
|
||||
color:#9ca3af;
|
||||
text-align:center;
|
||||
}
|
||||
.sparkle{
|
||||
font-size:1.3rem;
|
||||
vertical-align:middle;
|
||||
}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Banner graphic for the booth. Served from /static. -->
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="banner">
|
||||
|
||||
<div class="card">
|
||||
<h1><span class="sparkle">🎵</span> Get Your Custom Theme Song <span class="sparkle">🎶</span></h1>
|
||||
<p class="subtitle">Tell us about yourself and we'll craft a one-of-a-kind song just for you.</p>
|
||||
<p class="subtitle"><a href="{{ url_for('faq') }}" target="_blank" rel="noopener noreferrer">Questions? Read our FAQ →</a> · <a href="{{ url_for('status_lookup') }}" target="_blank" rel="noopener noreferrer">Already ordered? Check status →</a></p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST">
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" value="{{ form.email if form else '' }}" required>
|
||||
|
||||
<label for="name">Name / persona you want in the song</label>
|
||||
<input type="text" id="name" name="name" value="{{ form.name if form else '' }}" required>
|
||||
|
||||
<label for="pronouns">Pronouns <span style="color:#f87171">*</span></label>
|
||||
<select id="pronouns" name="pronouns" required>
|
||||
<option value="" {% if not form or not form.pronouns %}selected{% endif %}>— Select pronouns —</option>
|
||||
<option value="He/Him/His" {% if form and form.pronouns == 'He/Him/His' %}selected{% endif %}>He/Him/His</option>
|
||||
<option value="She/Her/Hers" {% if form and form.pronouns == 'She/Her/Hers' %}selected{% endif %}>She/Her/Hers</option>
|
||||
<option value="They/Them/Their" {% if form and form.pronouns == 'They/Them/Their' %}selected{% endif %}>They/Them/Their</option>
|
||||
</select>
|
||||
|
||||
<label for="hobbies">Hobbies & interests</label>
|
||||
<textarea id="hobbies" name="hobbies" maxlength="2000" placeholder="e.g. rock climbing, retro gaming, sourdough baking">{{ form.hobbies if form else '' }}</textarea>
|
||||
|
||||
<label for="notable_facts">Notable things about you</label>
|
||||
<textarea id="notable_facts" name="notable_facts" maxlength="2000" placeholder="Anything fun, weird, or heroic we should mention">{{ form.notable_facts if form else '' }}</textarea>
|
||||
|
||||
<label for="decade">Decade / era <span style="color:#f87171">*</span></label>
|
||||
<select id="decade" name="decade" required>
|
||||
<option value="" {% if not form or not form.decade %}selected{% endif %}>— Select decade —</option>
|
||||
{% for d in decades %}
|
||||
<option value="{{ d }}" {% if form and form.decade == d %}selected{% endif %}>{{ d }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="basic_style">Basic style <span style="color:#f87171">*</span></label>
|
||||
<select id="basic_style" name="basic_style" required>
|
||||
<option value="" {% if not form or not form.basic_style %}selected{% endif %}>— Select style —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if form and form.basic_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="additional_style">Additional style (optional)</label>
|
||||
<select id="additional_style" name="additional_style">
|
||||
<option value="" {% if not form or not form.additional_style %}selected{% endif %}>— None —</option>
|
||||
{% for g in genres %}
|
||||
<option value="{{ g }}" {% if form and form.additional_style == g %}selected{% endif %}>{{ g }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<p style="font-size:.85rem;color:#9ca3af;margin-top:.3rem">Selected style: <span id="style-preview">{% if form %}{{ form.decade or '' }}{% if form.decade and form.basic_style %}, {% endif %}{{ form.basic_style or '' }}{% if form.basic_style and form.additional_style %}, {% endif %}{{ form.additional_style or '' }}{% else %}—{% endif %}</span></p>
|
||||
|
||||
<script>
|
||||
// Live preview of the combined style string as the customer changes dropdowns.
|
||||
(function(){
|
||||
const decade = document.getElementById('decade');
|
||||
const basic = document.getElementById('basic_style');
|
||||
const additional = document.getElementById('additional_style');
|
||||
const preview = document.getElementById('style-preview');
|
||||
function updatePreview(){
|
||||
const parts = [decade.value, basic.value, additional.value].filter(Boolean);
|
||||
preview.textContent = parts.length ? parts.join(', ') : '—';
|
||||
}
|
||||
decade.addEventListener('change', updatePreview);
|
||||
basic.addEventListener('change', updatePreview);
|
||||
additional.addEventListener('change', updatePreview);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<label for="vocal_gender">Preferred singer voice / gender</label>
|
||||
<select id="vocal_gender" name="vocal_gender">
|
||||
<option value="" {% if not form or not form.vocal_gender %}selected{% endif %}>No preference</option>
|
||||
<option value="female" {% if form and form.vocal_gender == 'female' %}selected{% endif %}>Female</option>
|
||||
<option value="male" {% if form and form.vocal_gender == 'male' %}selected{% endif %}>Male</option>
|
||||
<option value="non-binary" {% if form and form.vocal_gender == 'non-binary' %}selected{% endif %}>Non-binary / Gender-neutral</option>
|
||||
<option value="androgynous" {% if form and form.vocal_gender == 'androgynous' %}selected{% endif %}>Androgynous</option>
|
||||
<option value="other" {% if form and form.vocal_gender == 'other' %}selected{% endif %}>Other (mention in Extra Requests)</option>
|
||||
</select>
|
||||
|
||||
<label for="extra_requests">Anything else you want in the song? (Trollgorithm is very literal, so be careful!)</label>
|
||||
<textarea id="extra_requests" name="extra_requests" maxlength="2000" placeholder="Specific Lyrics, clean/explicit...">{{ form.extra_requests if form else '' }}</textarea>
|
||||
|
||||
<label for="stems_interest" style="display:flex;align-items:center;gap:.5rem;margin-top:1rem;cursor:pointer;">
|
||||
<input type="checkbox" id="stems_interest" name="stems_interest" value="1" {% if form and form.stems_interest %}checked{% endif %} style="width:auto;margin:0">
|
||||
I'm interested in STEMS / multitrack files (if you know, you know)
|
||||
</label>
|
||||
|
||||
<button type="submit">Submit Request</button>
|
||||
</form>
|
||||
<p class="note">Your info is only used to create and deliver your song.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
191
templates/status.html
Normal file
191
templates/status.html
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Check Your Order Status — Trollgorithm Theme Song Booth</title>
|
||||
<style>
|
||||
/*
|
||||
Public order status lookup page.
|
||||
Customers enter their email to see request status, approval choice,
|
||||
and the private player link once two songs have been uploaded.
|
||||
*/
|
||||
*{box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:linear-gradient(135deg,#111827 0%,#1e3a8a 50%,#111827 100%);
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{max-width:680px;margin:0 auto;}
|
||||
.banner{
|
||||
width:100%;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 40px rgba(0,0,0,.4);
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
.card{
|
||||
background:rgba(31,41,55,.9);
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
box-shadow:0 10px 25px rgba(0,0,0,.3);
|
||||
border:1px solid rgba(96,165,250,.2);
|
||||
}
|
||||
h1{
|
||||
margin-top:0;
|
||||
color:#60a5fa;
|
||||
text-align:center;
|
||||
font-size:1.7rem;
|
||||
}
|
||||
.subtitle{
|
||||
text-align:center;
|
||||
color:#9ca3af;
|
||||
margin-bottom:1.5rem;
|
||||
}
|
||||
label{
|
||||
display:block;
|
||||
margin-top:1rem;
|
||||
font-weight:600;
|
||||
color:#93c5fd;
|
||||
}
|
||||
input{
|
||||
width:100%;
|
||||
padding:.7rem;
|
||||
border-radius:.5rem;
|
||||
border:1px solid #374151;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
font:inherit;
|
||||
margin-top:.25rem;
|
||||
}
|
||||
input:focus{
|
||||
outline:none;
|
||||
border-color:#60a5fa;
|
||||
box-shadow:0 0 0 3px rgba(96,165,250,.2);
|
||||
}
|
||||
button{
|
||||
margin-top:1.5rem;
|
||||
width:100%;
|
||||
padding:1rem;
|
||||
border:none;
|
||||
border-radius:.5rem;
|
||||
background:linear-gradient(90deg,#3b82f6,#8b5cf6);
|
||||
color:#fff;
|
||||
font-weight:700;
|
||||
font-size:1.1rem;
|
||||
cursor:pointer;
|
||||
box-shadow:0 4px 15px rgba(59,130,246,.4);
|
||||
transition:transform .1s,box-shadow .1s;
|
||||
}
|
||||
button:hover{
|
||||
transform:translateY(-2px);
|
||||
box-shadow:0 6px 20px rgba(139,92,246,.5);
|
||||
}
|
||||
.flash{
|
||||
padding:.8rem;
|
||||
background:#064e3b;
|
||||
border-radius:.5rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.flash.error{background:#450a0a;}
|
||||
.result{
|
||||
margin-top:1.5rem;
|
||||
}
|
||||
.request{
|
||||
background:#111827;
|
||||
border:1px solid #374151;
|
||||
border-radius:.75rem;
|
||||
padding:1rem;
|
||||
margin-bottom:1rem;
|
||||
}
|
||||
.request h3{margin-top:0;color:#93c5fd;}
|
||||
.status{
|
||||
display:inline-block;
|
||||
padding:.25rem .6rem;
|
||||
border-radius:9999px;
|
||||
font-size:.85rem;
|
||||
font-weight:600;
|
||||
background:#374151;
|
||||
color:#f3f4f6;
|
||||
margin:.3rem 0;
|
||||
}
|
||||
.status.pending,.status.prompt_ready{background:#60a5fa;color:#000;}
|
||||
.status.songs_uploaded{background:#a78bfa;color:#000;}
|
||||
.status.awaiting_payment{background:#f59e0b;color:#000;}
|
||||
.status.paid,.status.delivered{background:#10b981;color:#000;}
|
||||
.status.revisions_requested{background:#f87171;color:#000;}
|
||||
.link-button{
|
||||
display:inline-block;
|
||||
margin-top:.5rem;
|
||||
padding:.6rem 1rem;
|
||||
background:#3b82f6;
|
||||
border-radius:.5rem;
|
||||
color:#fff;
|
||||
text-decoration:none;
|
||||
font-weight:600;
|
||||
}
|
||||
.approved{font-weight:600;color:#fbbf24;}
|
||||
.back{
|
||||
display:block;
|
||||
text-align:center;
|
||||
margin-top:1.5rem;
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
}
|
||||
.back:hover{text-decoration:underline;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="banner">
|
||||
|
||||
<div class="card">
|
||||
<h1>🔍 Check Your Order Status</h1>
|
||||
<p class="subtitle">Enter the email address you used when you requested your song.</p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST">
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" value="{{ email }}" required>
|
||||
<button type="submit">Look Up My Song</button>
|
||||
</form>
|
||||
|
||||
{% if searched %}
|
||||
<div class="result">
|
||||
{% if requests %}
|
||||
{% for req in requests %}
|
||||
<div class="request">
|
||||
<h3>Request #{{ req.id }} — {{ req.name }}</h3>
|
||||
<p><strong>Status:</strong> <span class="status {{ req.status }}">{{ statuses[req.status] }}</span></p>
|
||||
|
||||
{% if req.customer_approved and req.customer_approved != 'none' %}
|
||||
<p class="approved">You selected: {% if req.customer_approved == 'both' %}Both Versions{% else %}Version {{ (req.customer_approved or 'none').upper() }}{% endif %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if req.song_a_path and req.song_b_path %}
|
||||
<p><strong>Your preview link:</strong></p>
|
||||
<a class="link-button" href="{{ url_for('play', token=req.player_token) }}" target="_blank" rel="noopener noreferrer">🎧 Listen & Pick Your Version</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No requests found for that email address. Make sure you used the same email you gave us at the booth.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a class="back" href="{{ url_for('request_form') }}">← Back to the request form</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
44
templates/thanks.html
Normal file
44
templates/thanks.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request Received</title>
|
||||
<style>
|
||||
/*
|
||||
Simple confirmation page shown after a customer submits a request.
|
||||
Gives them a request number they can reference at the booth.
|
||||
*/
|
||||
body{
|
||||
font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
|
||||
background:#111827;
|
||||
color:#f3f4f6;
|
||||
margin:0;
|
||||
padding:1rem;
|
||||
line-height:1.5;
|
||||
display:flex;
|
||||
justify-content:center;
|
||||
align-items:center;
|
||||
min-height:100vh;
|
||||
}
|
||||
.container{
|
||||
max-width:640px;
|
||||
margin:0 auto;
|
||||
background:#1f2937;
|
||||
padding:1.5rem;
|
||||
border-radius:1rem;
|
||||
text-align:center;
|
||||
}
|
||||
h1{color:#34d399;}
|
||||
.note{font-size:.9rem;color:#9ca3af;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✅ Request Received</h1>
|
||||
<p>Thanks, {{ req.name }}! We'll craft your song and email you a link when it's ready.</p>
|
||||
<p><strong>Your request number:</strong> #{{ req.id }}</p>
|
||||
<p class="note">Bring this number to the booth if you want to check on progress.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue