mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat(admin): style search + clarify bulk actions
This commit is contained in:
@@ -80,6 +80,7 @@ Notes:
|
||||
- First choice: use dashboard bulk actions (`/admin`) with search + checkbox selection.
|
||||
- Use **Table** view for bulk delete.
|
||||
- Table columns are resizable and sortable; widths persist per-browser via localStorage.
|
||||
- **Select Results** selects all rows currently shown by the search filter; **Clear Selection** unselects everything.
|
||||
- Avoid wildcard deletion; prefer search + small batches to reduce risk of deleting legitimate feeds.
|
||||
|
||||
## Cloudflare/Wrangler conventions
|
||||
|
||||
@@ -114,8 +114,8 @@ npm run build
|
||||
3. Use the search box to filter obvious spam feeds.
|
||||
- Long titles/URLs are truncated; hover to see the full value. Click to copy.
|
||||
- Drag the column separators to resize; click headers to sort (double-click a separator to reset width).
|
||||
4. Select rows and use **Delete Selected Feeds**.
|
||||
5. For legitimate feeds that got spam emails, open **Emails**, filter by subject, then **Delete Selected Emails**.
|
||||
4. Use **Select Results** to select the filtered rows, then click **Delete Selected**.
|
||||
5. For legitimate feeds that got spam emails, open **Emails**, filter by subject, then **Select Results** and **Delete Selected**.
|
||||
|
||||
## Upgrading dependencies
|
||||
|
||||
|
||||
+104
-55
@@ -352,6 +352,14 @@ app.get("/", async (c) => {
|
||||
: view === "table"
|
||||
? html`
|
||||
<div class="card">
|
||||
<form
|
||||
id="bulk-feed-delete-form"
|
||||
action="/admin/feeds/bulk-delete"
|
||||
method="post"
|
||||
onsubmit="return confirmBulkFeedDelete()"
|
||||
>
|
||||
<input type="hidden" name="view" value="table" />
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group toolbar-group-fill">
|
||||
<input
|
||||
@@ -361,33 +369,36 @@ app.get("/", async (c) => {
|
||||
placeholder="Search title, feed id, or description"
|
||||
oninput="scheduleFeedFilter()"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="button button-small"
|
||||
onclick="setVisibleFeedSelection(true)"
|
||||
<span class="pill" id="feed-match-count"
|
||||
>Showing ${feedsWithConfig.length}</span
|
||||
>
|
||||
Select Visible
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button button-small"
|
||||
onclick="setVisibleFeedSelection(false)"
|
||||
>
|
||||
Clear Visible
|
||||
</button>
|
||||
<span class="pill" id="selected-feed-count"
|
||||
>0 selected</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
id="bulk-feed-delete-form"
|
||||
action="/admin/feeds/bulk-delete"
|
||||
method="post"
|
||||
onsubmit="return confirmBulkFeedDelete()"
|
||||
<button
|
||||
type="button"
|
||||
class="button button-small button-secondary"
|
||||
onclick="selectMatchingFeeds()"
|
||||
>
|
||||
<input type="hidden" name="view" value="table" />
|
||||
Select Results
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button button-small button-secondary"
|
||||
onclick="clearFeedSelection()"
|
||||
>
|
||||
Clear Selection
|
||||
</button>
|
||||
<button
|
||||
id="bulk-delete-feeds-button"
|
||||
type="submit"
|
||||
class="button button-small button-danger"
|
||||
disabled
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="table table-feeds">
|
||||
@@ -617,17 +628,6 @@ app.get("/", async (c) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="actions-row">
|
||||
<button
|
||||
id="bulk-delete-feeds-button"
|
||||
type="submit"
|
||||
class="button button-danger"
|
||||
disabled
|
||||
>
|
||||
Delete Selected Feeds
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`
|
||||
@@ -810,6 +810,7 @@ app.get("/", async (c) => {
|
||||
let FEED_ROWS = [];
|
||||
let FEED_CHECKBOXES = [];
|
||||
let FEED_SELECTED_COUNT_EL = null;
|
||||
let FEED_MATCH_COUNT_EL = null;
|
||||
let FEED_BULK_DELETE_BUTTON_EL = null;
|
||||
let FEED_SELECT_ALL_EL = null;
|
||||
let FEED_FILTER_TIMER = null;
|
||||
@@ -821,13 +822,23 @@ app.get("/", async (c) => {
|
||||
FEED_ROWS = Array.from(document.querySelectorAll('.feed-row'));
|
||||
FEED_CHECKBOXES = Array.from(document.querySelectorAll('.feed-select'));
|
||||
FEED_SELECTED_COUNT_EL = document.getElementById('selected-feed-count');
|
||||
FEED_MATCH_COUNT_EL = document.getElementById('feed-match-count');
|
||||
FEED_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-feeds-button');
|
||||
FEED_SELECT_ALL_EL = document.getElementById('select-all-feeds');
|
||||
setupFeedTableResizing();
|
||||
setupFeedTableSorting();
|
||||
updateFeedMatchCount();
|
||||
updateFeedSelectionState();
|
||||
}
|
||||
|
||||
function updateFeedMatchCount() {
|
||||
if (!FEED_MATCH_COUNT_EL) return;
|
||||
const total = FEED_ROWS.length;
|
||||
const visible = FEED_ROWS.filter((row) => !row.hidden).length;
|
||||
const query = (document.getElementById('feed-search')?.value || '').trim();
|
||||
FEED_MATCH_COUNT_EL.textContent = query ? ('Showing ' + visible + ' of ' + total) : ('Showing ' + total);
|
||||
}
|
||||
|
||||
function scheduleFeedFilter() {
|
||||
if (FEED_FILTER_TIMER) {
|
||||
clearTimeout(FEED_FILTER_TIMER);
|
||||
@@ -1052,12 +1063,24 @@ app.get("/", async (c) => {
|
||||
updateFeedSelectionState();
|
||||
}
|
||||
|
||||
function selectMatchingFeeds() {
|
||||
setVisibleFeedSelection(true);
|
||||
}
|
||||
|
||||
function clearFeedSelection() {
|
||||
FEED_CHECKBOXES.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
})
|
||||
updateFeedSelectionState();
|
||||
}
|
||||
|
||||
function filterFeedRows() {
|
||||
const query = (document.getElementById('feed-search')?.value || '').toLowerCase().trim();
|
||||
FEED_ROWS.forEach((row) => {
|
||||
const haystack = row.getAttribute('data-search') || '';
|
||||
row.hidden = !!query && !haystack.includes(query);
|
||||
});
|
||||
updateFeedMatchCount();
|
||||
updateFeedSelectionState();
|
||||
}
|
||||
|
||||
@@ -1520,6 +1543,11 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
: ""}
|
||||
${feedMetadata.emails.length > 0
|
||||
? html`
|
||||
<form
|
||||
action="/admin/feeds/${feedId}/emails/bulk-delete"
|
||||
method="post"
|
||||
onsubmit="return confirmBulkEmailDelete()"
|
||||
>
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group toolbar-group-fill">
|
||||
<input
|
||||
@@ -1529,29 +1557,37 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
placeholder="Search email subjects"
|
||||
oninput="scheduleEmailFilter()"
|
||||
/>
|
||||
<span class="pill" id="email-match-count"
|
||||
>Showing ${feedMetadata.emails.length}</span
|
||||
>
|
||||
<span class="pill" id="selected-email-count"
|
||||
>0 selected</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="button button-small"
|
||||
onclick="setVisibleEmailSelection(true)"
|
||||
class="button button-small button-secondary"
|
||||
onclick="selectMatchingEmails()"
|
||||
>
|
||||
Select Visible
|
||||
Select Results
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="button button-small"
|
||||
onclick="setVisibleEmailSelection(false)"
|
||||
class="button button-small button-secondary"
|
||||
onclick="clearEmailSelection()"
|
||||
>
|
||||
Clear Visible
|
||||
Clear Selection
|
||||
</button>
|
||||
<button
|
||||
id="bulk-delete-emails-button"
|
||||
type="submit"
|
||||
class="button button-small button-danger"
|
||||
disabled
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
<span class="pill" id="selected-email-count">0 selected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form
|
||||
action="/admin/feeds/${feedId}/emails/bulk-delete"
|
||||
method="post"
|
||||
onsubmit="return confirmBulkEmailDelete()"
|
||||
>
|
||||
<div class="table-wrap">
|
||||
<table class="table table-emails">
|
||||
<colgroup>
|
||||
@@ -1641,16 +1677,6 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="actions-row">
|
||||
<button
|
||||
id="bulk-delete-emails-button"
|
||||
type="submit"
|
||||
class="button button-danger"
|
||||
disabled
|
||||
>
|
||||
Delete Selected Emails
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
`
|
||||
: html`<div class="card">
|
||||
@@ -1666,6 +1692,7 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
let EMAIL_ROWS = [];
|
||||
let EMAIL_CHECKBOXES = [];
|
||||
let EMAIL_SELECTED_COUNT_EL = null;
|
||||
let EMAIL_MATCH_COUNT_EL = null;
|
||||
let EMAIL_BULK_DELETE_BUTTON_EL = null;
|
||||
let EMAIL_SELECT_ALL_EL = null;
|
||||
let EMAIL_FILTER_TIMER = null;
|
||||
@@ -1677,13 +1704,23 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
EMAIL_ROWS = Array.from(document.querySelectorAll('.email-row'));
|
||||
EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
|
||||
EMAIL_SELECTED_COUNT_EL = document.getElementById('selected-email-count');
|
||||
EMAIL_MATCH_COUNT_EL = document.getElementById('email-match-count');
|
||||
EMAIL_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-emails-button');
|
||||
EMAIL_SELECT_ALL_EL = document.getElementById('select-all-emails');
|
||||
setupEmailTableResizing();
|
||||
setupEmailTableSorting();
|
||||
updateEmailMatchCount();
|
||||
updateEmailSelectionState();
|
||||
}
|
||||
|
||||
function updateEmailMatchCount() {
|
||||
if (!EMAIL_MATCH_COUNT_EL) return;
|
||||
const total = EMAIL_ROWS.length;
|
||||
const visible = EMAIL_ROWS.filter((row) => !row.hidden).length;
|
||||
const query = (document.getElementById('email-search')?.value || '').trim();
|
||||
EMAIL_MATCH_COUNT_EL.textContent = query ? ('Showing ' + visible + ' of ' + total) : ('Showing ' + total);
|
||||
}
|
||||
|
||||
function scheduleEmailFilter() {
|
||||
if (EMAIL_FILTER_TIMER) {
|
||||
clearTimeout(EMAIL_FILTER_TIMER);
|
||||
@@ -1903,12 +1940,24 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
||||
updateEmailSelectionState();
|
||||
}
|
||||
|
||||
function selectMatchingEmails() {
|
||||
setVisibleEmailSelection(true);
|
||||
}
|
||||
|
||||
function clearEmailSelection() {
|
||||
EMAIL_CHECKBOXES.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
})
|
||||
updateEmailSelectionState();
|
||||
}
|
||||
|
||||
function filterEmailRows() {
|
||||
const query = (document.getElementById('email-search')?.value || '').toLowerCase().trim();
|
||||
EMAIL_ROWS.forEach((row) => {
|
||||
const haystack = row.getAttribute('data-search') || '';
|
||||
row.hidden = !!query && !haystack.includes(query);
|
||||
});
|
||||
updateEmailMatchCount();
|
||||
updateEmailSelectionState();
|
||||
}
|
||||
|
||||
|
||||
@@ -240,6 +240,7 @@ export const componentStyles = `
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="search"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
display: block;
|
||||
@@ -257,6 +258,18 @@ export const componentStyles = `
|
||||
-webkit-backdrop-filter: blur(var(--blur-sm));
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input[type="search"]::-webkit-search-decoration,
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-results-button,
|
||||
input[type="search"]::-webkit-search-results-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
@@ -507,6 +520,26 @@ export const componentStyles = `
|
||||
input.search {
|
||||
min-width: 280px;
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 0 14px;
|
||||
padding-left: 38px;
|
||||
background-color: rgba(60, 60, 67, 0.14);
|
||||
background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%238e8e93'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Ccircle%20cx='11'%20cy='11'%20r='7'/%3E%3Cpath%20d='M21%2021l-4.3-4.3'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 14px center;
|
||||
background-size: 16px 16px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
input.search {
|
||||
background-color: rgba(60, 60, 67, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
input.search::placeholder {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
|
||||
@@ -38,7 +38,7 @@ export const layoutStyles = `
|
||||
|
||||
/* Wider layout for data-dense pages (tables) */
|
||||
.container-wide {
|
||||
max-width: 1280px;
|
||||
max-width: 1440px;
|
||||
}
|
||||
|
||||
/* Header Styles */
|
||||
|
||||
Reference in New Issue
Block a user