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.
|
- First choice: use dashboard bulk actions (`/admin`) with search + checkbox selection.
|
||||||
- Use **Table** view for bulk delete.
|
- Use **Table** view for bulk delete.
|
||||||
- Table columns are resizable and sortable; widths persist per-browser via localStorage.
|
- 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.
|
- Avoid wildcard deletion; prefer search + small batches to reduce risk of deleting legitimate feeds.
|
||||||
|
|
||||||
## Cloudflare/Wrangler conventions
|
## Cloudflare/Wrangler conventions
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ npm run build
|
|||||||
3. Use the search box to filter obvious spam feeds.
|
3. Use the search box to filter obvious spam feeds.
|
||||||
- Long titles/URLs are truncated; hover to see the full value. Click to copy.
|
- 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).
|
- 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**.
|
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 **Delete Selected Emails**.
|
5. For legitimate feeds that got spam emails, open **Emails**, filter by subject, then **Select Results** and **Delete Selected**.
|
||||||
|
|
||||||
## Upgrading dependencies
|
## Upgrading dependencies
|
||||||
|
|
||||||
|
|||||||
+104
-55
@@ -352,6 +352,14 @@ app.get("/", async (c) => {
|
|||||||
: view === "table"
|
: view === "table"
|
||||||
? html`
|
? html`
|
||||||
<div class="card">
|
<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">
|
||||||
<div class="toolbar-group toolbar-group-fill">
|
<div class="toolbar-group toolbar-group-fill">
|
||||||
<input
|
<input
|
||||||
@@ -361,33 +369,36 @@ app.get("/", async (c) => {
|
|||||||
placeholder="Search title, feed id, or description"
|
placeholder="Search title, feed id, or description"
|
||||||
oninput="scheduleFeedFilter()"
|
oninput="scheduleFeedFilter()"
|
||||||
/>
|
/>
|
||||||
<button
|
<span class="pill" id="feed-match-count"
|
||||||
type="button"
|
>Showing ${feedsWithConfig.length}</span
|
||||||
class="button button-small"
|
|
||||||
onclick="setVisibleFeedSelection(true)"
|
|
||||||
>
|
>
|
||||||
Select Visible
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button button-small"
|
|
||||||
onclick="setVisibleFeedSelection(false)"
|
|
||||||
>
|
|
||||||
Clear Visible
|
|
||||||
</button>
|
|
||||||
<span class="pill" id="selected-feed-count"
|
<span class="pill" id="selected-feed-count"
|
||||||
>0 selected</span
|
>0 selected</span
|
||||||
>
|
>
|
||||||
</div>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
class="button button-small button-secondary"
|
||||||
<form
|
onclick="selectMatchingFeeds()"
|
||||||
id="bulk-feed-delete-form"
|
|
||||||
action="/admin/feeds/bulk-delete"
|
|
||||||
method="post"
|
|
||||||
onsubmit="return confirmBulkFeedDelete()"
|
|
||||||
>
|
>
|
||||||
<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">
|
<div class="table-wrap">
|
||||||
<table class="table table-feeds">
|
<table class="table table-feeds">
|
||||||
@@ -617,17 +628,6 @@ app.get("/", async (c) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions-row">
|
|
||||||
<button
|
|
||||||
id="bulk-delete-feeds-button"
|
|
||||||
type="submit"
|
|
||||||
class="button button-danger"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
Delete Selected Feeds
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@@ -810,6 +810,7 @@ app.get("/", async (c) => {
|
|||||||
let FEED_ROWS = [];
|
let FEED_ROWS = [];
|
||||||
let FEED_CHECKBOXES = [];
|
let FEED_CHECKBOXES = [];
|
||||||
let FEED_SELECTED_COUNT_EL = null;
|
let FEED_SELECTED_COUNT_EL = null;
|
||||||
|
let FEED_MATCH_COUNT_EL = null;
|
||||||
let FEED_BULK_DELETE_BUTTON_EL = null;
|
let FEED_BULK_DELETE_BUTTON_EL = null;
|
||||||
let FEED_SELECT_ALL_EL = null;
|
let FEED_SELECT_ALL_EL = null;
|
||||||
let FEED_FILTER_TIMER = null;
|
let FEED_FILTER_TIMER = null;
|
||||||
@@ -821,13 +822,23 @@ app.get("/", async (c) => {
|
|||||||
FEED_ROWS = Array.from(document.querySelectorAll('.feed-row'));
|
FEED_ROWS = Array.from(document.querySelectorAll('.feed-row'));
|
||||||
FEED_CHECKBOXES = Array.from(document.querySelectorAll('.feed-select'));
|
FEED_CHECKBOXES = Array.from(document.querySelectorAll('.feed-select'));
|
||||||
FEED_SELECTED_COUNT_EL = document.getElementById('selected-feed-count');
|
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_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-feeds-button');
|
||||||
FEED_SELECT_ALL_EL = document.getElementById('select-all-feeds');
|
FEED_SELECT_ALL_EL = document.getElementById('select-all-feeds');
|
||||||
setupFeedTableResizing();
|
setupFeedTableResizing();
|
||||||
setupFeedTableSorting();
|
setupFeedTableSorting();
|
||||||
|
updateFeedMatchCount();
|
||||||
updateFeedSelectionState();
|
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() {
|
function scheduleFeedFilter() {
|
||||||
if (FEED_FILTER_TIMER) {
|
if (FEED_FILTER_TIMER) {
|
||||||
clearTimeout(FEED_FILTER_TIMER);
|
clearTimeout(FEED_FILTER_TIMER);
|
||||||
@@ -1052,12 +1063,24 @@ app.get("/", async (c) => {
|
|||||||
updateFeedSelectionState();
|
updateFeedSelectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectMatchingFeeds() {
|
||||||
|
setVisibleFeedSelection(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFeedSelection() {
|
||||||
|
FEED_CHECKBOXES.forEach((checkbox) => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
})
|
||||||
|
updateFeedSelectionState();
|
||||||
|
}
|
||||||
|
|
||||||
function filterFeedRows() {
|
function filterFeedRows() {
|
||||||
const query = (document.getElementById('feed-search')?.value || '').toLowerCase().trim();
|
const query = (document.getElementById('feed-search')?.value || '').toLowerCase().trim();
|
||||||
FEED_ROWS.forEach((row) => {
|
FEED_ROWS.forEach((row) => {
|
||||||
const haystack = row.getAttribute('data-search') || '';
|
const haystack = row.getAttribute('data-search') || '';
|
||||||
row.hidden = !!query && !haystack.includes(query);
|
row.hidden = !!query && !haystack.includes(query);
|
||||||
});
|
});
|
||||||
|
updateFeedMatchCount();
|
||||||
updateFeedSelectionState();
|
updateFeedSelectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1520,6 +1543,11 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
|||||||
: ""}
|
: ""}
|
||||||
${feedMetadata.emails.length > 0
|
${feedMetadata.emails.length > 0
|
||||||
? html`
|
? html`
|
||||||
|
<form
|
||||||
|
action="/admin/feeds/${feedId}/emails/bulk-delete"
|
||||||
|
method="post"
|
||||||
|
onsubmit="return confirmBulkEmailDelete()"
|
||||||
|
>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="toolbar-group toolbar-group-fill">
|
<div class="toolbar-group toolbar-group-fill">
|
||||||
<input
|
<input
|
||||||
@@ -1529,29 +1557,37 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
|||||||
placeholder="Search email subjects"
|
placeholder="Search email subjects"
|
||||||
oninput="scheduleEmailFilter()"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button button-small"
|
class="button button-small button-secondary"
|
||||||
onclick="setVisibleEmailSelection(true)"
|
onclick="selectMatchingEmails()"
|
||||||
>
|
>
|
||||||
Select Visible
|
Select Results
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button button-small"
|
class="button button-small button-secondary"
|
||||||
onclick="setVisibleEmailSelection(false)"
|
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>
|
</button>
|
||||||
<span class="pill" id="selected-email-count">0 selected</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
|
||||||
action="/admin/feeds/${feedId}/emails/bulk-delete"
|
|
||||||
method="post"
|
|
||||||
onsubmit="return confirmBulkEmailDelete()"
|
|
||||||
>
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="table table-emails">
|
<table class="table table-emails">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
@@ -1641,16 +1677,6 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions-row">
|
|
||||||
<button
|
|
||||||
id="bulk-delete-emails-button"
|
|
||||||
type="submit"
|
|
||||||
class="button button-danger"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
Delete Selected Emails
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
`
|
`
|
||||||
: html`<div class="card">
|
: html`<div class="card">
|
||||||
@@ -1666,6 +1692,7 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
|||||||
let EMAIL_ROWS = [];
|
let EMAIL_ROWS = [];
|
||||||
let EMAIL_CHECKBOXES = [];
|
let EMAIL_CHECKBOXES = [];
|
||||||
let EMAIL_SELECTED_COUNT_EL = null;
|
let EMAIL_SELECTED_COUNT_EL = null;
|
||||||
|
let EMAIL_MATCH_COUNT_EL = null;
|
||||||
let EMAIL_BULK_DELETE_BUTTON_EL = null;
|
let EMAIL_BULK_DELETE_BUTTON_EL = null;
|
||||||
let EMAIL_SELECT_ALL_EL = null;
|
let EMAIL_SELECT_ALL_EL = null;
|
||||||
let EMAIL_FILTER_TIMER = 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_ROWS = Array.from(document.querySelectorAll('.email-row'));
|
||||||
EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
|
EMAIL_CHECKBOXES = Array.from(document.querySelectorAll('.email-select'));
|
||||||
EMAIL_SELECTED_COUNT_EL = document.getElementById('selected-email-count');
|
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_BULK_DELETE_BUTTON_EL = document.getElementById('bulk-delete-emails-button');
|
||||||
EMAIL_SELECT_ALL_EL = document.getElementById('select-all-emails');
|
EMAIL_SELECT_ALL_EL = document.getElementById('select-all-emails');
|
||||||
setupEmailTableResizing();
|
setupEmailTableResizing();
|
||||||
setupEmailTableSorting();
|
setupEmailTableSorting();
|
||||||
|
updateEmailMatchCount();
|
||||||
updateEmailSelectionState();
|
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() {
|
function scheduleEmailFilter() {
|
||||||
if (EMAIL_FILTER_TIMER) {
|
if (EMAIL_FILTER_TIMER) {
|
||||||
clearTimeout(EMAIL_FILTER_TIMER);
|
clearTimeout(EMAIL_FILTER_TIMER);
|
||||||
@@ -1903,12 +1940,24 @@ app.get("/feeds/:feedId/emails", async (c) => {
|
|||||||
updateEmailSelectionState();
|
updateEmailSelectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectMatchingEmails() {
|
||||||
|
setVisibleEmailSelection(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearEmailSelection() {
|
||||||
|
EMAIL_CHECKBOXES.forEach((checkbox) => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
})
|
||||||
|
updateEmailSelectionState();
|
||||||
|
}
|
||||||
|
|
||||||
function filterEmailRows() {
|
function filterEmailRows() {
|
||||||
const query = (document.getElementById('email-search')?.value || '').toLowerCase().trim();
|
const query = (document.getElementById('email-search')?.value || '').toLowerCase().trim();
|
||||||
EMAIL_ROWS.forEach((row) => {
|
EMAIL_ROWS.forEach((row) => {
|
||||||
const haystack = row.getAttribute('data-search') || '';
|
const haystack = row.getAttribute('data-search') || '';
|
||||||
row.hidden = !!query && !haystack.includes(query);
|
row.hidden = !!query && !haystack.includes(query);
|
||||||
});
|
});
|
||||||
|
updateEmailMatchCount();
|
||||||
updateEmailSelectionState();
|
updateEmailSelectionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ export const componentStyles = `
|
|||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
|
input[type="search"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
textarea {
|
textarea {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -257,6 +258,18 @@ export const componentStyles = `
|
|||||||
-webkit-backdrop-filter: blur(var(--blur-sm));
|
-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,
|
input:focus,
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
@@ -507,6 +520,26 @@ export const componentStyles = `
|
|||||||
input.search {
|
input.search {
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
flex: 1;
|
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 {
|
.actions-row {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const layoutStyles = `
|
|||||||
|
|
||||||
/* Wider layout for data-dense pages (tables) */
|
/* Wider layout for data-dense pages (tables) */
|
||||||
.container-wide {
|
.container-wide {
|
||||||
max-width: 1280px;
|
max-width: 1440px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header Styles */
|
/* Header Styles */
|
||||||
|
|||||||
Reference in New Issue
Block a user