anicove / api /templates /shared /bug_report.html
mwask's picture
bug reoprt section
0e74473
Raw
History Blame Contribute Delete
25.6 kB
{% extends "shared/base.html" %}
{% block title %}Report an Issue - AniCove{% endblock %}
{% block meta_description %}Report bugs, issues, or request features on AniCove.{% endblock %}
{% block extra_css %}
<style>
:root {
--bug-bg: #0a0a0a;
--bug-surface: #121212;
--bug-border: #1e1e1e;
--bug-text: #e0e0e0;
--bug-muted: #777;
--bug-accent: #3b82f6;
--bug-accent-glow: rgba(59,130,246,0.25);
--bug-success: #22c55e;
--bug-warning: #f59e0b;
--bug-error: #ef4444;
}
.bug-page {
max-width: 800px;
margin: 0 auto;
padding: 32px 20px;
}
.bug-header {
margin-bottom: 32px;
}
.bug-header-icon {
width: 48px;
height: 48px;
border-radius: 14px;
background: rgba(239, 68, 68, 0.15);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
color: #ef4444;
}
.bug-header h1 {
font-size: 1.6rem;
font-weight: 700;
margin: 0 0 6px 0;
color: #fff;
}
.bug-header p {
font-size: 0.9rem;
color: var(--bug-muted);
margin: 0;
line-height: 1.5;
}
/* Form */
.bug-form {
background: var(--bug-surface);
border: 1px solid var(--bug-border);
border-radius: 12px;
padding: 24px;
margin-bottom: 28px;
}
.bug-form-group {
margin-bottom: 18px;
}
.bug-form-label {
display: block;
font-size: 0.82rem;
font-weight: 600;
color: #bbb;
margin-bottom: 6px;
}
.bug-form-label .required {
color: #ef4444;
margin-left: 2px;
}
.bug-form-input,
.bug-form-textarea {
width: 100%;
padding: 10px 14px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--bug-border);
border-radius: 8px;
color: #fff;
font-size: 0.9rem;
transition: border-color 0.2s, box-shadow 0.2s;
font-family: inherit;
box-sizing: border-box;
}
.bug-form-input:focus,
.bug-form-textarea:focus {
outline: none;
border-color: var(--bug-accent);
box-shadow: 0 0 0 3px var(--bug-accent-glow);
}
.bug-form-textarea {
min-height: 120px;
resize: vertical;
}
.bug-form-hint {
font-size: 0.75rem;
color: var(--bug-muted);
margin-top: 4px;
}
/* Category pills */
.bug-category-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.bug-category-pill {
padding: 8px 18px;
border-radius: 999px;
border: 1px solid var(--bug-border);
background: rgba(0,0,0,0.2);
color: #999;
font-size: 0.82rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.bug-category-pill:hover {
border-color: rgba(59,130,246,0.3);
color: #fff;
}
.bug-category-pill.active {
border-color: var(--bug-accent);
background: rgba(59,130,246,0.15);
color: var(--bug-accent);
}
.bug-category-pill[data-category="bug"].active {
border-color: #ef4444;
background: rgba(239,68,68,0.15);
color: #ef4444;
}
.bug-category-pill[data-category="feature"].active {
border-color: #22c55e;
background: rgba(34,197,94,0.15);
color: #22c55e;
}
.bug-category-pill[data-category="other"].active {
border-color: #f59e0b;
background: rgba(245,158,11,0.15);
color: #f59e0b;
}
/* Submit button */
.bug-submit-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
background: var(--bug-accent);
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s, transform 0.15s, box-shadow 0.2s;
font-family: inherit;
}
.bug-submit-btn:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 15px var(--bug-accent-glow);
}
.bug-submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Success message */
.bug-success {
display: none;
background: rgba(34,197,94,0.1);
border: 1px solid rgba(34,197,94,0.25);
border-radius: 10px;
padding: 16px 20px;
margin-bottom: 20px;
align-items: center;
gap: 12px;
}
.bug-success.visible {
display: flex;
}
.bug-success-icon {
color: var(--bug-success);
flex-shrink: 0;
}
.bug-success-text {
color: #bbb;
font-size: 0.9rem;
line-height: 1.5;
}
.bug-success-text strong {
color: #fff;
}
/* Error */
.bug-error {
display: none;
background: rgba(239,68,68,0.1);
border: 1px solid rgba(239,68,68,0.25);
border-radius: 8px;
padding: 10px 14px;
margin-bottom: 14px;
color: #fca5a5;
font-size: 0.85rem;
}
.bug-error.visible {
display: block;
}
/* My reports section */
.bug-my-reports {
background: var(--bug-surface);
border: 1px solid var(--bug-border);
border-radius: 12px;
overflow: hidden;
}
.bug-my-reports-header {
padding: 16px 20px;
border-bottom: 1px solid var(--bug-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.bug-my-reports-header h2 {
font-size: 1rem;
font-weight: 600;
margin: 0;
color: #fff;
}
.bug-my-reports-count {
font-size: 0.8rem;
color: var(--bug-muted);
background: rgba(255,255,255,0.05);
padding: 2px 10px;
border-radius: 999px;
}
.bug-report-item {
border-bottom: 1px solid rgba(255,255,255,0.04);
transition: background 0.15s;
}
.bug-report-item:last-child {
border-bottom: none;
}
.bug-report-summary {
padding: 16px 20px;
cursor: pointer;
display: flex;
align-items: flex-start;
gap: 10px;
transition: background 0.15s;
}
.bug-report-summary:hover {
background: rgba(255,255,255,0.03);
}
.bug-report-expand-icon {
flex-shrink: 0;
color: #555;
transition: transform 0.2s;
margin-top: 4px;
}
.bug-report-item.expanded .bug-report-expand-icon {
transform: rotate(90deg);
}
.bug-report-summary-content {
flex: 1;
min-width: 0;
}
.bug-report-title {
font-size: 0.9rem;
font-weight: 600;
color: #ddd;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.bug-report-body-preview {
font-size: 0.82rem;
color: var(--bug-muted);
line-height: 1.5;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.bug-report-meta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.bug-status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.7rem;
font-weight: 600;
padding: 2px 10px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.bug-status-badge.open {
background: rgba(239,68,68,0.12);
color: #ef4444;
}
.bug-status-badge.in_progress {
background: rgba(59,130,246,0.12);
color: var(--bug-accent);
}
.bug-status-badge.resolved {
background: rgba(34,197,94,0.12);
color: var(--bug-success);
}
.bug-status-badge.closed {
background: rgba(255,255,255,0.05);
color: var(--bug-muted);
}
.bug-report-date {
font-size: 0.75rem;
color: #555;
}
.bug-report-reply-count {
font-size: 0.75rem;
color: var(--bug-muted);
display: flex;
align-items: center;
gap: 4px;
}
.bug-report-reply-count svg {
width: 12px;
height: 12px;
}
/* Expanded report detail */
.bug-report-detail {
display: none;
padding: 0 20px 16px;
}
.bug-report-item.expanded .bug-report-detail {
display: block;
}
.bug-report-full-body {
background: rgba(0,0,0,0.2);
border: 1px solid rgba(255,255,255,0.04);
border-radius: 8px;
padding: 12px 14px;
font-size: 0.85rem;
color: #bbb;
line-height: 1.6;
white-space: pre-wrap;
margin-bottom: 12px;
}
/* Conversation thread */
.bug-thread {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.bug-thread-message {
display: flex;
gap: 10px;
align-items: flex-start;
}
.bug-thread-message.admin {
flex-direction: row-reverse;
}
.bug-thread-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
flex-shrink: 0;
background: #222;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
color: #fff;
}
.bug-thread-avatar.admin-avatar {
background: var(--bug-accent);
}
.bug-thread-avatar.user-avatar {
background: #555;
}
.bug-thread-bubble {
max-width: 80%;
padding: 10px 14px;
border-radius: 12px;
font-size: 0.83rem;
line-height: 1.5;
position: relative;
}
.bug-thread-message.user .bug-thread-bubble {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.06);
color: #ddd;
border-bottom-left-radius: 4px;
}
.bug-thread-message.admin .bug-thread-bubble {
background: rgba(59,130,246,0.1);
border: 1px solid rgba(59,130,246,0.15);
color: #cbd5e1;
border-bottom-right-radius: 4px;
text-align: right;
}
.bug-thread-bubble .bubble-meta {
font-size: 0.65rem;
color: #555;
margin-top: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.bug-thread-message.admin .bubble-meta {
justify-content: flex-end;
}
.bug-thread-badge {
font-size: 0.6rem;
font-weight: 600;
padding: 1px 6px;
border-radius: 4px;
text-transform: uppercase;
background: rgba(59,130,246,0.15);
color: var(--bug-accent);
}
.bug-thread-badge.broadcast {
background: rgba(245,158,11,0.15);
color: #f59e0b;
}
/* User reply form in expanded report */
.bug-thread-reply {
display: flex;
gap: 8px;
align-items: flex-end;
}
.bug-thread-reply-input {
flex: 1;
padding: 8px 12px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--bug-border);
border-radius: 8px;
color: #fff;
font-size: 0.83rem;
font-family: inherit;
resize: vertical;
min-height: 36px;
}
.bug-thread-reply-input:focus {
outline: none;
border-color: var(--bug-accent);
}
.bug-thread-reply-btn {
padding: 8px 14px;
background: var(--bug-accent);
color: #fff;
border: none;
border-radius: 8px;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
font-family: inherit;
white-space: nowrap;
transition: background 0.2s;
}
.bug-thread-reply-btn:hover {
background: #2563eb;
}
.bug-report-empty {
padding: 40px 20px;
text-align: center;
color: var(--bug-muted);
font-size: 0.85rem;
}
.bug-report-empty-icon {
font-size: 2rem;
margin-bottom: 8px;
opacity: 0.5;
}
.bug-loading {
padding: 40px;
text-align: center;
color: var(--bug-muted);
}
.bug-spinner {
display: inline-block;
width: 24px;
height: 24px;
border: 2px solid rgba(255,255,255,0.1);
border-top-color: var(--bug-accent);
border-radius: 50%;
animation: bugSpin 0.6s linear infinite;
}
@keyframes bugSpin {
to { transform: rotate(360deg); }
}
@media (max-width: 600px) {
.bug-page {
padding: 20px 14px;
}
.bug-form {
padding: 18px;
}
}
</style>
{% endblock %}
{% block content %}
<div class="bug-page">
<div class="bug-header">
<div class="bug-header-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
<line x1="4" y1="22" x2="4" y2="15"></line>
</svg>
</div>
<h1>Report an Issue</h1>
<p>Found a bug, something not working, or have a feature idea? Let us know and we'll look into it.</p>
</div>
<!-- Success Message -->
<div class="bug-success" id="bug-success">
<div class="bug-success-icon">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<div class="bug-success-text">
<strong>Report submitted!</strong> Thank you for your feedback. We'll review it and get back to you if needed.
</div>
</div>
<!-- Error Message -->
<div class="bug-error" id="bug-error"></div>
{% if session.get('username') %}
<!-- Submission Form -->
<form class="bug-form" id="bug-report-form">
<div class="bug-form-group">
<label class="bug-form-label">
Category <span class="required">*</span>
</label>
<div class="bug-category-group" id="bug-category-group">
<button type="button" class="bug-category-pill active" data-category="bug" onclick="selectCategory(this)">
🐛 Bug Report
</button>
<button type="button" class="bug-category-pill" data-category="feature" onclick="selectCategory(this)">
💡 Feature Request
</button>
<button type="button" class="bug-category-pill" data-category="other" onclick="selectCategory(this)">
📝 Other
</button>
</div>
</div>
<div class="bug-form-group">
<label class="bug-form-label" for="bug-title">
Title <span class="required">*</span>
</label>
<input type="text" id="bug-title" class="bug-form-input" placeholder="Brief summary of the issue..." maxlength="200" required>
<div class="bug-form-hint">Be specific — this helps us find and fix it faster.</div>
</div>
<div class="bug-form-group">
<label class="bug-form-label" for="bug-body">
Description <span class="required">*</span>
</label>
<textarea id="bug-body" class="bug-form-textarea" placeholder="Describe what happened, steps to reproduce, what you expected to happen..." maxlength="5000" required></textarea>
<div class="bug-form-hint">Include any relevant details like error messages, browser, or device info.</div>
</div>
<div class="bug-form-group">
<label class="bug-form-label" for="bug-page-url">
Page URL <span style="color:#555;">(optional)</span>
</label>
<input type="text" id="bug-page-url" class="bug-form-input" placeholder="e.g. /watch/example-ep-1" maxlength="500">
<div class="bug-form-hint">Which page were you on when you encountered this issue?</div>
</div>
<button type="submit" class="bug-submit-btn" id="bug-submit-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
Submit Report
</button>
</form>
<!-- My Previous Reports -->
<div class="bug-my-reports" id="bug-my-reports">
<div class="bug-my-reports-header">
<h2>My Reports</h2>
<span class="bug-my-reports-count" id="bug-reports-count">0</span>
</div>
<div id="bug-reports-list">
<div class="bug-loading">
<div class="bug-spinner"></div>
</div>
</div>
</div>
{% else %}
<div class="bug-form" style="text-align:center; padding: 40px 20px;">
<p style="color: var(--bug-muted); margin-bottom: 16px;">Please sign in to submit a bug report.</p>
<button class="bug-submit-btn" onclick="openLoginModal()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
Sign In
</button>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script>
function selectCategory(btn) {
document.querySelectorAll('.bug-category-pill').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
}
{% if session.get('username') %}
// Load user's reports
async function loadMyReports() {
const listEl = document.getElementById('bug-reports-list');
try {
const res = await fetch('/api/bug-reports/my');
const data = await res.json();
if (!data.success) {
listEl.innerHTML = '<div class="bug-report-empty">Could not load reports.</div>';
return;
}
document.getElementById('bug-reports-count').textContent = data.total;
if (data.reports.length === 0) {
listEl.innerHTML = '<div class="bug-report-empty"><div class="bug-report-empty-icon">📭</div>No reports yet. Submit one above!</div>';
return;
}
listEl.innerHTML = data.reports.map(r => {
const statusLabel = r.status === 'in_progress' ? 'In Progress' : r.status.charAt(0).toUpperCase() + r.status.slice(1);
const date = r.created_at ? new Date(r.created_at).toLocaleDateString() : '';
// Build conversation thread
let threadHtml = '';
const allMessages = [];
// Admin replies
(r.admin_replies || []).forEach(rep => {
allMessages.push({ ...rep, type: 'admin' });
});
// User replies
(r.user_replies || []).forEach(rep => {
allMessages.push({ ...rep, type: 'user' });
});
// Sort by created_at
allMessages.sort((a, b) => (a.created_at || '').localeCompare(b.created_at || ''));
if (allMessages.length > 0) {
threadHtml = '<div class="bug-thread">' + allMessages.map(msg => {
const msgDate = msg.created_at ? new Date(msg.created_at).toLocaleString() : '';
if (msg.type === 'admin') {
return `
<div class="bug-thread-message admin">
<div class="bug-thread-avatar admin-avatar">A</div>
<div class="bug-thread-bubble">
<div>${msg.body}</div>
<div class="bubble-meta">
<span>${msgDate}</span>
${msg.is_broadcast ? '<span class="bug-thread-badge broadcast">Broadcast</span>' : '<span class="bug-thread-badge">Admin</span>'}
</div>
</div>
</div>
`;
} else {
return `
<div class="bug-thread-message user">
<div class="bug-thread-avatar user-avatar">U</div>
<div class="bug-thread-bubble">
<div>${msg.body}</div>
<div class="bubble-meta">
<span>${msgDate}</span>
</div>
</div>
</div>
`;
}
}).join('') + '</div>';
}
return `
<div class="bug-report-item" id="report-${r._id}">
<div class="bug-report-summary" onclick="toggleReport('${r._id}')">
<svg class="bug-report-expand-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
<div class="bug-report-summary-content">
<div class="bug-report-title">
${r.title}
<span class="bug-status-badge ${r.status}">${statusLabel}</span>
</div>
<div class="bug-report-body-preview">${r.body}</div>
<div class="bug-report-meta">
<span class="bug-report-date">${date}</span>
${r.reply_count > 0 ? `
<span class="bug-report-reply-count">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
${r.reply_count} ${r.reply_count === 1 ? 'reply' : 'replies'}
</span>
` : ''}
</div>
</div>
</div>
<div class="bug-report-detail">
<div class="bug-report-full-body">${r.body}</div>
${threadHtml}
<div class="bug-thread-reply">
<textarea class="bug-thread-reply-input" id="user-reply-input-${r._id}" placeholder="Reply to admin..." rows="1"></textarea>
<button class="bug-thread-reply-btn" onclick="sendUserReply('${r._id}')">Send</button>
</div>
</div>
</div>
`;
}).join('');
} catch (e) {
listEl.innerHTML = '<div class="bug-report-empty">Failed to load reports.</div>';
}
}
// Submit report
document.getElementById('bug-report-form').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('bug-submit-btn');
const errorEl = document.getElementById('bug-error');
const successEl = document.getElementById('bug-success');
errorEl.classList.remove('visible');
successEl.classList.remove('visible');
const activeCategory = document.querySelector('.bug-category-pill.active');
const category = activeCategory ? activeCategory.dataset.category : 'bug';
const title = document.getElementById('bug-title').value.trim();
const body = document.getElementById('bug-body').value.trim();
const pageUrl = document.getElementById('bug-page-url').value.trim();
if (!title || title.length < 3) {
errorEl.textContent = 'Title must be at least 3 characters.';
errorEl.classList.add('visible');
return;
}
if (!body || body.length < 10) {
errorEl.textContent = 'Description must be at least 10 characters.';
errorEl.classList.add('visible');
return;
}
btn.disabled = true;
btn.textContent = 'Submitting...';
try {
const res = await fetch('/api/bug-reports', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ title, body, category, page_url: pageUrl }),
});
const data = await res.json();
if (data.success) {
document.getElementById('bug-report-form').reset();
// Re-set default category
document.querySelectorAll('.bug-category-pill').forEach(p => p.classList.remove('active'));
document.querySelector('.bug-category-pill[data-category="bug"]').classList.add('active');
successEl.classList.add('visible');
loadMyReports();
} else {
errorEl.textContent = data.message || 'Failed to submit report.';
errorEl.classList.add('visible');
}
} catch (e) {
errorEl.textContent = 'Network error. Please try again.';
errorEl.classList.add('visible');
} finally {
btn.disabled = false;
btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg> Submit Report`;
}
});
function toggleReport(id) {
const item = document.getElementById('report-' + id);
if (item) {
item.classList.toggle('expanded');
}
}
async function sendUserReply(reportId) {
const input = document.getElementById('user-reply-input-' + reportId);
const body = input.value.trim();
if (!body) return;
const btn = input.nextElementSibling;
btn.disabled = true;
btn.textContent = 'Sending...';
try {
const res = await fetch('/api/bug-reports/' + reportId + '/user-reply', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ body }),
});
const data = await res.json();
if (data.success) {
input.value = '';
loadMyReports();
} else {
alert(data.message || 'Failed to send reply.');
}
} catch (e) {
alert('Network error. Please try again.');
} finally {
btn.disabled = false;
btn.textContent = 'Send';
}
}
loadMyReports();
{% endif %}
</script>
{% endblock %}