542 lines
24 KiB
HTML
542 lines
24 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Bid Tracker — Website Checker{% endblock %}
|
||
{% block content %}
|
||
|
||
<div class="page-header">
|
||
<h1>📌 Bid Tracker</h1>
|
||
<button class="btn btn-primary" onclick="openModal('modal-add-bid')">+ Add Bid</button>
|
||
</div>
|
||
|
||
<!-- ── Toolbar ─────────────────────────────────────────────── -->
|
||
<div class="bt-toolbar">
|
||
<div class="search-wrap" style="max-width:260px">
|
||
<span class="search-icon">🔍</span>
|
||
<input type="text" id="bid-search" placeholder="Search title, source, sol #…" autocomplete="off">
|
||
</div>
|
||
|
||
<div class="bt-filters">
|
||
<button class="bt-filter-btn active" data-status="">All</button>
|
||
{% for s in bid_statuses %}
|
||
<button class="bt-filter-btn" data-status="{{ s }}">{{ status_labels[s] }}</button>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<button class="btn btn-secondary btn-sm" onclick="loadBids()">↻ Refresh</button>
|
||
</div>
|
||
|
||
<!-- ── Split pane ─────────────────────────────────────────── -->
|
||
<div class="bt-layout">
|
||
|
||
<!-- Left — bid list -->
|
||
<div class="bt-left">
|
||
<div id="bid-list" class="bt-list">
|
||
<div class="bt-loading">Loading…</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right — detail panel -->
|
||
<div class="bt-right" id="bt-detail">
|
||
<div class="bt-empty-state">
|
||
<div class="empty-icon">📌</div>
|
||
<p>Select a bid to view its details and updates.</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════
|
||
ADD BID MODAL
|
||
══════════════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-add-bid">
|
||
<div class="modal" style="max-width:600px">
|
||
<div class="modal-header">
|
||
<span class="modal-title">Add Opportunity</span>
|
||
<button class="modal-close" onclick="closeModal('modal-add-bid')">✕</button>
|
||
</div>
|
||
<form method="post" action="{{ url_for('bid_tracker.create') }}">
|
||
<div class="modal-body">
|
||
{% include "_bid_form_fields.html" ignore missing %}
|
||
<div class="form-group">
|
||
<label>Title *</label>
|
||
<input type="text" name="title" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>URL *</label>
|
||
<input type="url" name="url" placeholder="https://" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Source</label>
|
||
<input type="text" name="source" placeholder="e.g. SAM.gov">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Solicitation #</label>
|
||
<input type="text" name="solicitation_number">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Status</label>
|
||
<select name="status">
|
||
{% for s in bid_statuses %}
|
||
<option value="{{ s }}" {% if s == 'open' %}selected{% endif %}>{{ status_labels[s] }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Due Date</label>
|
||
<input type="date" name="due_date">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Notes</label>
|
||
<textarea name="notes" rows="3" placeholder="Any relevant notes…"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-add-bid')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Add Bid</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════
|
||
EDIT BID MODAL
|
||
══════════════════════════════════════════════════════════ -->
|
||
<div class="modal-overlay" id="modal-edit-bid">
|
||
<div class="modal" style="max-width:600px">
|
||
<div class="modal-header">
|
||
<span class="modal-title">Edit Opportunity</span>
|
||
<button class="modal-close" onclick="closeModal('modal-edit-bid')">✕</button>
|
||
</div>
|
||
<form method="post" id="edit-bid-form">
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label>Title *</label>
|
||
<input type="text" name="title" id="eb-title" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>URL *</label>
|
||
<input type="url" name="url" id="eb-url" placeholder="https://" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Source</label>
|
||
<input type="text" name="source" id="eb-source">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Solicitation #</label>
|
||
<input type="text" name="solicitation_number" id="eb-sol">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>Status</label>
|
||
<select name="status" id="eb-status">
|
||
{% for s in bid_statuses %}
|
||
<option value="{{ s }}">{{ status_labels[s] }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Due Date</label>
|
||
<input type="date" name="due_date" id="eb-due">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Notes</label>
|
||
<textarea name="notes" id="eb-notes" rows="3"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-edit-bid')">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ══════════════════════════════════════════════════════════
|
||
State
|
||
══════════════════════════════════════════════════════════ */
|
||
var _activeBidId = null;
|
||
var _activeStatus = '';
|
||
var _allBids = []; // cached from last load
|
||
var _currentUserId = {{ session['user']['id'] }};
|
||
var _isAdmin = {{ 'true' if session['user']['role'] == 'admin' else 'false' }};
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
Status config
|
||
══════════════════════════════════════════════════════════ */
|
||
var STATUS_LABELS = {
|
||
'open': '🟢 Open',
|
||
'monitoring': '🔵 Monitoring',
|
||
'awarded': '🏆 Awarded',
|
||
'no_bid': '⛔ No Bid',
|
||
'cancelled': '🚫 Cancelled',
|
||
};
|
||
var STATUS_BADGE = {
|
||
'open': 'badge-success',
|
||
'monitoring': 'badge-info',
|
||
'awarded': 'badge-warning',
|
||
'no_bid': 'badge-danger',
|
||
'cancelled': 'badge-muted',
|
||
};
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
Load / render bid list
|
||
══════════════════════════════════════════════════════════ */
|
||
function loadBids() {
|
||
fetch('/bids/list/json?status=' + encodeURIComponent(_activeStatus))
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(bids) {
|
||
_allBids = bids;
|
||
renderBidList(bids);
|
||
// Restore selection
|
||
if (_activeBidId) {
|
||
var row = document.querySelector('.bt-row[data-id="' + _activeBidId + '"]');
|
||
if (row) row.classList.add('active');
|
||
}
|
||
})
|
||
.catch(function() {
|
||
document.getElementById('bid-list').innerHTML =
|
||
'<p class="text-muted text-center" style="padding:1rem">Failed to load bids.</p>';
|
||
});
|
||
}
|
||
|
||
function renderBidList(bids) {
|
||
var q = document.getElementById('bid-search').value.trim().toLowerCase();
|
||
var filtered = q ? bids.filter(function(b) {
|
||
return (b.title || '').toLowerCase().includes(q)
|
||
|| (b.source || '').toLowerCase().includes(q)
|
||
|| (b.solicitation_number || '').toLowerCase().includes(q);
|
||
}) : bids;
|
||
|
||
var list = document.getElementById('bid-list');
|
||
if (!filtered.length) {
|
||
list.innerHTML = '<p class="text-muted text-center" style="padding:1.5rem">No bids found.</p>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = filtered.map(function(b) {
|
||
var due = b.due_date ? b.due_date.slice(0, 10) : '—';
|
||
var badge = STATUS_BADGE[b.status] || 'badge-muted';
|
||
var label = STATUS_LABELS[b.status] || b.status;
|
||
var active = b.id === _activeBidId ? ' active' : '';
|
||
var upds = b.update_count || 0;
|
||
return '<div class="bt-row' + active + '" data-id="' + b.id + '" onclick="selectBid(' + b.id + ')">'
|
||
+ '<div class="bt-row-title">' + esc(b.title) + '</div>'
|
||
+ '<div class="bt-row-meta">'
|
||
+ '<span class="badge ' + badge + ' badge-sm">' + label + '</span>'
|
||
+ '<span class="text-muted text-xs">Due: ' + due + '</span>'
|
||
+ (upds ? '<span class="text-muted text-xs">💬 ' + upds + '</span>' : '')
|
||
+ '</div>'
|
||
+ '</div>';
|
||
}).join('');
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
Select + load detail
|
||
══════════════════════════════════════════════════════════ */
|
||
function selectBid(id) {
|
||
_activeBidId = id;
|
||
document.querySelectorAll('.bt-row').forEach(function(r) {
|
||
r.classList.toggle('active', r.dataset.id == id);
|
||
});
|
||
loadDetail(id);
|
||
}
|
||
|
||
function loadDetail(id) {
|
||
var panel = document.getElementById('bt-detail');
|
||
panel.innerHTML = '<div class="bt-detail-loading">Loading…</div>';
|
||
|
||
fetch('/bids/' + id + '/json')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
if (data.error) { panel.innerHTML = '<p class="text-muted" style="padding:1rem">' + esc(data.error) + '</p>'; return; }
|
||
renderDetail(data);
|
||
})
|
||
.catch(function() {
|
||
panel.innerHTML = '<p class="text-muted" style="padding:1rem">Failed to load details.</p>';
|
||
});
|
||
}
|
||
|
||
function renderDetail(data) {
|
||
var b = data.bid;
|
||
var updates = data.updates;
|
||
var canEdit = data.can_edit;
|
||
var userId = data.user_id;
|
||
|
||
var due = b.due_date ? b.due_date.slice(0, 10) : 'Not specified';
|
||
var badge = STATUS_BADGE[b.status] || 'badge-muted';
|
||
var label = STATUS_LABELS[b.status] || b.status;
|
||
var sol = b.solicitation_number || '—';
|
||
var src = b.source || '—';
|
||
var addedBy = b.added_by_username || '—';
|
||
var addedAt = (b.created_at || '').slice(0, 16);
|
||
var notes = b.notes || '';
|
||
|
||
var updatesHtml = updates.length ? updates.map(function(u) {
|
||
var poster = u.posted_by_full_name || u.posted_by_username || 'Unknown';
|
||
var dt = (u.created_at || '').slice(0, 16);
|
||
var canDel = data.is_admin || u.user_id === userId;
|
||
var delBtn = canDel
|
||
? '<button class="bt-upd-del" onclick="deleteUpdate(' + u.id + ',' + b.id + ')" title="Delete update">✕</button>'
|
||
: '';
|
||
return '<div class="bt-update-card">'
|
||
+ '<div class="bt-upd-hdr">'
|
||
+ '<span class="bt-upd-poster">👤 ' + esc(poster) + '</span>'
|
||
+ '<span class="bt-upd-dt text-muted text-xs">' + dt + '</span>'
|
||
+ delBtn
|
||
+ '</div>'
|
||
+ '<div class="bt-upd-body">' + esc(u.content) + '</div>'
|
||
+ '</div>';
|
||
}).join('') : '<p class="text-muted text-sm" style="padding:.5rem 0">No updates yet. Be the first to post one.</p>';
|
||
|
||
var editBtn = canEdit
|
||
? '<button class="btn btn-secondary btn-sm" onclick="openEditModal(' + b.id + ')">✎ Edit</button>'
|
||
+ '<form method="post" action="/bids/' + b.id + '/delete" style="display:inline"'
|
||
+ ' onsubmit="return confirm(\'Delete \\\'' + esc(b.title) + '\\\'?\')">'
|
||
+ '<input type="hidden" name="csrf_token" value="' + getCsrfToken() + '">'
|
||
+ '<button class="btn btn-danger btn-sm" type="submit">✕ Delete</button></form>'
|
||
: '';
|
||
|
||
document.getElementById('bt-detail').innerHTML = ''
|
||
+ '<div class="bt-detail-header">'
|
||
+ '<div class="bt-detail-title-row">'
|
||
+ '<h2 class="bt-detail-title">' + esc(b.title) + '</h2>'
|
||
+ '<span class="badge ' + badge + '">' + label + '</span>'
|
||
+ '</div>'
|
||
+ '<div class="bt-detail-actions">'
|
||
+ editBtn
|
||
+ '<a href="' + esc(b.url) + '" target="_blank" rel="noopener" class="btn btn-secondary btn-sm">↗ Open URL</a>'
|
||
+ '</div>'
|
||
+ '</div>'
|
||
|
||
+ '<div class="bt-meta-grid">'
|
||
+ metaItem('Due Date', due)
|
||
+ metaItem('Sol #', sol)
|
||
+ metaItem('Source', src)
|
||
+ metaItem('Added by', addedBy + (addedAt ? ' · ' + addedAt : ''))
|
||
+ '</div>'
|
||
|
||
+ (notes ? '<div class="bt-notes"><strong>Notes:</strong> ' + esc(notes) + '</div>' : '')
|
||
|
||
+ '<div class="bt-updates-section">'
|
||
+ '<div class="bt-updates-header">📝 Updates</div>'
|
||
|
||
+ '<div class="bt-compose">'
|
||
+ '<textarea id="upd-text-' + b.id + '" class="bt-compose-txt" rows="2"'
|
||
+ ' placeholder="Write an update, e.g. \'Amendment 1 issued — due date extended to…\'"></textarea>'
|
||
+ '<button class="btn btn-primary btn-sm" onclick="postUpdate(' + b.id + ')">📤 Post Update</button>'
|
||
+ '</div>'
|
||
|
||
+ '<div class="bt-updates-list" id="upd-list-' + b.id + '">' + updatesHtml + '</div>'
|
||
+ '</div>';
|
||
}
|
||
|
||
function metaItem(label, value) {
|
||
return '<div class="bt-meta-item"><span class="bt-meta-key">' + label + '</span>'
|
||
+ '<span class="bt-meta-val">' + esc(value) + '</span></div>';
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
Post update (AJAX — no page reload)
|
||
══════════════════════════════════════════════════════════ */
|
||
function postUpdate(bidId) {
|
||
var ta = document.getElementById('upd-text-' + bidId);
|
||
var content = ta ? ta.value.trim() : '';
|
||
if (!content) { alert('Please write an update before posting.'); return; }
|
||
|
||
var fd = new FormData();
|
||
fd.append('content', content);
|
||
|
||
fetch('/bids/' + bidId + '/updates/json', { method: 'POST', body: fd, credentials: 'same-origin', headers: {'X-CSRFToken': getCsrfToken()} })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.error) { alert(d.error); return; }
|
||
ta.value = '';
|
||
// Reload detail to show new update
|
||
loadDetail(bidId);
|
||
// Refresh list to update count
|
||
loadBids();
|
||
})
|
||
.catch(function(e) { alert('Failed to post update: ' + e); });
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
Delete update (AJAX)
|
||
══════════════════════════════════════════════════════════ */
|
||
function deleteUpdate(updateId, bidId) {
|
||
if (!confirm('Delete this update?')) return;
|
||
var fd = new FormData();
|
||
fetch('/bids/updates/' + updateId + '/delete/json', { method: 'POST', body: fd, credentials: 'same-origin', headers: {'X-CSRFToken': getCsrfToken()} })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.error) { alert(d.error); return; }
|
||
loadDetail(bidId);
|
||
loadBids();
|
||
})
|
||
.catch(function(e) { alert('Failed to delete update: ' + e); });
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
Edit modal
|
||
══════════════════════════════════════════════════════════ */
|
||
function openEditModal(bidId) {
|
||
fetch('/bids/' + bidId + '/json')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
var b = data.bid;
|
||
document.getElementById('eb-title').value = b.title || '';
|
||
document.getElementById('eb-url').value = b.url || '';
|
||
document.getElementById('eb-source').value = b.source || '';
|
||
document.getElementById('eb-sol').value = b.solicitation_number || '';
|
||
document.getElementById('eb-status').value = b.status || 'open';
|
||
document.getElementById('eb-due').value = b.due_date ? b.due_date.slice(0, 10) : '';
|
||
document.getElementById('eb-notes').value = b.notes || '';
|
||
document.getElementById('edit-bid-form').action = '/bids/' + bidId + '/edit';
|
||
openModal('modal-edit-bid');
|
||
});
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
Filter buttons
|
||
══════════════════════════════════════════════════════════ */
|
||
document.querySelectorAll('.bt-filter-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
document.querySelectorAll('.bt-filter-btn').forEach(function(b) { b.classList.remove('active'); });
|
||
btn.classList.add('active');
|
||
_activeStatus = btn.dataset.status;
|
||
loadBids();
|
||
});
|
||
});
|
||
|
||
/* ── Search ─────────────────────────────────────────────── */
|
||
document.getElementById('bid-search').addEventListener('input', function() {
|
||
renderBidList(_allBids);
|
||
// Restore active class after re-render
|
||
if (_activeBidId) {
|
||
var row = document.querySelector('.bt-row[data-id="' + _activeBidId + '"]');
|
||
if (row) row.classList.add('active');
|
||
}
|
||
});
|
||
|
||
/* ── Escape ─────────────────────────────────────────────── */
|
||
function esc(str) {
|
||
return String(str || '').replace(/[&<>"']/g, function(c) {
|
||
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
||
});
|
||
}
|
||
|
||
/* ── Init ───────────────────────────────────────────────── */
|
||
loadBids();
|
||
</script>
|
||
|
||
<style>
|
||
/* ── Toolbar ───────────────────────────────────────────── */
|
||
.bt-toolbar{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-bottom:1rem}
|
||
.bt-filters{display:flex;gap:.35rem;flex-wrap:wrap}
|
||
.bt-filter-btn{
|
||
padding:.3rem .75rem;border-radius:999px;font-size:.75rem;font-weight:600;
|
||
border:1px solid var(--border-strong);background:var(--bg-white);
|
||
color:var(--text-muted);cursor:pointer;transition:all var(--t);
|
||
font-family:'DM Sans',sans-serif;
|
||
}
|
||
.bt-filter-btn:hover{background:var(--bg-hover);color:var(--text)}
|
||
.bt-filter-btn.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||
|
||
/* ── Layout ────────────────────────────────────────────── */
|
||
.bt-layout{display:grid;grid-template-columns:340px 1fr;gap:1rem;
|
||
align-items:start;min-height:calc(100vh - 200px)}
|
||
.bt-left{background:var(--bg-white);border:1px solid var(--border);
|
||
border-radius:var(--r-lg);overflow:hidden;box-shadow:var(--shadow-sm)}
|
||
.bt-right{background:var(--bg-white);border:1px solid var(--border);
|
||
border-radius:var(--r-lg);box-shadow:var(--shadow-sm);
|
||
min-height:400px;overflow:hidden}
|
||
|
||
/* ── Bid list rows ─────────────────────────────────────── */
|
||
.bt-list{display:flex;flex-direction:column}
|
||
.bt-loading{padding:1.5rem;text-align:center;color:var(--text-muted);font-size:.875rem}
|
||
.bt-row{
|
||
padding:.8rem 1rem;border-bottom:1px solid var(--border);
|
||
cursor:pointer;transition:background var(--t);
|
||
}
|
||
.bt-row:last-child{border-bottom:none}
|
||
.bt-row:hover{background:var(--bg-subtle)}
|
||
.bt-row.active{background:var(--accent-light);border-left:3px solid var(--accent)}
|
||
.bt-row-title{font-size:.875rem;font-weight:600;color:var(--text);
|
||
margin-bottom:.3rem;
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.bt-row-meta{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}
|
||
|
||
/* ── Detail panel ──────────────────────────────────────── */
|
||
.bt-empty-state{display:flex;flex-direction:column;align-items:center;
|
||
justify-content:center;padding:3rem 1rem;color:var(--text-muted);
|
||
text-align:center}
|
||
.bt-detail-loading{padding:2rem;text-align:center;color:var(--text-muted)}
|
||
.bt-detail-header{
|
||
padding:1.1rem 1.25rem .9rem;border-bottom:1px solid var(--border);
|
||
background:var(--bg-subtle);
|
||
}
|
||
.bt-detail-title-row{display:flex;align-items:flex-start;gap:.6rem;margin-bottom:.6rem}
|
||
.bt-detail-title{font-size:1.1rem;font-weight:700;color:var(--text);flex:1;line-height:1.3}
|
||
.bt-detail-actions{display:flex;align-items:center;gap:.4rem;flex-wrap:wrap}
|
||
|
||
/* ── Metadata grid ─────────────────────────────────────── */
|
||
.bt-meta-grid{
|
||
display:grid;grid-template-columns:1fr 1fr;gap:0;
|
||
padding:.75rem 1.25rem;border-bottom:1px solid var(--border);
|
||
}
|
||
.bt-meta-item{padding:.35rem 0;display:flex;flex-direction:column;gap:2px}
|
||
.bt-meta-key{font-size:.68rem;font-weight:700;letter-spacing:.07em;
|
||
text-transform:uppercase;color:var(--text-muted)}
|
||
.bt-meta-val{font-size:.85rem;color:var(--text-secondary)}
|
||
.bt-notes{
|
||
padding:.7rem 1.25rem;font-size:.85rem;color:var(--text-secondary);
|
||
border-bottom:1px solid var(--border);line-height:1.6;
|
||
}
|
||
|
||
/* ── Updates section ───────────────────────────────────── */
|
||
.bt-updates-section{padding:0 1.25rem 1.25rem}
|
||
.bt-updates-header{font-size:.85rem;font-weight:700;color:var(--text);
|
||
padding:.9rem 0 .6rem;border-bottom:1px solid var(--border);
|
||
margin-bottom:.75rem}
|
||
.bt-compose{display:flex;flex-direction:column;gap:.5rem;margin-bottom:.9rem}
|
||
.bt-compose-txt{
|
||
width:100%;padding:.55rem .75rem;border:1px solid var(--border-strong);
|
||
border-radius:var(--r-sm);font-family:'DM Sans',sans-serif;font-size:.875rem;
|
||
color:var(--text);background:var(--bg-white);resize:vertical;outline:none;
|
||
transition:border-color var(--t),box-shadow var(--t);
|
||
}
|
||
.bt-compose-txt:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
|
||
.bt-compose .btn{align-self:flex-end}
|
||
|
||
/* ── Update cards ──────────────────────────────────────── */
|
||
.bt-updates-list{display:flex;flex-direction:column;gap:.6rem;
|
||
max-height:380px;overflow-y:auto;padding-right:2px}
|
||
.bt-update-card{
|
||
background:var(--bg-subtle);border:1px solid var(--border);
|
||
border-radius:var(--r);padding:.7rem .9rem;
|
||
}
|
||
.bt-upd-hdr{display:flex;align-items:center;gap:.6rem;margin-bottom:.4rem}
|
||
.bt-upd-poster{font-size:.8rem;font-weight:600;color:var(--text);flex:1}
|
||
.bt-upd-dt{flex-shrink:0}
|
||
.bt-upd-del{
|
||
background:none;border:none;color:var(--text-muted);cursor:pointer;
|
||
font-size:.8rem;padding:.1rem .3rem;border-radius:var(--r-sm);
|
||
transition:all var(--t);
|
||
}
|
||
.bt-upd-del:hover{background:var(--danger-bg);color:var(--danger)}
|
||
.bt-upd-body{font-size:.875rem;color:var(--text-secondary);line-height:1.6;
|
||
white-space:pre-wrap}
|
||
|
||
@media(max-width:900px){
|
||
.bt-layout{grid-template-columns:1fr;grid-template-rows:auto auto}
|
||
.bt-right{min-height:300px}
|
||
}
|
||
</style>
|
||
{% endblock %}
|