aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Mathes <adam@adammathes.com>2026-02-14 14:49:09 -0800
committerAdam Mathes <adam@adammathes.com>2026-02-14 14:49:09 -0800
commit490edf9e9f2911231df6c76ef4afeb3b1fb763d2 (patch)
treeab3ed784ecb79f93af52714a993096e09040a455
parentafa87af01c79a9baa539f2992d32154d2a4739bd (diff)
downloadneko-490edf9e9f2911231df6c76ef4afeb3b1fb763d2.tar.gz
neko-490edf9e9f2911231df6c76ef4afeb3b1fb763d2.tar.bz2
neko-490edf9e9f2911231df6c76ef4afeb3b1fb763d2.zip
task: improve mobile responsiveness of React UI\n\n- Added media queries to App.css to handle mobile sidebar layout (overlay with backdrop)\n- Implemented auto-hiding sidebar on mobile when links are clicked\n- Reduced padding and adjusted max-widths for smaller screens in App.css, FeedItem.css, and Settings.css\n- Added window resize listener to Dashboard to manage sidebar visibility based on width\n- Verified all existing tests pass\n\nFixes NK-g818qn
-rw-r--r--.thicket/tickets.jsonl4
-rw-r--r--frontend/src/App.css65
-rw-r--r--frontend/src/App.tsx33
-rw-r--r--frontend/src/components/FeedItem.css22
-rw-r--r--frontend/src/components/FeedList.tsx18
-rw-r--r--frontend/src/components/Settings.css15
6 files changed, 144 insertions, 13 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl
index 7a4a919..6639ac0 100644
--- a/.thicket/tickets.jsonl
+++ b/.thicket/tickets.jsonl
@@ -22,6 +22,7 @@
{"id":"NK-7jh6re","title":"sidebar still ugly","description":"still very ugly, even compared to the original v1 static version\n\neither make it nicer or just copy the v1 version more directly","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T17:59:17.948112909Z","updated":"2026-02-14T18:01:26.48034794Z"}
{"id":"NK-7tzbql","title":"Fix TUI Content View Navigation and Interaction","description":"The TUI content view (reading a single item) is currently non-functional or severely limited. Users cannot easily navigate back, scroll, or interact with the content. This task involves improving the 'viewContent' state in the TUI.","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:02:57.382793121Z","updated":"2026-02-13T05:06:15.144485446Z"}
{"id":"NK-7xuajb","title":"[security] Add HTTP Security Headers","description":"Add middleware to set standard security headers: Content-Security-Policy (restrict sources), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:59.320775688Z","updated":"2026-02-14T17:20:46.397582923Z"}
+{"id":"NK-897v23","title":"Enhance UI with better loading indicators and error states","description":"The application should have a consistent and premium feel for loading and error states. Currently, it uses simple text. We should implement skeleton screens or more polished animations.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T22:49:05.942464799Z","updated":"2026-02-14T22:49:05.942464799Z"}
{"id":"NK-8hu7z1","title":"scrape full text button","description":"add this feature back in to the v2 ui and verify it is still working (not sure if we have any tests)","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:27:49.815938946Z","updated":"2026-02-14T17:58:19.083695149Z"}
{"id":"NK-8rhpp3","title":"v2 frontend: when selected, don't change style of feed items","description":"Just leave them the same when j/k \"selects\" an item. No blue side thing, no change in background, it's distracting. Just scroll it to the right place.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T00:39:50.01934312Z","updated":"2026-02-14T01:02:54.204739756Z"}
{"id":"NK-8s75ec","title":"page size and performance","description":"Do some analysis of page size (css/html/javascript) on the legacy version vs. new version and give me a report. We want it to be small and fast! If the new version is much worse file some tickets to investigate further.","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T20:16:13.898081788Z","updated":"2026-02-13T21:50:12.004391671Z"}
@@ -45,7 +46,7 @@
{"id":"NK-fm15vq","title":"UI: Improve accessibility for star icon","description":"The new star button should have proper aria-labels and potentially better focus states for screen readers.","type":"task","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T20:27:36.768034045Z","updated":"2026-02-13T20:27:36.768034045Z"}
{"id":"NK-fnaohu","title":"UI Styling: Dark Mode Support","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:19.59504351Z","updated":"2026-02-13T18:11:46.326064329Z"}
{"id":"NK-fpzx66","title":"v2 ui - title styling","description":"The title of the article stays blue and bold regardless of read state.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:22:55.339956853Z","updated":"2026-02-14T03:28:01.555909701Z"}
-{"id":"NK-g818qn","title":"Improve mobile responsiveness of React UI","description":"The React UI should be fully responsive and work well on small screens. Now that the vanilla JS prototype is removed, we should ensure the main interface is a great experience for mobile users.","type":"task","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-14T22:46:32.850472479Z","updated":"2026-02-14T22:46:32.850472479Z"}
+{"id":"NK-g818qn","title":"Improve mobile responsiveness of React UI","description":"The React UI should be fully responsive and work well on small screens. Now that the vanilla JS prototype is removed, we should ensure the main interface is a great experience for mobile users.","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T22:46:32.850472479Z","updated":"2026-02-14T22:49:01.411224187Z"}
{"id":"NK-gdf99z","title":"TUI is terrible and needs fixing","description":"The TUI doesn't really work and doesn't make sense. Think very hard and look at the v2 HTML UI implementation and make something cool like that. Probably needs tests too.","type":"epic","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T03:51:59.882212859Z","updated":"2026-02-14T04:31:28.290051717Z"}
{"id":"NK-gfh33y","title":"[security] Implement CSRF Protection for API","description":"Add CSRF protection to all state-changing API endpoints. 1. Implement a middleware that generates a CSRF token and sets it in a cookie (readable by JS) or header. 2. Update the AuthWrap middleware to validate the presence of this token in the X-CSRF-Token header for all unsafe methods (POST, PUT, DELETE). 3. Update the React frontend to read the token and include it in all API requests.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:56.341543505Z","updated":"2026-02-14T17:08:53.079904915Z"}
{"id":"NK-gnxc6e","title":"Feed list collapsed by default","description":"The list of feeds on the left side should be collapsed by default, with a little control to extend it.","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T00:58:15.661695308Z","updated":"2026-02-14T01:29:12.82081713Z"}
@@ -100,6 +101,7 @@
{"id":"NK-d0lgaab","from_ticket_id":"NK-sne5ox","to_ticket_id":"NK-0ppv3f","type":"created_from","created":"2026-02-13T15:05:23.289745853Z"}
{"id":"NK-d1q1e8a","from_ticket_id":"NK-wjnczv","to_ticket_id":"NK-chns2b","type":"created_from","created":"2026-02-14T05:13:23.323181359Z"}
{"id":"NK-d1uyy71","from_ticket_id":"NK-27or4b","to_ticket_id":"NK-bsdwqz","type":"created_from","created":"2026-02-13T05:03:09.689282214Z"}
+{"id":"NK-d2jhk2w","from_ticket_id":"NK-897v23","to_ticket_id":"NK-g818qn","type":"created_from","created":"2026-02-14T22:49:05.962608417Z"}
{"id":"NK-d3uzxij","from_ticket_id":"NK-g818qn","to_ticket_id":"NK-2tcnmq","type":"created_from","created":"2026-02-14T22:46:32.880451865Z"}
{"id":"NK-d50pbhs","from_ticket_id":"NK-zt4e32","to_ticket_id":"NK-t0nmbj","type":"created_from","created":"2026-02-13T05:44:01.598803513Z"}
{"id":"NK-d58l5y5","from_ticket_id":"NK-ojdcmq","to_ticket_id":"NK-5ocxgm","type":"created_from","created":"2026-02-13T19:45:07.395109046Z"}
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 773e114..f47c179 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -58,16 +58,75 @@ body {
top: 1rem;
left: 1rem;
z-index: 1000;
- background: transparent;
+ background: var(--bg-color);
+ /* Added bg to be visible over content if needed */
border: none;
font-size: 2rem;
line-height: 1;
cursor: pointer;
- padding: 0;
+ padding: 0.2rem;
color: var(--text-color);
- /* Inherit didn't work well if parent is transparent */
+ border-radius: 50%;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.fixed-toggle:hover {
transform: scale(1.1);
+}
+
+/* Mobile Responsiveness */
+@media (max-width: 768px) {
+ .dashboard-sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ z-index: 1100;
+ box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
+ width: 14rem;
+ /* Slightly wider on mobile for better target area */
+ }
+
+ .dashboard-sidebar.hidden {
+ margin-left: -14rem;
+ }
+
+ .dashboard-main {
+ padding: 1rem;
+ padding-top: 4rem;
+ /* Space for the toggle button */
+ }
+
+ .dashboard-main>* {
+ max-width: 100%;
+ }
+
+ /* When sidebar is visible on mobile, we show a backdrop */
+ .sidebar-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.4);
+ z-index: 1050;
+ animation: fadeIn 0.3s ease;
+ }
+
+ .dashboard.sidebar-visible::after {
+ display: none;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
} \ No newline at end of file
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1812451..478444c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -44,24 +44,47 @@ interface DashboardProps {
}
function Dashboard({ theme, setTheme, fontTheme, setFontTheme }: DashboardProps) {
- const [sidebarVisible, setSidebarVisible] = useState(true);
+ const [sidebarVisible, setSidebarVisible] = useState(window.innerWidth > 768);
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth > 768) {
+ setSidebarVisible(true);
+ } else {
+ setSidebarVisible(false);
+ }
+ };
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
return (
<div
className={`dashboard ${sidebarVisible ? 'sidebar-visible' : 'sidebar-hidden'} theme-${theme} font-${fontTheme}`}
>
<div className="dashboard-content">
- {!sidebarVisible && (
+ {(!sidebarVisible || window.innerWidth <= 768) && (
<button
className="sidebar-toggle fixed-toggle"
- onClick={() => setSidebarVisible(true)}
- title="Show Sidebar"
+ onClick={() => setSidebarVisible(!sidebarVisible)}
+ title={sidebarVisible ? "Hide Sidebar" : "Show Sidebar"}
>
🐱
</button>
)}
+ {sidebarVisible && (
+ <div
+ className="sidebar-backdrop"
+ onClick={() => setSidebarVisible(false)}
+ />
+ )}
<aside className={`dashboard-sidebar ${sidebarVisible ? '' : 'hidden'}`}>
- <FeedList theme={theme} setTheme={setTheme} setSidebarVisible={setSidebarVisible} />
+ <FeedList
+ theme={theme}
+ setTheme={setTheme}
+ setSidebarVisible={setSidebarVisible}
+ isMobile={window.innerWidth <= 768}
+ />
</aside>
<main className="dashboard-main">
<Routes>
diff --git a/frontend/src/components/FeedItem.css b/frontend/src/components/FeedItem.css
index 4eca2b1..944b66a 100644
--- a/frontend/src/components/FeedItem.css
+++ b/frontend/src/components/FeedItem.css
@@ -123,4 +123,26 @@
.scrape-btn:hover {
background: var(--sidebar-bg);
+}
+
+@media (max-width: 768px) {
+ .feed-item {
+ margin-top: 2rem;
+ padding: 0.5rem;
+ }
+
+ .item-title {
+ font-size: 1.4rem;
+ word-break: break-word;
+ }
+
+ .item-header {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .item-actions {
+ margin-left: 0;
+ margin-bottom: 0.5rem;
+ }
} \ No newline at end of file
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
index 4ce2d71..250ce7d 100644
--- a/frontend/src/components/FeedList.tsx
+++ b/frontend/src/components/FeedList.tsx
@@ -8,10 +8,12 @@ export default function FeedList({
theme,
setTheme,
setSidebarVisible,
+ isMobile,
}: {
theme: string;
setTheme: (t: string) => void;
setSidebarVisible: (visible: boolean) => void;
+ isMobile: boolean;
}) {
const [feeds, setFeeds] = useState<Feed[]>([]);
const [tags, setTags] = useState<Category[]>([]);
@@ -39,6 +41,12 @@ export default function FeedList({
setFeedsExpanded(!feedsExpanded);
};
+ const handleLinkClick = () => {
+ if (isMobile) {
+ setSidebarVisible(false);
+ }
+ };
+
useEffect(() => {
Promise.all([
apiFetch('/api/feed/').then((res) => {
@@ -89,17 +97,17 @@ export default function FeedList({
<div className="filter-section">
<ul className="filter-list">
<li className="unread_filter">
- <Link to="/?filter=unread" className={currentFilter === 'unread' ? 'active' : ''}>
+ <Link to="/?filter=unread" className={currentFilter === 'unread' ? 'active' : ''} onClick={handleLinkClick}>
unread
</Link>
</li>
<li className="all_filter">
- <Link to="/?filter=all" className={currentFilter === 'all' ? 'active' : ''}>
+ <Link to="/?filter=all" className={currentFilter === 'all' ? 'active' : ''} onClick={handleLinkClick}>
all
</Link>
</li>
<li className="starred_filter">
- <Link to="/?filter=starred" className={currentFilter === 'starred' ? 'active' : ''}>
+ <Link to="/?filter=starred" className={currentFilter === 'starred' ? 'active' : ''} onClick={handleLinkClick}>
starred
</Link>
</li>
@@ -116,6 +124,7 @@ export default function FeedList({
<Link
to={`/tag/${encodeURIComponent(tag.title)}`}
className={`tag-link ${tagName === tag.title ? 'active' : ''}`}
+ onClick={handleLinkClick}
>
{tag.title}
</Link>
@@ -138,6 +147,7 @@ export default function FeedList({
<Link
to={`/feed/${feed._id}`}
className={`feed-title ${feedId === String(feed._id) ? 'active' : ''}`}
+ onClick={handleLinkClick}
>
{feed.title || feed.url}
</Link>
@@ -150,7 +160,7 @@ export default function FeedList({
<div className="nav-section">
<ul className="nav-list">
<li>
- <Link to="/settings" className="nav-link">
+ <Link to="/settings" className="nav-link" onClick={handleLinkClick}>
settings
</Link>
</li>
diff --git a/frontend/src/components/Settings.css b/frontend/src/components/Settings.css
index c8784a0..3738e61 100644
--- a/frontend/src/components/Settings.css
+++ b/frontend/src/components/Settings.css
@@ -110,8 +110,23 @@
}
@media (max-width: 600px) {
+ .settings-page {
+ padding: 1rem;
+ }
+
+ .add-feed-form {
+ flex-direction: column;
+ }
+
.import-export-section {
flex-direction: column;
+ gap: 1rem;
+ }
+
+ .settings-feed-item {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1rem;
}
}