Add comprehensive comments to all source files and expand documentation
This commit is contained in:
parent
9e532a1920
commit
b4f0222bd5
16 changed files with 1230 additions and 552 deletions
|
|
@ -1,110 +1,163 @@
|
|||
<!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>
|
||||
<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}
|
||||
.filters{margin-bottom:1rem}
|
||||
.filters a{color:#93c5fd;text-decoration:none;margin-right:1rem}
|
||||
.filters a.active{font-weight:bold;color:#fff}
|
||||
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}
|
||||
.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}
|
||||
.topbar{float:right;display:flex;gap:.75rem;align-items:center}
|
||||
.topbar form{display:inline}
|
||||
.topbar button.reset-sm{
|
||||
background:#dc2626;
|
||||
color:#fff;
|
||||
border:none;
|
||||
border-radius:.4rem;
|
||||
padding:.4rem .8rem;
|
||||
font-weight:600;
|
||||
cursor:pointer;
|
||||
font-size:.9rem;
|
||||
}
|
||||
.topbar button.reset-sm:hover{background:#b91c1c}
|
||||
.flash{padding:.8rem;background:#064e3b;border-radius:.5rem;margin-bottom:1rem}
|
||||
.flash.error{background:#450a0a}
|
||||
.reset-box{display:none}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Dashboard</title>
|
||||
<style>
|
||||
/*
|
||||
Operator dashboard queue.
|
||||
Shows all requests in a table, with status filters,
|
||||
per-row Open/Delete actions, and a topbar Reset System button.
|
||||
*/
|
||||
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;}
|
||||
.filters{margin-bottom:1rem;}
|
||||
.filters a{
|
||||
color:#93c5fd;
|
||||
text-decoration:none;
|
||||
margin-right:1rem;
|
||||
}
|
||||
.filters a.active{font-weight:bold;color:#fff;}
|
||||
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;}
|
||||
.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 Reset System button and Log out link */
|
||||
.topbar{
|
||||
float:right;
|
||||
display:flex;
|
||||
gap:.75rem;
|
||||
align-items:center;
|
||||
}
|
||||
.topbar form{display:inline;}
|
||||
.topbar button.reset-sm{
|
||||
background:#dc2626;
|
||||
color:#fff;
|
||||
border:none;
|
||||
border-radius:.4rem;
|
||||
padding:.4rem .8rem;
|
||||
font-weight:600;
|
||||
cursor:pointer;
|
||||
font-size:.9rem;
|
||||
}
|
||||
.topbar button.reset-sm:hover{background:#b91c1c;}
|
||||
|
||||
/* Hidden legacy reset box (kept CSS class for compatibility, not displayed) */
|
||||
.reset-box{display:none;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="topbar">
|
||||
<form method="POST" action="{{ url_for('admin_reset_system') }}" onsubmit="return confirm('ARE YOU SURE? This will delete ALL requests and ALL uploaded files. This cannot be undone.')">
|
||||
<button type="submit" class="reset-sm">Reset System</button>
|
||||
</form>
|
||||
<a href="{{ url_for('admin_logout') }}" class="logout">Log out</a>
|
||||
</div>
|
||||
<h1>Theme Song Booth — Admin Dashboard</h1>
|
||||
<div class="container">
|
||||
<!-- Topbar: Reset System button and Log out link -->
|
||||
<div class="topbar">
|
||||
<form method="POST" action="{{ url_for('admin_reset_system') }}" onsubmit="return confirm('ARE YOU SURE? This will delete ALL requests and ALL uploaded files. This cannot be undone.')">
|
||||
<button type="submit" class="reset-sm">Reset System</button>
|
||||
</form>
|
||||
<a href="{{ url_for('admin_logout') }}" class="logout">Log out</a>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
<h1>Theme Song Booth — Admin Dashboard</h1>
|
||||
|
||||
<div class="filters">
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="{% if not current_status %}active{% endif %}">All</a>
|
||||
{% for key,label in statuses.items() %}
|
||||
<a href="{{ url_for('admin_dashboard', status=key) }}" class="{% if current_status == key %}active{% endif %}">{{ label }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<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 != 'none' %}{{ r.customer_approved.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>
|
||||
<!-- Status filter links -->
|
||||
<div class="filters">
|
||||
<a href="{{ url_for('admin_dashboard') }}" class="{% if not current_status %}active{% endif %}">All</a>
|
||||
{% for key,label in statuses.items() %}
|
||||
<a href="{{ url_for('admin_dashboard', status=key) }}" class="{% if current_status == key %}active{% endif %}">{{ label }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="reset-box">
|
||||
<h2>⚠️ Reset System</h2>
|
||||
<p>Use this once at the start of an event to clear all orders and uploaded files.</p>
|
||||
<form method="POST" action="{{ url_for('admin_reset_system') }}" onsubmit="return confirm('ARE YOU SURE? This will delete ALL requests and ALL uploaded files. This cannot be undone.')">
|
||||
<button type="submit">Reset System</button>
|
||||
</form>
|
||||
</div>
|
||||
</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 != 'none' %}{{ r.customer_approved.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>
|
||||
|
|
|
|||
|
|
@ -1,30 +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>
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,212 +1,283 @@
|
|||
<!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>
|
||||
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:900px;margin:0 auto}
|
||||
h1,h2{color:#60a5fa}
|
||||
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:120px}
|
||||
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}
|
||||
.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}
|
||||
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}
|
||||
.info-grid div{background:#111827;padding:.6rem;border-radius:.5rem}
|
||||
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24}
|
||||
.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}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Request #{{ req.id }} — Admin</title>
|
||||
<style>
|
||||
/*
|
||||
Single-request admin detail page.
|
||||
Four sections:
|
||||
1. Generate Suno prompt (copy to Hermes, paste response, extract/copy)
|
||||
2. Upload Songs (with status badges)
|
||||
3. Notify Customer (preview email status + send button)
|
||||
4. Payment & Delivery (approval status + deliver button)
|
||||
*/
|
||||
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:900px;margin:0 auto;}
|
||||
h1,h2{color:#60a5fa;}
|
||||
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:120px;}
|
||||
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;}
|
||||
.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;}
|
||||
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem;}
|
||||
.info-grid div{
|
||||
background:#111827;
|
||||
padding:.6rem;
|
||||
border-radius:.5rem;
|
||||
}
|
||||
.approved-box{font-size:1.2rem;font-weight:bold;color:#fbbf24;}
|
||||
.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;}
|
||||
</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="container">
|
||||
<p><a href="{{ url_for('admin_dashboard') }}">← Dashboard</a></p>
|
||||
<h1>Request #{{ req.id }} — {{ req.name }}</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="flash {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="section">
|
||||
<h2>Customer Info</h2>
|
||||
<div class="info-grid">
|
||||
<div><strong>Email:</strong> {{ req.email }}</div>
|
||||
<div><strong>Status:</strong> {{ statuses[req.status] }}</div>
|
||||
</div>
|
||||
<p><strong>Hobbies:</strong><br>{{ req.hobbies or '-' }}</p>
|
||||
<p><strong>Notable facts:</strong><br>{{ req.notable_facts or '-' }}</p>
|
||||
<p><strong>Style / genre:</strong><br>{{ req.style_genre or '-' }}</p>
|
||||
<p><strong>Extra requests:</strong><br>{{ req.extra_requests or '-' }}</p>
|
||||
</div>
|
||||
<!-- Section 1: Customer info summary -->
|
||||
<div class="section">
|
||||
<h2>Customer Info</h2>
|
||||
<div class="info-grid">
|
||||
<div><strong>Email:</strong> {{ req.email }}</div>
|
||||
<div><strong>Status:</strong> {{ statuses[req.status] }}</div>
|
||||
</div>
|
||||
<p><strong>Hobbies:</strong><br>{{ req.hobbies or '-' }}</p>
|
||||
<p><strong>Notable facts:</strong><br>{{ req.notable_facts or '-' }}</p>
|
||||
<p><strong>Style / genre:</strong><br>{{ req.style_genre or '-' }}</p>
|
||||
<p><strong>Extra requests:</strong><br>{{ req.extra_requests or '-' }}</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. Generate Suno Prompt</h2>
|
||||
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
|
||||
<p class="copy-hint">Paste Hermes response into the box below. It will auto-extract Title, Style, and Lyrics.</p>
|
||||
<!-- Section 2: Generate Suno prompt -->
|
||||
<div class="section">
|
||||
<h2>1. Generate Suno Prompt</h2>
|
||||
<button type="button" onclick="copyPromptForHermes()">Copy customer info for Hermes</button>
|
||||
<p class="copy-hint">Paste Hermes response into the box below. It will auto-extract Title, Style, and Lyrics.</p>
|
||||
|
||||
<label for="hermes_response">Paste Hermes response here</label>
|
||||
<textarea id="hermes_response" rows="14" placeholder="Title: ...\nStyle: ...\nLyrics: ..."></textarea>
|
||||
<label for="hermes_response">Paste Hermes response here</label>
|
||||
<textarea id="hermes_response" rows="14" placeholder="Title: ...\nStyle: ...\nLyrics: ..."></textarea>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick="extractHermesResponse()">Extract Title / Style / Lyrics</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick="extractHermesResponse()">Extract Title / Style / Lyrics</button>
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_prompt">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="save_prompt">
|
||||
|
||||
<label for="suno_title">Suggested Title</label>
|
||||
<input type="text" id="suno_title" name="suno_title" value="{{ req.suno_title or '' }}" readonly>
|
||||
<label for="suno_title">Suggested Title</label>
|
||||
<input type="text" id="suno_title" name="suno_title" value="{{ req.suno_title or '' }}" readonly>
|
||||
|
||||
<label for="suno_style">Suno Style</label>
|
||||
<textarea id="suno_style" name="suno_style">{{ req.suno_style or '' }}</textarea>
|
||||
<label for="suno_style">Suno Style</label>
|
||||
<textarea id="suno_style" name="suno_style">{{ req.suno_style or '' }}</textarea>
|
||||
|
||||
<label for="suno_lyrics">Suno Lyrics (with metatags)</label>
|
||||
<textarea id="suno_lyrics" name="suno_lyrics" rows="12">{{ req.suno_lyrics or '' }}</textarea>
|
||||
<label for="suno_lyrics">Suno Lyrics (with metatags)</label>
|
||||
<textarea id="suno_lyrics" name="suno_lyrics" rows="12">{{ req.suno_lyrics or '' }}</textarea>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
|
||||
<button type="submit">Save Prompt</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_title', 'Title')">Copy Title</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_style', 'Style')">Copy Style</button>
|
||||
<button type="button" class="secondary" onclick="copyToClipboard('suno_lyrics', 'Lyrics')">Copy Lyrics</button>
|
||||
<button type="submit">Save Prompt</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Upload Songs</h2>
|
||||
<p>
|
||||
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 %}
|
||||
</p>
|
||||
<p>
|
||||
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 %}
|
||||
</p>
|
||||
<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>
|
||||
<!-- Section 3: Upload songs -->
|
||||
<div class="section">
|
||||
<h2>2. Upload Songs</h2>
|
||||
<p>
|
||||
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 %}
|
||||
</p>
|
||||
<p>
|
||||
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 %}
|
||||
</p>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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.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>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="mark_paid_deliver">
|
||||
<label for="square_payment_ref">Square Payment Reference</label>
|
||||
<input type="text" id="square_payment_ref" name="square_payment_ref" placeholder="e.g. sq0idp-... or receipt number">
|
||||
<div class="actions">
|
||||
<button type="submit" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Section 4: 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>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyPromptForHermes() {
|
||||
const data = {
|
||||
name: {{ req.name | tojson }},
|
||||
hobbies: {{ req.hobbies | tojson }},
|
||||
notable_facts: {{ req.notable_facts | tojson }},
|
||||
style_genre: {{ req.style_genre | tojson }},
|
||||
extra_requests: {{ req.extra_requests | tojson }}
|
||||
};
|
||||
const text = `Please write a Suno Custom Mode prompt for this customer. Suggest a song title too.\n\nCustomer data:\nName: ${data.name}\nHobbies: ${data.hobbies || '-'}\nNotable facts: ${data.notable_facts || '-'}\nStyle / genre: ${data.style_genre || '-'}\nExtra requests: ${data.extra_requests || '-'}\n\nReply with exactly this format:\n\nTitle: [suggested title]\n\nStyle:\n[the style description here]\n\nLyrics:\n[the lyrics here, with metatags]`;
|
||||
navigator.clipboard.writeText(text).then(() => alert("Copied! Paste Hermes response into the box above and click Extract."));
|
||||
}
|
||||
<!-- Section 5: 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.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>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="mark_paid_deliver">
|
||||
<label for="square_payment_ref">Square Payment Reference</label>
|
||||
<input type="text" id="square_payment_ref" name="square_payment_ref" placeholder="e.g. sq0idp-... or receipt number">
|
||||
<div class="actions">
|
||||
<button type="submit" class="success" {% if req.customer_approved == 'none' %}disabled{% endif %}>Mark Paid & Deliver</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
function extractHermesResponse() {
|
||||
const raw = document.getElementById('hermes_response').value;
|
||||
if (!raw.trim()) {
|
||||
alert("Paste Hermes response first.");
|
||||
return;
|
||||
}
|
||||
<script>
|
||||
/*
|
||||
Client-side helpers for the admin detail page.
|
||||
- copyPromptForHermes(): builds a plain-text prompt and copies it.
|
||||
- extractHermesResponse(): parses Title/Style/Lyrics from pasted text.
|
||||
- copyToClipboard(): copies a field to the clipboard for pasting into Suno.
|
||||
*/
|
||||
|
||||
const titleMatch = raw.match(/^[\s]*Title:\s*([\s\S]*?)(?=\n\s*Style:|\n\s*Lyrics:|$)/im);
|
||||
const styleMatch = raw.match(/^[\s]*Style:\s*([\s\S]*?)(?=\n\s*Lyrics:|$)/im);
|
||||
const lyricsMatch = raw.match(/^[\s]*Lyrics:\s*([\s\S]*?)$/im);
|
||||
function copyPromptForHermes() {
|
||||
const data = {
|
||||
name: {{ req.name | tojson }},
|
||||
hobbies: {{ req.hobbies | tojson }},
|
||||
notable_facts: {{ req.notable_facts | tojson }},
|
||||
style_genre: {{ req.style_genre | tojson }},
|
||||
extra_requests: {{ req.extra_requests | tojson }}
|
||||
};
|
||||
const text = `Please write a Suno Custom Mode prompt for this customer. Suggest a song title too.\n\nCustomer data:\nName: ${data.name}\nHobbies: ${data.hobbies || '-'}\nNotable facts: ${data.notable_facts || '-'}\nStyle / genre: ${data.style_genre || '-'}\nExtra requests: ${data.extra_requests || '-'}\n\nReply with exactly this format:\n\nTitle: [suggested title]\n\nStyle:\n[the style description here]\n\nLyrics:\n[the lyrics here, with metatags]`;
|
||||
navigator.clipboard.writeText(text).then(() => alert("Copied! Paste Hermes response into the box above and click Extract."));
|
||||
}
|
||||
|
||||
if (titleMatch) {
|
||||
document.getElementById('suno_title').value = titleMatch[1].trim();
|
||||
}
|
||||
if (styleMatch) {
|
||||
document.getElementById('suno_style').value = styleMatch[1].trim();
|
||||
}
|
||||
if (lyricsMatch) {
|
||||
document.getElementById('suno_lyrics').value = lyricsMatch[1].trim();
|
||||
}
|
||||
function extractHermesResponse() {
|
||||
const raw = document.getElementById('hermes_response').value;
|
||||
if (!raw.trim()) {
|
||||
alert("Paste Hermes response first.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!styleMatch && !lyricsMatch) {
|
||||
alert("Could not find Style or Lyrics sections. Make sure the response uses the exact format.");
|
||||
} else {
|
||||
alert("Title, Style, and Lyrics extracted. Use Copy buttons to paste into Suno.");
|
||||
}
|
||||
}
|
||||
const titleMatch = raw.match(/^[\s]*Title:\s*([\s\S]*?)(?=\n\s*Style:|\n\s*Lyrics:|$)/im);
|
||||
const styleMatch = raw.match(/^[\s]*Style:\s*([\s\S]*?)(?=\n\s*Lyrics:|$)/im);
|
||||
const lyricsMatch = raw.match(/^[\s]*Lyrics:\s*([\s\S]*?)$/im);
|
||||
|
||||
function copyToClipboard(elementId, label) {
|
||||
const el = document.getElementById(elementId);
|
||||
el.select();
|
||||
el.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(el.value).then(() => alert(label + " copied!"));
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
if (titleMatch) {
|
||||
document.getElementById('suno_title').value = titleMatch[1].trim();
|
||||
}
|
||||
if (styleMatch) {
|
||||
document.getElementById('suno_style').value = styleMatch[1].trim();
|
||||
}
|
||||
if (lyricsMatch) {
|
||||
document.getElementById('suno_lyrics').value = lyricsMatch[1].trim();
|
||||
}
|
||||
|
||||
if (!styleMatch && !lyricsMatch) {
|
||||
alert("Could not find Style or Lyrics sections. Make sure the response uses the exact format.");
|
||||
} else {
|
||||
alert("Title, Style, and Lyrics extracted. Use Copy buttons to paste into Suno.");
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(elementId, label) {
|
||||
const el = document.getElementById(elementId);
|
||||
el.select();
|
||||
el.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(el.value).then(() => alert(label + " copied!"));
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,68 +1,125 @@
|
|||
<!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>
|
||||
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.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}
|
||||
</style>
|
||||
<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 and a revision note form.
|
||||
*/
|
||||
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.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;}
|
||||
</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>
|
||||
<div class="container">
|
||||
<h1>🎧 Your Custom Theme Song</h1>
|
||||
<p>Hi {{ req.name }}! Listen to both versions and pick the one you want.</p>
|
||||
|
||||
<div class="player">
|
||||
<h3>Version A</h3>
|
||||
<audio controls src="{{ url_for('audio', token=req.player_token, version='a') }}"></audio>
|
||||
</div>
|
||||
<!-- Version A player -->
|
||||
<div class="player">
|
||||
<h3>Version A</h3>
|
||||
<audio controls src="{{ url_for('audio', token=req.player_token, version='a') }}"></audio>
|
||||
</div>
|
||||
|
||||
<div class="player">
|
||||
<h3>Version B</h3>
|
||||
<audio controls src="{{ url_for('audio', token=req.player_token, version='b') }}"></audio>
|
||||
</div>
|
||||
<!-- Version B player -->
|
||||
<div class="player">
|
||||
<h3>Version B</h3>
|
||||
<audio controls src="{{ url_for('audio', token=req.player_token, version='b') }}"></audio>
|
||||
</div>
|
||||
|
||||
{% if req.status in ['songs_uploaded','awaiting_payment','paid','delivered'] %}
|
||||
<form method="POST" action="{{ url_for('approve', token=req.player_token) }}">
|
||||
<input type="hidden" name="choice" id="choice">
|
||||
<div class="actions">
|
||||
<button type="submit" class="{% if req.customer_approved == 'a' %}selected{% endif %}" onclick="document.getElementById('choice').value='a'">I want Version A</button>
|
||||
<button type="submit" class="{% if req.customer_approved == 'b' %}selected{% endif %}" onclick="document.getElementById('choice').value='b'">I want Version B</button>
|
||||
<button type="submit" class="both {% if req.customer_approved == 'both' %}selected{% endif %}" onclick="document.getElementById('choice').value='both'">I want both</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if req.status in ['songs_uploaded','awaiting_payment','paid','delivered'] %}
|
||||
<!-- Approval form: customer picks 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" class="{% if req.customer_approved == 'a' %}selected{% endif %}" onclick="document.getElementById('choice').value='a'">I want Version A</button>
|
||||
<button type="submit" class="{% if req.customer_approved == 'b' %}selected{% endif %}" onclick="document.getElementById('choice').value='b'">I want Version B</button>
|
||||
<button type="submit" class="both {% if req.customer_approved == 'both' %}selected{% endif %}" onclick="document.getElementById('choice').value='both'">I want both</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="{{ url_for('revise', token=req.player_token) }}">
|
||||
<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>
|
||||
{% endif %}
|
||||
<!-- Revision form: customer asks for changes -->
|
||||
<form method="POST" action="{{ url_for('revise', token=req.player_token) }}">
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
{% if req.status == 'awaiting_payment' %}
|
||||
<div class="status waiting"><strong>Thanks for choosing {{ req.customer_approved.upper() }}!</strong> Please head to the booth to finalize payment and collect your files.</div>
|
||||
{% endif %}
|
||||
{% if req.status == 'awaiting_payment' %}
|
||||
<!-- Shown after customer approves a version -->
|
||||
<div class="status waiting">
|
||||
<strong>Thanks for choosing {{ req.customer_approved.upper() }}!</strong> Please head to the booth to finalize payment and collect your files.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if req.status == 'delivered' %}
|
||||
<div class="status"><strong>Delivered! ✅</strong> Check your email for the MP3 attachment(s).</div>
|
||||
{% endif %}
|
||||
{% if req.status == 'delivered' %}
|
||||
<!-- Shown after operator marks paid and delivers -->
|
||||
<div class="status">
|
||||
<strong>Delivered! ✅</strong> Check your email for the MP3 attachment(s).
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,136 +1,134 @@
|
|||
<!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>
|
||||
*{
|
||||
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;
|
||||
}
|
||||
input:focus,textarea: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;
|
||||
}
|
||||
</style>
|
||||
<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 to look fun and inviting on a convention booth tablet or phone.
|
||||
Uses a gradient background, banner image, and a styled card form.
|
||||
*/
|
||||
*{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;
|
||||
}
|
||||
input:focus,textarea: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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img src="{{ url_for('static', filename='Trollgorithm_booth.jpg') }}" alt="Trollgorithm Theme Song Booth" class="banner">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<form method="POST">
|
||||
<label for="name">Name / persona you want in the song</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
<form method="POST">
|
||||
<label for="name">Name / persona you want in the song</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
|
||||
<label for="hobbies">Hobbies & interests</label>
|
||||
<textarea id="hobbies" name="hobbies" placeholder="e.g. rock climbing, retro gaming, sourdough baking"></textarea>
|
||||
<label for="hobbies">Hobbies & interests</label>
|
||||
<textarea id="hobbies" name="hobbies" placeholder="e.g. rock climbing, retro gaming, sourdough baking"></textarea>
|
||||
|
||||
<label for="notable_facts">Notable things about you</label>
|
||||
<textarea id="notable_facts" name="notable_facts" placeholder="Anything fun, weird, or heroic we should mention"></textarea>
|
||||
<label for="notable_facts">Notable things about you</label>
|
||||
<textarea id="notable_facts" name="notable_facts" placeholder="Anything fun, weird, or heroic we should mention"></textarea>
|
||||
|
||||
<label for="style_genre">Style / genre / mood</label>
|
||||
<input type="text" id="style_genre" name="style_genre" placeholder="e.g. 80s power ballad, cinematic orchestral, lo-fi synthwave">
|
||||
<label for="style_genre">Style / genre / mood</label>
|
||||
<input type="text" id="style_genre" name="style_genre" placeholder="e.g. 80s power ballad, cinematic orchestral, lo-fi synthwave">
|
||||
|
||||
<label for="extra_requests">Anything else you want in the song?</label>
|
||||
<textarea id="extra_requests" name="extra_requests" placeholder="Specific lyrics, vibe, clean/explicit, vocal gender..."></textarea>
|
||||
<label for="extra_requests">Anything else you want in the song?</label>
|
||||
<textarea id="extra_requests" name="extra_requests" placeholder="Specific lyrics, vibe, clean/explicit, vocal gender..."></textarea>
|
||||
|
||||
<button type="submit">Submit Request</button>
|
||||
</form>
|
||||
<p class="note">Your info is only used to create and deliver your song.</p>
|
||||
<button type="submit">Submit Request</button>
|
||||
</form>
|
||||
<p class="note">Your info is only used to create and deliver your song.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,22 +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>
|
||||
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;text-align:center}
|
||||
h1{color:#34d399}
|
||||
.token{font-family:monospace;background:#111827;padding:.6rem;border-radius:.5rem;word-break:break-all}
|
||||
</style>
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
|||
Reference in a new issue