From e3c379d069ffa9661561d25cdbf2f5894a2f8ee8 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Sat, 14 Feb 2026 08:58:38 -0800 Subject: Refactor: project structure, implement dependency injection, and align v2 UI with v1 --- .github/workflows/ci.yml | 8 + .gitignore | 2 + .golangci.yml | 20 + .thicket/tickets.jsonl | 19 +- Makefile | 5 +- api/api.go | 51 +- api/api_test.go | 113 +- cmd/neko/main.go | 152 + cmd/neko/main_test.go | 124 + crawler/crawler.go | 160 - crawler/crawler_test.go | 278 - crawler/integration_test.go | 67 - exporter/exporter.go | 61 - exporter/exporter_test.go | 111 - frontend/.prettierrc | 7 + frontend/README.md | 8 +- frontend/coverage/base.css | 384 +- frontend/coverage/block-navigation.js | 133 +- frontend/coverage/coverage-final.json | 2251 +- frontend/coverage/index.html | 238 +- frontend/coverage/prettify.css | 102 +- frontend/coverage/prettify.js | 939 +- frontend/coverage/sorter.js | 379 +- frontend/coverage/src/App.css.html | 128 +- frontend/coverage/src/App.tsx.html | 128 +- frontend/coverage/src/components/FeedItem.css.html | 133 +- frontend/coverage/src/components/FeedItem.tsx.html | 133 +- .../coverage/src/components/FeedItems.css.html | 133 +- .../coverage/src/components/FeedItems.tsx.html | 133 +- frontend/coverage/src/components/FeedList.css.html | 133 +- frontend/coverage/src/components/FeedList.tsx.html | 133 +- frontend/coverage/src/components/Login.css.html | 133 +- frontend/coverage/src/components/Login.tsx.html | 133 +- frontend/coverage/src/components/Settings.css.html | 133 +- frontend/coverage/src/components/Settings.tsx.html | 133 +- frontend/coverage/src/components/index.html | 504 +- frontend/coverage/src/index.html | 236 +- frontend/eslint.config.js | 34 +- frontend/package-lock.json | 108 + frontend/package.json | 8 +- frontend/playwright-report/index.html | 21860 ++++++++++++++++++- frontend/playwright.config.ts | 40 +- frontend/src/App.css | 4 +- frontend/src/App.test.tsx | 79 +- frontend/src/App.tsx | 42 +- frontend/src/components/FeedItem.css | 124 +- frontend/src/components/FeedItem.test.tsx | 103 +- frontend/src/components/FeedItem.tsx | 146 +- frontend/src/components/FeedItems.css | 23 +- frontend/src/components/FeedItems.test.tsx | 431 +- frontend/src/components/FeedItems.tsx | 423 +- frontend/src/components/FeedList.css | 182 +- frontend/src/components/FeedList.test.tsx | 200 +- frontend/src/components/FeedList.tsx | 246 +- frontend/src/components/Login.css | 4 +- frontend/src/components/Login.test.tsx | 101 +- frontend/src/components/Login.tsx | 84 +- frontend/src/components/Settings.css | 87 +- frontend/src/components/Settings.test.tsx | 145 +- frontend/src/components/Settings.tsx | 214 +- frontend/src/components/TagView.test.tsx | 128 +- frontend/src/index.css | 10 +- frontend/src/main.tsx | 12 +- frontend/src/setupTests.ts | 48 +- frontend/src/types.ts | 34 +- frontend/test-results/.last-run.json | 2 +- frontend/tests/e2e.spec.ts | 114 +- frontend/tsconfig.app.json | 21 +- frontend/tsconfig.json | 5 +- frontend/tsconfig.node.json | 15 +- frontend/vite.config.ts | 8 +- frontend/vitest.config.ts | 16 +- importer/importer.go | 89 - importer/importer_test.go | 149 - internal/crawler/crawler.go | 161 + internal/crawler/crawler_test.go | 278 + internal/crawler/integration_test.go | 67 + internal/exporter/exporter.go | 61 + internal/exporter/exporter_test.go | 111 + internal/importer/importer.go | 89 + internal/importer/importer_test.go | 149 + internal/vlog/vlog.go | 25 + internal/vlog/vlog_test.go | 78 + main.go | 152 - main_test.go | 124 - models/db.go | 7 +- models/item/item.go | 2 +- vlog/vlog.go | 25 - vlog/vlog_test.go | 78 - web/dist/v2/assets/index-B3U-SMew.css | 1 - web/dist/v2/assets/index-DHAgSqjC.js | 11 + web/dist/v2/assets/index-f8d4YxQn.js | 11 - web/dist/v2/assets/index-mxokH1ey.css | 1 + web/dist/v2/index.html | 4 +- web/web.go | 32 +- web/web_test.go | 10 +- 96 files changed, 30088 insertions(+), 4666 deletions(-) create mode 100644 .golangci.yml create mode 100644 cmd/neko/main.go create mode 100644 cmd/neko/main_test.go delete mode 100644 crawler/crawler.go delete mode 100644 crawler/crawler_test.go delete mode 100644 crawler/integration_test.go delete mode 100644 exporter/exporter.go delete mode 100644 exporter/exporter_test.go create mode 100644 frontend/.prettierrc delete mode 100644 importer/importer.go delete mode 100644 importer/importer_test.go create mode 100644 internal/crawler/crawler.go create mode 100644 internal/crawler/crawler_test.go create mode 100644 internal/crawler/integration_test.go create mode 100644 internal/exporter/exporter.go create mode 100644 internal/exporter/exporter_test.go create mode 100644 internal/importer/importer.go create mode 100644 internal/importer/importer_test.go create mode 100644 internal/vlog/vlog.go create mode 100644 internal/vlog/vlog_test.go delete mode 100644 main.go delete mode 100644 main_test.go delete mode 100644 vlog/vlog.go delete mode 100644 vlog/vlog_test.go delete mode 100644 web/dist/v2/assets/index-B3U-SMew.css create mode 100644 web/dist/v2/assets/index-DHAgSqjC.js delete mode 100644 web/dist/v2/assets/index-f8d4YxQn.js create mode 100644 web/dist/v2/assets/index-mxokH1ey.css diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98141be..0a87c1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,11 @@ jobs: - name: Vet run: go vet ./... + - name: Lint + uses: golangci/golangci-lint-action@v4 + with: + version: v1.54 + - name: Test run: go test -v ./... @@ -43,5 +48,8 @@ jobs: - name: Install dependencies run: npm ci + - name: Lint + run: npm run lint + - name: Test run: npm test -- --run diff --git a/.gitignore b/.gitignore index d67bd06..c690d45 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ run_e2e.sh .agent/workflows/ frontend/playwright-report/ frontend/test-results/ +frontend/coverage/ +readme.html diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..61937fb --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +run: + timeout: 5m + tests: true + +linters: + enable: + - govet + - staticcheck + - gofmt + - goimports + - misspell + - gocyclo + - unparam + - unused + +linters-settings: + gocyclo: + min-complexity: 15 + goimports: + local-prefixes: adammathes.com/neko diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl index 29ccb06..732d5d5 100644 --- a/.thicket/tickets.jsonl +++ b/.thicket/tickets.jsonl @@ -1,3 +1,4 @@ +{"id":"NK-0ca7nq","title":"[security] Mitigate SSRF in Image Proxy and Feed Fetcher","description":"Restrict outbound HTTP requests to prevent access to internal networks. 1. Create a custom http.Transport for the fetcher clients. 2. In the DialContext, resolve the IP address of the target hostname. 3. Block connections to private IP ranges (RFC 1918) and loopback addresses (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16). 4. Disable following redirects to private IPs.","type":"","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:57.352011404Z","updated":"2026-02-14T16:36:49.362993079Z"} {"id":"NK-0nf7hu","title":"Implement Frontend Logout","description":"Add logout button/link in dashboard and call /api/logout.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:50:46.760744241Z","updated":"2026-02-13T15:28:14.486180285Z"} {"id":"NK-0ppv3f","title":"Implement Frontend Settings","description":"Create settings page for managing feeds/categories.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.631640578Z","updated":"2026-02-13T15:04:12.408401691Z"} {"id":"NK-13v159","title":"docker compose fails","description":"When running docker compose up I got the following error:\n[2/3] STEP 1/9: FROM golang:1.23-bullseye AS backend-builder\nResolved \"golang\" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)\nTrying to pull docker.io/library/golang:1.23-bullseye...\nGetting image source signatures\nCopying blob sha256:bb30cea4afcbc0a0405508119c28d87afb4518e3558e2f1fb0a52a0498994287\nCopying blob sha256:a9acb5a6634ff8f020bd4562c483cdd83503103d2c080d87e777643b57123e41\nCopying blob sha256:b26972d9a448e4dba0ac85216372d6ee52bc89839590b4e97f94b77ced5571fe\nCopying blob sha256:b1efd17e5717172aa4463c9c599bce51a6939b602dbb135bf6c26d672a6e7496\nCopying blob sha256:6a887974b056452b76229e3392cbf8513e741cd3dcd1f05e7397eea6fce361a0\nCopying blob sha256:382d65ac76ebcbc7ba7ee0d232ae7afbec48e2b3b673983ac8ced522dabe3abb\nCopying blob sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1\nCopying config sha256:6f1643fb9acc4fc0b9bf95efd1b4b1cd274dcd73a39cc99ef57da203d7088f42\nWriting manifest to image destination\n[2/3] STEP 2/9: RUN go install github.com/GeertJohan/go.rice/rice@latest\ngo: downloading github.com/GeertJohan/go.rice v1.0.3\ngo: downloading github.com/GeertJohan/go.incremental v1.0.0\ngo: downloading github.com/akavel/rsrc v0.8.0\ngo: downloading github.com/daaku/go.zipexe v1.0.2\ngo: downloading github.com/jessevdk/go-flags v1.4.0\ngo: downloading github.com/nkovacs/streamquote v1.0.0\ngo: downloading github.com/valyala/fasttemplate v1.0.1\ngo: downloading github.com/valyala/bytebufferpool v1.0.0\n--\u003e 181d1254149e\n[2/3] STEP 3/9: WORKDIR /app\n--\u003e 3f497307e2ca\n[2/3] STEP 4/9: COPY go.mod go.sum ./\n--\u003e 339c24ca12cd\n[2/3] STEP 5/9: RUN go mod download\ngo: go.mod requires go \u003e= 1.24.2 (running go 1.23.12; GOTOOLCHAIN=local)\nError: building at STEP \"RUN go mod download\": while running runtime: exit status 1","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T01:10:13.257269131Z","updated":"2026-02-14T01:15:05.19401879Z"} @@ -12,11 +13,12 @@ {"id":"NK-4juza2","title":"v2 ui - collapsible feed list","description":"In the left side navigation list of feeds, the feeds should be hidden by default. The word \"feeds\" should be a toggle to show/hide the list, with a triangle that indicates state.","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T02:48:25.194856022Z","updated":"2026-02-14T03:16:19.259726648Z"} {"id":"NK-59kbij","title":"Implement Frontend Logout","description":"Add logout button to dashboard header. Call /api/logout (need to create this potentially?). Redirect to /login","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:58:18.343464645Z","updated":"2026-02-13T15:01:33.783216589Z"} {"id":"NK-5ocxgm","title":"Infinite scroll","description":"a key feature of the original version that when you scroll to the bottom, it catches that and loads more (based on the current filters, etc)","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T19:27:01.14879752Z","updated":"2026-02-13T19:45:02.283640203Z"} -{"id":"NK-5zgzee","title":"Vanilla JS: Mobile Layout","description":"Make vanilla JS prototype responsive for mobile devices.","type":"feature","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T04:47:42.972969683Z","updated":"2026-02-14T04:47:42.972969683Z"} +{"id":"NK-5zgzee","title":"Vanilla JS: Mobile Layout","description":"Make vanilla JS prototype responsive for mobile devices.","type":"feature","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T04:47:42.972969683Z","updated":"2026-02-14T04:47:42.972969683Z"} {"id":"NK-6b4a2e","title":"v2 frontend BLUE LINKS","description":"Make most of the links BLUE and BOLD like in the old legacy version. Thanks","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T00:44:47.147880845Z","updated":"2026-02-14T01:09:26.770086073Z"} -{"id":"NK-6o87rr","title":"Vanilla JS: Implement Pagination","description":"Implement 'Load More' or infinite scroll for item list in vanilla JS prototype.","type":"feature","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T04:47:40.618957267Z","updated":"2026-02-14T04:47:40.618957267Z"} +{"id":"NK-6o87rr","title":"Vanilla JS: Implement Pagination","description":"Implement 'Load More' or infinite scroll for item list in vanilla JS prototype.","type":"feature","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T04:47:40.618957267Z","updated":"2026-02-14T04:47:40.618957267Z"} {"id":"NK-6q9nyg","title":"Refactor HTTP-dependent functions for testability","description":"Several functions use http.Get or external libraries directly (GetFullContent uses goose, ResolveFeedURL uses http.Get + goquery, imageProxyHandler uses http.Client). Refactor these to accept interfaces for HTTP fetching so they can be unit tested with mocks. This is the primary blocker for reaching 90% coverage.","type":"cleanup","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:37.630148644Z","updated":"2026-02-14T02:44:05.328784994Z"} {"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":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:59.320775688Z","updated":"2026-02-14T16:36:49.446994841Z"} {"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"} {"id":"NK-9hx0y7","title":"Implement Frontend Login","description":"Create login page and auth logic in the new React frontend. Port functionality from legacy login.html.","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.546395342Z","updated":"2026-02-13T05:50:33.877452063Z"} @@ -25,9 +27,11 @@ {"id":"NK-acq08a","title":"update Makefile","description":"Ensure the Makefile builds things and works\nTest it by running it regularly before checking in!","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T00:55:40.127322076Z","updated":"2026-02-14T01:26:31.564799193Z"} {"id":"NK-ahzf5c","title":"drop \"mark read\" button","description":"there's no mark read/unread buttons, it's just by scrolling!","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T19:28:20.708443259Z","updated":"2026-02-13T20:26:43.029168286Z"} {"id":"NK-bsdwqz","title":"terminal UI","description":"once there is good test coverage and a clean backend API, work on a nice efficient TUI with https://github.com/charmbracelet/bubbletea","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T01:54:02.285738454Z","updated":"2026-02-13T04:42:09.824268427Z"} -{"id":"NK-ca9t70","title":"Vanilla JS: Add Feed UI","description":"Add UI to add a new feed by URL in vanilla JS prototype.","type":"feature","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T04:47:41.764330544Z","updated":"2026-02-14T04:47:41.764330544Z"} -{"id":"NK-chns2b","title":"reach parity between vanilla js and react v2 ui","description":"Continue implementing the vanilla js one with minimal overhad/depdnencies to be fast and lean. Make sure there are tests and rely on the v2 ui and legacy version as references.","type":"epic","status":"open","priority":0,"labels":null,"assignee":"","created":"2026-02-14T04:45:06.813453353Z","updated":"2026-02-14T04:45:06.813453353Z"} +{"id":"NK-ca9t70","title":"Vanilla JS: Add Feed UI","description":"Add UI to add a new feed by URL in vanilla JS prototype.","type":"feature","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T04:47:41.764330544Z","updated":"2026-02-14T04:47:41.764330544Z"} +{"id":"NK-chns2b","title":"reach parity between vanilla js and react v2 ui","description":"Continue implementing the vanilla js one with minimal overhad/depdnencies to be fast and lean. Make sure there are tests and rely on the v2 ui and legacy version as references.","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T04:45:06.813453353Z","updated":"2026-02-14T04:45:06.813453353Z"} {"id":"NK-d4c8jv","title":"Vanilla JS Parity: Read/Star/Filter","description":"Implement read/unread toggle, star/unstar, and special filters (All, Unread, Starred) in vanilla JS prototype.","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T04:46:32.113504545Z","updated":"2026-02-14T04:47:46.412290355Z"} +{"id":"NK-doss0v","title":"v2 ui: change title fonts to Helvetica Neue","description":"to match style in legacy change font to match Helventic Neue where applicable","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T06:27:57.270935467Z","updated":"2026-02-14T06:31:42.798620609Z"} +{"id":"NK-dp5efo","title":"v2 ui: themes","description":"simplify the themes selector by getting of the giant THEMES title\n\nloook at the logic for the theme colors, it doesn't look like the feed item text is changing right. look at the legacy one for behavior.","type":"task","status":"open","priority":0,"labels":null,"assignee":"","created":"2026-02-14T06:30:23.170098963Z","updated":"2026-02-14T06:30:23.170098963Z"} {"id":"NK-ed1iah","title":"Make feed crawling async in API","description":"Currently, POST /api/feed triggers an immediate crawl which blocks the response (or at least keeps the goroutine alive). Refactor the crawling architecture to be truly async with a job queue or status updates, improving API responsiveness and reliability.","type":"cleanup","status":"icebox","priority":4,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.908243985Z","updated":"2026-02-13T04:26:55.908243985Z"} {"id":"NK-ek0cox","title":"Implement Item Interactions","description":"Add ability to toggle read/unread and star/unstar status for items. Use PUT /item/:id","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:55:14.825454967Z","updated":"2026-02-13T14:58:18.307521003Z"} {"id":"NK-fkc119","title":"setup github ci","description":"Maybe it'd be nice to have github run the tests. Is that a thing we can try to setup","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T03:16:32.574415787Z","updated":"2026-02-14T03:23:01.837550873Z"} @@ -35,9 +39,10 @@ {"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-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":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:56.341543505Z","updated":"2026-02-14T16:36:49.305429179Z"} {"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"} {"id":"NK-gqkh96","title":"Remaining test coverage gaps","description":"Cross-package test coverage is at 81.2%. The remaining untested functions are: GetFullContent (goose HTTP extraction), indexHandler/serveBoxedFile (rice.MustFindBox), Serve (starts HTTP server), main, util.init. To reach 90%, consider: (1) refactoring GetFullContent to accept an interface for HTTP fetching, (2) refactoring Serve to extract route setup into a testable function, (3) mocking rice.MustFindBox, (4) using feeds from https://trenchant.org/feeds.txt as static test fixtures for integration tests.","type":"cleanup","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:30.298141982Z","updated":"2026-02-14T02:44:05.399097286Z"} -{"id":"NK-hspao2","title":"Vanilla JS: Implement Test Infrastructure","description":"Setup testing infrastructure for vanilla JS prototype to ensure 80% coverage. Refactor app.js for testability and add unit tests.","type":"task","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-14T05:13:11.587767054Z","updated":"2026-02-14T05:13:11.587767054Z"} +{"id":"NK-hspao2","title":"Vanilla JS: Implement Test Infrastructure","description":"Setup testing infrastructure for vanilla JS prototype to ensure 80% coverage. Refactor app.js for testability and add unit tests.","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T05:13:11.587767054Z","updated":"2026-02-14T05:13:11.587767054Z"} {"id":"NK-hyej38","title":"[ui] when a left menu item is \"active\" make it bold","description":"The \"default\" is UNREAD - this should be in the \"bold\" state when you're seeing that. When you filter out to \"ALL\" that should instead be bold. Same with the individual feeds if one is selected. And Starred.","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T00:47:25.74838134Z","updated":"2026-02-14T01:25:07.267016355Z"} {"id":"NK-iw9l7h","title":"Improve scraper heuristics","description":"The scraper currently uses a simple fallback between CleanedText and TopNode. It could be improved to better handle different article layouts.","type":"feature","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-14T01:04:11.588135487Z","updated":"2026-02-14T01:04:11.588135487Z"} {"id":"NK-jhludy","title":"600px width by default, closer to left panel","description":"On desktop the feed items are too narrow (~500px) compared to legacy version which is ~600px.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T20:14:48.84900972Z","updated":"2026-02-13T20:50:22.207833479Z"} @@ -49,12 +54,16 @@ {"id":"NK-lrew5z","title":"UI Styling: Global Typography \u0026 Layout (Fixed Width)","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:04:57.254341682Z","updated":"2026-02-13T18:11:31.436752093Z"} {"id":"NK-m8bya7","title":"Fix and Re-enable Playwright E2E Tests","description":"E2E tests were crashing the VM and timing out. Disabled them in package.json. Need to investigate resource usage and re-enable.","type":"bug","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T03:08:18.501189975Z","updated":"2026-02-14T04:00:03.995357386Z"} {"id":"NK-mbuw7q","title":"v2 ui bug - panel open by default","description":"Panel is closed by default, it should be open by default on desktop.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T02:46:56.870671639Z","updated":"2026-02-14T03:08:17.322841854Z"} +{"id":"NK-mgmn5m","title":"serve \"legacy\" version UI at /v1/ instead of /","description":"Let's \"softly\" start to deprecated the legacy version by moving it to /v1/ -- ideally this won't require any changes but there may be some relative/absolute URLs to adjust in the static files there or in rouoting","type":"task","status":"open","priority":0,"labels":null,"assignee":"","created":"2026-02-14T16:41:04.710679944Z","updated":"2026-02-14T16:41:04.710679944Z"} {"id":"NK-mwf9q2","title":"Implement Tag View","description":"Create frontend view for browsing items by tag/category. Use /tag/:id endpoint.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:04:12.441165286Z","updated":"2026-02-13T18:04:38.644796168Z"} {"id":"NK-n7nuyy","title":"Fix TypeScript Lint Errors in Tests","description":"There are lint errors in test files regarding jest-dom matchers (toBeInTheDocument, etc). Ensure proper types are included.","type":"bug","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T21:50:15.140702806Z","updated":"2026-02-13T21:50:15.140702806Z"} +{"id":"NK-o3n9jf","title":"[security] Run Docker Container as Non-Root User","description":"Update the Dockerfile to create and use a non-privileged user. 1. Create a user (e.g., neko) in the final stage. 2. Ensure the /app/data directory is owned by this user. 3. Switch to this user using USER neko before the CMD.","type":"","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:58.328232962Z","updated":"2026-02-14T16:36:49.406727944Z"} {"id":"NK-ojdcmq","title":"UI: Add skeleton loaders for feed item loading","description":"The currently 'Loading more...' text is basic. We should add skeleton loaders for a smoother infinite scroll experience.","type":"task","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T19:45:07.376295295Z","updated":"2026-02-13T19:45:07.376295295Z"} {"id":"NK-op5594","title":"Ensure 80% Frontend Test Coverage","description":"Configure coverage reporting in vitest and ensure the frontend codebase maintains at least 80% test coverage.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:46:24.13314466Z","updated":"2026-02-13T05:50:46.728239299Z"} +{"id":"NK-p89hyt","title":"make new v2 UI the default and serve at /","description":"After we move the old UI to be served at v1, serve the new UI at /\n\nWe can keep serving it at v2/ as well if we want.","type":"task","status":"open","priority":0,"labels":null,"assignee":"","created":"2026-02-14T16:42:20.13241547Z","updated":"2026-02-14T16:42:20.13241547Z"} {"id":"NK-pumdm4","title":"get rid of the \"selected\" highlight thing","description":"the legacy version doesn't do that and i find it distracting, j/k just move things up/down","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T20:27:51.434041661Z","updated":"2026-02-13T22:37:06.185341246Z"} {"id":"NK-qwef98","title":"UI Styling: Controls \u0026 Header","description":"","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T18:05:18.450759919Z","updated":"2026-02-13T18:11:46.291830432Z"} +{"id":"NK-r6nhj0","title":"import/export","description":"Import/Export has only ever been partially implemented. Let's finish it up across OPML (de facto standard) but also simple txt line oriented input/output. We may need to file a ticket to deal with the async crawling as part of this.","type":"feature","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-14T16:45:04.739162003Z","updated":"2026-02-14T16:45:04.739162003Z"} {"id":"NK-ric1zs","title":"Migrate frontend to /api/ endpoints","description":"The backend now provides a clean REST API at /api/. Update the frontend UI to use these new endpoints instead of the legacy backward-compatibility routes (/stream/, /feed/, etc.). This will allow for cleaner separation and better utilization of proper REST patterns.","type":"cleanup","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.864725765Z","updated":"2026-02-13T04:26:55.864725765Z"} {"id":"NK-rohuiq","title":"titles changing on read state and hover","description":"Titles are changing on read state from blue to grey. They should just stay blue all the time.\n\nTitles are getting underlined on hover. They should have no underline regardless of hover state.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:36:26.36373162Z","updated":"2026-02-14T03:37:50.73870586Z"} {"id":"NK-shpyxh","title":"add search to new ui","description":"","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T19:29:44.251257089Z","updated":"2026-02-14T01:02:58.547025683Z"} diff --git a/Makefile b/Makefile index 30504d1..e7348c2 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ vanilla-ui: cp vanilla/index.html vanilla/app.js vanilla/style.css web/dist/vanilla/ build: - ${GO} build ${LDFLAGS} -o ${BINARY} + ${GO} build ${LDFLAGS} -o ${BINARY} ./cmd/neko install: build cp ${BINARY} ${GOBIN} @@ -39,6 +39,9 @@ install: build test: ${GO} test ./... cd frontend && ${NPM} test -- --run + +lint: + golangci-lint run run: build ./${BINARY} diff --git a/api/api.go b/api/api.go index 1ce6e5a..5e7618d 100644 --- a/api/api.go +++ b/api/api.go @@ -7,23 +7,36 @@ import ( "strconv" "strings" - "adammathes.com/neko/crawler" - "adammathes.com/neko/exporter" + "adammathes.com/neko/config" + "adammathes.com/neko/internal/crawler" + "adammathes.com/neko/internal/exporter" "adammathes.com/neko/models/feed" "adammathes.com/neko/models/item" ) -// NewRouter returns a configured mux with all API routes. -func NewRouter() *http.ServeMux { - mux := http.NewServeMux() - mux.HandleFunc("/stream", HandleStream) - mux.HandleFunc("/item/", HandleItem) - mux.HandleFunc("/feed", HandleFeed) - mux.HandleFunc("/feed/", HandleFeed) - mux.HandleFunc("/tag", HandleCategory) - mux.HandleFunc("/export/", HandleExport) - mux.HandleFunc("/crawl", HandleCrawl) - return mux +type Server struct { + Config *config.Settings + *http.ServeMux +} + +// NewServer returns a configured server with all API routes. +func NewServer(cfg *config.Settings) *Server { + s := &Server{ + Config: cfg, + ServeMux: http.NewServeMux(), + } + s.routes() + return s +} + +func (s *Server) routes() { + s.HandleFunc("/stream", s.HandleStream) + s.HandleFunc("/item/", s.HandleItem) + s.HandleFunc("/feed", s.HandleFeed) + s.HandleFunc("/feed/", s.HandleFeed) + s.HandleFunc("/tag", s.HandleCategory) + s.HandleFunc("/export/", s.HandleExport) + s.HandleFunc("/crawl", s.HandleCrawl) } func jsonError(w http.ResponseWriter, msg string, code int) { @@ -37,7 +50,7 @@ func jsonResponse(w http.ResponseWriter, data interface{}) { json.NewEncoder(w).Encode(data) } -func HandleStream(w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleStream(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonError(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -72,7 +85,7 @@ func HandleStream(w http.ResponseWriter, r *http.Request) { jsonResponse(w, items) } -func HandleItem(w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleItem(w http.ResponseWriter, r *http.Request) { idStr := strings.TrimPrefix(r.URL.Path, "/item/") id, _ := strconv.ParseInt(idStr, 10, 64) @@ -115,7 +128,7 @@ func HandleItem(w http.ResponseWriter, r *http.Request) { } } -func HandleFeed(w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleFeed(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: feeds, err := feed.All() @@ -180,7 +193,7 @@ func HandleFeed(w http.ResponseWriter, r *http.Request) { } } -func HandleCategory(w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleCategory(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonError(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -194,7 +207,7 @@ func HandleCategory(w http.ResponseWriter, r *http.Request) { jsonResponse(w, categories) } -func HandleExport(w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleExport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonError(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -208,7 +221,7 @@ func HandleExport(w http.ResponseWriter, r *http.Request) { w.Write([]byte(exporter.ExportFeeds(format))) } -func HandleCrawl(w http.ResponseWriter, r *http.Request) { +func (s *Server) HandleCrawl(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { jsonError(w, "method not allowed", http.StatusMethodNotAllowed) return diff --git a/api/api_test.go b/api/api_test.go index 2adc357..15679f7 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -46,14 +46,18 @@ func seedData(t *testing.T) { i.Create() } +func newTestServer() *Server { + return NewServer(&config.Config) +} + func TestStream(t *testing.T) { setupTestDB(t) seedData(t) - router := NewRouter() + server := newTestServer() req := httptest.NewRequest("GET", "/stream", nil) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) @@ -68,14 +72,14 @@ func TestStream(t *testing.T) { func TestFeedCRUD(t *testing.T) { setupTestDB(t) - router := NewRouter() + server := newTestServer() // Create f := feed.Feed{Url: "http://example.com", Title: "New Feed"} b, _ := json.Marshal(f) req := httptest.NewRequest("POST", "/feed", bytes.NewBuffer(b)) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusCreated { t.Errorf("expected 201, got %d", rr.Code) @@ -84,7 +88,7 @@ func TestFeedCRUD(t *testing.T) { // List req = httptest.NewRequest("GET", "/feed", nil) rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) var feeds []feed.Feed json.NewDecoder(rr.Body).Decode(&feeds) @@ -99,7 +103,7 @@ func TestFeedCRUD(t *testing.T) { b, _ = json.Marshal(feeds[0]) req = httptest.NewRequest("PUT", "/feed", bytes.NewBuffer(b)) rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) @@ -108,7 +112,7 @@ func TestFeedCRUD(t *testing.T) { // Delete req = httptest.NewRequest("DELETE", "/feed/"+strconv.FormatInt(feedID, 10), nil) rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", rr.Code) @@ -118,7 +122,7 @@ func TestFeedCRUD(t *testing.T) { func TestItemUpdate(t *testing.T) { setupTestDB(t) seedData(t) - router := NewRouter() + server := newTestServer() // Get an item first to know its ID var id int64 @@ -131,7 +135,7 @@ func TestItemUpdate(t *testing.T) { b, _ := json.Marshal(i) req := httptest.NewRequest("PUT", "/item/"+strconv.FormatInt(id, 10), bytes.NewBuffer(b)) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) @@ -141,11 +145,11 @@ func TestItemUpdate(t *testing.T) { func TestGetCategories(t *testing.T) { setupTestDB(t) seedData(t) - router := NewRouter() + server := newTestServer() req := httptest.NewRequest("GET", "/tag", nil) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) @@ -153,20 +157,21 @@ func TestGetCategories(t *testing.T) { var cats []feed.Category json.NewDecoder(rr.Body).Decode(&cats) - if len(cats) != 1 { // Corrected 'categories' to 'cats' for syntactic correctness - t.Errorf("Expected 1 category, got %d", len(cats)) // Corrected 'categories' to 'cats' + if len(cats) != 1 { + t.Errorf("Expected 1 category, got %d", len(cats)) } } func TestHandleExport(t *testing.T) { setupTestDB(t) seedData(t) + server := newTestServer() formats := []string{"text", "json", "opml", "html"} for _, fmt := range formats { req := httptest.NewRequest("GET", "/export/"+fmt, nil) rr := httptest.NewRecorder() - HandleExport(rr, req) + server.HandleExport(rr, req) if rr.Code != http.StatusOK { t.Errorf("Expected 200 for format %s, got %d", fmt, rr.Code) @@ -175,18 +180,19 @@ func TestHandleExport(t *testing.T) { req := httptest.NewRequest("GET", "/export/unknown", nil) rr := httptest.NewRecorder() - HandleExport(rr, req) - if rr.Code != http.StatusOK { // This should probably be http.StatusBadRequest or similar for unknown format + server.HandleExport(rr, req) + if rr.Code != http.StatusOK { t.Errorf("Expected 200 for unknown format, got %d", rr.Code) } } func TestHandleCrawl(t *testing.T) { setupTestDB(t) + server := newTestServer() req := httptest.NewRequest("POST", "/crawl", nil) rr := httptest.NewRecorder() - HandleCrawl(rr, req) + server.HandleCrawl(rr, req) if rr.Code != http.StatusOK { t.Errorf("Expected 200, got %d", rr.Code) @@ -194,14 +200,14 @@ func TestHandleCrawl(t *testing.T) { if !strings.Contains(rr.Body.String(), "crawl started") { t.Error("Expected crawl started message in response") } - // Wait for background goroutine to at least start/finish before DB is closed by cleanup time.Sleep(100 * time.Millisecond) } func TestJsonError(t *testing.T) { + server := newTestServer() req := httptest.NewRequest("PUT", "/item/notanumber", nil) rr := httptest.NewRecorder() - HandleItem(rr, req) + server.HandleItem(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", rr.Code) @@ -216,7 +222,7 @@ func TestJsonError(t *testing.T) { func TestHandleStreamFilters(t *testing.T) { setupTestDB(t) seedData(t) - router := NewRouter() + server := newTestServer() testCases := []struct { url string @@ -225,14 +231,14 @@ func TestHandleStreamFilters(t *testing.T) { {"/stream?tag=tech", 1}, {"/stream?tag=missing", 0}, {"/stream?feed_url=http://example.com", 1}, - {"/stream?starred=1", 0}, // none starred in seed + {"/stream?starred=1", 0}, {"/stream?q=Test", 1}, } for _, tc := range testCases { req := httptest.NewRequest("GET", tc.url, nil) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) var items []item.Item json.NewDecoder(rr.Body).Decode(&items) @@ -244,29 +250,26 @@ func TestHandleStreamFilters(t *testing.T) { func TestHandleFeedErrors(t *testing.T) { setupTestDB(t) - router := NewRouter() + server := newTestServer() - // Post missing URL b, _ := json.Marshal(feed.Feed{Title: "No URL"}) req := httptest.NewRequest("POST", "/feed", bytes.NewBuffer(b)) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400 for missing URL, got %d", rr.Code) } - // Invalid JSON req = httptest.NewRequest("POST", "/feed", strings.NewReader("not json")) rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400 for invalid JSON, got %d", rr.Code) } - // Method Not Allowed req = httptest.NewRequest("PATCH", "/feed", nil) rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("Expected 405, got %d", rr.Code) } @@ -275,25 +278,22 @@ func TestHandleFeedErrors(t *testing.T) { func TestHandleItemEdgeCases(t *testing.T) { setupTestDB(t) seedData(t) - router := NewRouter() + server := newTestServer() - // Item not found req := httptest.NewRequest("GET", "/item/999", nil) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusNotFound { t.Errorf("Expected 404, got %d", rr.Code) } - // Method not allowed req = httptest.NewRequest("DELETE", "/item/1", nil) rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("Expected 405, got %d", rr.Code) } - // GET/POST for content extraction (mocked content extraction is tested in models/item) var id int64 err := models.DB.QueryRow("SELECT id FROM item LIMIT 1").Scan(&id) if err != nil { @@ -302,7 +302,7 @@ func TestHandleItemEdgeCases(t *testing.T) { req = httptest.NewRequest("GET", "/item/"+strconv.FormatInt(id, 10), nil) rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Errorf("Expected 200, got %d", rr.Code) } @@ -310,11 +310,11 @@ func TestHandleItemEdgeCases(t *testing.T) { func TestHandleFeedDeleteNoId(t *testing.T) { setupTestDB(t) - router := NewRouter() + server := newTestServer() req := httptest.NewRequest("DELETE", "/feed/", nil) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400, got %d", rr.Code) } @@ -322,7 +322,7 @@ func TestHandleFeedDeleteNoId(t *testing.T) { func TestMethodNotAllowed(t *testing.T) { setupTestDB(t) - router := NewRouter() + server := newTestServer() testCases := []struct { method string @@ -336,7 +336,7 @@ func TestMethodNotAllowed(t *testing.T) { for _, tc := range testCases { req := httptest.NewRequest(tc.method, tc.url, nil) rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) + server.ServeHTTP(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("Expected 405 for %s %s, got %d", tc.method, tc.url, rr.Code) } @@ -345,9 +345,10 @@ func TestMethodNotAllowed(t *testing.T) { func TestExportBadRequest(t *testing.T) { setupTestDB(t) + server := newTestServer() req := httptest.NewRequest("GET", "/export/", nil) rr := httptest.NewRecorder() - HandleExport(rr, req) + server.HandleExport(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400 for empty format, got %d", rr.Code) } @@ -355,9 +356,10 @@ func TestExportBadRequest(t *testing.T) { func TestHandleFeedPutInvalidJson(t *testing.T) { setupTestDB(t) + server := newTestServer() req := httptest.NewRequest("PUT", "/feed", strings.NewReader("not json")) rr := httptest.NewRecorder() - HandleFeed(rr, req) + server.HandleFeed(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400 for invalid JSON in PUT, got %d", rr.Code) } @@ -365,10 +367,11 @@ func TestHandleFeedPutInvalidJson(t *testing.T) { func TestHandleFeedPutMissingId(t *testing.T) { setupTestDB(t) + server := newTestServer() b, _ := json.Marshal(feed.Feed{Title: "No ID"}) req := httptest.NewRequest("PUT", "/feed", bytes.NewBuffer(b)) rr := httptest.NewRecorder() - HandleFeed(rr, req) + server.HandleFeed(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400 for missing ID in PUT, got %d", rr.Code) } @@ -377,22 +380,24 @@ func TestHandleFeedPutMissingId(t *testing.T) { func TestHandleItemIdMismatch(t *testing.T) { setupTestDB(t) seedData(t) - b, _ := json.Marshal(item.Item{Id: 999}) // mismatch with path 1 + server := newTestServer() + b, _ := json.Marshal(item.Item{Id: 999}) req := httptest.NewRequest("PUT", "/item/1", bytes.NewBuffer(b)) rr := httptest.NewRecorder() - HandleItem(rr, req) + server.HandleItem(rr, req) if rr.Code != http.StatusBadRequest { t.Errorf("Expected 400 for ID mismatch, got %d", rr.Code) } } + func TestHandleCategoryError(t *testing.T) { setupTestDB(t) - // Close DB to force error + server := newTestServer() models.DB.Close() req := httptest.NewRequest("GET", "/tag", nil) rr := httptest.NewRecorder() - HandleCategory(rr, req) + server.HandleCategory(rr, req) if rr.Code != http.StatusInternalServerError { t.Errorf("Expected 500, got %d", rr.Code) @@ -402,15 +407,15 @@ func TestHandleCategoryError(t *testing.T) { func TestHandleItemAlreadyHasContent(t *testing.T) { setupTestDB(t) seedData(t) + server := newTestServer() var id int64 models.DB.QueryRow("SELECT id FROM item LIMIT 1").Scan(&id) - // Pre-set content models.DB.Exec("UPDATE item SET full_content = 'existing' WHERE id = ?", id) req := httptest.NewRequest("GET", "/item/"+strconv.FormatInt(id, 10), nil) rr := httptest.NewRecorder() - HandleItem(rr, req) + server.HandleItem(rr, req) if rr.Code != http.StatusOK { t.Errorf("Expected 200, got %d", rr.Code) @@ -418,9 +423,10 @@ func TestHandleItemAlreadyHasContent(t *testing.T) { } func TestHandleCrawlMethodNotAllowed(t *testing.T) { + server := newTestServer() req := httptest.NewRequest("GET", "/crawl", nil) rr := httptest.NewRecorder() - HandleCrawl(rr, req) + server.HandleCrawl(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("Expected 405, got %d", rr.Code) } @@ -429,11 +435,11 @@ func TestHandleCrawlMethodNotAllowed(t *testing.T) { func TestHandleStreamComplexFilters(t *testing.T) { setupTestDB(t) seedData(t) + server := newTestServer() - // Test max_id, feed_id combo req := httptest.NewRequest("GET", "/stream?max_id=999&feed_id=1", nil) rr := httptest.NewRecorder() - HandleStream(rr, req) + server.HandleStream(rr, req) if rr.Code != http.StatusOK { t.Errorf("Expected 200, got %d", rr.Code) } @@ -441,12 +447,13 @@ func TestHandleStreamComplexFilters(t *testing.T) { func TestHandleCategorySuccess(t *testing.T) { setupTestDB(t) + server := newTestServer() f := &feed.Feed{Url: "http://example.com/cat", Category: "News"} f.Create() req := httptest.NewRequest("GET", "/api/categories", nil) rr := httptest.NewRecorder() - HandleCategory(rr, req) + server.HandleCategory(rr, req) if rr.Code != http.StatusOK { t.Errorf("Expected 200, got %d", rr.Code) } diff --git a/cmd/neko/main.go b/cmd/neko/main.go new file mode 100644 index 0000000..47385b1 --- /dev/null +++ b/cmd/neko/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "os" + "time" + + "adammathes.com/neko/config" + "adammathes.com/neko/internal/crawler" + "adammathes.com/neko/internal/exporter" + "adammathes.com/neko/models" + "adammathes.com/neko/models/feed" + + "flag" + + "adammathes.com/neko/internal/vlog" + "adammathes.com/neko/web" +) + +var Version, Build string + +func main() { + if err := Run(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func Run(args []string) error { + var help, update, verbose, proxyImages bool + var configFile, dbfile, newFeed, export, password string + var port, minutes int + + f := flag.NewFlagSet("neko", flag.ContinueOnError) + + // config file + f.StringVar(&configFile, "config", "", "read configuration from file") + f.StringVar(&configFile, "c", "", "read configuration from file (short)") + + // commands + f.BoolVar(&help, "help", false, "display help") + f.BoolVar(&help, "h", false, "display help (short)") + + f.BoolVar(&update, "update", false, "fetch feeds and store new items") + f.BoolVar(&update, "u", false, "fetch feeds and store new items (short)") + + f.StringVar(&newFeed, "add", "", "add the feed at URL") + f.StringVar(&newFeed, "a", "", "add the feed at URL (short)") + + f.StringVar(&export, "export", "", "export feed: text, opml, html, json") + f.StringVar(&export, "x", "", "export feed (short)") + + // options + f.StringVar(&dbfile, "database", "", "sqlite database file") + f.StringVar(&dbfile, "d", "", "sqlite database file (short)") + + f.IntVar(&port, "http", 0, "HTTP port to serve on") + f.IntVar(&port, "s", 0, "HTTP port to serve on (short)") + + f.IntVar(&minutes, "minutes", 0, "minutes between crawling feeds") + f.IntVar(&minutes, "m", 0, "minutes between crawling feeds (short)") + + f.BoolVar(&proxyImages, "imageproxy", false, "rewrite and proxy all image requests") + f.BoolVar(&proxyImages, "i", false, "rewrite and proxy all image requests (short)") + + f.BoolVar(&verbose, "verbose", false, "verbose output") + f.BoolVar(&verbose, "v", false, "verbose output (short)") + + f.StringVar(&password, "password", "", "password for web interface") + f.StringVar(&password, "p", "", "password for web interface (short)") + + f.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + f.PrintDefaults() + } + + if err := f.Parse(args); err != nil { + return err + } + + if help { + fmt.Printf("neko v%s | build %s\n", Version, Build) + f.Usage() + return nil + } + // reads config if present and sets defaults + if err := config.Init(configFile); err != nil { + return fmt.Errorf("config error: %v", err) + } + + // override config file with flags if present + vlog.VERBOSE = verbose + if dbfile != "" { + config.Config.DBFile = dbfile + } + + if port != 0 { + config.Config.Port = port + } + + if password != "" { + config.Config.DigestPassword = password + } + + if minutes != 0 { + config.Config.CrawlMinutes = minutes + } + + if proxyImages != false { + config.Config.ProxyImages = proxyImages + } + + models.InitDB() + + if update { + vlog.Printf("starting crawl\n") + crawler.Crawl() + return nil + } + if newFeed != "" { + vlog.Printf("creating new feed\n") + feed.NewFeed(newFeed) + return nil + } + if export != "" { + vlog.Printf("exporting feeds in format %s\n", export) + fmt.Printf("%s", exporter.ExportFeeds(export)) + return nil + } + + // For testing, we might want to avoid starting a web server + if config.Config.Port == -1 { + return nil + } + + go backgroundCrawl(config.Config.CrawlMinutes) + vlog.Printf("starting web server at 127.0.0.1:%d\n", + config.Config.Port) + web.Serve(&config.Config) + return nil +} + +func backgroundCrawl(minutes int) { + if minutes < 1 { + return + } + vlog.Printf("starting background crawl every %d minutes\n", minutes) + for { + time.Sleep(time.Minute * time.Duration(minutes)) + crawler.Crawl() + } +} diff --git a/cmd/neko/main_test.go b/cmd/neko/main_test.go new file mode 100644 index 0000000..fd36fdd --- /dev/null +++ b/cmd/neko/main_test.go @@ -0,0 +1,124 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "adammathes.com/neko/config" + "adammathes.com/neko/models" +) + +func TestRunHelp(t *testing.T) { + err := Run([]string{"--help"}) + if err != nil { + t.Errorf("Run(--help) should not error, got %v", err) + } +} + +func TestRunInvalidFlag(t *testing.T) { + err := Run([]string{"--invalid"}) + if err == nil { + t.Error("Run(--invalid) should error") + } +} + +func TestRunCrawl(t *testing.T) { + // Setup test DB + config.Config.DBFile = filepath.Join(t.TempDir(), "test_main.db") + models.InitDB() + defer models.DB.Close() + + // Use --update flag + err := Run([]string{"-u", "-d", config.Config.DBFile}) + if err != nil { + t.Errorf("Run(-u) should not error, got %v", err) + } +} + +func TestBackgroundCrawlZero(t *testing.T) { + backgroundCrawl(0) // Should return immediately +} + +func TestRunServerConfig(t *testing.T) { + // Setup test DB + config.Config.DBFile = filepath.Join(t.TempDir(), "test_main_server.db") + models.InitDB() + defer models.DB.Close() + + // Use config.Config.Port = -1 to signal Run to exit instead of starting server + config.Config.Port = -1 + err := Run([]string{"-d", config.Config.DBFile}) + if err != nil { + t.Errorf("Run should not error with Port=-1, got %v", err) + } +} + +func TestRunAdd(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test_add.db") + err := Run([]string{"-d", dbPath, "-a", "http://example.com/rss"}) + if err != nil { + t.Errorf("Run -a failed: %v", err) + } +} + +func TestRunExport(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test_export.db") + err := Run([]string{"-d", dbPath, "-x", "text"}) + if err != nil { + t.Errorf("Run -x failed: %v", err) + } +} + +func TestRunOptions(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test_options.db") + err := Run([]string{"-d", dbPath, "-v", "-i", "-s", "-1"}) + if err != nil { + t.Errorf("Run with options failed: %v", err) + } +} + +func TestRunSetPassword(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test_pass.db") + err := Run([]string{"-d", dbPath, "-p", "newpassword"}) + if err != nil { + t.Errorf("Run -p should succeed, got %v", err) + } + if config.Config.DigestPassword != "newpassword" { + t.Errorf("Expected password to be updated") + } +} + +func TestRunConfigError(t *testing.T) { + err := Run([]string{"-c", "/nonexistent/config.yaml"}) + if err == nil { + t.Error("Run should error for nonexistent config file") + } +} + +func TestRunExportFormat(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test_export_format.db") + err := Run([]string{"-d", dbPath, "-x", "json"}) + if err != nil { + t.Errorf("Run -x json failed: %v", err) + } +} + +func TestRunConfigInvalidContent(t *testing.T) { + tmpDir := t.TempDir() + confPath := filepath.Join(tmpDir, "bad.yaml") + os.WriteFile(confPath, []byte("invalid: : yaml"), 0644) + err := Run([]string{"-c", confPath}) + if err == nil { + t.Error("Run should error for malformed config file") + } +} + +func TestRunNoArgs(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "test_noargs.db") + config.Config.Port = -1 + err := Run([]string{"-d", dbPath}) + if err != nil { + t.Errorf("Run with no args failed: %v", err) + } +} diff --git a/crawler/crawler.go b/crawler/crawler.go deleted file mode 100644 index f8794d4..0000000 --- a/crawler/crawler.go +++ /dev/null @@ -1,160 +0,0 @@ -package crawler - -import ( - "adammathes.com/neko/models/feed" - "adammathes.com/neko/models/item" - "adammathes.com/neko/vlog" - "github.com/mmcdole/gofeed" - "io/ioutil" - "log" - "net/http" - "time" -) - -const MAX_CRAWLERS = 5 - -func Crawl() { - crawlJobs := make(chan *feed.Feed, 100) - results := make(chan string, 100) - - feeds, err := feed.All() - if err != nil { - log.Fatal(err) - } - - for i := 0; i < MAX_CRAWLERS; i++ { - vlog.Printf("spawning crawl worker %d\n", i) - go CrawlWorker(crawlJobs, results) - } - - for _, f := range feeds { - vlog.Printf("sending crawl job %s\n", f.Url) - crawlJobs <- f - } - close(crawlJobs) - - for i := 0; i < len(feeds); i++ { - vlog.Println(<-results) - } - close(results) -} - -func CrawlWorker(feeds <-chan *feed.Feed, results chan<- string) { - - for f := range feeds { - vlog.Printf("crawl job received %s\n", f.Url) - CrawlFeed(f, results) - vlog.Printf("crawl job finished %s\n", f.Url) - } -} - -/* -Simple HTTP Get fnx with custom user agent header -*/ -func GetFeedContent(feedURL string) string { - - // introduce delays for testing - // n := time.Duration(rand.Int63n(3)) - // time.Sleep(n * time.Second) - - c := &http.Client{ - // give up after 5 seconds - Timeout: 5 * time.Second, - } - - request, err := http.NewRequest("GET", feedURL, nil) - if err != nil { - log.Fatalln(err) - } - - userAgent := "neko RSS Crawler +https://github.com/adammathes/neko" - request.Header.Set("User-Agent", userAgent) - resp, err := c.Do(request) - - if err != nil { - return "" - } - - if resp != nil { - defer func() { - ce := resp.Body.Close() - if ce != nil { - err = ce - } - }() - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return "" - } - - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "" - } - return string(bodyBytes) -} - -/* - TODO: sanitize input on crawl -*/ -func CrawlFeed(f *feed.Feed, ch chan<- string) { - c := &http.Client{ - // give up after 5 seconds - Timeout: 5 * time.Second, - } - - fp := gofeed.NewParser() - fp.Client = c - - content := GetFeedContent(f.Url) - feed, err := fp.ParseString(content) - if err != nil { - vlog.Println(err) - ch <- "failed parse for " + f.Url + "\n" - return - } - - f.Title = feed.Title - f.WebUrl = feed.Link - f.Update() - - for _, i := range feed.Items { - vlog.Printf("storing item: %s\n", i.Link) - var item item.Item - item.Title = i.Title - item.Url = i.Link - - item.Description = i.Description - if len(i.Content) > len(item.Description) { - item.Description = i.Content - } - - // a lot of RSS2.0 generated by wordpress and others - // uses - e, ok := i.Extensions["content"]["encoded"] - var encoded = "" - if ok { - encoded = e[0].Value - } - if len(encoded) > len(item.Description) { - item.Description = encoded - } - - if i.PublishedParsed != nil { - item.PublishDate = i.PublishedParsed.Format("2006-01-02 15:04:05") - } else { - item.PublishDate = time.Now().Format("2006-01-02 15:04:05") - } - - item.FeedId = f.Id - err := item.Create() - if err != nil { - vlog.Println(err) - } - // else { - // item.GetFullContent() - //} - } - ch <- "successfully crawled " + f.Url + "\n" -} diff --git a/crawler/crawler_test.go b/crawler/crawler_test.go deleted file mode 100644 index e0c4c6b..0000000 --- a/crawler/crawler_test.go +++ /dev/null @@ -1,278 +0,0 @@ -package crawler - -import ( - "log" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "adammathes.com/neko/config" - "adammathes.com/neko/models" - "adammathes.com/neko/models/feed" -) - -func setupTestDB(t *testing.T) { - t.Helper() - config.Config.DBFile = ":memory:" - models.InitDB() - t.Cleanup(func() { - if models.DB != nil { - models.DB.Close() - } - }) -} - -func TestGetFeedContentSuccess(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ua := r.Header.Get("User-Agent") - if ua == "" { - t.Error("Request should include User-Agent") - } - w.WriteHeader(200) - w.Write([]byte("Test")) - })) - defer ts.Close() - - content := GetFeedContent(ts.URL) - if content == "" { - t.Error("GetFeedContent should return content for valid URL") - } - if content != "Test" { - t.Errorf("Unexpected content: %q", content) - } -} - -func TestGetFeedContentBadURL(t *testing.T) { - content := GetFeedContent("http://invalid.invalid.invalid:99999/feed") - if content != "" { - t.Errorf("GetFeedContent should return empty string for bad URL, got %q", content) - } -} - -func TestGetFeedContent404(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(404) - })) - defer ts.Close() - - content := GetFeedContent(ts.URL) - if content != "" { - t.Errorf("GetFeedContent should return empty for 404, got %q", content) - } -} - -func TestGetFeedContent500(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(500) - })) - defer ts.Close() - - content := GetFeedContent(ts.URL) - if content != "" { - t.Errorf("GetFeedContent should return empty for 500, got %q", content) - } -} - -func TestGetFeedContentUserAgent(t *testing.T) { - var receivedUA string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - receivedUA = r.Header.Get("User-Agent") - w.WriteHeader(200) - w.Write([]byte("ok")) - })) - defer ts.Close() - - GetFeedContent(ts.URL) - expected := "neko RSS Crawler +https://github.com/adammathes/neko" - if receivedUA != expected { - t.Errorf("Expected UA %q, got %q", expected, receivedUA) - } -} - -func TestCrawlFeedWithTestServer(t *testing.T) { - setupTestDB(t) - - rssContent := ` - - - Test Feed - https://example.com - - Article 1 - https://example.com/article1 - First article - Mon, 01 Jan 2024 00:00:00 GMT - - - Article 2 - https://example.com/article2 - Second article - Tue, 02 Jan 2024 00:00:00 GMT - - -` - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/rss+xml") - w.WriteHeader(200) - w.Write([]byte(rssContent)) - })) - defer ts.Close() - - // Create a feed pointing to the test server - f := &feed.Feed{Url: ts.URL, Title: "Test"} - f.Create() - - ch := make(chan string, 1) - CrawlFeed(f, ch) - result := <-ch - - if result == "" { - t.Error("CrawlFeed should send a result") - } - - // Verify items were created - var count int - models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&count) - if count != 2 { - t.Errorf("Expected 2 items, got %d", count) - } -} - -func TestCrawlFeedBadContent(t *testing.T) { - setupTestDB(t) - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write([]byte("not xml at all")) - })) - defer ts.Close() - - f := &feed.Feed{Url: ts.URL, Title: "Bad"} - f.Create() - - ch := make(chan string, 1) - CrawlFeed(f, ch) - result := <-ch - - if result == "" { - t.Error("CrawlFeed should send a result even on failure") - } -} - -func TestCrawlWorker(t *testing.T) { - setupTestDB(t) - - rssContent := ` - - - Worker Feed - https://example.com - - Worker Article - https://example.com/worker-article - An article - - -` - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write([]byte(rssContent)) - })) - defer ts.Close() - - f := &feed.Feed{Url: ts.URL, Title: "Worker Test"} - f.Create() - - feeds := make(chan *feed.Feed, 1) - results := make(chan string, 1) - - feeds <- f - close(feeds) - - CrawlWorker(feeds, results) - result := <-results - - if result == "" { - t.Error("CrawlWorker should produce a result") - } -} - -func TestCrawl(t *testing.T) { - setupTestDB(t) - - rssContent := ` - - - Crawl Feed - https://example.com - - Crawl Article - https://example.com/crawl-article - Article for crawl test - - -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write([]byte(rssContent)) - })) - defer ts.Close() - - f := &feed.Feed{Url: ts.URL, Title: "Full Crawl"} - f.Create() - - // Should not panic - Crawl() - - var count int - models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&count) - if count != 1 { - t.Errorf("Expected 1 item after crawl, got %d", count) - } -} - -func TestCrawlFeedWithExtensions(t *testing.T) { - setupTestDB(t) - - rssContent := ` - - - Extension Feed - - Extension Article - https://example.com/ext - Short description - - - -` - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write([]byte(rssContent)) - })) - defer ts.Close() - - f := &feed.Feed{Url: ts.URL, Title: "Extension Test"} - f.Create() - - ch := make(chan string, 1) - CrawlFeed(f, ch) - <-ch - - var itemTitle, itemDesc string - err := models.DB.QueryRow("SELECT title, description FROM item WHERE feed_id = ?", f.Id).Scan(&itemTitle, &itemDesc) - if err != nil { - log.Fatal(err) - } - - if itemTitle != "Extension Article" { - t.Errorf("Expected title 'Extension Article', got %q", itemTitle) - } - if !strings.Contains(itemDesc, "Much longer content") { - t.Errorf("Expected description to contain encoded content, got %q", itemDesc) - } -} diff --git a/crawler/integration_test.go b/crawler/integration_test.go deleted file mode 100644 index 633b60f..0000000 --- a/crawler/integration_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package crawler - -import ( - "fmt" - "net/http" - "net/http/httptest" - "os" - "testing" - - "adammathes.com/neko/models/feed" - "adammathes.com/neko/models/item" -) - -func TestCrawlIntegration(t *testing.T) { - setupTestDB(t) - - // Mock RSS feed server - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/rss+xml") - os.Stdout.Write([]byte("serving mock rss\n")) - fmt.Fprint(w, ` - - - Test Feed - http://example.com/ - Test Description - - Test Item 1 - http://example.com/item1 - Item 1 Description - Mon, 01 Jan 2024 00:00:00 +0000 - - -`) - })) - defer ts.Close() - - // Add the feed - f := &feed.Feed{Url: ts.URL} - err := f.Create() - if err != nil { - t.Fatalf("Failed to create feed: %v", err) - } - - // Crawl - ch := make(chan string, 1) - CrawlFeed(f, ch) - - res := <-ch - if res == "" { - t.Fatal("CrawlFeed returned empty result") - } - - // Verify items were stored - items, err := item.Filter(0, f.Id, "", false, false, 0, "") - if err != nil { - t.Fatalf("Failed to filter items: %v", err) - } - - if len(items) != 1 { - t.Fatalf("Expected 1 item, got %d", len(items)) - } - - if items[0].Title != "Test Item 1" { - t.Errorf("Expected 'Test Item 1', got %q", items[0].Title) - } -} diff --git a/exporter/exporter.go b/exporter/exporter.go deleted file mode 100644 index 9172fec..0000000 --- a/exporter/exporter.go +++ /dev/null @@ -1,61 +0,0 @@ -package exporter - -import ( - "adammathes.com/neko/models/feed" - "bytes" - "encoding/json" - "encoding/xml" - "fmt" - "html/template" -) - -func ExportFeeds(format string) string { - feeds, err := feed.All() - if err != nil { - panic(err) - } - - s := "" - switch format { - case "text": - for _, f := range feeds { - s = s + fmt.Sprintf("%s\n", f.Url) - } - - case "opml": - s = s + fmt.Sprintf(`neko feeds`) - s = s + fmt.Sprintf("\n") - for _, f := range feeds { - b, _ := xml.Marshal(f) - s = s + fmt.Sprintf("%s\n", string(b)) - } - s = s + fmt.Sprintf(``) - - case "json": - js, _ := json.Marshal(feeds) - s = fmt.Sprintf("%s\n", js) - - case "html": - htmlTemplateString := ` - -feeds - - - - -` - var bts bytes.Buffer - htmlTemplate, err := template.New("feeds").Parse(htmlTemplateString) - err = htmlTemplate.Execute(&bts, feeds) - if err != nil { - panic(err) - } - s = bts.String() - } - - return s -} diff --git a/exporter/exporter_test.go b/exporter/exporter_test.go deleted file mode 100644 index d4cc994..0000000 --- a/exporter/exporter_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package exporter - -import ( - "encoding/json" - "strings" - "testing" - - "adammathes.com/neko/config" - "adammathes.com/neko/models" -) - -func setupTestDB(t *testing.T) { - t.Helper() - config.Config.DBFile = ":memory:" - models.InitDB() - t.Cleanup(func() { - if models.DB != nil { - models.DB.Close() - } - }) -} - -func seedFeeds(t *testing.T) { - t.Helper() - _, err := models.DB.Exec("INSERT INTO feed(url, web_url, title, category) VALUES(?, ?, ?, ?)", - "https://a.com/feed", "https://a.com", "Alpha Feed", "tech") - if err != nil { - t.Fatal(err) - } - _, err = models.DB.Exec("INSERT INTO feed(url, web_url, title, category) VALUES(?, ?, ?, ?)", - "https://b.com/feed", "https://b.com", "Beta Feed", "news") - if err != nil { - t.Fatal(err) - } -} - -func TestExportText(t *testing.T) { - setupTestDB(t) - seedFeeds(t) - - result := ExportFeeds("text") - if !strings.Contains(result, "https://a.com/feed") { - t.Error("text export should contain feed URL a") - } - if !strings.Contains(result, "https://b.com/feed") { - t.Error("text export should contain feed URL b") - } -} - -func TestExportJSON(t *testing.T) { - setupTestDB(t) - seedFeeds(t) - - result := ExportFeeds("json") - var feeds []interface{} - err := json.Unmarshal([]byte(result), &feeds) - if err != nil { - t.Fatalf("JSON export should be valid JSON: %v", err) - } - if len(feeds) != 2 { - t.Errorf("JSON export should contain 2 feeds, got %d", len(feeds)) - } -} - -func TestExportOPML(t *testing.T) { - setupTestDB(t) - seedFeeds(t) - - result := ExportFeeds("opml") - if !strings.Contains(result, "") { - t.Error("OPML export should close opml tag") - } -} - -func TestExportHTML(t *testing.T) { - setupTestDB(t) - seedFeeds(t) - - result := ExportFeeds("html") - if !strings.Contains(result, "") { - t.Error("HTML export should contain html tag") - } - if !strings.Contains(result, "Alpha Feed") { - t.Error("HTML export should contain feed title") - } -} - -func TestExportUnknownFormat(t *testing.T) { - setupTestDB(t) - seedFeeds(t) - - result := ExportFeeds("unknown") - if result != "" { - t.Errorf("Unknown format should return empty string, got %q", result) - } -} - -func TestExportEmpty(t *testing.T) { - setupTestDB(t) - - result := ExportFeeds("text") - if result != "" { - t.Errorf("Export with no feeds should be empty, got %q", result) - } -} diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..1f4c4bb --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/frontend/README.md b/frontend/README.md index d2e7761..c987b94 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -40,15 +40,15 @@ export default defineConfig([ // other options... }, }, -]) +]); ``` You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ```js // eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; export default defineConfig([ globalIgnores(['dist']), @@ -69,5 +69,5 @@ export default defineConfig([ // other options... }, }, -]) +]); ``` diff --git a/frontend/coverage/base.css b/frontend/coverage/base.css index f418035..8cd9700 100644 --- a/frontend/coverage/base.css +++ b/frontend/coverage/base.css @@ -1,71 +1,129 @@ -body, html { - margin:0; padding: 0; +body, +html { + margin: 0; + padding: 0; height: 100%; } body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } + font-family: + Helvetica Neue, + Helvetica, + Arial; + font-size: 14px; + color: #333; +} +.small { + font-size: 12px; +} +*, +*:after, +*:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +h1 { + font-size: 20px; + margin: 0; +} +h2 { + font-size: 14px; +} pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } + font: + 12px/1.4 Consolas, + 'Liberation Mono', + Menlo, + Courier, + monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { + color: #0074d9; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +.strong { + font-weight: bold; +} +.space-top1 { + padding: 10px 0 0 0; +} +.pad2y { + padding: 20px 0; +} +.pad1y { + padding: 10px 0; +} +.pad2x { + padding: 0 20px; +} +.pad2 { + padding: 20px; +} +.pad1 { + padding: 10px; +} +.space-left2 { + padding-left: 55px; +} +.space-right2 { + padding-right: 20px; +} +.center { + text-align: center; +} +.clearfix { + display: block; +} .clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; + content: ''; + display: block; + height: 0; + clear: both; + visibility: hidden; +} +.fl { + float: left; +} +@media only screen and (max-width: 640px) { + .col3 { + width: 100%; + max-width: 100%; + } + .hide-mobile { + display: none !important; } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } } .quiet { color: #7f7f7f; - color: rgba(0,0,0,0.5); + color: rgba(0, 0, 0, 0.5); +} +.quiet a { + opacity: 0.7; } -.quiet a { opacity: 0.7; } .fraction { font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: 10px; color: #555; - background: #E8E8E8; + background: #e8e8e8; padding: 4px 5px; border-radius: 3px; vertical-align: middle; } -div.path a:link, div.path a:visited { color: #333; } +div.path a:link, +div.path a:visited { + color: #333; +} table.coverage { border-collapse: collapse; margin: 10px 0 0 0; @@ -78,140 +136,219 @@ table.coverage td { vertical-align: top; } table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; + text-align: right; + padding: 0 5px 0 20px; } table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; + text-align: right; + padding-right: 10px; + min-width: 20px; } table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; + display: inline-block; + padding: 0 5px; + width: 100%; } .missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; } .skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; } -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; +.missing-if-branch .typ, +.skip-if-branch .typ { + color: inherit !important; } .coverage-summary { border-collapse: collapse; width: 100%; } -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } +.coverage-summary tr { + border-bottom: 1px solid #bbb; +} +.keyline-all { + border: 1px solid #ddd; +} +.coverage-summary td, +.coverage-summary th { + padding: 10px; +} +.coverage-summary tbody { + border: 1px solid #bbb; +} +.coverage-summary td { + border-right: 1px solid #bbb; +} +.coverage-summary td:last-child { + border-right: none; +} .coverage-summary th { text-align: left; font-weight: normal; white-space: nowrap; } -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } +.coverage-summary th.file { + border-right: none !important; +} +.coverage-summary th.pct { +} .coverage-summary th.pic, .coverage-summary th.abs, .coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } +.coverage-summary td.abs { + text-align: right; +} +.coverage-summary td.file { + white-space: nowrap; +} +.coverage-summary td.pic { + min-width: 120px !important; +} +.coverage-summary tfoot td { +} .coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; } .coverage-summary .sorted .sorter { - background-position: 0 -20px; + background-position: 0 -20px; } .coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; + background-position: 0 -10px; +} +.status-line { + height: 10px; } -.status-line { height: 10px; } /* yellow */ -.cbranch-no { background: yellow !important; color: #111; } +.cbranch-no { + background: yellow !important; + color: #111; +} /* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } +.red.solid, +.status-line.low, +.low .cover-fill { + background: #c21f39; +} +.low .chart { + border: 1px solid #c21f39; +} .highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; +.highlighted .cstat-no, +.highlighted .fstat-no, +.highlighted .cbranch-no { + background: #c21f39 !important; } /* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +.cstat-no, +.fstat-no, +.cbranch-no, +.cbranch-no { + background: #f6c6ce; +} /* light red */ -.low, .cline-no { background:#FCE1E5 } +.low, +.cline-no { + background: #fce1e5; +} /* light green */ -.high, .cline-yes { background:rgb(230,245,208) } +.high, +.cline-yes { + background: rgb(230, 245, 208); +} /* medium green */ -.cstat-yes { background:rgb(161,215,106) } +.cstat-yes { + background: rgb(161, 215, 106); +} /* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } +.status-line.high, +.high .cover-fill { + background: rgb(77, 146, 33); +} +.high .chart { + border: 1px solid rgb(77, 146, 33); +} /* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } +.status-line.medium, +.medium .cover-fill { + background: #f9cd0b; +} +.medium .chart { + border: 1px solid #f9cd0b; +} /* light yellow */ -.medium { background: #fff4c2; } +.medium { + background: #fff4c2; +} -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } +.cstat-skip { + background: #ddd; + color: #111; +} +.fstat-skip { + background: #ddd; + color: #111 !important; +} +.cbranch-skip { + background: #ddd !important; + color: #111; +} -span.cline-neutral { background: #eaeaea; } +span.cline-neutral { + background: #eaeaea; +} .coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; + opacity: 0.5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; } -.cover-fill, .cover-empty { - display:inline-block; +.cover-fill, +.cover-empty { + display: inline-block; height: 12px; } .chart { line-height: 0; } .cover-empty { - background: white; + background: white; } .cover-full { - border-right: none !important; + border-right: none !important; } pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { + color: #999 !important; +} +.ignore-none { + color: #999; + font-weight: normal; } -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } .wrapper { min-height: 100%; @@ -219,6 +356,7 @@ pre.prettyprint { height: 100%; margin: 0 auto -48px; } -.footer, .push { +.footer, +.push { height: 48px; } diff --git a/frontend/coverage/block-navigation.js b/frontend/coverage/block-navigation.js index 530d1ed..05f7569 100644 --- a/frontend/coverage/block-navigation.js +++ b/frontend/coverage/block-navigation.js @@ -1,87 +1,82 @@ -/* eslint-disable */ + var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); - var currentIndex; + var currentIndex; - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } + function toggleClass(index) { + missingCoverageElements.item(currentIndex).classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center', + }); + } - makeCurrent(nextIndex); + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; } - function goToNext() { - var nextIndex = 0; + makeCurrent(nextIndex); + } - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } + function goToNext() { + var nextIndex = 0; - makeCurrent(nextIndex); + if (typeof currentIndex === 'number' && currentIndex < missingCoverageElements.length - 1) { + nextIndex = currentIndex + 1; } - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } + makeCurrent(nextIndex); + } - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; })(); window.addEventListener('keydown', jumpToCode); diff --git a/frontend/coverage/coverage-final.json b/frontend/coverage/coverage-final.json index 80fb28a..e46821b 100644 --- a/frontend/coverage/coverage-final.json +++ b/frontend/coverage/coverage-final.json @@ -1,13 +1,2240 @@ -{"/Users/adam/workspace/vibecode/neko/frontend/src/App.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx","statementMap":{"0":{"start":{"line":8,"column":22},"end":{"line":8,"column":null}},"1":{"start":{"line":9,"column":8},"end":{"line":9,"column":null}},"2":{"start":{"line":11,"column":2},"end":{"line":21,"column":null}},"3":{"start":{"line":12,"column":4},"end":{"line":20,"column":null}},"4":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"5":{"start":{"line":15,"column":10},"end":{"line":15,"column":null}},"6":{"start":{"line":17,"column":10},"end":{"line":17,"column":null}},"7":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"8":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"9":{"start":{"line":24,"column":4},"end":{"line":24,"column":null}},"10":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"11":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"12":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"13":{"start":{"line":39,"column":8},"end":{"line":39,"column":null}},"14":{"start":{"line":40,"column":2},"end":{"line":68,"column":null}},"15":{"start":{"line":45,"column":33},"end":{"line":45,"column":56}},"16":{"start":{"line":48,"column":12},"end":{"line":49,"column":null}},"17":{"start":{"line":49,"column":26},"end":{"line":49,"column":60}},"18":{"start":{"line":73,"column":2},"end":{"line":86,"column":null}}},"fnMap":{"0":{"name":"RequireAuth","decl":{"start":{"line":7,"column":9},"end":{"line":7,"column":21}},"loc":{"start":{"line":7,"column":69},"end":{"line":32,"column":null}},"line":7},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":12},"end":{"line":11,"column":18}},"loc":{"start":{"line":11,"column":18},"end":{"line":21,"column":5}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":12},"end":{"line":13,"column":13}},"loc":{"start":{"line":13,"column":21},"end":{"line":19,"column":7}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":13},"end":{"line":20,"column":19}},"loc":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"line":20},"4":{"name":"Dashboard","decl":{"start":{"line":38,"column":9},"end":{"line":38,"column":21}},"loc":{"start":{"line":38,"column":21},"end":{"line":70,"column":null}},"line":38},"5":{"name":"(anonymous_5)","decl":{"start":{"line":45,"column":27},"end":{"line":45,"column":33}},"loc":{"start":{"line":45,"column":33},"end":{"line":45,"column":56}},"line":45},"6":{"name":"(anonymous_6)","decl":{"start":{"line":47,"column":27},"end":{"line":47,"column":33}},"loc":{"start":{"line":47,"column":33},"end":{"line":50,"column":13}},"line":47},"7":{"name":"(anonymous_7)","decl":{"start":{"line":49,"column":20},"end":{"line":49,"column":26}},"loc":{"start":{"line":49,"column":26},"end":{"line":49,"column":60}},"line":49},"8":{"name":"App","decl":{"start":{"line":72,"column":9},"end":{"line":72,"column":15}},"loc":{"start":{"line":72,"column":15},"end":{"line":88,"column":null}},"line":72}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},{"start":{"line":16,"column":15},"end":{"line":18,"column":null}}],"line":14},"1":{"loc":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":23},"2":{"loc":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":27}},"s":{"0":2,"1":2,"2":2,"3":1,"4":1,"5":1,"6":0,"7":0,"8":2,"9":1,"10":1,"11":0,"12":1,"13":1,"14":1,"15":0,"16":1,"17":1,"18":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":0,"6":1,"7":1,"8":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":9,"lastStatement":19,"seen":{"f:7:9:7:21":0,"s:8:22:8:Infinity":0,"s:9:8:9:Infinity":1,"s:11:2:21:Infinity":2,"f:11:12:11:18":1,"s:12:4:20:Infinity":3,"f:13:12:13:13":2,"b:14:8:18:Infinity:16:15:18:Infinity":0,"s:14:8:18:Infinity":4,"s:15:10:15:Infinity":5,"s:17:10:17:Infinity":6,"f:20:13:20:19":3,"s:20:19:20:33":7,"b:23:2:25:Infinity:undefined:undefined:undefined:undefined":1,"s:23:2:25:Infinity":8,"s:24:4:24:Infinity":9,"b:27:2:29:Infinity:undefined:undefined:undefined:undefined":2,"s:27:2:29:Infinity":10,"s:28:4:28:Infinity":11,"s:31:2:31:Infinity":12,"f:38:9:38:21":4,"s:39:8:39:Infinity":13,"s:40:2:68:Infinity":14,"f:45:27:45:33":5,"s:45:33:45:56":15,"f:47:27:47:33":6,"s:48:12:49:Infinity":16,"f:49:20:49:26":7,"s:49:26:49:60":17,"f:72:9:72:15":8,"s:73:2:86:Infinity":18}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx","statementMap":{"0":{"start":{"line":10,"column":24},"end":{"line":10,"column":null}},"1":{"start":{"line":11,"column":30},"end":{"line":11,"column":null}},"2":{"start":{"line":14,"column":23},"end":{"line":16,"column":null}},"3":{"start":{"line":15,"column":8},"end":{"line":15,"column":null}},"4":{"start":{"line":18,"column":23},"end":{"line":52,"column":null}},"5":{"start":{"line":19,"column":8},"end":{"line":19,"column":null}},"6":{"start":{"line":21,"column":29},"end":{"line":21,"column":null}},"7":{"start":{"line":22,"column":8},"end":{"line":22,"column":null}},"8":{"start":{"line":24,"column":8},"end":{"line":51,"column":null}},"9":{"start":{"line":36,"column":16},"end":{"line":38,"column":null}},"10":{"start":{"line":37,"column":20},"end":{"line":37,"column":null}},"11":{"start":{"line":39,"column":16},"end":{"line":39,"column":null}},"12":{"start":{"line":44,"column":16},"end":{"line":44,"column":null}},"13":{"start":{"line":47,"column":16},"end":{"line":47,"column":null}},"14":{"start":{"line":49,"column":16},"end":{"line":49,"column":null}},"15":{"start":{"line":50,"column":16},"end":{"line":50,"column":null}},"16":{"start":{"line":54,"column":4},"end":{"line":82,"column":null}},"17":{"start":{"line":59,"column":24},"end":{"line":59,"column":null}},"18":{"start":{"line":60,"column":24},"end":{"line":60,"column":null}}},"fnMap":{"0":{"name":"FeedItem","decl":{"start":{"line":9,"column":24},"end":{"line":9,"column":33}},"loc":{"start":{"line":9,"column":71},"end":{"line":84,"column":null}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":14,"column":23},"end":{"line":14,"column":29}},"loc":{"start":{"line":14,"column":29},"end":{"line":16,"column":null}},"line":14},"2":{"name":"(anonymous_2)","decl":{"start":{"line":18,"column":23},"end":{"line":18,"column":24}},"loc":{"start":{"line":18,"column":42},"end":{"line":52,"column":null}},"line":18},"3":{"name":"(anonymous_3)","decl":{"start":{"line":35,"column":18},"end":{"line":35,"column":19}},"loc":{"start":{"line":35,"column":27},"end":{"line":40,"column":13}},"line":35},"4":{"name":"(anonymous_4)","decl":{"start":{"line":41,"column":18},"end":{"line":41,"column":24}},"loc":{"start":{"line":41,"column":24},"end":{"line":45,"column":13}},"line":41},"5":{"name":"(anonymous_5)","decl":{"start":{"line":46,"column":19},"end":{"line":46,"column":20}},"loc":{"start":{"line":46,"column":28},"end":{"line":51,"column":13}},"line":46},"6":{"name":"(anonymous_6)","decl":{"start":{"line":58,"column":29},"end":{"line":58,"column":30}},"loc":{"start":{"line":58,"column":36},"end":{"line":61,"column":null}},"line":58}},"branchMap":{"0":{"loc":{"start":{"line":36,"column":16},"end":{"line":38,"column":null}},"type":"if","locations":[{"start":{"line":36,"column":16},"end":{"line":38,"column":null}},{"start":{},"end":{}}],"line":36},"1":{"loc":{"start":{"line":55,"column":36},"end":{"line":55,"column":65}},"type":"cond-expr","locations":[{"start":{"line":55,"column":48},"end":{"line":55,"column":57}},{"start":{"line":55,"column":57},"end":{"line":55,"column":65}}],"line":55},"2":{"loc":{"start":{"line":55,"column":69},"end":{"line":55,"column":93}},"type":"cond-expr","locations":[{"start":{"line":55,"column":79},"end":{"line":55,"column":91}},{"start":{"line":55,"column":91},"end":{"line":55,"column":93}}],"line":55},"3":{"loc":{"start":{"line":62,"column":43},"end":{"line":62,"column":87}},"type":"cond-expr","locations":[{"start":{"line":62,"column":58},"end":{"line":62,"column":73}},{"start":{"line":62,"column":73},"end":{"line":62,"column":87}}],"line":62},"4":{"loc":{"start":{"line":63,"column":27},"end":{"line":63,"column":null}},"type":"cond-expr","locations":[{"start":{"line":63,"column":42},"end":{"line":63,"column":53}},{"start":{"line":63,"column":53},"end":{"line":63,"column":null}}],"line":63},"5":{"loc":{"start":{"line":65,"column":21},"end":{"line":65,"column":null}},"type":"cond-expr","locations":[{"start":{"line":65,"column":36},"end":{"line":65,"column":42}},{"start":{"line":65,"column":42},"end":{"line":65,"column":null}}],"line":65},"6":{"loc":{"start":{"line":68,"column":21},"end":{"line":68,"column":null}},"type":"binary-expr","locations":[{"start":{"line":68,"column":21},"end":{"line":68,"column":35}},{"start":{"line":68,"column":35},"end":{"line":68,"column":null}}],"line":68},"7":{"loc":{"start":{"line":74,"column":21},"end":{"line":74,"column":null}},"type":"binary-expr","locations":[{"start":{"line":74,"column":21},"end":{"line":74,"column":40}},{"start":{"line":74,"column":40},"end":{"line":74,"column":null}}],"line":74},"8":{"loc":{"start":{"line":79,"column":13},"end":{"line":80,"column":null}},"type":"binary-expr","locations":[{"start":{"line":79,"column":13},"end":{"line":79,"column":null}},{"start":{"line":80,"column":16},"end":{"line":80,"column":null}}],"line":79}},"s":{"0":21,"1":21,"2":21,"3":1,"4":21,"5":1,"6":1,"7":1,"8":1,"9":1,"10":0,"11":1,"12":1,"13":0,"14":0,"15":0,"16":21,"17":1,"18":1},"f":{"0":21,"1":1,"2":1,"3":1,"4":1,"5":0,"6":1},"b":{"0":[0,1],"1":[9,12],"2":[1,20],"3":[2,19],"4":[2,19],"5":[2,19],"6":[21,0],"7":[21,5],"8":[21,4]},"meta":{"lastBranch":9,"lastFunction":7,"lastStatement":19,"seen":{"f:9:24:9:33":0,"s:10:24:10:Infinity":0,"s:11:30:11:Infinity":1,"s:14:23:16:Infinity":2,"f:14:23:14:29":1,"s:15:8:15:Infinity":3,"s:18:23:52:Infinity":4,"f:18:23:18:24":2,"s:19:8:19:Infinity":5,"s:21:29:21:Infinity":6,"s:22:8:22:Infinity":7,"s:24:8:51:Infinity":8,"f:35:18:35:19":3,"b:36:16:38:Infinity:undefined:undefined:undefined:undefined":0,"s:36:16:38:Infinity":9,"s:37:20:37:Infinity":10,"s:39:16:39:Infinity":11,"f:41:18:41:24":4,"s:44:16:44:Infinity":12,"f:46:19:46:20":5,"s:47:16:47:Infinity":13,"s:49:16:49:Infinity":14,"s:50:16:50:Infinity":15,"s:54:4:82:Infinity":16,"b:55:48:55:57:55:57:55:65":1,"b:55:79:55:91:55:91:55:93":2,"f:58:29:58:30":6,"s:59:24:59:Infinity":17,"s:60:24:60:Infinity":18,"b:62:58:62:73:62:73:62:87":3,"b:63:42:63:53:63:53:63:Infinity":4,"b:65:36:65:42:65:42:65:Infinity":5,"b:68:21:68:35:68:35:68:Infinity":6,"b:74:21:74:40:74:40:74:Infinity":7,"b:79:13:79:Infinity:80:16:80:Infinity":8}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx","statementMap":{"0":{"start":{"line":8,"column":28},"end":{"line":8,"column":null}},"1":{"start":{"line":9,"column":23},"end":{"line":9,"column":null}},"2":{"start":{"line":10,"column":21},"end":{"line":10,"column":null}},"3":{"start":{"line":12,"column":26},"end":{"line":12,"column":null}},"4":{"start":{"line":13,"column":30},"end":{"line":13,"column":null}},"5":{"start":{"line":14,"column":38},"end":{"line":14,"column":null}},"6":{"start":{"line":15,"column":30},"end":{"line":15,"column":null}},"7":{"start":{"line":16,"column":26},"end":{"line":16,"column":null}},"8":{"start":{"line":18,"column":23},"end":{"line":78,"column":null}},"9":{"start":{"line":19,"column":8},"end":{"line":24,"column":null}},"10":{"start":{"line":20,"column":12},"end":{"line":20,"column":null}},"11":{"start":{"line":22,"column":12},"end":{"line":22,"column":null}},"12":{"start":{"line":23,"column":12},"end":{"line":23,"column":null}},"13":{"start":{"line":25,"column":8},"end":{"line":25,"column":null}},"14":{"start":{"line":27,"column":18},"end":{"line":27,"column":null}},"15":{"start":{"line":28,"column":23},"end":{"line":28,"column":null}},"16":{"start":{"line":30,"column":8},"end":{"line":34,"column":null}},"17":{"start":{"line":31,"column":12},"end":{"line":31,"column":null}},"18":{"start":{"line":32,"column":8},"end":{"line":34,"column":null}},"19":{"start":{"line":33,"column":12},"end":{"line":33,"column":null}},"20":{"start":{"line":36,"column":8},"end":{"line":38,"column":null}},"21":{"start":{"line":37,"column":12},"end":{"line":37,"column":null}},"22":{"start":{"line":41,"column":8},"end":{"line":49,"column":null}},"23":{"start":{"line":42,"column":12},"end":{"line":42,"column":null}},"24":{"start":{"line":43,"column":8},"end":{"line":49,"column":null}},"25":{"start":{"line":44,"column":12},"end":{"line":44,"column":null}},"26":{"start":{"line":45,"column":12},"end":{"line":45,"column":null}},"27":{"start":{"line":48,"column":12},"end":{"line":48,"column":null}},"28":{"start":{"line":51,"column":28},"end":{"line":51,"column":null}},"29":{"start":{"line":52,"column":8},"end":{"line":54,"column":null}},"30":{"start":{"line":53,"column":12},"end":{"line":53,"column":null}},"31":{"start":{"line":56,"column":8},"end":{"line":77,"column":null}},"32":{"start":{"line":58,"column":16},"end":{"line":60,"column":null}},"33":{"start":{"line":59,"column":20},"end":{"line":59,"column":null}},"34":{"start":{"line":61,"column":16},"end":{"line":61,"column":null}},"35":{"start":{"line":64,"column":16},"end":{"line":68,"column":null}},"36":{"start":{"line":65,"column":20},"end":{"line":65,"column":null}},"37":{"start":{"line":65,"column":39},"end":{"line":65,"column":57}},"38":{"start":{"line":67,"column":20},"end":{"line":67,"column":null}},"39":{"start":{"line":69,"column":16},"end":{"line":69,"column":null}},"40":{"start":{"line":70,"column":16},"end":{"line":70,"column":null}},"41":{"start":{"line":71,"column":16},"end":{"line":71,"column":null}},"42":{"start":{"line":74,"column":16},"end":{"line":74,"column":null}},"43":{"start":{"line":75,"column":16},"end":{"line":75,"column":null}},"44":{"start":{"line":76,"column":16},"end":{"line":76,"column":null}},"45":{"start":{"line":80,"column":4},"end":{"line":82,"column":null}},"46":{"start":{"line":81,"column":8},"end":{"line":81,"column":null}},"47":{"start":{"line":84,"column":42},"end":{"line":84,"column":null}},"48":{"start":{"line":86,"column":4},"end":{"line":122,"column":null}},"49":{"start":{"line":87,"column":30},"end":{"line":118,"column":null}},"50":{"start":{"line":88,"column":12},"end":{"line":88,"column":null}},"51":{"start":{"line":88,"column":36},"end":{"line":88,"column":null}},"52":{"start":{"line":90,"column":12},"end":{"line":117,"column":null}},"53":{"start":{"line":91,"column":16},"end":{"line":101,"column":null}},"54":{"start":{"line":92,"column":38},"end":{"line":92,"column":null}},"55":{"start":{"line":93,"column":20},"end":{"line":99,"column":null}},"56":{"start":{"line":94,"column":37},"end":{"line":94,"column":null}},"57":{"start":{"line":95,"column":24},"end":{"line":97,"column":null}},"58":{"start":{"line":96,"column":28},"end":{"line":96,"column":null}},"59":{"start":{"line":98,"column":24},"end":{"line":98,"column":null}},"60":{"start":{"line":100,"column":20},"end":{"line":100,"column":null}},"61":{"start":{"line":102,"column":12},"end":{"line":117,"column":null}},"62":{"start":{"line":103,"column":16},"end":{"line":109,"column":null}},"63":{"start":{"line":104,"column":38},"end":{"line":104,"column":null}},"64":{"start":{"line":105,"column":20},"end":{"line":107,"column":null}},"65":{"start":{"line":106,"column":24},"end":{"line":106,"column":null}},"66":{"start":{"line":108,"column":20},"end":{"line":108,"column":null}},"67":{"start":{"line":110,"column":12},"end":{"line":117,"column":null}},"68":{"start":{"line":111,"column":16},"end":{"line":116,"column":null}},"69":{"start":{"line":112,"column":20},"end":{"line":114,"column":null}},"70":{"start":{"line":113,"column":24},"end":{"line":113,"column":null}},"71":{"start":{"line":115,"column":20},"end":{"line":115,"column":null}},"72":{"start":{"line":120,"column":8},"end":{"line":120,"column":null}},"73":{"start":{"line":121,"column":8},"end":{"line":121,"column":null}},"74":{"start":{"line":121,"column":21},"end":{"line":121,"column":null}},"75":{"start":{"line":124,"column":25},"end":{"line":129,"column":null}},"76":{"start":{"line":125,"column":24},"end":{"line":125,"column":null}},"77":{"start":{"line":126,"column":8},"end":{"line":128,"column":null}},"78":{"start":{"line":127,"column":12},"end":{"line":127,"column":null}},"79":{"start":{"line":131,"column":23},"end":{"line":141,"column":null}},"80":{"start":{"line":132,"column":28},"end":{"line":132,"column":null}},"81":{"start":{"line":134,"column":8},"end":{"line":134,"column":null}},"82":{"start":{"line":134,"column":32},"end":{"line":134,"column":92}},"83":{"start":{"line":134,"column":54},"end":{"line":134,"column":91}},"84":{"start":{"line":136,"column":8},"end":{"line":140,"column":null}},"85":{"start":{"line":140,"column":26},"end":{"line":140,"column":67}},"86":{"start":{"line":143,"column":23},"end":{"line":153,"column":null}},"87":{"start":{"line":144,"column":28},"end":{"line":144,"column":null}},"88":{"start":{"line":146,"column":8},"end":{"line":146,"column":null}},"89":{"start":{"line":146,"column":32},"end":{"line":146,"column":92}},"90":{"start":{"line":146,"column":54},"end":{"line":146,"column":91}},"91":{"start":{"line":148,"column":8},"end":{"line":152,"column":null}},"92":{"start":{"line":152,"column":26},"end":{"line":152,"column":69}},"93":{"start":{"line":155,"column":4},"end":{"line":191,"column":null}},"94":{"start":{"line":156,"column":25},"end":{"line":180,"column":null}},"95":{"start":{"line":158,"column":16},"end":{"line":177,"column":null}},"96":{"start":{"line":160,"column":20},"end":{"line":165,"column":null}},"97":{"start":{"line":161,"column":24},"end":{"line":163,"column":null}},"98":{"start":{"line":162,"column":28},"end":{"line":162,"column":null}},"99":{"start":{"line":164,"column":24},"end":{"line":164,"column":null}},"100":{"start":{"line":168,"column":20},"end":{"line":176,"column":null}},"101":{"start":{"line":169,"column":38},"end":{"line":169,"column":null}},"102":{"start":{"line":170,"column":24},"end":{"line":175,"column":null}},"103":{"start":{"line":171,"column":41},"end":{"line":171,"column":null}},"104":{"start":{"line":172,"column":28},"end":{"line":174,"column":null}},"105":{"start":{"line":173,"column":32},"end":{"line":173,"column":null}},"106":{"start":{"line":182,"column":8},"end":{"line":185,"column":null}},"107":{"start":{"line":183,"column":23},"end":{"line":183,"column":null}},"108":{"start":{"line":184,"column":12},"end":{"line":184,"column":null}},"109":{"start":{"line":184,"column":20},"end":{"line":184,"column":null}},"110":{"start":{"line":187,"column":25},"end":{"line":187,"column":null}},"111":{"start":{"line":188,"column":8},"end":{"line":188,"column":null}},"112":{"start":{"line":188,"column":22},"end":{"line":188,"column":null}},"113":{"start":{"line":190,"column":8},"end":{"line":190,"column":null}},"114":{"start":{"line":190,"column":21},"end":{"line":190,"column":null}},"115":{"start":{"line":193,"column":4},"end":{"line":193,"column":null}},"116":{"start":{"line":193,"column":17},"end":{"line":193,"column":null}},"117":{"start":{"line":194,"column":4},"end":{"line":194,"column":null}},"118":{"start":{"line":194,"column":15},"end":{"line":194,"column":null}},"119":{"start":{"line":197,"column":4},"end":{"line":221,"column":null}},"120":{"start":{"line":204,"column":24},"end":{"line":212,"column":null}},"121":{"start":{"line":209,"column":43},"end":{"line":209,"column":null}}},"fnMap":{"0":{"name":"FeedItems","decl":{"start":{"line":7,"column":24},"end":{"line":7,"column":36}},"loc":{"start":{"line":7,"column":36},"end":{"line":223,"column":null}},"line":7},"1":{"name":"(anonymous_1)","decl":{"start":{"line":18,"column":23},"end":{"line":18,"column":24}},"loc":{"start":{"line":18,"column":43},"end":{"line":78,"column":null}},"line":18},"2":{"name":"(anonymous_2)","decl":{"start":{"line":57,"column":18},"end":{"line":57,"column":19}},"loc":{"start":{"line":57,"column":27},"end":{"line":62,"column":13}},"line":57},"3":{"name":"(anonymous_3)","decl":{"start":{"line":63,"column":18},"end":{"line":63,"column":19}},"loc":{"start":{"line":63,"column":28},"end":{"line":72,"column":13}},"line":63},"4":{"name":"(anonymous_4)","decl":{"start":{"line":65,"column":29},"end":{"line":65,"column":30}},"loc":{"start":{"line":65,"column":39},"end":{"line":65,"column":57}},"line":65},"5":{"name":"(anonymous_5)","decl":{"start":{"line":73,"column":19},"end":{"line":73,"column":20}},"loc":{"start":{"line":73,"column":28},"end":{"line":77,"column":13}},"line":73},"6":{"name":"(anonymous_6)","decl":{"start":{"line":80,"column":14},"end":{"line":80,"column":20}},"loc":{"start":{"line":80,"column":20},"end":{"line":82,"column":7}},"line":80},"7":{"name":"(anonymous_7)","decl":{"start":{"line":86,"column":14},"end":{"line":86,"column":20}},"loc":{"start":{"line":86,"column":20},"end":{"line":122,"column":7}},"line":86},"8":{"name":"(anonymous_8)","decl":{"start":{"line":87,"column":30},"end":{"line":87,"column":31}},"loc":{"start":{"line":87,"column":52},"end":{"line":118,"column":null}},"line":87},"9":{"name":"(anonymous_9)","decl":{"start":{"line":91,"column":33},"end":{"line":91,"column":34}},"loc":{"start":{"line":91,"column":43},"end":{"line":101,"column":17}},"line":91},"10":{"name":"(anonymous_10)","decl":{"start":{"line":103,"column":33},"end":{"line":103,"column":34}},"loc":{"start":{"line":103,"column":43},"end":{"line":109,"column":17}},"line":103},"11":{"name":"(anonymous_11)","decl":{"start":{"line":111,"column":33},"end":{"line":111,"column":34}},"loc":{"start":{"line":111,"column":51},"end":{"line":116,"column":17}},"line":111},"12":{"name":"(anonymous_12)","decl":{"start":{"line":121,"column":15},"end":{"line":121,"column":21}},"loc":{"start":{"line":121,"column":21},"end":{"line":121,"column":null}},"line":121},"13":{"name":"(anonymous_13)","decl":{"start":{"line":124,"column":25},"end":{"line":124,"column":26}},"loc":{"start":{"line":124,"column":44},"end":{"line":129,"column":null}},"line":124},"14":{"name":"(anonymous_14)","decl":{"start":{"line":131,"column":23},"end":{"line":131,"column":24}},"loc":{"start":{"line":131,"column":39},"end":{"line":141,"column":null}},"line":131},"15":{"name":"(anonymous_15)","decl":{"start":{"line":134,"column":17},"end":{"line":134,"column":18}},"loc":{"start":{"line":134,"column":32},"end":{"line":134,"column":92}},"line":134},"16":{"name":"(anonymous_16)","decl":{"start":{"line":134,"column":46},"end":{"line":134,"column":47}},"loc":{"start":{"line":134,"column":54},"end":{"line":134,"column":91}},"line":134},"17":{"name":"(anonymous_17)","decl":{"start":{"line":140,"column":17},"end":{"line":140,"column":18}},"loc":{"start":{"line":140,"column":26},"end":{"line":140,"column":67}},"line":140},"18":{"name":"(anonymous_18)","decl":{"start":{"line":143,"column":23},"end":{"line":143,"column":24}},"loc":{"start":{"line":143,"column":39},"end":{"line":153,"column":null}},"line":143},"19":{"name":"(anonymous_19)","decl":{"start":{"line":146,"column":17},"end":{"line":146,"column":18}},"loc":{"start":{"line":146,"column":32},"end":{"line":146,"column":92}},"line":146},"20":{"name":"(anonymous_20)","decl":{"start":{"line":146,"column":46},"end":{"line":146,"column":47}},"loc":{"start":{"line":146,"column":54},"end":{"line":146,"column":91}},"line":146},"21":{"name":"(anonymous_21)","decl":{"start":{"line":152,"column":17},"end":{"line":152,"column":18}},"loc":{"start":{"line":152,"column":26},"end":{"line":152,"column":69}},"line":152},"22":{"name":"(anonymous_22)","decl":{"start":{"line":155,"column":14},"end":{"line":155,"column":20}},"loc":{"start":{"line":155,"column":20},"end":{"line":191,"column":7}},"line":155},"23":{"name":"(anonymous_23)","decl":{"start":{"line":157,"column":12},"end":{"line":157,"column":13}},"loc":{"start":{"line":157,"column":25},"end":{"line":178,"column":null}},"line":157},"24":{"name":"(anonymous_24)","decl":{"start":{"line":158,"column":32},"end":{"line":158,"column":33}},"loc":{"start":{"line":158,"column":43},"end":{"line":177,"column":17}},"line":158},"25":{"name":"(anonymous_25)","decl":{"start":{"line":182,"column":22},"end":{"line":182,"column":23}},"loc":{"start":{"line":182,"column":36},"end":{"line":185,"column":9}},"line":182},"26":{"name":"(anonymous_26)","decl":{"start":{"line":190,"column":15},"end":{"line":190,"column":21}},"loc":{"start":{"line":190,"column":21},"end":{"line":190,"column":null}},"line":190},"27":{"name":"(anonymous_27)","decl":{"start":{"line":203,"column":31},"end":{"line":203,"column":32}},"loc":{"start":{"line":204,"column":24},"end":{"line":212,"column":null}},"line":204},"28":{"name":"(anonymous_28)","decl":{"start":{"line":209,"column":37},"end":{"line":209,"column":43}},"loc":{"start":{"line":209,"column":43},"end":{"line":209,"column":null}},"line":209}},"branchMap":{"0":{"loc":{"start":{"line":10,"column":21},"end":{"line":10,"column":null}},"type":"binary-expr","locations":[{"start":{"line":10,"column":21},"end":{"line":10,"column":51}},{"start":{"line":10,"column":51},"end":{"line":10,"column":null}}],"line":10},"1":{"loc":{"start":{"line":19,"column":8},"end":{"line":24,"column":null}},"type":"if","locations":[{"start":{"line":19,"column":8},"end":{"line":24,"column":null}},{"start":{"line":21,"column":15},"end":{"line":24,"column":null}}],"line":19},"2":{"loc":{"start":{"line":30,"column":8},"end":{"line":34,"column":null}},"type":"if","locations":[{"start":{"line":30,"column":8},"end":{"line":34,"column":null}},{"start":{"line":32,"column":8},"end":{"line":34,"column":null}}],"line":30},"3":{"loc":{"start":{"line":32,"column":8},"end":{"line":34,"column":null}},"type":"if","locations":[{"start":{"line":32,"column":8},"end":{"line":34,"column":null}},{"start":{},"end":{}}],"line":32},"4":{"loc":{"start":{"line":36,"column":8},"end":{"line":38,"column":null}},"type":"if","locations":[{"start":{"line":36,"column":8},"end":{"line":38,"column":null}},{"start":{},"end":{}}],"line":36},"5":{"loc":{"start":{"line":41,"column":8},"end":{"line":49,"column":null}},"type":"if","locations":[{"start":{"line":41,"column":8},"end":{"line":49,"column":null}},{"start":{"line":43,"column":8},"end":{"line":49,"column":null}}],"line":41},"6":{"loc":{"start":{"line":43,"column":8},"end":{"line":49,"column":null}},"type":"if","locations":[{"start":{"line":43,"column":8},"end":{"line":49,"column":null}},{"start":{"line":46,"column":15},"end":{"line":49,"column":null}}],"line":43},"7":{"loc":{"start":{"line":52,"column":8},"end":{"line":54,"column":null}},"type":"if","locations":[{"start":{"line":52,"column":8},"end":{"line":54,"column":null}},{"start":{},"end":{}}],"line":52},"8":{"loc":{"start":{"line":58,"column":16},"end":{"line":60,"column":null}},"type":"if","locations":[{"start":{"line":58,"column":16},"end":{"line":60,"column":null}},{"start":{},"end":{}}],"line":58},"9":{"loc":{"start":{"line":64,"column":16},"end":{"line":68,"column":null}},"type":"if","locations":[{"start":{"line":64,"column":16},"end":{"line":68,"column":null}},{"start":{"line":66,"column":23},"end":{"line":68,"column":null}}],"line":64},"10":{"loc":{"start":{"line":88,"column":12},"end":{"line":88,"column":null}},"type":"if","locations":[{"start":{"line":88,"column":12},"end":{"line":88,"column":null}},{"start":{},"end":{}}],"line":88},"11":{"loc":{"start":{"line":90,"column":12},"end":{"line":117,"column":null}},"type":"if","locations":[{"start":{"line":90,"column":12},"end":{"line":117,"column":null}},{"start":{"line":102,"column":12},"end":{"line":117,"column":null}}],"line":90},"12":{"loc":{"start":{"line":93,"column":20},"end":{"line":99,"column":null}},"type":"if","locations":[{"start":{"line":93,"column":20},"end":{"line":99,"column":null}},{"start":{},"end":{}}],"line":93},"13":{"loc":{"start":{"line":95,"column":24},"end":{"line":97,"column":null}},"type":"if","locations":[{"start":{"line":95,"column":24},"end":{"line":97,"column":null}},{"start":{},"end":{}}],"line":95},"14":{"loc":{"start":{"line":102,"column":12},"end":{"line":117,"column":null}},"type":"if","locations":[{"start":{"line":102,"column":12},"end":{"line":117,"column":null}},{"start":{"line":110,"column":12},"end":{"line":117,"column":null}}],"line":102},"15":{"loc":{"start":{"line":105,"column":20},"end":{"line":107,"column":null}},"type":"if","locations":[{"start":{"line":105,"column":20},"end":{"line":107,"column":null}},{"start":{},"end":{}}],"line":105},"16":{"loc":{"start":{"line":110,"column":12},"end":{"line":117,"column":null}},"type":"if","locations":[{"start":{"line":110,"column":12},"end":{"line":117,"column":null}},{"start":{},"end":{}}],"line":110},"17":{"loc":{"start":{"line":112,"column":20},"end":{"line":114,"column":null}},"type":"if","locations":[{"start":{"line":112,"column":20},"end":{"line":114,"column":null}},{"start":{},"end":{}}],"line":112},"18":{"loc":{"start":{"line":112,"column":24},"end":{"line":112,"column":74}},"type":"binary-expr","locations":[{"start":{"line":112,"column":24},"end":{"line":112,"column":45}},{"start":{"line":112,"column":45},"end":{"line":112,"column":74}}],"line":112},"19":{"loc":{"start":{"line":126,"column":8},"end":{"line":128,"column":null}},"type":"if","locations":[{"start":{"line":126,"column":8},"end":{"line":128,"column":null}},{"start":{},"end":{}}],"line":126},"20":{"loc":{"start":{"line":134,"column":54},"end":{"line":134,"column":91}},"type":"cond-expr","locations":[{"start":{"line":134,"column":75},"end":{"line":134,"column":89}},{"start":{"line":134,"column":89},"end":{"line":134,"column":91}}],"line":134},"21":{"loc":{"start":{"line":146,"column":54},"end":{"line":146,"column":91}},"type":"cond-expr","locations":[{"start":{"line":146,"column":75},"end":{"line":146,"column":89}},{"start":{"line":146,"column":89},"end":{"line":146,"column":91}}],"line":146},"22":{"loc":{"start":{"line":160,"column":20},"end":{"line":165,"column":null}},"type":"if","locations":[{"start":{"line":160,"column":20},"end":{"line":165,"column":null}},{"start":{},"end":{}}],"line":160},"23":{"loc":{"start":{"line":161,"column":24},"end":{"line":163,"column":null}},"type":"if","locations":[{"start":{"line":161,"column":24},"end":{"line":163,"column":null}},{"start":{},"end":{}}],"line":161},"24":{"loc":{"start":{"line":161,"column":28},"end":{"line":161,"column":97}},"type":"binary-expr","locations":[{"start":{"line":161,"column":28},"end":{"line":161,"column":52}},{"start":{"line":161,"column":52},"end":{"line":161,"column":68}},{"start":{"line":161,"column":68},"end":{"line":161,"column":79}},{"start":{"line":161,"column":79},"end":{"line":161,"column":97}}],"line":161},"25":{"loc":{"start":{"line":168,"column":20},"end":{"line":176,"column":null}},"type":"if","locations":[{"start":{"line":168,"column":20},"end":{"line":176,"column":null}},{"start":{},"end":{}}],"line":168},"26":{"loc":{"start":{"line":168,"column":24},"end":{"line":168,"column":83}},"type":"binary-expr","locations":[{"start":{"line":168,"column":24},"end":{"line":168,"column":49}},{"start":{"line":168,"column":49},"end":{"line":168,"column":83}}],"line":168},"27":{"loc":{"start":{"line":170,"column":24},"end":{"line":175,"column":null}},"type":"if","locations":[{"start":{"line":170,"column":24},"end":{"line":175,"column":null}},{"start":{},"end":{}}],"line":170},"28":{"loc":{"start":{"line":170,"column":28},"end":{"line":170,"column":81}},"type":"binary-expr","locations":[{"start":{"line":170,"column":28},"end":{"line":170,"column":45}},{"start":{"line":170,"column":45},"end":{"line":170,"column":59}},{"start":{"line":170,"column":59},"end":{"line":170,"column":81}}],"line":170},"29":{"loc":{"start":{"line":172,"column":28},"end":{"line":174,"column":null}},"type":"if","locations":[{"start":{"line":172,"column":28},"end":{"line":174,"column":null}},{"start":{},"end":{}}],"line":172},"30":{"loc":{"start":{"line":184,"column":12},"end":{"line":184,"column":null}},"type":"if","locations":[{"start":{"line":184,"column":12},"end":{"line":184,"column":null}},{"start":{},"end":{}}],"line":184},"31":{"loc":{"start":{"line":188,"column":8},"end":{"line":188,"column":null}},"type":"if","locations":[{"start":{"line":188,"column":8},"end":{"line":188,"column":null}},{"start":{},"end":{}}],"line":188},"32":{"loc":{"start":{"line":193,"column":4},"end":{"line":193,"column":null}},"type":"if","locations":[{"start":{"line":193,"column":4},"end":{"line":193,"column":null}},{"start":{},"end":{}}],"line":193},"33":{"loc":{"start":{"line":194,"column":4},"end":{"line":194,"column":null}},"type":"if","locations":[{"start":{"line":194,"column":4},"end":{"line":194,"column":null}},{"start":{},"end":{}}],"line":194},"34":{"loc":{"start":{"line":199,"column":13},"end":{"line":219,"column":null}},"type":"cond-expr","locations":[{"start":{"line":200,"column":16},"end":{"line":200,"column":null}},{"start":{"line":202,"column":16},"end":{"line":219,"column":null}}],"line":199},"35":{"loc":{"start":{"line":208,"column":39},"end":{"line":208,"column":null}},"type":"cond-expr","locations":[{"start":{"line":208,"column":65},"end":{"line":208,"column":93}},{"start":{"line":208,"column":93},"end":{"line":208,"column":null}}],"line":208},"36":{"loc":{"start":{"line":214,"column":21},"end":{"line":217,"column":null}},"type":"binary-expr","locations":[{"start":{"line":214,"column":21},"end":{"line":214,"column":null}},{"start":{"line":215,"column":24},"end":{"line":217,"column":null}}],"line":214},"37":{"loc":{"start":{"line":216,"column":29},"end":{"line":216,"column":null}},"type":"cond-expr","locations":[{"start":{"line":216,"column":43},"end":{"line":216,"column":63}},{"start":{"line":216,"column":63},"end":{"line":216,"column":null}}],"line":216}},"s":{"0":27,"1":27,"2":27,"3":27,"4":27,"5":27,"6":27,"7":27,"8":27,"9":8,"10":1,"11":7,"12":7,"13":8,"14":8,"15":8,"16":8,"17":2,"18":6,"19":1,"20":8,"21":1,"22":8,"23":0,"24":8,"25":0,"26":0,"27":8,"28":8,"29":8,"30":8,"31":8,"32":7,"33":0,"34":7,"35":6,"36":1,"37":1,"38":5,"39":6,"40":6,"41":6,"42":1,"43":1,"44":1,"45":27,"46":7,"47":27,"48":27,"49":23,"50":3,"51":0,"52":3,"53":2,"54":2,"55":2,"56":2,"57":2,"58":1,"59":2,"60":2,"61":1,"62":0,"63":0,"64":0,"65":0,"66":0,"67":1,"68":1,"69":1,"70":1,"71":1,"72":23,"73":23,"74":23,"75":27,"76":2,"77":2,"78":2,"79":27,"80":2,"81":2,"82":2,"83":3,"84":2,"85":0,"86":27,"87":1,"88":1,"89":1,"90":2,"91":1,"92":0,"93":27,"94":24,"95":2,"96":2,"97":1,"98":1,"99":1,"100":1,"101":1,"102":1,"103":1,"104":1,"105":1,"106":24,"107":15,"108":15,"109":15,"110":24,"111":24,"112":10,"113":24,"114":24,"115":27,"116":13,"117":14,"118":14,"119":13,"120":21,"121":0},"f":{"0":27,"1":8,"2":7,"3":6,"4":1,"5":1,"6":7,"7":23,"8":3,"9":2,"10":0,"11":1,"12":23,"13":2,"14":2,"15":2,"16":3,"17":0,"18":1,"19":1,"20":2,"21":0,"22":24,"23":2,"24":2,"25":15,"26":24,"27":21,"28":0},"b":{"0":[27,27],"1":[1,7],"2":[2,6],"3":[1,5],"4":[1,7],"5":[0,8],"6":[0,8],"7":[8,0],"8":[0,7],"9":[1,5],"10":[0,3],"11":[2,1],"12":[2,0],"13":[1,1],"14":[0,1],"15":[0,0],"16":[1,0],"17":[1,0],"18":[1,1],"19":[2,0],"20":[2,1],"21":[1,1],"22":[1,1],"23":[1,0],"24":[1,1,1,1],"25":[1,0],"26":[1,1],"27":[1,0],"28":[1,1,1],"29":[1,0],"30":[15,0],"31":[10,14],"32":[13,14],"33":[1,13],"34":[0,13],"35":[5,16],"36":[13,13],"37":[1,12]},"meta":{"lastBranch":38,"lastFunction":29,"lastStatement":122,"seen":{"f:7:24:7:36":0,"s:8:28:8:Infinity":0,"s:9:23:9:Infinity":1,"s:10:21:10:Infinity":2,"b:10:21:10:51:10:51:10:Infinity":0,"s:12:26:12:Infinity":3,"s:13:30:13:Infinity":4,"s:14:38:14:Infinity":5,"s:15:30:15:Infinity":6,"s:16:26:16:Infinity":7,"s:18:23:78:Infinity":8,"f:18:23:18:24":1,"b:19:8:24:Infinity:21:15:24:Infinity":1,"s:19:8:24:Infinity":9,"s:20:12:20:Infinity":10,"s:22:12:22:Infinity":11,"s:23:12:23:Infinity":12,"s:25:8:25:Infinity":13,"s:27:18:27:Infinity":14,"s:28:23:28:Infinity":15,"b:30:8:34:Infinity:32:8:34:Infinity":2,"s:30:8:34:Infinity":16,"s:31:12:31:Infinity":17,"b:32:8:34:Infinity:undefined:undefined:undefined:undefined":3,"s:32:8:34:Infinity":18,"s:33:12:33:Infinity":19,"b:36:8:38:Infinity:undefined:undefined:undefined:undefined":4,"s:36:8:38:Infinity":20,"s:37:12:37:Infinity":21,"b:41:8:49:Infinity:43:8:49:Infinity":5,"s:41:8:49:Infinity":22,"s:42:12:42:Infinity":23,"b:43:8:49:Infinity:46:15:49:Infinity":6,"s:43:8:49:Infinity":24,"s:44:12:44:Infinity":25,"s:45:12:45:Infinity":26,"s:48:12:48:Infinity":27,"s:51:28:51:Infinity":28,"b:52:8:54:Infinity:undefined:undefined:undefined:undefined":7,"s:52:8:54:Infinity":29,"s:53:12:53:Infinity":30,"s:56:8:77:Infinity":31,"f:57:18:57:19":2,"b:58:16:60:Infinity:undefined:undefined:undefined:undefined":8,"s:58:16:60:Infinity":32,"s:59:20:59:Infinity":33,"s:61:16:61:Infinity":34,"f:63:18:63:19":3,"b:64:16:68:Infinity:66:23:68:Infinity":9,"s:64:16:68:Infinity":35,"s:65:20:65:Infinity":36,"f:65:29:65:30":4,"s:65:39:65:57":37,"s:67:20:67:Infinity":38,"s:69:16:69:Infinity":39,"s:70:16:70:Infinity":40,"s:71:16:71:Infinity":41,"f:73:19:73:20":5,"s:74:16:74:Infinity":42,"s:75:16:75:Infinity":43,"s:76:16:76:Infinity":44,"s:80:4:82:Infinity":45,"f:80:14:80:20":6,"s:81:8:81:Infinity":46,"s:84:42:84:Infinity":47,"s:86:4:122:Infinity":48,"f:86:14:86:20":7,"s:87:30:118:Infinity":49,"f:87:30:87:31":8,"b:88:12:88:Infinity:undefined:undefined:undefined:undefined":10,"s:88:12:88:Infinity":50,"s:88:36:88:Infinity":51,"b:90:12:117:Infinity:102:12:117:Infinity":11,"s:90:12:117:Infinity":52,"s:91:16:101:Infinity":53,"f:91:33:91:34":9,"s:92:38:92:Infinity":54,"b:93:20:99:Infinity:undefined:undefined:undefined:undefined":12,"s:93:20:99:Infinity":55,"s:94:37:94:Infinity":56,"b:95:24:97:Infinity:undefined:undefined:undefined:undefined":13,"s:95:24:97:Infinity":57,"s:96:28:96:Infinity":58,"s:98:24:98:Infinity":59,"s:100:20:100:Infinity":60,"b:102:12:117:Infinity:110:12:117:Infinity":14,"s:102:12:117:Infinity":61,"s:103:16:109:Infinity":62,"f:103:33:103:34":10,"s:104:38:104:Infinity":63,"b:105:20:107:Infinity:undefined:undefined:undefined:undefined":15,"s:105:20:107:Infinity":64,"s:106:24:106:Infinity":65,"s:108:20:108:Infinity":66,"b:110:12:117:Infinity:undefined:undefined:undefined:undefined":16,"s:110:12:117:Infinity":67,"s:111:16:116:Infinity":68,"f:111:33:111:34":11,"b:112:20:114:Infinity:undefined:undefined:undefined:undefined":17,"s:112:20:114:Infinity":69,"b:112:24:112:45:112:45:112:74":18,"s:113:24:113:Infinity":70,"s:115:20:115:Infinity":71,"s:120:8:120:Infinity":72,"s:121:8:121:Infinity":73,"f:121:15:121:21":12,"s:121:21:121:Infinity":74,"s:124:25:129:Infinity":75,"f:124:25:124:26":13,"s:125:24:125:Infinity":76,"b:126:8:128:Infinity:undefined:undefined:undefined:undefined":19,"s:126:8:128:Infinity":77,"s:127:12:127:Infinity":78,"s:131:23:141:Infinity":79,"f:131:23:131:24":14,"s:132:28:132:Infinity":80,"s:134:8:134:Infinity":81,"f:134:17:134:18":15,"s:134:32:134:92":82,"f:134:46:134:47":16,"s:134:54:134:91":83,"b:134:75:134:89:134:89:134:91":20,"s:136:8:140:Infinity":84,"f:140:17:140:18":17,"s:140:26:140:67":85,"s:143:23:153:Infinity":86,"f:143:23:143:24":18,"s:144:28:144:Infinity":87,"s:146:8:146:Infinity":88,"f:146:17:146:18":19,"s:146:32:146:92":89,"f:146:46:146:47":20,"s:146:54:146:91":90,"b:146:75:146:89:146:89:146:91":21,"s:148:8:152:Infinity":91,"f:152:17:152:18":21,"s:152:26:152:69":92,"s:155:4:191:Infinity":93,"f:155:14:155:20":22,"s:156:25:180:Infinity":94,"f:157:12:157:13":23,"s:158:16:177:Infinity":95,"f:158:32:158:33":24,"b:160:20:165:Infinity:undefined:undefined:undefined:undefined":22,"s:160:20:165:Infinity":96,"b:161:24:163:Infinity:undefined:undefined:undefined:undefined":23,"s:161:24:163:Infinity":97,"b:161:28:161:52:161:52:161:68:161:68:161:79:161:79:161:97":24,"s:162:28:162:Infinity":98,"s:164:24:164:Infinity":99,"b:168:20:176:Infinity:undefined:undefined:undefined:undefined":25,"s:168:20:176:Infinity":100,"b:168:24:168:49:168:49:168:83":26,"s:169:38:169:Infinity":101,"b:170:24:175:Infinity:undefined:undefined:undefined:undefined":27,"s:170:24:175:Infinity":102,"b:170:28:170:45:170:45:170:59:170:59:170:81":28,"s:171:41:171:Infinity":103,"b:172:28:174:Infinity:undefined:undefined:undefined:undefined":29,"s:172:28:174:Infinity":104,"s:173:32:173:Infinity":105,"s:182:8:185:Infinity":106,"f:182:22:182:23":25,"s:183:23:183:Infinity":107,"b:184:12:184:Infinity:undefined:undefined:undefined:undefined":30,"s:184:12:184:Infinity":108,"s:184:20:184:Infinity":109,"s:187:25:187:Infinity":110,"b:188:8:188:Infinity:undefined:undefined:undefined:undefined":31,"s:188:8:188:Infinity":111,"s:188:22:188:Infinity":112,"s:190:8:190:Infinity":113,"f:190:15:190:21":26,"s:190:21:190:Infinity":114,"b:193:4:193:Infinity:undefined:undefined:undefined:undefined":32,"s:193:4:193:Infinity":115,"s:193:17:193:Infinity":116,"b:194:4:194:Infinity:undefined:undefined:undefined:undefined":33,"s:194:4:194:Infinity":117,"s:194:15:194:Infinity":118,"s:197:4:221:Infinity":119,"b:200:16:200:Infinity:202:16:219:Infinity":34,"f:203:31:203:32":27,"s:204:24:212:Infinity":120,"b:208:65:208:93:208:93:208:Infinity":35,"f:209:37:209:43":28,"s:209:43:209:Infinity":121,"b:214:21:214:Infinity:215:24:217:Infinity":36,"b:216:43:216:63:216:63:216:Infinity":37}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx","statementMap":{"0":{"start":{"line":7,"column":26},"end":{"line":7,"column":null}},"1":{"start":{"line":8,"column":24},"end":{"line":8,"column":null}},"2":{"start":{"line":9,"column":30},"end":{"line":9,"column":null}},"3":{"start":{"line":10,"column":26},"end":{"line":10,"column":null}},"4":{"start":{"line":12,"column":4},"end":{"line":32,"column":null}},"5":{"start":{"line":13,"column":8},"end":{"line":31,"column":null}},"6":{"start":{"line":15,"column":16},"end":{"line":15,"column":null}},"7":{"start":{"line":15,"column":29},"end":{"line":15,"column":null}},"8":{"start":{"line":16,"column":16},"end":{"line":16,"column":null}},"9":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"10":{"start":{"line":19,"column":29},"end":{"line":19,"column":null}},"11":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"12":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"13":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"14":{"start":{"line":26,"column":16},"end":{"line":26,"column":null}},"15":{"start":{"line":29,"column":16},"end":{"line":29,"column":null}},"16":{"start":{"line":30,"column":16},"end":{"line":30,"column":null}},"17":{"start":{"line":34,"column":4},"end":{"line":34,"column":null}},"18":{"start":{"line":34,"column":17},"end":{"line":34,"column":null}},"19":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"20":{"start":{"line":35,"column":15},"end":{"line":35,"column":null}},"21":{"start":{"line":37,"column":4},"end":{"line":78,"column":null}},"22":{"start":{"line":53,"column":28},"end":{"line":58,"column":null}},"23":{"start":{"line":69,"column":28},"end":{"line":73,"column":null}}},"fnMap":{"0":{"name":"FeedList","decl":{"start":{"line":6,"column":24},"end":{"line":6,"column":35}},"loc":{"start":{"line":6,"column":35},"end":{"line":80,"column":null}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":12,"column":14},"end":{"line":12,"column":20}},"loc":{"start":{"line":12,"column":20},"end":{"line":32,"column":7}},"line":12},"2":{"name":"(anonymous_2)","decl":{"start":{"line":14,"column":37},"end":{"line":14,"column":44}},"loc":{"start":{"line":14,"column":44},"end":{"line":17,"column":13}},"line":14},"3":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":35},"end":{"line":18,"column":42}},"loc":{"start":{"line":18,"column":42},"end":{"line":21,"column":13}},"line":18},"4":{"name":"(anonymous_4)","decl":{"start":{"line":23,"column":18},"end":{"line":23,"column":19}},"loc":{"start":{"line":23,"column":45},"end":{"line":27,"column":13}},"line":23},"5":{"name":"(anonymous_5)","decl":{"start":{"line":28,"column":19},"end":{"line":28,"column":20}},"loc":{"start":{"line":28,"column":28},"end":{"line":31,"column":13}},"line":28},"6":{"name":"(anonymous_6)","decl":{"start":{"line":52,"column":35},"end":{"line":52,"column":36}},"loc":{"start":{"line":53,"column":28},"end":{"line":58,"column":null}},"line":53},"7":{"name":"(anonymous_7)","decl":{"start":{"line":68,"column":34},"end":{"line":68,"column":35}},"loc":{"start":{"line":69,"column":28},"end":{"line":73,"column":null}},"line":69}},"branchMap":{"0":{"loc":{"start":{"line":15,"column":16},"end":{"line":15,"column":null}},"type":"if","locations":[{"start":{"line":15,"column":16},"end":{"line":15,"column":null}},{"start":{},"end":{}}],"line":15},"1":{"loc":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"type":"if","locations":[{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},{"start":{},"end":{}}],"line":19},"2":{"loc":{"start":{"line":34,"column":4},"end":{"line":34,"column":null}},"type":"if","locations":[{"start":{"line":34,"column":4},"end":{"line":34,"column":null}},{"start":{},"end":{}}],"line":34},"3":{"loc":{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},"type":"if","locations":[{"start":{"line":35,"column":4},"end":{"line":35,"column":null}},{"start":{},"end":{}}],"line":35},"4":{"loc":{"start":{"line":48,"column":17},"end":{"line":60,"column":null}},"type":"cond-expr","locations":[{"start":{"line":49,"column":20},"end":{"line":49,"column":null}},{"start":{"line":51,"column":20},"end":{"line":60,"column":null}}],"line":48},"5":{"loc":{"start":{"line":55,"column":37},"end":{"line":55,"column":null}},"type":"binary-expr","locations":[{"start":{"line":55,"column":37},"end":{"line":55,"column":51}},{"start":{"line":55,"column":51},"end":{"line":55,"column":null}}],"line":55},"6":{"loc":{"start":{"line":57,"column":33},"end":{"line":57,"column":null}},"type":"binary-expr","locations":[{"start":{"line":57,"column":33},"end":{"line":57,"column":50}},{"start":{"line":57,"column":50},"end":{"line":57,"column":null}}],"line":57},"7":{"loc":{"start":{"line":64,"column":13},"end":{"line":76,"column":null}},"type":"binary-expr","locations":[{"start":{"line":64,"column":13},"end":{"line":64,"column":21}},{"start":{"line":64,"column":21},"end":{"line":64,"column":null}},{"start":{"line":65,"column":16},"end":{"line":76,"column":null}}],"line":64}},"s":{"0":11,"1":11,"2":11,"3":11,"4":11,"5":6,"6":4,"7":0,"8":4,"9":4,"10":0,"11":4,"12":4,"13":4,"14":4,"15":1,"16":1,"17":11,"18":6,"19":5,"20":5,"21":4,"22":3,"23":3},"f":{"0":11,"1":6,"2":4,"3":4,"4":4,"5":1,"6":3,"7":3},"b":{"0":[0,4],"1":[0,4],"2":[6,5],"3":[1,4],"4":[2,2],"5":[3,0],"6":[3,3],"7":[11,4,2]},"meta":{"lastBranch":8,"lastFunction":8,"lastStatement":24,"seen":{"f:6:24:6:35":0,"s:7:26:7:Infinity":0,"s:8:24:8:Infinity":1,"s:9:30:9:Infinity":2,"s:10:26:10:Infinity":3,"s:12:4:32:Infinity":4,"f:12:14:12:20":1,"s:13:8:31:Infinity":5,"f:14:37:14:44":2,"b:15:16:15:Infinity:undefined:undefined:undefined:undefined":0,"s:15:16:15:Infinity":6,"s:15:29:15:Infinity":7,"s:16:16:16:Infinity":8,"f:18:35:18:42":3,"b:19:16:19:Infinity:undefined:undefined:undefined:undefined":1,"s:19:16:19:Infinity":9,"s:19:29:19:Infinity":10,"s:20:16:20:Infinity":11,"f:23:18:23:19":4,"s:24:16:24:Infinity":12,"s:25:16:25:Infinity":13,"s:26:16:26:Infinity":14,"f:28:19:28:20":5,"s:29:16:29:Infinity":15,"s:30:16:30:Infinity":16,"b:34:4:34:Infinity:undefined:undefined:undefined:undefined":2,"s:34:4:34:Infinity":17,"s:34:17:34:Infinity":18,"b:35:4:35:Infinity:undefined:undefined:undefined:undefined":3,"s:35:4:35:Infinity":19,"s:35:15:35:Infinity":20,"s:37:4:78:Infinity":21,"b:49:20:49:Infinity:51:20:60:Infinity":4,"f:52:35:52:36":6,"s:53:28:58:Infinity":22,"b:55:37:55:51:55:51:55:Infinity":5,"b:57:33:57:50:57:50:57:Infinity":6,"b:64:13:64:21:64:21:64:Infinity:65:16:76:Infinity":7,"f:68:34:68:35":7,"s:69:28:73:Infinity":23}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx","statementMap":{"0":{"start":{"line":6,"column":32},"end":{"line":6,"column":null}},"1":{"start":{"line":7,"column":26},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":10},"end":{"line":8,"column":null}},"3":{"start":{"line":10,"column":25},"end":{"line":33,"column":null}},"4":{"start":{"line":11,"column":8},"end":{"line":11,"column":null}},"5":{"start":{"line":12,"column":8},"end":{"line":12,"column":null}},"6":{"start":{"line":14,"column":8},"end":{"line":32,"column":null}},"7":{"start":{"line":16,"column":27},"end":{"line":16,"column":null}},"8":{"start":{"line":17,"column":12},"end":{"line":17,"column":null}},"9":{"start":{"line":19,"column":24},"end":{"line":22,"column":null}},"10":{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"12":{"start":{"line":27,"column":29},"end":{"line":27,"column":null}},"13":{"start":{"line":28,"column":16},"end":{"line":28,"column":null}},"14":{"start":{"line":31,"column":12},"end":{"line":31,"column":null}},"15":{"start":{"line":35,"column":4},"end":{"line":52,"column":null}},"16":{"start":{"line":45,"column":41},"end":{"line":45,"column":null}}},"fnMap":{"0":{"name":"Login","decl":{"start":{"line":5,"column":24},"end":{"line":5,"column":32}},"loc":{"start":{"line":5,"column":32},"end":{"line":54,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":10,"column":25},"end":{"line":10,"column":32}},"loc":{"start":{"line":10,"column":49},"end":{"line":33,"column":null}},"line":10},"2":{"name":"(anonymous_2)","decl":{"start":{"line":45,"column":34},"end":{"line":45,"column":35}},"loc":{"start":{"line":45,"column":41},"end":{"line":45,"column":null}},"line":45}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},{"start":{"line":26,"column":19},"end":{"line":29,"column":null}}],"line":24},"1":{"loc":{"start":{"line":28,"column":25},"end":{"line":28,"column":55}},"type":"binary-expr","locations":[{"start":{"line":28,"column":25},"end":{"line":28,"column":41}},{"start":{"line":28,"column":41},"end":{"line":28,"column":55}}],"line":28},"2":{"loc":{"start":{"line":49,"column":17},"end":{"line":49,"column":null}},"type":"binary-expr","locations":[{"start":{"line":49,"column":17},"end":{"line":49,"column":26}},{"start":{"line":49,"column":26},"end":{"line":49,"column":null}}],"line":49}},"s":{"0":14,"1":14,"2":14,"3":14,"4":3,"5":3,"6":3,"7":3,"8":3,"9":3,"10":2,"11":1,"12":1,"13":1,"14":1,"15":14,"16":3},"f":{"0":14,"1":3,"2":3},"b":{"0":[1,1],"1":[1,0],"2":[14,2]},"meta":{"lastBranch":3,"lastFunction":3,"lastStatement":17,"seen":{"f:5:24:5:32":0,"s:6:32:6:Infinity":0,"s:7:26:7:Infinity":1,"s:8:10:8:Infinity":2,"s:10:25:33:Infinity":3,"f:10:25:10:32":1,"s:11:8:11:Infinity":4,"s:12:8:12:Infinity":5,"s:14:8:32:Infinity":6,"s:16:27:16:Infinity":7,"s:17:12:17:Infinity":8,"s:19:24:22:Infinity":9,"b:24:12:29:Infinity:26:19:29:Infinity":0,"s:24:12:29:Infinity":10,"s:25:16:25:Infinity":11,"s:27:29:27:Infinity":12,"s:28:16:28:Infinity":13,"b:28:25:28:41:28:41:28:55":1,"s:31:12:31:Infinity":14,"s:35:4:52:Infinity":15,"f:45:34:45:35":2,"s:45:41:45:Infinity":16,"b:49:17:49:26:49:26:49:Infinity":2}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.tsx","statementMap":{"0":{"start":{"line":6,"column":26},"end":{"line":6,"column":null}},"1":{"start":{"line":7,"column":36},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":30},"end":{"line":8,"column":null}},"3":{"start":{"line":9,"column":26},"end":{"line":9,"column":null}},"4":{"start":{"line":11,"column":4},"end":{"line":13,"column":null}},"5":{"start":{"line":12,"column":8},"end":{"line":12,"column":null}},"6":{"start":{"line":15,"column":23},"end":{"line":30,"column":null}},"7":{"start":{"line":16,"column":8},"end":{"line":16,"column":null}},"8":{"start":{"line":17,"column":8},"end":{"line":29,"column":null}},"9":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"10":{"start":{"line":19,"column":29},"end":{"line":19,"column":null}},"11":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"12":{"start":{"line":23,"column":16},"end":{"line":23,"column":null}},"13":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"14":{"start":{"line":27,"column":16},"end":{"line":27,"column":null}},"15":{"start":{"line":28,"column":16},"end":{"line":28,"column":null}},"16":{"start":{"line":32,"column":26},"end":{"line":54,"column":null}},"17":{"start":{"line":33,"column":8},"end":{"line":33,"column":null}},"18":{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},"19":{"start":{"line":34,"column":25},"end":{"line":34,"column":null}},"20":{"start":{"line":36,"column":8},"end":{"line":36,"column":null}},"21":{"start":{"line":37,"column":8},"end":{"line":53,"column":null}},"22":{"start":{"line":43,"column":16},"end":{"line":43,"column":null}},"23":{"start":{"line":43,"column":29},"end":{"line":43,"column":null}},"24":{"start":{"line":44,"column":16},"end":{"line":44,"column":null}},"25":{"start":{"line":47,"column":16},"end":{"line":47,"column":null}},"26":{"start":{"line":48,"column":16},"end":{"line":48,"column":null}},"27":{"start":{"line":51,"column":16},"end":{"line":51,"column":null}},"28":{"start":{"line":52,"column":16},"end":{"line":52,"column":null}},"29":{"start":{"line":56,"column":29},"end":{"line":72,"column":null}},"30":{"start":{"line":57,"column":8},"end":{"line":57,"column":null}},"31":{"start":{"line":57,"column":79},"end":{"line":57,"column":null}},"32":{"start":{"line":59,"column":8},"end":{"line":59,"column":null}},"33":{"start":{"line":60,"column":8},"end":{"line":71,"column":null}},"34":{"start":{"line":64,"column":16},"end":{"line":64,"column":null}},"35":{"start":{"line":64,"column":29},"end":{"line":64,"column":null}},"36":{"start":{"line":65,"column":16},"end":{"line":65,"column":null}},"37":{"start":{"line":65,"column":45},"end":{"line":65,"column":57}},"38":{"start":{"line":66,"column":16},"end":{"line":66,"column":null}},"39":{"start":{"line":69,"column":16},"end":{"line":69,"column":null}},"40":{"start":{"line":70,"column":16},"end":{"line":70,"column":null}},"41":{"start":{"line":74,"column":4},"end":{"line":119,"column":null}},"42":{"start":{"line":84,"column":41},"end":{"line":84,"column":null}},"43":{"start":{"line":102,"column":24},"end":{"line":115,"column":null}},"44":{"start":{"line":108,"column":47},"end":{"line":108,"column":null}}},"fnMap":{"0":{"name":"Settings","decl":{"start":{"line":5,"column":24},"end":{"line":5,"column":35}},"loc":{"start":{"line":5,"column":35},"end":{"line":121,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":14},"end":{"line":11,"column":20}},"loc":{"start":{"line":11,"column":20},"end":{"line":13,"column":7}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":15,"column":23},"end":{"line":15,"column":29}},"loc":{"start":{"line":15,"column":29},"end":{"line":30,"column":null}},"line":15},"3":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":18},"end":{"line":18,"column":19}},"loc":{"start":{"line":18,"column":27},"end":{"line":21,"column":13}},"line":18},"4":{"name":"(anonymous_4)","decl":{"start":{"line":22,"column":18},"end":{"line":22,"column":19}},"loc":{"start":{"line":22,"column":28},"end":{"line":25,"column":13}},"line":22},"5":{"name":"(anonymous_5)","decl":{"start":{"line":26,"column":19},"end":{"line":26,"column":20}},"loc":{"start":{"line":26,"column":28},"end":{"line":29,"column":13}},"line":26},"6":{"name":"(anonymous_6)","decl":{"start":{"line":32,"column":26},"end":{"line":32,"column":27}},"loc":{"start":{"line":32,"column":50},"end":{"line":54,"column":null}},"line":32},"7":{"name":"(anonymous_7)","decl":{"start":{"line":42,"column":18},"end":{"line":42,"column":19}},"loc":{"start":{"line":42,"column":27},"end":{"line":45,"column":13}},"line":42},"8":{"name":"(anonymous_8)","decl":{"start":{"line":46,"column":18},"end":{"line":46,"column":24}},"loc":{"start":{"line":46,"column":24},"end":{"line":49,"column":13}},"line":46},"9":{"name":"(anonymous_9)","decl":{"start":{"line":50,"column":19},"end":{"line":50,"column":20}},"loc":{"start":{"line":50,"column":28},"end":{"line":53,"column":13}},"line":50},"10":{"name":"(anonymous_10)","decl":{"start":{"line":56,"column":29},"end":{"line":56,"column":30}},"loc":{"start":{"line":56,"column":45},"end":{"line":72,"column":null}},"line":56},"11":{"name":"(anonymous_11)","decl":{"start":{"line":63,"column":18},"end":{"line":63,"column":19}},"loc":{"start":{"line":63,"column":27},"end":{"line":67,"column":13}},"line":63},"12":{"name":"(anonymous_12)","decl":{"start":{"line":65,"column":38},"end":{"line":65,"column":39}},"loc":{"start":{"line":65,"column":45},"end":{"line":65,"column":57}},"line":65},"13":{"name":"(anonymous_13)","decl":{"start":{"line":68,"column":19},"end":{"line":68,"column":20}},"loc":{"start":{"line":68,"column":28},"end":{"line":71,"column":13}},"line":68},"14":{"name":"(anonymous_14)","decl":{"start":{"line":84,"column":34},"end":{"line":84,"column":35}},"loc":{"start":{"line":84,"column":41},"end":{"line":84,"column":null}},"line":84},"15":{"name":"(anonymous_15)","decl":{"start":{"line":101,"column":31},"end":{"line":101,"column":32}},"loc":{"start":{"line":102,"column":24},"end":{"line":115,"column":null}},"line":102},"16":{"name":"(anonymous_16)","decl":{"start":{"line":108,"column":41},"end":{"line":108,"column":47}},"loc":{"start":{"line":108,"column":47},"end":{"line":108,"column":null}},"line":108}},"branchMap":{"0":{"loc":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"type":"if","locations":[{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},{"start":{},"end":{}}],"line":19},"1":{"loc":{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},"type":"if","locations":[{"start":{"line":34,"column":8},"end":{"line":34,"column":null}},{"start":{},"end":{}}],"line":34},"2":{"loc":{"start":{"line":43,"column":16},"end":{"line":43,"column":null}},"type":"if","locations":[{"start":{"line":43,"column":16},"end":{"line":43,"column":null}},{"start":{},"end":{}}],"line":43},"3":{"loc":{"start":{"line":57,"column":8},"end":{"line":57,"column":null}},"type":"if","locations":[{"start":{"line":57,"column":8},"end":{"line":57,"column":null}},{"start":{},"end":{}}],"line":57},"4":{"loc":{"start":{"line":64,"column":16},"end":{"line":64,"column":null}},"type":"if","locations":[{"start":{"line":64,"column":16},"end":{"line":64,"column":null}},{"start":{},"end":{}}],"line":64},"5":{"loc":{"start":{"line":94,"column":17},"end":{"line":94,"column":null}},"type":"binary-expr","locations":[{"start":{"line":94,"column":17},"end":{"line":94,"column":26}},{"start":{"line":94,"column":26},"end":{"line":94,"column":null}}],"line":94},"6":{"loc":{"start":{"line":99,"column":17},"end":{"line":99,"column":null}},"type":"binary-expr","locations":[{"start":{"line":99,"column":17},"end":{"line":99,"column":28}},{"start":{"line":99,"column":28},"end":{"line":99,"column":null}}],"line":99},"7":{"loc":{"start":{"line":104,"column":62},"end":{"line":104,"column":89}},"type":"binary-expr","locations":[{"start":{"line":104,"column":62},"end":{"line":104,"column":76}},{"start":{"line":104,"column":76},"end":{"line":104,"column":89}}],"line":104}},"s":{"0":14,"1":14,"2":14,"3":14,"4":14,"5":3,"6":14,"7":4,"8":4,"9":4,"10":0,"11":4,"12":4,"13":4,"14":0,"15":0,"16":14,"17":1,"18":1,"19":0,"20":1,"21":1,"22":1,"23":0,"24":1,"25":1,"26":1,"27":0,"28":0,"29":14,"30":1,"31":0,"32":1,"33":1,"34":1,"35":0,"36":1,"37":1,"38":1,"39":0,"40":0,"41":14,"42":1,"43":5,"44":1},"f":{"0":14,"1":3,"2":4,"3":4,"4":4,"5":0,"6":1,"7":1,"8":1,"9":0,"10":1,"11":1,"12":1,"13":0,"14":1,"15":5,"16":1},"b":{"0":[0,4],"1":[0,1],"2":[0,1],"3":[0,1],"4":[0,1],"5":[14,0],"6":[14,5],"7":[5,0]},"meta":{"lastBranch":8,"lastFunction":17,"lastStatement":45,"seen":{"f:5:24:5:35":0,"s:6:26:6:Infinity":0,"s:7:36:7:Infinity":1,"s:8:30:8:Infinity":2,"s:9:26:9:Infinity":3,"s:11:4:13:Infinity":4,"f:11:14:11:20":1,"s:12:8:12:Infinity":5,"s:15:23:30:Infinity":6,"f:15:23:15:29":2,"s:16:8:16:Infinity":7,"s:17:8:29:Infinity":8,"f:18:18:18:19":3,"b:19:16:19:Infinity:undefined:undefined:undefined:undefined":0,"s:19:16:19:Infinity":9,"s:19:29:19:Infinity":10,"s:20:16:20:Infinity":11,"f:22:18:22:19":4,"s:23:16:23:Infinity":12,"s:24:16:24:Infinity":13,"f:26:19:26:20":5,"s:27:16:27:Infinity":14,"s:28:16:28:Infinity":15,"s:32:26:54:Infinity":16,"f:32:26:32:27":6,"s:33:8:33:Infinity":17,"b:34:8:34:Infinity:undefined:undefined:undefined:undefined":1,"s:34:8:34:Infinity":18,"s:34:25:34:Infinity":19,"s:36:8:36:Infinity":20,"s:37:8:53:Infinity":21,"f:42:18:42:19":7,"b:43:16:43:Infinity:undefined:undefined:undefined:undefined":2,"s:43:16:43:Infinity":22,"s:43:29:43:Infinity":23,"s:44:16:44:Infinity":24,"f:46:18:46:24":8,"s:47:16:47:Infinity":25,"s:48:16:48:Infinity":26,"f:50:19:50:20":9,"s:51:16:51:Infinity":27,"s:52:16:52:Infinity":28,"s:56:29:72:Infinity":29,"f:56:29:56:30":10,"b:57:8:57:Infinity:undefined:undefined:undefined:undefined":3,"s:57:8:57:Infinity":30,"s:57:79:57:Infinity":31,"s:59:8:59:Infinity":32,"s:60:8:71:Infinity":33,"f:63:18:63:19":11,"b:64:16:64:Infinity:undefined:undefined:undefined:undefined":4,"s:64:16:64:Infinity":34,"s:64:29:64:Infinity":35,"s:65:16:65:Infinity":36,"f:65:38:65:39":12,"s:65:45:65:57":37,"s:66:16:66:Infinity":38,"f:68:19:68:20":13,"s:69:16:69:Infinity":39,"s:70:16:70:Infinity":40,"s:74:4:119:Infinity":41,"f:84:34:84:35":14,"s:84:41:84:Infinity":42,"b:94:17:94:26:94:26:94:Infinity":5,"b:99:17:99:28:99:28:99:Infinity":6,"f:101:31:101:32":15,"s:102:24:115:Infinity":43,"b:104:62:104:76:104:76:104:89":7,"f:108:41:108:47":16,"s:108:47:108:Infinity":44}}} +{ + "/Users/adam/workspace/vibecode/neko/frontend/src/App.css": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/App.css", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "meta": { "lastBranch": 0, "lastFunction": 0, "lastStatement": 0, "seen": {} } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx", + "statementMap": { + "0": { "start": { "line": 8, "column": 22 }, "end": { "line": 8, "column": null } }, + "1": { "start": { "line": 9, "column": 8 }, "end": { "line": 9, "column": null } }, + "2": { "start": { "line": 11, "column": 2 }, "end": { "line": 21, "column": null } }, + "3": { "start": { "line": 12, "column": 4 }, "end": { "line": 20, "column": null } }, + "4": { "start": { "line": 14, "column": 8 }, "end": { "line": 18, "column": null } }, + "5": { "start": { "line": 15, "column": 10 }, "end": { "line": 15, "column": null } }, + "6": { "start": { "line": 17, "column": 10 }, "end": { "line": 17, "column": null } }, + "7": { "start": { "line": 20, "column": 19 }, "end": { "line": 20, "column": 33 } }, + "8": { "start": { "line": 23, "column": 2 }, "end": { "line": 25, "column": null } }, + "9": { "start": { "line": 24, "column": 4 }, "end": { "line": 24, "column": null } }, + "10": { "start": { "line": 27, "column": 2 }, "end": { "line": 29, "column": null } }, + "11": { "start": { "line": 28, "column": 4 }, "end": { "line": 28, "column": null } }, + "12": { "start": { "line": 31, "column": 2 }, "end": { "line": 31, "column": null } }, + "13": { "start": { "line": 39, "column": 8 }, "end": { "line": 39, "column": null } }, + "14": { "start": { "line": 40, "column": 2 }, "end": { "line": 68, "column": null } }, + "15": { "start": { "line": 45, "column": 33 }, "end": { "line": 45, "column": 56 } }, + "16": { "start": { "line": 48, "column": 12 }, "end": { "line": 49, "column": null } }, + "17": { "start": { "line": 49, "column": 26 }, "end": { "line": 49, "column": 60 } }, + "18": { "start": { "line": 73, "column": 2 }, "end": { "line": 86, "column": null } } + }, + "fnMap": { + "0": { + "name": "RequireAuth", + "decl": { "start": { "line": 7, "column": 9 }, "end": { "line": 7, "column": 21 } }, + "loc": { "start": { "line": 7, "column": 69 }, "end": { "line": 32, "column": null } }, + "line": 7 + }, + "1": { + "name": "(anonymous_1)", + "decl": { "start": { "line": 11, "column": 12 }, "end": { "line": 11, "column": 18 } }, + "loc": { "start": { "line": 11, "column": 18 }, "end": { "line": 21, "column": 5 } }, + "line": 11 + }, + "2": { + "name": "(anonymous_2)", + "decl": { "start": { "line": 13, "column": 12 }, "end": { "line": 13, "column": 13 } }, + "loc": { "start": { "line": 13, "column": 21 }, "end": { "line": 19, "column": 7 } }, + "line": 13 + }, + "3": { + "name": "(anonymous_3)", + "decl": { "start": { "line": 20, "column": 13 }, "end": { "line": 20, "column": 19 } }, + "loc": { "start": { "line": 20, "column": 19 }, "end": { "line": 20, "column": 33 } }, + "line": 20 + }, + "4": { + "name": "Dashboard", + "decl": { "start": { "line": 38, "column": 9 }, "end": { "line": 38, "column": 21 } }, + "loc": { "start": { "line": 38, "column": 21 }, "end": { "line": 70, "column": null } }, + "line": 38 + }, + "5": { + "name": "(anonymous_5)", + "decl": { "start": { "line": 45, "column": 27 }, "end": { "line": 45, "column": 33 } }, + "loc": { "start": { "line": 45, "column": 33 }, "end": { "line": 45, "column": 56 } }, + "line": 45 + }, + "6": { + "name": "(anonymous_6)", + "decl": { "start": { "line": 47, "column": 27 }, "end": { "line": 47, "column": 33 } }, + "loc": { "start": { "line": 47, "column": 33 }, "end": { "line": 50, "column": 13 } }, + "line": 47 + }, + "7": { + "name": "(anonymous_7)", + "decl": { "start": { "line": 49, "column": 20 }, "end": { "line": 49, "column": 26 } }, + "loc": { "start": { "line": 49, "column": 26 }, "end": { "line": 49, "column": 60 } }, + "line": 49 + }, + "8": { + "name": "App", + "decl": { "start": { "line": 72, "column": 9 }, "end": { "line": 72, "column": 15 } }, + "loc": { "start": { "line": 72, "column": 15 }, "end": { "line": 88, "column": null } }, + "line": 72 + } + }, + "branchMap": { + "0": { + "loc": { "start": { "line": 14, "column": 8 }, "end": { "line": 18, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 14, "column": 8 }, "end": { "line": 18, "column": null } }, + { "start": { "line": 16, "column": 15 }, "end": { "line": 18, "column": null } } + ], + "line": 14 + }, + "1": { + "loc": { "start": { "line": 23, "column": 2 }, "end": { "line": 25, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 23, "column": 2 }, "end": { "line": 25, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 23 + }, + "2": { + "loc": { "start": { "line": 27, "column": 2 }, "end": { "line": 29, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 27, "column": 2 }, "end": { "line": 29, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 27 + } + }, + "s": { + "0": 2, + "1": 2, + "2": 2, + "3": 1, + "4": 1, + "5": 1, + "6": 0, + "7": 0, + "8": 2, + "9": 1, + "10": 1, + "11": 0, + "12": 1, + "13": 1, + "14": 1, + "15": 0, + "16": 1, + "17": 1, + "18": 2 + }, + "f": { "0": 2, "1": 1, "2": 1, "3": 0, "4": 1, "5": 0, "6": 1, "7": 1, "8": 2 }, + "b": { "0": [1, 0], "1": [1, 1], "2": [0, 1] }, + "meta": { + "lastBranch": 3, + "lastFunction": 9, + "lastStatement": 19, + "seen": { + "f:7:9:7:21": 0, + "s:8:22:8:Infinity": 0, + "s:9:8:9:Infinity": 1, + "s:11:2:21:Infinity": 2, + "f:11:12:11:18": 1, + "s:12:4:20:Infinity": 3, + "f:13:12:13:13": 2, + "b:14:8:18:Infinity:16:15:18:Infinity": 0, + "s:14:8:18:Infinity": 4, + "s:15:10:15:Infinity": 5, + "s:17:10:17:Infinity": 6, + "f:20:13:20:19": 3, + "s:20:19:20:33": 7, + "b:23:2:25:Infinity:undefined:undefined:undefined:undefined": 1, + "s:23:2:25:Infinity": 8, + "s:24:4:24:Infinity": 9, + "b:27:2:29:Infinity:undefined:undefined:undefined:undefined": 2, + "s:27:2:29:Infinity": 10, + "s:28:4:28:Infinity": 11, + "s:31:2:31:Infinity": 12, + "f:38:9:38:21": 4, + "s:39:8:39:Infinity": 13, + "s:40:2:68:Infinity": 14, + "f:45:27:45:33": 5, + "s:45:33:45:56": 15, + "f:47:27:47:33": 6, + "s:48:12:49:Infinity": 16, + "f:49:20:49:26": 7, + "s:49:26:49:60": 17, + "f:72:9:72:15": 8, + "s:73:2:86:Infinity": 18 + } + } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "meta": { "lastBranch": 0, "lastFunction": 0, "lastStatement": 0, "seen": {} } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx", + "statementMap": { + "0": { "start": { "line": 10, "column": 24 }, "end": { "line": 10, "column": null } }, + "1": { "start": { "line": 11, "column": 30 }, "end": { "line": 11, "column": null } }, + "2": { "start": { "line": 14, "column": 23 }, "end": { "line": 16, "column": null } }, + "3": { "start": { "line": 15, "column": 8 }, "end": { "line": 15, "column": null } }, + "4": { "start": { "line": 18, "column": 23 }, "end": { "line": 52, "column": null } }, + "5": { "start": { "line": 19, "column": 8 }, "end": { "line": 19, "column": null } }, + "6": { "start": { "line": 21, "column": 29 }, "end": { "line": 21, "column": null } }, + "7": { "start": { "line": 22, "column": 8 }, "end": { "line": 22, "column": null } }, + "8": { "start": { "line": 24, "column": 8 }, "end": { "line": 51, "column": null } }, + "9": { "start": { "line": 36, "column": 16 }, "end": { "line": 38, "column": null } }, + "10": { "start": { "line": 37, "column": 20 }, "end": { "line": 37, "column": null } }, + "11": { "start": { "line": 39, "column": 16 }, "end": { "line": 39, "column": null } }, + "12": { "start": { "line": 44, "column": 16 }, "end": { "line": 44, "column": null } }, + "13": { "start": { "line": 47, "column": 16 }, "end": { "line": 47, "column": null } }, + "14": { "start": { "line": 49, "column": 16 }, "end": { "line": 49, "column": null } }, + "15": { "start": { "line": 50, "column": 16 }, "end": { "line": 50, "column": null } }, + "16": { "start": { "line": 54, "column": 4 }, "end": { "line": 82, "column": null } }, + "17": { "start": { "line": 59, "column": 24 }, "end": { "line": 59, "column": null } }, + "18": { "start": { "line": 60, "column": 24 }, "end": { "line": 60, "column": null } } + }, + "fnMap": { + "0": { + "name": "FeedItem", + "decl": { "start": { "line": 9, "column": 24 }, "end": { "line": 9, "column": 33 } }, + "loc": { "start": { "line": 9, "column": 71 }, "end": { "line": 84, "column": null } }, + "line": 9 + }, + "1": { + "name": "(anonymous_1)", + "decl": { "start": { "line": 14, "column": 23 }, "end": { "line": 14, "column": 29 } }, + "loc": { "start": { "line": 14, "column": 29 }, "end": { "line": 16, "column": null } }, + "line": 14 + }, + "2": { + "name": "(anonymous_2)", + "decl": { "start": { "line": 18, "column": 23 }, "end": { "line": 18, "column": 24 } }, + "loc": { "start": { "line": 18, "column": 42 }, "end": { "line": 52, "column": null } }, + "line": 18 + }, + "3": { + "name": "(anonymous_3)", + "decl": { "start": { "line": 35, "column": 18 }, "end": { "line": 35, "column": 19 } }, + "loc": { "start": { "line": 35, "column": 27 }, "end": { "line": 40, "column": 13 } }, + "line": 35 + }, + "4": { + "name": "(anonymous_4)", + "decl": { "start": { "line": 41, "column": 18 }, "end": { "line": 41, "column": 24 } }, + "loc": { "start": { "line": 41, "column": 24 }, "end": { "line": 45, "column": 13 } }, + "line": 41 + }, + "5": { + "name": "(anonymous_5)", + "decl": { "start": { "line": 46, "column": 19 }, "end": { "line": 46, "column": 20 } }, + "loc": { "start": { "line": 46, "column": 28 }, "end": { "line": 51, "column": 13 } }, + "line": 46 + }, + "6": { + "name": "(anonymous_6)", + "decl": { "start": { "line": 58, "column": 29 }, "end": { "line": 58, "column": 30 } }, + "loc": { "start": { "line": 58, "column": 36 }, "end": { "line": 61, "column": null } }, + "line": 58 + } + }, + "branchMap": { + "0": { + "loc": { "start": { "line": 36, "column": 16 }, "end": { "line": 38, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 36, "column": 16 }, "end": { "line": 38, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 36 + }, + "1": { + "loc": { "start": { "line": 55, "column": 36 }, "end": { "line": 55, "column": 65 } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 55, "column": 48 }, "end": { "line": 55, "column": 57 } }, + { "start": { "line": 55, "column": 57 }, "end": { "line": 55, "column": 65 } } + ], + "line": 55 + }, + "2": { + "loc": { "start": { "line": 55, "column": 69 }, "end": { "line": 55, "column": 93 } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 55, "column": 79 }, "end": { "line": 55, "column": 91 } }, + { "start": { "line": 55, "column": 91 }, "end": { "line": 55, "column": 93 } } + ], + "line": 55 + }, + "3": { + "loc": { "start": { "line": 62, "column": 43 }, "end": { "line": 62, "column": 87 } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 62, "column": 58 }, "end": { "line": 62, "column": 73 } }, + { "start": { "line": 62, "column": 73 }, "end": { "line": 62, "column": 87 } } + ], + "line": 62 + }, + "4": { + "loc": { "start": { "line": 63, "column": 27 }, "end": { "line": 63, "column": null } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 63, "column": 42 }, "end": { "line": 63, "column": 53 } }, + { "start": { "line": 63, "column": 53 }, "end": { "line": 63, "column": null } } + ], + "line": 63 + }, + "5": { + "loc": { "start": { "line": 65, "column": 21 }, "end": { "line": 65, "column": null } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 65, "column": 36 }, "end": { "line": 65, "column": 42 } }, + { "start": { "line": 65, "column": 42 }, "end": { "line": 65, "column": null } } + ], + "line": 65 + }, + "6": { + "loc": { "start": { "line": 68, "column": 21 }, "end": { "line": 68, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 68, "column": 21 }, "end": { "line": 68, "column": 35 } }, + { "start": { "line": 68, "column": 35 }, "end": { "line": 68, "column": null } } + ], + "line": 68 + }, + "7": { + "loc": { "start": { "line": 74, "column": 21 }, "end": { "line": 74, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 74, "column": 21 }, "end": { "line": 74, "column": 40 } }, + { "start": { "line": 74, "column": 40 }, "end": { "line": 74, "column": null } } + ], + "line": 74 + }, + "8": { + "loc": { "start": { "line": 79, "column": 13 }, "end": { "line": 80, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 79, "column": 13 }, "end": { "line": 79, "column": null } }, + { "start": { "line": 80, "column": 16 }, "end": { "line": 80, "column": null } } + ], + "line": 79 + } + }, + "s": { + "0": 21, + "1": 21, + "2": 21, + "3": 1, + "4": 21, + "5": 1, + "6": 1, + "7": 1, + "8": 1, + "9": 1, + "10": 0, + "11": 1, + "12": 1, + "13": 0, + "14": 0, + "15": 0, + "16": 21, + "17": 1, + "18": 1 + }, + "f": { "0": 21, "1": 1, "2": 1, "3": 1, "4": 1, "5": 0, "6": 1 }, + "b": { + "0": [0, 1], + "1": [9, 12], + "2": [1, 20], + "3": [2, 19], + "4": [2, 19], + "5": [2, 19], + "6": [21, 0], + "7": [21, 5], + "8": [21, 4] + }, + "meta": { + "lastBranch": 9, + "lastFunction": 7, + "lastStatement": 19, + "seen": { + "f:9:24:9:33": 0, + "s:10:24:10:Infinity": 0, + "s:11:30:11:Infinity": 1, + "s:14:23:16:Infinity": 2, + "f:14:23:14:29": 1, + "s:15:8:15:Infinity": 3, + "s:18:23:52:Infinity": 4, + "f:18:23:18:24": 2, + "s:19:8:19:Infinity": 5, + "s:21:29:21:Infinity": 6, + "s:22:8:22:Infinity": 7, + "s:24:8:51:Infinity": 8, + "f:35:18:35:19": 3, + "b:36:16:38:Infinity:undefined:undefined:undefined:undefined": 0, + "s:36:16:38:Infinity": 9, + "s:37:20:37:Infinity": 10, + "s:39:16:39:Infinity": 11, + "f:41:18:41:24": 4, + "s:44:16:44:Infinity": 12, + "f:46:19:46:20": 5, + "s:47:16:47:Infinity": 13, + "s:49:16:49:Infinity": 14, + "s:50:16:50:Infinity": 15, + "s:54:4:82:Infinity": 16, + "b:55:48:55:57:55:57:55:65": 1, + "b:55:79:55:91:55:91:55:93": 2, + "f:58:29:58:30": 6, + "s:59:24:59:Infinity": 17, + "s:60:24:60:Infinity": 18, + "b:62:58:62:73:62:73:62:87": 3, + "b:63:42:63:53:63:53:63:Infinity": 4, + "b:65:36:65:42:65:42:65:Infinity": 5, + "b:68:21:68:35:68:35:68:Infinity": 6, + "b:74:21:74:40:74:40:74:Infinity": 7, + "b:79:13:79:Infinity:80:16:80:Infinity": 8 + } + } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "meta": { "lastBranch": 0, "lastFunction": 0, "lastStatement": 0, "seen": {} } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx", + "statementMap": { + "0": { "start": { "line": 8, "column": 28 }, "end": { "line": 8, "column": null } }, + "1": { "start": { "line": 9, "column": 23 }, "end": { "line": 9, "column": null } }, + "2": { "start": { "line": 10, "column": 21 }, "end": { "line": 10, "column": null } }, + "3": { "start": { "line": 12, "column": 26 }, "end": { "line": 12, "column": null } }, + "4": { "start": { "line": 13, "column": 30 }, "end": { "line": 13, "column": null } }, + "5": { "start": { "line": 14, "column": 38 }, "end": { "line": 14, "column": null } }, + "6": { "start": { "line": 15, "column": 30 }, "end": { "line": 15, "column": null } }, + "7": { "start": { "line": 16, "column": 26 }, "end": { "line": 16, "column": null } }, + "8": { "start": { "line": 18, "column": 23 }, "end": { "line": 78, "column": null } }, + "9": { "start": { "line": 19, "column": 8 }, "end": { "line": 24, "column": null } }, + "10": { "start": { "line": 20, "column": 12 }, "end": { "line": 20, "column": null } }, + "11": { "start": { "line": 22, "column": 12 }, "end": { "line": 22, "column": null } }, + "12": { "start": { "line": 23, "column": 12 }, "end": { "line": 23, "column": null } }, + "13": { "start": { "line": 25, "column": 8 }, "end": { "line": 25, "column": null } }, + "14": { "start": { "line": 27, "column": 18 }, "end": { "line": 27, "column": null } }, + "15": { "start": { "line": 28, "column": 23 }, "end": { "line": 28, "column": null } }, + "16": { "start": { "line": 30, "column": 8 }, "end": { "line": 34, "column": null } }, + "17": { "start": { "line": 31, "column": 12 }, "end": { "line": 31, "column": null } }, + "18": { "start": { "line": 32, "column": 8 }, "end": { "line": 34, "column": null } }, + "19": { "start": { "line": 33, "column": 12 }, "end": { "line": 33, "column": null } }, + "20": { "start": { "line": 36, "column": 8 }, "end": { "line": 38, "column": null } }, + "21": { "start": { "line": 37, "column": 12 }, "end": { "line": 37, "column": null } }, + "22": { "start": { "line": 41, "column": 8 }, "end": { "line": 49, "column": null } }, + "23": { "start": { "line": 42, "column": 12 }, "end": { "line": 42, "column": null } }, + "24": { "start": { "line": 43, "column": 8 }, "end": { "line": 49, "column": null } }, + "25": { "start": { "line": 44, "column": 12 }, "end": { "line": 44, "column": null } }, + "26": { "start": { "line": 45, "column": 12 }, "end": { "line": 45, "column": null } }, + "27": { "start": { "line": 48, "column": 12 }, "end": { "line": 48, "column": null } }, + "28": { "start": { "line": 51, "column": 28 }, "end": { "line": 51, "column": null } }, + "29": { "start": { "line": 52, "column": 8 }, "end": { "line": 54, "column": null } }, + "30": { "start": { "line": 53, "column": 12 }, "end": { "line": 53, "column": null } }, + "31": { "start": { "line": 56, "column": 8 }, "end": { "line": 77, "column": null } }, + "32": { "start": { "line": 58, "column": 16 }, "end": { "line": 60, "column": null } }, + "33": { "start": { "line": 59, "column": 20 }, "end": { "line": 59, "column": null } }, + "34": { "start": { "line": 61, "column": 16 }, "end": { "line": 61, "column": null } }, + "35": { "start": { "line": 64, "column": 16 }, "end": { "line": 68, "column": null } }, + "36": { "start": { "line": 65, "column": 20 }, "end": { "line": 65, "column": null } }, + "37": { "start": { "line": 65, "column": 39 }, "end": { "line": 65, "column": 57 } }, + "38": { "start": { "line": 67, "column": 20 }, "end": { "line": 67, "column": null } }, + "39": { "start": { "line": 69, "column": 16 }, "end": { "line": 69, "column": null } }, + "40": { "start": { "line": 70, "column": 16 }, "end": { "line": 70, "column": null } }, + "41": { "start": { "line": 71, "column": 16 }, "end": { "line": 71, "column": null } }, + "42": { "start": { "line": 74, "column": 16 }, "end": { "line": 74, "column": null } }, + "43": { "start": { "line": 75, "column": 16 }, "end": { "line": 75, "column": null } }, + "44": { "start": { "line": 76, "column": 16 }, "end": { "line": 76, "column": null } }, + "45": { "start": { "line": 80, "column": 4 }, "end": { "line": 82, "column": null } }, + "46": { "start": { "line": 81, "column": 8 }, "end": { "line": 81, "column": null } }, + "47": { "start": { "line": 84, "column": 42 }, "end": { "line": 84, "column": null } }, + "48": { "start": { "line": 86, "column": 4 }, "end": { "line": 122, "column": null } }, + "49": { "start": { "line": 87, "column": 30 }, "end": { "line": 118, "column": null } }, + "50": { "start": { "line": 88, "column": 12 }, "end": { "line": 88, "column": null } }, + "51": { "start": { "line": 88, "column": 36 }, "end": { "line": 88, "column": null } }, + "52": { "start": { "line": 90, "column": 12 }, "end": { "line": 117, "column": null } }, + "53": { "start": { "line": 91, "column": 16 }, "end": { "line": 101, "column": null } }, + "54": { "start": { "line": 92, "column": 38 }, "end": { "line": 92, "column": null } }, + "55": { "start": { "line": 93, "column": 20 }, "end": { "line": 99, "column": null } }, + "56": { "start": { "line": 94, "column": 37 }, "end": { "line": 94, "column": null } }, + "57": { "start": { "line": 95, "column": 24 }, "end": { "line": 97, "column": null } }, + "58": { "start": { "line": 96, "column": 28 }, "end": { "line": 96, "column": null } }, + "59": { "start": { "line": 98, "column": 24 }, "end": { "line": 98, "column": null } }, + "60": { "start": { "line": 100, "column": 20 }, "end": { "line": 100, "column": null } }, + "61": { "start": { "line": 102, "column": 12 }, "end": { "line": 117, "column": null } }, + "62": { "start": { "line": 103, "column": 16 }, "end": { "line": 109, "column": null } }, + "63": { "start": { "line": 104, "column": 38 }, "end": { "line": 104, "column": null } }, + "64": { "start": { "line": 105, "column": 20 }, "end": { "line": 107, "column": null } }, + "65": { "start": { "line": 106, "column": 24 }, "end": { "line": 106, "column": null } }, + "66": { "start": { "line": 108, "column": 20 }, "end": { "line": 108, "column": null } }, + "67": { "start": { "line": 110, "column": 12 }, "end": { "line": 117, "column": null } }, + "68": { "start": { "line": 111, "column": 16 }, "end": { "line": 116, "column": null } }, + "69": { "start": { "line": 112, "column": 20 }, "end": { "line": 114, "column": null } }, + "70": { "start": { "line": 113, "column": 24 }, "end": { "line": 113, "column": null } }, + "71": { "start": { "line": 115, "column": 20 }, "end": { "line": 115, "column": null } }, + "72": { "start": { "line": 120, "column": 8 }, "end": { "line": 120, "column": null } }, + "73": { "start": { "line": 121, "column": 8 }, "end": { "line": 121, "column": null } }, + "74": { "start": { "line": 121, "column": 21 }, "end": { "line": 121, "column": null } }, + "75": { "start": { "line": 124, "column": 25 }, "end": { "line": 129, "column": null } }, + "76": { "start": { "line": 125, "column": 24 }, "end": { "line": 125, "column": null } }, + "77": { "start": { "line": 126, "column": 8 }, "end": { "line": 128, "column": null } }, + "78": { "start": { "line": 127, "column": 12 }, "end": { "line": 127, "column": null } }, + "79": { "start": { "line": 131, "column": 23 }, "end": { "line": 141, "column": null } }, + "80": { "start": { "line": 132, "column": 28 }, "end": { "line": 132, "column": null } }, + "81": { "start": { "line": 134, "column": 8 }, "end": { "line": 134, "column": null } }, + "82": { "start": { "line": 134, "column": 32 }, "end": { "line": 134, "column": 92 } }, + "83": { "start": { "line": 134, "column": 54 }, "end": { "line": 134, "column": 91 } }, + "84": { "start": { "line": 136, "column": 8 }, "end": { "line": 140, "column": null } }, + "85": { "start": { "line": 140, "column": 26 }, "end": { "line": 140, "column": 67 } }, + "86": { "start": { "line": 143, "column": 23 }, "end": { "line": 153, "column": null } }, + "87": { "start": { "line": 144, "column": 28 }, "end": { "line": 144, "column": null } }, + "88": { "start": { "line": 146, "column": 8 }, "end": { "line": 146, "column": null } }, + "89": { "start": { "line": 146, "column": 32 }, "end": { "line": 146, "column": 92 } }, + "90": { "start": { "line": 146, "column": 54 }, "end": { "line": 146, "column": 91 } }, + "91": { "start": { "line": 148, "column": 8 }, "end": { "line": 152, "column": null } }, + "92": { "start": { "line": 152, "column": 26 }, "end": { "line": 152, "column": 69 } }, + "93": { "start": { "line": 155, "column": 4 }, "end": { "line": 191, "column": null } }, + "94": { "start": { "line": 156, "column": 25 }, "end": { "line": 180, "column": null } }, + "95": { "start": { "line": 158, "column": 16 }, "end": { "line": 177, "column": null } }, + "96": { "start": { "line": 160, "column": 20 }, "end": { "line": 165, "column": null } }, + "97": { "start": { "line": 161, "column": 24 }, "end": { "line": 163, "column": null } }, + "98": { "start": { "line": 162, "column": 28 }, "end": { "line": 162, "column": null } }, + "99": { "start": { "line": 164, "column": 24 }, "end": { "line": 164, "column": null } }, + "100": { "start": { "line": 168, "column": 20 }, "end": { "line": 176, "column": null } }, + "101": { "start": { "line": 169, "column": 38 }, "end": { "line": 169, "column": null } }, + "102": { "start": { "line": 170, "column": 24 }, "end": { "line": 175, "column": null } }, + "103": { "start": { "line": 171, "column": 41 }, "end": { "line": 171, "column": null } }, + "104": { "start": { "line": 172, "column": 28 }, "end": { "line": 174, "column": null } }, + "105": { "start": { "line": 173, "column": 32 }, "end": { "line": 173, "column": null } }, + "106": { "start": { "line": 182, "column": 8 }, "end": { "line": 185, "column": null } }, + "107": { "start": { "line": 183, "column": 23 }, "end": { "line": 183, "column": null } }, + "108": { "start": { "line": 184, "column": 12 }, "end": { "line": 184, "column": null } }, + "109": { "start": { "line": 184, "column": 20 }, "end": { "line": 184, "column": null } }, + "110": { "start": { "line": 187, "column": 25 }, "end": { "line": 187, "column": null } }, + "111": { "start": { "line": 188, "column": 8 }, "end": { "line": 188, "column": null } }, + "112": { "start": { "line": 188, "column": 22 }, "end": { "line": 188, "column": null } }, + "113": { "start": { "line": 190, "column": 8 }, "end": { "line": 190, "column": null } }, + "114": { "start": { "line": 190, "column": 21 }, "end": { "line": 190, "column": null } }, + "115": { "start": { "line": 193, "column": 4 }, "end": { "line": 193, "column": null } }, + "116": { "start": { "line": 193, "column": 17 }, "end": { "line": 193, "column": null } }, + "117": { "start": { "line": 194, "column": 4 }, "end": { "line": 194, "column": null } }, + "118": { "start": { "line": 194, "column": 15 }, "end": { "line": 194, "column": null } }, + "119": { "start": { "line": 197, "column": 4 }, "end": { "line": 221, "column": null } }, + "120": { "start": { "line": 204, "column": 24 }, "end": { "line": 212, "column": null } }, + "121": { "start": { "line": 209, "column": 43 }, "end": { "line": 209, "column": null } } + }, + "fnMap": { + "0": { + "name": "FeedItems", + "decl": { "start": { "line": 7, "column": 24 }, "end": { "line": 7, "column": 36 } }, + "loc": { "start": { "line": 7, "column": 36 }, "end": { "line": 223, "column": null } }, + "line": 7 + }, + "1": { + "name": "(anonymous_1)", + "decl": { "start": { "line": 18, "column": 23 }, "end": { "line": 18, "column": 24 } }, + "loc": { "start": { "line": 18, "column": 43 }, "end": { "line": 78, "column": null } }, + "line": 18 + }, + "2": { + "name": "(anonymous_2)", + "decl": { "start": { "line": 57, "column": 18 }, "end": { "line": 57, "column": 19 } }, + "loc": { "start": { "line": 57, "column": 27 }, "end": { "line": 62, "column": 13 } }, + "line": 57 + }, + "3": { + "name": "(anonymous_3)", + "decl": { "start": { "line": 63, "column": 18 }, "end": { "line": 63, "column": 19 } }, + "loc": { "start": { "line": 63, "column": 28 }, "end": { "line": 72, "column": 13 } }, + "line": 63 + }, + "4": { + "name": "(anonymous_4)", + "decl": { "start": { "line": 65, "column": 29 }, "end": { "line": 65, "column": 30 } }, + "loc": { "start": { "line": 65, "column": 39 }, "end": { "line": 65, "column": 57 } }, + "line": 65 + }, + "5": { + "name": "(anonymous_5)", + "decl": { "start": { "line": 73, "column": 19 }, "end": { "line": 73, "column": 20 } }, + "loc": { "start": { "line": 73, "column": 28 }, "end": { "line": 77, "column": 13 } }, + "line": 73 + }, + "6": { + "name": "(anonymous_6)", + "decl": { "start": { "line": 80, "column": 14 }, "end": { "line": 80, "column": 20 } }, + "loc": { "start": { "line": 80, "column": 20 }, "end": { "line": 82, "column": 7 } }, + "line": 80 + }, + "7": { + "name": "(anonymous_7)", + "decl": { "start": { "line": 86, "column": 14 }, "end": { "line": 86, "column": 20 } }, + "loc": { "start": { "line": 86, "column": 20 }, "end": { "line": 122, "column": 7 } }, + "line": 86 + }, + "8": { + "name": "(anonymous_8)", + "decl": { "start": { "line": 87, "column": 30 }, "end": { "line": 87, "column": 31 } }, + "loc": { "start": { "line": 87, "column": 52 }, "end": { "line": 118, "column": null } }, + "line": 87 + }, + "9": { + "name": "(anonymous_9)", + "decl": { "start": { "line": 91, "column": 33 }, "end": { "line": 91, "column": 34 } }, + "loc": { "start": { "line": 91, "column": 43 }, "end": { "line": 101, "column": 17 } }, + "line": 91 + }, + "10": { + "name": "(anonymous_10)", + "decl": { "start": { "line": 103, "column": 33 }, "end": { "line": 103, "column": 34 } }, + "loc": { "start": { "line": 103, "column": 43 }, "end": { "line": 109, "column": 17 } }, + "line": 103 + }, + "11": { + "name": "(anonymous_11)", + "decl": { "start": { "line": 111, "column": 33 }, "end": { "line": 111, "column": 34 } }, + "loc": { "start": { "line": 111, "column": 51 }, "end": { "line": 116, "column": 17 } }, + "line": 111 + }, + "12": { + "name": "(anonymous_12)", + "decl": { "start": { "line": 121, "column": 15 }, "end": { "line": 121, "column": 21 } }, + "loc": { "start": { "line": 121, "column": 21 }, "end": { "line": 121, "column": null } }, + "line": 121 + }, + "13": { + "name": "(anonymous_13)", + "decl": { "start": { "line": 124, "column": 25 }, "end": { "line": 124, "column": 26 } }, + "loc": { "start": { "line": 124, "column": 44 }, "end": { "line": 129, "column": null } }, + "line": 124 + }, + "14": { + "name": "(anonymous_14)", + "decl": { "start": { "line": 131, "column": 23 }, "end": { "line": 131, "column": 24 } }, + "loc": { "start": { "line": 131, "column": 39 }, "end": { "line": 141, "column": null } }, + "line": 131 + }, + "15": { + "name": "(anonymous_15)", + "decl": { "start": { "line": 134, "column": 17 }, "end": { "line": 134, "column": 18 } }, + "loc": { "start": { "line": 134, "column": 32 }, "end": { "line": 134, "column": 92 } }, + "line": 134 + }, + "16": { + "name": "(anonymous_16)", + "decl": { "start": { "line": 134, "column": 46 }, "end": { "line": 134, "column": 47 } }, + "loc": { "start": { "line": 134, "column": 54 }, "end": { "line": 134, "column": 91 } }, + "line": 134 + }, + "17": { + "name": "(anonymous_17)", + "decl": { "start": { "line": 140, "column": 17 }, "end": { "line": 140, "column": 18 } }, + "loc": { "start": { "line": 140, "column": 26 }, "end": { "line": 140, "column": 67 } }, + "line": 140 + }, + "18": { + "name": "(anonymous_18)", + "decl": { "start": { "line": 143, "column": 23 }, "end": { "line": 143, "column": 24 } }, + "loc": { "start": { "line": 143, "column": 39 }, "end": { "line": 153, "column": null } }, + "line": 143 + }, + "19": { + "name": "(anonymous_19)", + "decl": { "start": { "line": 146, "column": 17 }, "end": { "line": 146, "column": 18 } }, + "loc": { "start": { "line": 146, "column": 32 }, "end": { "line": 146, "column": 92 } }, + "line": 146 + }, + "20": { + "name": "(anonymous_20)", + "decl": { "start": { "line": 146, "column": 46 }, "end": { "line": 146, "column": 47 } }, + "loc": { "start": { "line": 146, "column": 54 }, "end": { "line": 146, "column": 91 } }, + "line": 146 + }, + "21": { + "name": "(anonymous_21)", + "decl": { "start": { "line": 152, "column": 17 }, "end": { "line": 152, "column": 18 } }, + "loc": { "start": { "line": 152, "column": 26 }, "end": { "line": 152, "column": 69 } }, + "line": 152 + }, + "22": { + "name": "(anonymous_22)", + "decl": { "start": { "line": 155, "column": 14 }, "end": { "line": 155, "column": 20 } }, + "loc": { "start": { "line": 155, "column": 20 }, "end": { "line": 191, "column": 7 } }, + "line": 155 + }, + "23": { + "name": "(anonymous_23)", + "decl": { "start": { "line": 157, "column": 12 }, "end": { "line": 157, "column": 13 } }, + "loc": { "start": { "line": 157, "column": 25 }, "end": { "line": 178, "column": null } }, + "line": 157 + }, + "24": { + "name": "(anonymous_24)", + "decl": { "start": { "line": 158, "column": 32 }, "end": { "line": 158, "column": 33 } }, + "loc": { "start": { "line": 158, "column": 43 }, "end": { "line": 177, "column": 17 } }, + "line": 158 + }, + "25": { + "name": "(anonymous_25)", + "decl": { "start": { "line": 182, "column": 22 }, "end": { "line": 182, "column": 23 } }, + "loc": { "start": { "line": 182, "column": 36 }, "end": { "line": 185, "column": 9 } }, + "line": 182 + }, + "26": { + "name": "(anonymous_26)", + "decl": { "start": { "line": 190, "column": 15 }, "end": { "line": 190, "column": 21 } }, + "loc": { "start": { "line": 190, "column": 21 }, "end": { "line": 190, "column": null } }, + "line": 190 + }, + "27": { + "name": "(anonymous_27)", + "decl": { "start": { "line": 203, "column": 31 }, "end": { "line": 203, "column": 32 } }, + "loc": { "start": { "line": 204, "column": 24 }, "end": { "line": 212, "column": null } }, + "line": 204 + }, + "28": { + "name": "(anonymous_28)", + "decl": { "start": { "line": 209, "column": 37 }, "end": { "line": 209, "column": 43 } }, + "loc": { "start": { "line": 209, "column": 43 }, "end": { "line": 209, "column": null } }, + "line": 209 + } + }, + "branchMap": { + "0": { + "loc": { "start": { "line": 10, "column": 21 }, "end": { "line": 10, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 10, "column": 21 }, "end": { "line": 10, "column": 51 } }, + { "start": { "line": 10, "column": 51 }, "end": { "line": 10, "column": null } } + ], + "line": 10 + }, + "1": { + "loc": { "start": { "line": 19, "column": 8 }, "end": { "line": 24, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 19, "column": 8 }, "end": { "line": 24, "column": null } }, + { "start": { "line": 21, "column": 15 }, "end": { "line": 24, "column": null } } + ], + "line": 19 + }, + "2": { + "loc": { "start": { "line": 30, "column": 8 }, "end": { "line": 34, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 30, "column": 8 }, "end": { "line": 34, "column": null } }, + { "start": { "line": 32, "column": 8 }, "end": { "line": 34, "column": null } } + ], + "line": 30 + }, + "3": { + "loc": { "start": { "line": 32, "column": 8 }, "end": { "line": 34, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 32, "column": 8 }, "end": { "line": 34, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 32 + }, + "4": { + "loc": { "start": { "line": 36, "column": 8 }, "end": { "line": 38, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 36, "column": 8 }, "end": { "line": 38, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 36 + }, + "5": { + "loc": { "start": { "line": 41, "column": 8 }, "end": { "line": 49, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 41, "column": 8 }, "end": { "line": 49, "column": null } }, + { "start": { "line": 43, "column": 8 }, "end": { "line": 49, "column": null } } + ], + "line": 41 + }, + "6": { + "loc": { "start": { "line": 43, "column": 8 }, "end": { "line": 49, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 43, "column": 8 }, "end": { "line": 49, "column": null } }, + { "start": { "line": 46, "column": 15 }, "end": { "line": 49, "column": null } } + ], + "line": 43 + }, + "7": { + "loc": { "start": { "line": 52, "column": 8 }, "end": { "line": 54, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 52, "column": 8 }, "end": { "line": 54, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 52 + }, + "8": { + "loc": { "start": { "line": 58, "column": 16 }, "end": { "line": 60, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 58, "column": 16 }, "end": { "line": 60, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 58 + }, + "9": { + "loc": { "start": { "line": 64, "column": 16 }, "end": { "line": 68, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 64, "column": 16 }, "end": { "line": 68, "column": null } }, + { "start": { "line": 66, "column": 23 }, "end": { "line": 68, "column": null } } + ], + "line": 64 + }, + "10": { + "loc": { "start": { "line": 88, "column": 12 }, "end": { "line": 88, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 88, "column": 12 }, "end": { "line": 88, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 88 + }, + "11": { + "loc": { "start": { "line": 90, "column": 12 }, "end": { "line": 117, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 90, "column": 12 }, "end": { "line": 117, "column": null } }, + { "start": { "line": 102, "column": 12 }, "end": { "line": 117, "column": null } } + ], + "line": 90 + }, + "12": { + "loc": { "start": { "line": 93, "column": 20 }, "end": { "line": 99, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 93, "column": 20 }, "end": { "line": 99, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 93 + }, + "13": { + "loc": { "start": { "line": 95, "column": 24 }, "end": { "line": 97, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 95, "column": 24 }, "end": { "line": 97, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 95 + }, + "14": { + "loc": { "start": { "line": 102, "column": 12 }, "end": { "line": 117, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 102, "column": 12 }, "end": { "line": 117, "column": null } }, + { "start": { "line": 110, "column": 12 }, "end": { "line": 117, "column": null } } + ], + "line": 102 + }, + "15": { + "loc": { "start": { "line": 105, "column": 20 }, "end": { "line": 107, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 105, "column": 20 }, "end": { "line": 107, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 105 + }, + "16": { + "loc": { "start": { "line": 110, "column": 12 }, "end": { "line": 117, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 110, "column": 12 }, "end": { "line": 117, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 110 + }, + "17": { + "loc": { "start": { "line": 112, "column": 20 }, "end": { "line": 114, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 112, "column": 20 }, "end": { "line": 114, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 112 + }, + "18": { + "loc": { "start": { "line": 112, "column": 24 }, "end": { "line": 112, "column": 74 } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 112, "column": 24 }, "end": { "line": 112, "column": 45 } }, + { "start": { "line": 112, "column": 45 }, "end": { "line": 112, "column": 74 } } + ], + "line": 112 + }, + "19": { + "loc": { "start": { "line": 126, "column": 8 }, "end": { "line": 128, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 126, "column": 8 }, "end": { "line": 128, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 126 + }, + "20": { + "loc": { "start": { "line": 134, "column": 54 }, "end": { "line": 134, "column": 91 } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 134, "column": 75 }, "end": { "line": 134, "column": 89 } }, + { "start": { "line": 134, "column": 89 }, "end": { "line": 134, "column": 91 } } + ], + "line": 134 + }, + "21": { + "loc": { "start": { "line": 146, "column": 54 }, "end": { "line": 146, "column": 91 } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 146, "column": 75 }, "end": { "line": 146, "column": 89 } }, + { "start": { "line": 146, "column": 89 }, "end": { "line": 146, "column": 91 } } + ], + "line": 146 + }, + "22": { + "loc": { "start": { "line": 160, "column": 20 }, "end": { "line": 165, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 160, "column": 20 }, "end": { "line": 165, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 160 + }, + "23": { + "loc": { "start": { "line": 161, "column": 24 }, "end": { "line": 163, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 161, "column": 24 }, "end": { "line": 163, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 161 + }, + "24": { + "loc": { "start": { "line": 161, "column": 28 }, "end": { "line": 161, "column": 97 } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 161, "column": 28 }, "end": { "line": 161, "column": 52 } }, + { "start": { "line": 161, "column": 52 }, "end": { "line": 161, "column": 68 } }, + { "start": { "line": 161, "column": 68 }, "end": { "line": 161, "column": 79 } }, + { "start": { "line": 161, "column": 79 }, "end": { "line": 161, "column": 97 } } + ], + "line": 161 + }, + "25": { + "loc": { "start": { "line": 168, "column": 20 }, "end": { "line": 176, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 168, "column": 20 }, "end": { "line": 176, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 168 + }, + "26": { + "loc": { "start": { "line": 168, "column": 24 }, "end": { "line": 168, "column": 83 } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 168, "column": 24 }, "end": { "line": 168, "column": 49 } }, + { "start": { "line": 168, "column": 49 }, "end": { "line": 168, "column": 83 } } + ], + "line": 168 + }, + "27": { + "loc": { "start": { "line": 170, "column": 24 }, "end": { "line": 175, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 170, "column": 24 }, "end": { "line": 175, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 170 + }, + "28": { + "loc": { "start": { "line": 170, "column": 28 }, "end": { "line": 170, "column": 81 } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 170, "column": 28 }, "end": { "line": 170, "column": 45 } }, + { "start": { "line": 170, "column": 45 }, "end": { "line": 170, "column": 59 } }, + { "start": { "line": 170, "column": 59 }, "end": { "line": 170, "column": 81 } } + ], + "line": 170 + }, + "29": { + "loc": { "start": { "line": 172, "column": 28 }, "end": { "line": 174, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 172, "column": 28 }, "end": { "line": 174, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 172 + }, + "30": { + "loc": { "start": { "line": 184, "column": 12 }, "end": { "line": 184, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 184, "column": 12 }, "end": { "line": 184, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 184 + }, + "31": { + "loc": { "start": { "line": 188, "column": 8 }, "end": { "line": 188, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 188, "column": 8 }, "end": { "line": 188, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 188 + }, + "32": { + "loc": { "start": { "line": 193, "column": 4 }, "end": { "line": 193, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 193, "column": 4 }, "end": { "line": 193, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 193 + }, + "33": { + "loc": { "start": { "line": 194, "column": 4 }, "end": { "line": 194, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 194, "column": 4 }, "end": { "line": 194, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 194 + }, + "34": { + "loc": { "start": { "line": 199, "column": 13 }, "end": { "line": 219, "column": null } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 200, "column": 16 }, "end": { "line": 200, "column": null } }, + { "start": { "line": 202, "column": 16 }, "end": { "line": 219, "column": null } } + ], + "line": 199 + }, + "35": { + "loc": { "start": { "line": 208, "column": 39 }, "end": { "line": 208, "column": null } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 208, "column": 65 }, "end": { "line": 208, "column": 93 } }, + { "start": { "line": 208, "column": 93 }, "end": { "line": 208, "column": null } } + ], + "line": 208 + }, + "36": { + "loc": { "start": { "line": 214, "column": 21 }, "end": { "line": 217, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 214, "column": 21 }, "end": { "line": 214, "column": null } }, + { "start": { "line": 215, "column": 24 }, "end": { "line": 217, "column": null } } + ], + "line": 214 + }, + "37": { + "loc": { "start": { "line": 216, "column": 29 }, "end": { "line": 216, "column": null } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 216, "column": 43 }, "end": { "line": 216, "column": 63 } }, + { "start": { "line": 216, "column": 63 }, "end": { "line": 216, "column": null } } + ], + "line": 216 + } + }, + "s": { + "0": 27, + "1": 27, + "2": 27, + "3": 27, + "4": 27, + "5": 27, + "6": 27, + "7": 27, + "8": 27, + "9": 8, + "10": 1, + "11": 7, + "12": 7, + "13": 8, + "14": 8, + "15": 8, + "16": 8, + "17": 2, + "18": 6, + "19": 1, + "20": 8, + "21": 1, + "22": 8, + "23": 0, + "24": 8, + "25": 0, + "26": 0, + "27": 8, + "28": 8, + "29": 8, + "30": 8, + "31": 8, + "32": 7, + "33": 0, + "34": 7, + "35": 6, + "36": 1, + "37": 1, + "38": 5, + "39": 6, + "40": 6, + "41": 6, + "42": 1, + "43": 1, + "44": 1, + "45": 27, + "46": 7, + "47": 27, + "48": 27, + "49": 23, + "50": 3, + "51": 0, + "52": 3, + "53": 2, + "54": 2, + "55": 2, + "56": 2, + "57": 2, + "58": 1, + "59": 2, + "60": 2, + "61": 1, + "62": 0, + "63": 0, + "64": 0, + "65": 0, + "66": 0, + "67": 1, + "68": 1, + "69": 1, + "70": 1, + "71": 1, + "72": 23, + "73": 23, + "74": 23, + "75": 27, + "76": 2, + "77": 2, + "78": 2, + "79": 27, + "80": 2, + "81": 2, + "82": 2, + "83": 3, + "84": 2, + "85": 0, + "86": 27, + "87": 1, + "88": 1, + "89": 1, + "90": 2, + "91": 1, + "92": 0, + "93": 27, + "94": 24, + "95": 2, + "96": 2, + "97": 1, + "98": 1, + "99": 1, + "100": 1, + "101": 1, + "102": 1, + "103": 1, + "104": 1, + "105": 1, + "106": 24, + "107": 15, + "108": 15, + "109": 15, + "110": 24, + "111": 24, + "112": 10, + "113": 24, + "114": 24, + "115": 27, + "116": 13, + "117": 14, + "118": 14, + "119": 13, + "120": 21, + "121": 0 + }, + "f": { + "0": 27, + "1": 8, + "2": 7, + "3": 6, + "4": 1, + "5": 1, + "6": 7, + "7": 23, + "8": 3, + "9": 2, + "10": 0, + "11": 1, + "12": 23, + "13": 2, + "14": 2, + "15": 2, + "16": 3, + "17": 0, + "18": 1, + "19": 1, + "20": 2, + "21": 0, + "22": 24, + "23": 2, + "24": 2, + "25": 15, + "26": 24, + "27": 21, + "28": 0 + }, + "b": { + "0": [27, 27], + "1": [1, 7], + "2": [2, 6], + "3": [1, 5], + "4": [1, 7], + "5": [0, 8], + "6": [0, 8], + "7": [8, 0], + "8": [0, 7], + "9": [1, 5], + "10": [0, 3], + "11": [2, 1], + "12": [2, 0], + "13": [1, 1], + "14": [0, 1], + "15": [0, 0], + "16": [1, 0], + "17": [1, 0], + "18": [1, 1], + "19": [2, 0], + "20": [2, 1], + "21": [1, 1], + "22": [1, 1], + "23": [1, 0], + "24": [1, 1, 1, 1], + "25": [1, 0], + "26": [1, 1], + "27": [1, 0], + "28": [1, 1, 1], + "29": [1, 0], + "30": [15, 0], + "31": [10, 14], + "32": [13, 14], + "33": [1, 13], + "34": [0, 13], + "35": [5, 16], + "36": [13, 13], + "37": [1, 12] + }, + "meta": { + "lastBranch": 38, + "lastFunction": 29, + "lastStatement": 122, + "seen": { + "f:7:24:7:36": 0, + "s:8:28:8:Infinity": 0, + "s:9:23:9:Infinity": 1, + "s:10:21:10:Infinity": 2, + "b:10:21:10:51:10:51:10:Infinity": 0, + "s:12:26:12:Infinity": 3, + "s:13:30:13:Infinity": 4, + "s:14:38:14:Infinity": 5, + "s:15:30:15:Infinity": 6, + "s:16:26:16:Infinity": 7, + "s:18:23:78:Infinity": 8, + "f:18:23:18:24": 1, + "b:19:8:24:Infinity:21:15:24:Infinity": 1, + "s:19:8:24:Infinity": 9, + "s:20:12:20:Infinity": 10, + "s:22:12:22:Infinity": 11, + "s:23:12:23:Infinity": 12, + "s:25:8:25:Infinity": 13, + "s:27:18:27:Infinity": 14, + "s:28:23:28:Infinity": 15, + "b:30:8:34:Infinity:32:8:34:Infinity": 2, + "s:30:8:34:Infinity": 16, + "s:31:12:31:Infinity": 17, + "b:32:8:34:Infinity:undefined:undefined:undefined:undefined": 3, + "s:32:8:34:Infinity": 18, + "s:33:12:33:Infinity": 19, + "b:36:8:38:Infinity:undefined:undefined:undefined:undefined": 4, + "s:36:8:38:Infinity": 20, + "s:37:12:37:Infinity": 21, + "b:41:8:49:Infinity:43:8:49:Infinity": 5, + "s:41:8:49:Infinity": 22, + "s:42:12:42:Infinity": 23, + "b:43:8:49:Infinity:46:15:49:Infinity": 6, + "s:43:8:49:Infinity": 24, + "s:44:12:44:Infinity": 25, + "s:45:12:45:Infinity": 26, + "s:48:12:48:Infinity": 27, + "s:51:28:51:Infinity": 28, + "b:52:8:54:Infinity:undefined:undefined:undefined:undefined": 7, + "s:52:8:54:Infinity": 29, + "s:53:12:53:Infinity": 30, + "s:56:8:77:Infinity": 31, + "f:57:18:57:19": 2, + "b:58:16:60:Infinity:undefined:undefined:undefined:undefined": 8, + "s:58:16:60:Infinity": 32, + "s:59:20:59:Infinity": 33, + "s:61:16:61:Infinity": 34, + "f:63:18:63:19": 3, + "b:64:16:68:Infinity:66:23:68:Infinity": 9, + "s:64:16:68:Infinity": 35, + "s:65:20:65:Infinity": 36, + "f:65:29:65:30": 4, + "s:65:39:65:57": 37, + "s:67:20:67:Infinity": 38, + "s:69:16:69:Infinity": 39, + "s:70:16:70:Infinity": 40, + "s:71:16:71:Infinity": 41, + "f:73:19:73:20": 5, + "s:74:16:74:Infinity": 42, + "s:75:16:75:Infinity": 43, + "s:76:16:76:Infinity": 44, + "s:80:4:82:Infinity": 45, + "f:80:14:80:20": 6, + "s:81:8:81:Infinity": 46, + "s:84:42:84:Infinity": 47, + "s:86:4:122:Infinity": 48, + "f:86:14:86:20": 7, + "s:87:30:118:Infinity": 49, + "f:87:30:87:31": 8, + "b:88:12:88:Infinity:undefined:undefined:undefined:undefined": 10, + "s:88:12:88:Infinity": 50, + "s:88:36:88:Infinity": 51, + "b:90:12:117:Infinity:102:12:117:Infinity": 11, + "s:90:12:117:Infinity": 52, + "s:91:16:101:Infinity": 53, + "f:91:33:91:34": 9, + "s:92:38:92:Infinity": 54, + "b:93:20:99:Infinity:undefined:undefined:undefined:undefined": 12, + "s:93:20:99:Infinity": 55, + "s:94:37:94:Infinity": 56, + "b:95:24:97:Infinity:undefined:undefined:undefined:undefined": 13, + "s:95:24:97:Infinity": 57, + "s:96:28:96:Infinity": 58, + "s:98:24:98:Infinity": 59, + "s:100:20:100:Infinity": 60, + "b:102:12:117:Infinity:110:12:117:Infinity": 14, + "s:102:12:117:Infinity": 61, + "s:103:16:109:Infinity": 62, + "f:103:33:103:34": 10, + "s:104:38:104:Infinity": 63, + "b:105:20:107:Infinity:undefined:undefined:undefined:undefined": 15, + "s:105:20:107:Infinity": 64, + "s:106:24:106:Infinity": 65, + "s:108:20:108:Infinity": 66, + "b:110:12:117:Infinity:undefined:undefined:undefined:undefined": 16, + "s:110:12:117:Infinity": 67, + "s:111:16:116:Infinity": 68, + "f:111:33:111:34": 11, + "b:112:20:114:Infinity:undefined:undefined:undefined:undefined": 17, + "s:112:20:114:Infinity": 69, + "b:112:24:112:45:112:45:112:74": 18, + "s:113:24:113:Infinity": 70, + "s:115:20:115:Infinity": 71, + "s:120:8:120:Infinity": 72, + "s:121:8:121:Infinity": 73, + "f:121:15:121:21": 12, + "s:121:21:121:Infinity": 74, + "s:124:25:129:Infinity": 75, + "f:124:25:124:26": 13, + "s:125:24:125:Infinity": 76, + "b:126:8:128:Infinity:undefined:undefined:undefined:undefined": 19, + "s:126:8:128:Infinity": 77, + "s:127:12:127:Infinity": 78, + "s:131:23:141:Infinity": 79, + "f:131:23:131:24": 14, + "s:132:28:132:Infinity": 80, + "s:134:8:134:Infinity": 81, + "f:134:17:134:18": 15, + "s:134:32:134:92": 82, + "f:134:46:134:47": 16, + "s:134:54:134:91": 83, + "b:134:75:134:89:134:89:134:91": 20, + "s:136:8:140:Infinity": 84, + "f:140:17:140:18": 17, + "s:140:26:140:67": 85, + "s:143:23:153:Infinity": 86, + "f:143:23:143:24": 18, + "s:144:28:144:Infinity": 87, + "s:146:8:146:Infinity": 88, + "f:146:17:146:18": 19, + "s:146:32:146:92": 89, + "f:146:46:146:47": 20, + "s:146:54:146:91": 90, + "b:146:75:146:89:146:89:146:91": 21, + "s:148:8:152:Infinity": 91, + "f:152:17:152:18": 21, + "s:152:26:152:69": 92, + "s:155:4:191:Infinity": 93, + "f:155:14:155:20": 22, + "s:156:25:180:Infinity": 94, + "f:157:12:157:13": 23, + "s:158:16:177:Infinity": 95, + "f:158:32:158:33": 24, + "b:160:20:165:Infinity:undefined:undefined:undefined:undefined": 22, + "s:160:20:165:Infinity": 96, + "b:161:24:163:Infinity:undefined:undefined:undefined:undefined": 23, + "s:161:24:163:Infinity": 97, + "b:161:28:161:52:161:52:161:68:161:68:161:79:161:79:161:97": 24, + "s:162:28:162:Infinity": 98, + "s:164:24:164:Infinity": 99, + "b:168:20:176:Infinity:undefined:undefined:undefined:undefined": 25, + "s:168:20:176:Infinity": 100, + "b:168:24:168:49:168:49:168:83": 26, + "s:169:38:169:Infinity": 101, + "b:170:24:175:Infinity:undefined:undefined:undefined:undefined": 27, + "s:170:24:175:Infinity": 102, + "b:170:28:170:45:170:45:170:59:170:59:170:81": 28, + "s:171:41:171:Infinity": 103, + "b:172:28:174:Infinity:undefined:undefined:undefined:undefined": 29, + "s:172:28:174:Infinity": 104, + "s:173:32:173:Infinity": 105, + "s:182:8:185:Infinity": 106, + "f:182:22:182:23": 25, + "s:183:23:183:Infinity": 107, + "b:184:12:184:Infinity:undefined:undefined:undefined:undefined": 30, + "s:184:12:184:Infinity": 108, + "s:184:20:184:Infinity": 109, + "s:187:25:187:Infinity": 110, + "b:188:8:188:Infinity:undefined:undefined:undefined:undefined": 31, + "s:188:8:188:Infinity": 111, + "s:188:22:188:Infinity": 112, + "s:190:8:190:Infinity": 113, + "f:190:15:190:21": 26, + "s:190:21:190:Infinity": 114, + "b:193:4:193:Infinity:undefined:undefined:undefined:undefined": 32, + "s:193:4:193:Infinity": 115, + "s:193:17:193:Infinity": 116, + "b:194:4:194:Infinity:undefined:undefined:undefined:undefined": 33, + "s:194:4:194:Infinity": 117, + "s:194:15:194:Infinity": 118, + "s:197:4:221:Infinity": 119, + "b:200:16:200:Infinity:202:16:219:Infinity": 34, + "f:203:31:203:32": 27, + "s:204:24:212:Infinity": 120, + "b:208:65:208:93:208:93:208:Infinity": 35, + "f:209:37:209:43": 28, + "s:209:43:209:Infinity": 121, + "b:214:21:214:Infinity:215:24:217:Infinity": 36, + "b:216:43:216:63:216:63:216:Infinity": 37 + } + } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "meta": { "lastBranch": 0, "lastFunction": 0, "lastStatement": 0, "seen": {} } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx", + "statementMap": { + "0": { "start": { "line": 7, "column": 26 }, "end": { "line": 7, "column": null } }, + "1": { "start": { "line": 8, "column": 24 }, "end": { "line": 8, "column": null } }, + "2": { "start": { "line": 9, "column": 30 }, "end": { "line": 9, "column": null } }, + "3": { "start": { "line": 10, "column": 26 }, "end": { "line": 10, "column": null } }, + "4": { "start": { "line": 12, "column": 4 }, "end": { "line": 32, "column": null } }, + "5": { "start": { "line": 13, "column": 8 }, "end": { "line": 31, "column": null } }, + "6": { "start": { "line": 15, "column": 16 }, "end": { "line": 15, "column": null } }, + "7": { "start": { "line": 15, "column": 29 }, "end": { "line": 15, "column": null } }, + "8": { "start": { "line": 16, "column": 16 }, "end": { "line": 16, "column": null } }, + "9": { "start": { "line": 19, "column": 16 }, "end": { "line": 19, "column": null } }, + "10": { "start": { "line": 19, "column": 29 }, "end": { "line": 19, "column": null } }, + "11": { "start": { "line": 20, "column": 16 }, "end": { "line": 20, "column": null } }, + "12": { "start": { "line": 24, "column": 16 }, "end": { "line": 24, "column": null } }, + "13": { "start": { "line": 25, "column": 16 }, "end": { "line": 25, "column": null } }, + "14": { "start": { "line": 26, "column": 16 }, "end": { "line": 26, "column": null } }, + "15": { "start": { "line": 29, "column": 16 }, "end": { "line": 29, "column": null } }, + "16": { "start": { "line": 30, "column": 16 }, "end": { "line": 30, "column": null } }, + "17": { "start": { "line": 34, "column": 4 }, "end": { "line": 34, "column": null } }, + "18": { "start": { "line": 34, "column": 17 }, "end": { "line": 34, "column": null } }, + "19": { "start": { "line": 35, "column": 4 }, "end": { "line": 35, "column": null } }, + "20": { "start": { "line": 35, "column": 15 }, "end": { "line": 35, "column": null } }, + "21": { "start": { "line": 37, "column": 4 }, "end": { "line": 78, "column": null } }, + "22": { "start": { "line": 53, "column": 28 }, "end": { "line": 58, "column": null } }, + "23": { "start": { "line": 69, "column": 28 }, "end": { "line": 73, "column": null } } + }, + "fnMap": { + "0": { + "name": "FeedList", + "decl": { "start": { "line": 6, "column": 24 }, "end": { "line": 6, "column": 35 } }, + "loc": { "start": { "line": 6, "column": 35 }, "end": { "line": 80, "column": null } }, + "line": 6 + }, + "1": { + "name": "(anonymous_1)", + "decl": { "start": { "line": 12, "column": 14 }, "end": { "line": 12, "column": 20 } }, + "loc": { "start": { "line": 12, "column": 20 }, "end": { "line": 32, "column": 7 } }, + "line": 12 + }, + "2": { + "name": "(anonymous_2)", + "decl": { "start": { "line": 14, "column": 37 }, "end": { "line": 14, "column": 44 } }, + "loc": { "start": { "line": 14, "column": 44 }, "end": { "line": 17, "column": 13 } }, + "line": 14 + }, + "3": { + "name": "(anonymous_3)", + "decl": { "start": { "line": 18, "column": 35 }, "end": { "line": 18, "column": 42 } }, + "loc": { "start": { "line": 18, "column": 42 }, "end": { "line": 21, "column": 13 } }, + "line": 18 + }, + "4": { + "name": "(anonymous_4)", + "decl": { "start": { "line": 23, "column": 18 }, "end": { "line": 23, "column": 19 } }, + "loc": { "start": { "line": 23, "column": 45 }, "end": { "line": 27, "column": 13 } }, + "line": 23 + }, + "5": { + "name": "(anonymous_5)", + "decl": { "start": { "line": 28, "column": 19 }, "end": { "line": 28, "column": 20 } }, + "loc": { "start": { "line": 28, "column": 28 }, "end": { "line": 31, "column": 13 } }, + "line": 28 + }, + "6": { + "name": "(anonymous_6)", + "decl": { "start": { "line": 52, "column": 35 }, "end": { "line": 52, "column": 36 } }, + "loc": { "start": { "line": 53, "column": 28 }, "end": { "line": 58, "column": null } }, + "line": 53 + }, + "7": { + "name": "(anonymous_7)", + "decl": { "start": { "line": 68, "column": 34 }, "end": { "line": 68, "column": 35 } }, + "loc": { "start": { "line": 69, "column": 28 }, "end": { "line": 73, "column": null } }, + "line": 69 + } + }, + "branchMap": { + "0": { + "loc": { "start": { "line": 15, "column": 16 }, "end": { "line": 15, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 15, "column": 16 }, "end": { "line": 15, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 15 + }, + "1": { + "loc": { "start": { "line": 19, "column": 16 }, "end": { "line": 19, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 19, "column": 16 }, "end": { "line": 19, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 19 + }, + "2": { + "loc": { "start": { "line": 34, "column": 4 }, "end": { "line": 34, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 34, "column": 4 }, "end": { "line": 34, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 34 + }, + "3": { + "loc": { "start": { "line": 35, "column": 4 }, "end": { "line": 35, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 35, "column": 4 }, "end": { "line": 35, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 35 + }, + "4": { + "loc": { "start": { "line": 48, "column": 17 }, "end": { "line": 60, "column": null } }, + "type": "cond-expr", + "locations": [ + { "start": { "line": 49, "column": 20 }, "end": { "line": 49, "column": null } }, + { "start": { "line": 51, "column": 20 }, "end": { "line": 60, "column": null } } + ], + "line": 48 + }, + "5": { + "loc": { "start": { "line": 55, "column": 37 }, "end": { "line": 55, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 55, "column": 37 }, "end": { "line": 55, "column": 51 } }, + { "start": { "line": 55, "column": 51 }, "end": { "line": 55, "column": null } } + ], + "line": 55 + }, + "6": { + "loc": { "start": { "line": 57, "column": 33 }, "end": { "line": 57, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 57, "column": 33 }, "end": { "line": 57, "column": 50 } }, + { "start": { "line": 57, "column": 50 }, "end": { "line": 57, "column": null } } + ], + "line": 57 + }, + "7": { + "loc": { "start": { "line": 64, "column": 13 }, "end": { "line": 76, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 64, "column": 13 }, "end": { "line": 64, "column": 21 } }, + { "start": { "line": 64, "column": 21 }, "end": { "line": 64, "column": null } }, + { "start": { "line": 65, "column": 16 }, "end": { "line": 76, "column": null } } + ], + "line": 64 + } + }, + "s": { + "0": 11, + "1": 11, + "2": 11, + "3": 11, + "4": 11, + "5": 6, + "6": 4, + "7": 0, + "8": 4, + "9": 4, + "10": 0, + "11": 4, + "12": 4, + "13": 4, + "14": 4, + "15": 1, + "16": 1, + "17": 11, + "18": 6, + "19": 5, + "20": 5, + "21": 4, + "22": 3, + "23": 3 + }, + "f": { "0": 11, "1": 6, "2": 4, "3": 4, "4": 4, "5": 1, "6": 3, "7": 3 }, + "b": { + "0": [0, 4], + "1": [0, 4], + "2": [6, 5], + "3": [1, 4], + "4": [2, 2], + "5": [3, 0], + "6": [3, 3], + "7": [11, 4, 2] + }, + "meta": { + "lastBranch": 8, + "lastFunction": 8, + "lastStatement": 24, + "seen": { + "f:6:24:6:35": 0, + "s:7:26:7:Infinity": 0, + "s:8:24:8:Infinity": 1, + "s:9:30:9:Infinity": 2, + "s:10:26:10:Infinity": 3, + "s:12:4:32:Infinity": 4, + "f:12:14:12:20": 1, + "s:13:8:31:Infinity": 5, + "f:14:37:14:44": 2, + "b:15:16:15:Infinity:undefined:undefined:undefined:undefined": 0, + "s:15:16:15:Infinity": 6, + "s:15:29:15:Infinity": 7, + "s:16:16:16:Infinity": 8, + "f:18:35:18:42": 3, + "b:19:16:19:Infinity:undefined:undefined:undefined:undefined": 1, + "s:19:16:19:Infinity": 9, + "s:19:29:19:Infinity": 10, + "s:20:16:20:Infinity": 11, + "f:23:18:23:19": 4, + "s:24:16:24:Infinity": 12, + "s:25:16:25:Infinity": 13, + "s:26:16:26:Infinity": 14, + "f:28:19:28:20": 5, + "s:29:16:29:Infinity": 15, + "s:30:16:30:Infinity": 16, + "b:34:4:34:Infinity:undefined:undefined:undefined:undefined": 2, + "s:34:4:34:Infinity": 17, + "s:34:17:34:Infinity": 18, + "b:35:4:35:Infinity:undefined:undefined:undefined:undefined": 3, + "s:35:4:35:Infinity": 19, + "s:35:15:35:Infinity": 20, + "s:37:4:78:Infinity": 21, + "b:49:20:49:Infinity:51:20:60:Infinity": 4, + "f:52:35:52:36": 6, + "s:53:28:58:Infinity": 22, + "b:55:37:55:51:55:51:55:Infinity": 5, + "b:57:33:57:50:57:50:57:Infinity": 6, + "b:64:13:64:21:64:21:64:Infinity:65:16:76:Infinity": 7, + "f:68:34:68:35": 7, + "s:69:28:73:Infinity": 23 + } + } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "meta": { "lastBranch": 0, "lastFunction": 0, "lastStatement": 0, "seen": {} } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx", + "statementMap": { + "0": { "start": { "line": 6, "column": 32 }, "end": { "line": 6, "column": null } }, + "1": { "start": { "line": 7, "column": 26 }, "end": { "line": 7, "column": null } }, + "2": { "start": { "line": 8, "column": 10 }, "end": { "line": 8, "column": null } }, + "3": { "start": { "line": 10, "column": 25 }, "end": { "line": 33, "column": null } }, + "4": { "start": { "line": 11, "column": 8 }, "end": { "line": 11, "column": null } }, + "5": { "start": { "line": 12, "column": 8 }, "end": { "line": 12, "column": null } }, + "6": { "start": { "line": 14, "column": 8 }, "end": { "line": 32, "column": null } }, + "7": { "start": { "line": 16, "column": 27 }, "end": { "line": 16, "column": null } }, + "8": { "start": { "line": 17, "column": 12 }, "end": { "line": 17, "column": null } }, + "9": { "start": { "line": 19, "column": 24 }, "end": { "line": 22, "column": null } }, + "10": { "start": { "line": 24, "column": 12 }, "end": { "line": 29, "column": null } }, + "11": { "start": { "line": 25, "column": 16 }, "end": { "line": 25, "column": null } }, + "12": { "start": { "line": 27, "column": 29 }, "end": { "line": 27, "column": null } }, + "13": { "start": { "line": 28, "column": 16 }, "end": { "line": 28, "column": null } }, + "14": { "start": { "line": 31, "column": 12 }, "end": { "line": 31, "column": null } }, + "15": { "start": { "line": 35, "column": 4 }, "end": { "line": 52, "column": null } }, + "16": { "start": { "line": 45, "column": 41 }, "end": { "line": 45, "column": null } } + }, + "fnMap": { + "0": { + "name": "Login", + "decl": { "start": { "line": 5, "column": 24 }, "end": { "line": 5, "column": 32 } }, + "loc": { "start": { "line": 5, "column": 32 }, "end": { "line": 54, "column": null } }, + "line": 5 + }, + "1": { + "name": "(anonymous_1)", + "decl": { "start": { "line": 10, "column": 25 }, "end": { "line": 10, "column": 32 } }, + "loc": { "start": { "line": 10, "column": 49 }, "end": { "line": 33, "column": null } }, + "line": 10 + }, + "2": { + "name": "(anonymous_2)", + "decl": { "start": { "line": 45, "column": 34 }, "end": { "line": 45, "column": 35 } }, + "loc": { "start": { "line": 45, "column": 41 }, "end": { "line": 45, "column": null } }, + "line": 45 + } + }, + "branchMap": { + "0": { + "loc": { "start": { "line": 24, "column": 12 }, "end": { "line": 29, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 24, "column": 12 }, "end": { "line": 29, "column": null } }, + { "start": { "line": 26, "column": 19 }, "end": { "line": 29, "column": null } } + ], + "line": 24 + }, + "1": { + "loc": { "start": { "line": 28, "column": 25 }, "end": { "line": 28, "column": 55 } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 28, "column": 25 }, "end": { "line": 28, "column": 41 } }, + { "start": { "line": 28, "column": 41 }, "end": { "line": 28, "column": 55 } } + ], + "line": 28 + }, + "2": { + "loc": { "start": { "line": 49, "column": 17 }, "end": { "line": 49, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 49, "column": 17 }, "end": { "line": 49, "column": 26 } }, + { "start": { "line": 49, "column": 26 }, "end": { "line": 49, "column": null } } + ], + "line": 49 + } + }, + "s": { + "0": 14, + "1": 14, + "2": 14, + "3": 14, + "4": 3, + "5": 3, + "6": 3, + "7": 3, + "8": 3, + "9": 3, + "10": 2, + "11": 1, + "12": 1, + "13": 1, + "14": 1, + "15": 14, + "16": 3 + }, + "f": { "0": 14, "1": 3, "2": 3 }, + "b": { "0": [1, 1], "1": [1, 0], "2": [14, 2] }, + "meta": { + "lastBranch": 3, + "lastFunction": 3, + "lastStatement": 17, + "seen": { + "f:5:24:5:32": 0, + "s:6:32:6:Infinity": 0, + "s:7:26:7:Infinity": 1, + "s:8:10:8:Infinity": 2, + "s:10:25:33:Infinity": 3, + "f:10:25:10:32": 1, + "s:11:8:11:Infinity": 4, + "s:12:8:12:Infinity": 5, + "s:14:8:32:Infinity": 6, + "s:16:27:16:Infinity": 7, + "s:17:12:17:Infinity": 8, + "s:19:24:22:Infinity": 9, + "b:24:12:29:Infinity:26:19:29:Infinity": 0, + "s:24:12:29:Infinity": 10, + "s:25:16:25:Infinity": 11, + "s:27:29:27:Infinity": 12, + "s:28:16:28:Infinity": 13, + "b:28:25:28:41:28:41:28:55": 1, + "s:31:12:31:Infinity": 14, + "s:35:4:52:Infinity": 15, + "f:45:34:45:35": 2, + "s:45:41:45:Infinity": 16, + "b:49:17:49:26:49:26:49:Infinity": 2 + } + } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.css": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.css", + "statementMap": {}, + "fnMap": {}, + "branchMap": {}, + "s": {}, + "f": {}, + "b": {}, + "meta": { "lastBranch": 0, "lastFunction": 0, "lastStatement": 0, "seen": {} } + }, + "/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.tsx": { + "path": "/Users/adam/workspace/vibecode/neko/frontend/src/components/Settings.tsx", + "statementMap": { + "0": { "start": { "line": 6, "column": 26 }, "end": { "line": 6, "column": null } }, + "1": { "start": { "line": 7, "column": 36 }, "end": { "line": 7, "column": null } }, + "2": { "start": { "line": 8, "column": 30 }, "end": { "line": 8, "column": null } }, + "3": { "start": { "line": 9, "column": 26 }, "end": { "line": 9, "column": null } }, + "4": { "start": { "line": 11, "column": 4 }, "end": { "line": 13, "column": null } }, + "5": { "start": { "line": 12, "column": 8 }, "end": { "line": 12, "column": null } }, + "6": { "start": { "line": 15, "column": 23 }, "end": { "line": 30, "column": null } }, + "7": { "start": { "line": 16, "column": 8 }, "end": { "line": 16, "column": null } }, + "8": { "start": { "line": 17, "column": 8 }, "end": { "line": 29, "column": null } }, + "9": { "start": { "line": 19, "column": 16 }, "end": { "line": 19, "column": null } }, + "10": { "start": { "line": 19, "column": 29 }, "end": { "line": 19, "column": null } }, + "11": { "start": { "line": 20, "column": 16 }, "end": { "line": 20, "column": null } }, + "12": { "start": { "line": 23, "column": 16 }, "end": { "line": 23, "column": null } }, + "13": { "start": { "line": 24, "column": 16 }, "end": { "line": 24, "column": null } }, + "14": { "start": { "line": 27, "column": 16 }, "end": { "line": 27, "column": null } }, + "15": { "start": { "line": 28, "column": 16 }, "end": { "line": 28, "column": null } }, + "16": { "start": { "line": 32, "column": 26 }, "end": { "line": 54, "column": null } }, + "17": { "start": { "line": 33, "column": 8 }, "end": { "line": 33, "column": null } }, + "18": { "start": { "line": 34, "column": 8 }, "end": { "line": 34, "column": null } }, + "19": { "start": { "line": 34, "column": 25 }, "end": { "line": 34, "column": null } }, + "20": { "start": { "line": 36, "column": 8 }, "end": { "line": 36, "column": null } }, + "21": { "start": { "line": 37, "column": 8 }, "end": { "line": 53, "column": null } }, + "22": { "start": { "line": 43, "column": 16 }, "end": { "line": 43, "column": null } }, + "23": { "start": { "line": 43, "column": 29 }, "end": { "line": 43, "column": null } }, + "24": { "start": { "line": 44, "column": 16 }, "end": { "line": 44, "column": null } }, + "25": { "start": { "line": 47, "column": 16 }, "end": { "line": 47, "column": null } }, + "26": { "start": { "line": 48, "column": 16 }, "end": { "line": 48, "column": null } }, + "27": { "start": { "line": 51, "column": 16 }, "end": { "line": 51, "column": null } }, + "28": { "start": { "line": 52, "column": 16 }, "end": { "line": 52, "column": null } }, + "29": { "start": { "line": 56, "column": 29 }, "end": { "line": 72, "column": null } }, + "30": { "start": { "line": 57, "column": 8 }, "end": { "line": 57, "column": null } }, + "31": { "start": { "line": 57, "column": 79 }, "end": { "line": 57, "column": null } }, + "32": { "start": { "line": 59, "column": 8 }, "end": { "line": 59, "column": null } }, + "33": { "start": { "line": 60, "column": 8 }, "end": { "line": 71, "column": null } }, + "34": { "start": { "line": 64, "column": 16 }, "end": { "line": 64, "column": null } }, + "35": { "start": { "line": 64, "column": 29 }, "end": { "line": 64, "column": null } }, + "36": { "start": { "line": 65, "column": 16 }, "end": { "line": 65, "column": null } }, + "37": { "start": { "line": 65, "column": 45 }, "end": { "line": 65, "column": 57 } }, + "38": { "start": { "line": 66, "column": 16 }, "end": { "line": 66, "column": null } }, + "39": { "start": { "line": 69, "column": 16 }, "end": { "line": 69, "column": null } }, + "40": { "start": { "line": 70, "column": 16 }, "end": { "line": 70, "column": null } }, + "41": { "start": { "line": 74, "column": 4 }, "end": { "line": 119, "column": null } }, + "42": { "start": { "line": 84, "column": 41 }, "end": { "line": 84, "column": null } }, + "43": { "start": { "line": 102, "column": 24 }, "end": { "line": 115, "column": null } }, + "44": { "start": { "line": 108, "column": 47 }, "end": { "line": 108, "column": null } } + }, + "fnMap": { + "0": { + "name": "Settings", + "decl": { "start": { "line": 5, "column": 24 }, "end": { "line": 5, "column": 35 } }, + "loc": { "start": { "line": 5, "column": 35 }, "end": { "line": 121, "column": null } }, + "line": 5 + }, + "1": { + "name": "(anonymous_1)", + "decl": { "start": { "line": 11, "column": 14 }, "end": { "line": 11, "column": 20 } }, + "loc": { "start": { "line": 11, "column": 20 }, "end": { "line": 13, "column": 7 } }, + "line": 11 + }, + "2": { + "name": "(anonymous_2)", + "decl": { "start": { "line": 15, "column": 23 }, "end": { "line": 15, "column": 29 } }, + "loc": { "start": { "line": 15, "column": 29 }, "end": { "line": 30, "column": null } }, + "line": 15 + }, + "3": { + "name": "(anonymous_3)", + "decl": { "start": { "line": 18, "column": 18 }, "end": { "line": 18, "column": 19 } }, + "loc": { "start": { "line": 18, "column": 27 }, "end": { "line": 21, "column": 13 } }, + "line": 18 + }, + "4": { + "name": "(anonymous_4)", + "decl": { "start": { "line": 22, "column": 18 }, "end": { "line": 22, "column": 19 } }, + "loc": { "start": { "line": 22, "column": 28 }, "end": { "line": 25, "column": 13 } }, + "line": 22 + }, + "5": { + "name": "(anonymous_5)", + "decl": { "start": { "line": 26, "column": 19 }, "end": { "line": 26, "column": 20 } }, + "loc": { "start": { "line": 26, "column": 28 }, "end": { "line": 29, "column": 13 } }, + "line": 26 + }, + "6": { + "name": "(anonymous_6)", + "decl": { "start": { "line": 32, "column": 26 }, "end": { "line": 32, "column": 27 } }, + "loc": { "start": { "line": 32, "column": 50 }, "end": { "line": 54, "column": null } }, + "line": 32 + }, + "7": { + "name": "(anonymous_7)", + "decl": { "start": { "line": 42, "column": 18 }, "end": { "line": 42, "column": 19 } }, + "loc": { "start": { "line": 42, "column": 27 }, "end": { "line": 45, "column": 13 } }, + "line": 42 + }, + "8": { + "name": "(anonymous_8)", + "decl": { "start": { "line": 46, "column": 18 }, "end": { "line": 46, "column": 24 } }, + "loc": { "start": { "line": 46, "column": 24 }, "end": { "line": 49, "column": 13 } }, + "line": 46 + }, + "9": { + "name": "(anonymous_9)", + "decl": { "start": { "line": 50, "column": 19 }, "end": { "line": 50, "column": 20 } }, + "loc": { "start": { "line": 50, "column": 28 }, "end": { "line": 53, "column": 13 } }, + "line": 50 + }, + "10": { + "name": "(anonymous_10)", + "decl": { "start": { "line": 56, "column": 29 }, "end": { "line": 56, "column": 30 } }, + "loc": { "start": { "line": 56, "column": 45 }, "end": { "line": 72, "column": null } }, + "line": 56 + }, + "11": { + "name": "(anonymous_11)", + "decl": { "start": { "line": 63, "column": 18 }, "end": { "line": 63, "column": 19 } }, + "loc": { "start": { "line": 63, "column": 27 }, "end": { "line": 67, "column": 13 } }, + "line": 63 + }, + "12": { + "name": "(anonymous_12)", + "decl": { "start": { "line": 65, "column": 38 }, "end": { "line": 65, "column": 39 } }, + "loc": { "start": { "line": 65, "column": 45 }, "end": { "line": 65, "column": 57 } }, + "line": 65 + }, + "13": { + "name": "(anonymous_13)", + "decl": { "start": { "line": 68, "column": 19 }, "end": { "line": 68, "column": 20 } }, + "loc": { "start": { "line": 68, "column": 28 }, "end": { "line": 71, "column": 13 } }, + "line": 68 + }, + "14": { + "name": "(anonymous_14)", + "decl": { "start": { "line": 84, "column": 34 }, "end": { "line": 84, "column": 35 } }, + "loc": { "start": { "line": 84, "column": 41 }, "end": { "line": 84, "column": null } }, + "line": 84 + }, + "15": { + "name": "(anonymous_15)", + "decl": { "start": { "line": 101, "column": 31 }, "end": { "line": 101, "column": 32 } }, + "loc": { "start": { "line": 102, "column": 24 }, "end": { "line": 115, "column": null } }, + "line": 102 + }, + "16": { + "name": "(anonymous_16)", + "decl": { "start": { "line": 108, "column": 41 }, "end": { "line": 108, "column": 47 } }, + "loc": { "start": { "line": 108, "column": 47 }, "end": { "line": 108, "column": null } }, + "line": 108 + } + }, + "branchMap": { + "0": { + "loc": { "start": { "line": 19, "column": 16 }, "end": { "line": 19, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 19, "column": 16 }, "end": { "line": 19, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 19 + }, + "1": { + "loc": { "start": { "line": 34, "column": 8 }, "end": { "line": 34, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 34, "column": 8 }, "end": { "line": 34, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 34 + }, + "2": { + "loc": { "start": { "line": 43, "column": 16 }, "end": { "line": 43, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 43, "column": 16 }, "end": { "line": 43, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 43 + }, + "3": { + "loc": { "start": { "line": 57, "column": 8 }, "end": { "line": 57, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 57, "column": 8 }, "end": { "line": 57, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 57 + }, + "4": { + "loc": { "start": { "line": 64, "column": 16 }, "end": { "line": 64, "column": null } }, + "type": "if", + "locations": [ + { "start": { "line": 64, "column": 16 }, "end": { "line": 64, "column": null } }, + { "start": {}, "end": {} } + ], + "line": 64 + }, + "5": { + "loc": { "start": { "line": 94, "column": 17 }, "end": { "line": 94, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 94, "column": 17 }, "end": { "line": 94, "column": 26 } }, + { "start": { "line": 94, "column": 26 }, "end": { "line": 94, "column": null } } + ], + "line": 94 + }, + "6": { + "loc": { "start": { "line": 99, "column": 17 }, "end": { "line": 99, "column": null } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 99, "column": 17 }, "end": { "line": 99, "column": 28 } }, + { "start": { "line": 99, "column": 28 }, "end": { "line": 99, "column": null } } + ], + "line": 99 + }, + "7": { + "loc": { "start": { "line": 104, "column": 62 }, "end": { "line": 104, "column": 89 } }, + "type": "binary-expr", + "locations": [ + { "start": { "line": 104, "column": 62 }, "end": { "line": 104, "column": 76 } }, + { "start": { "line": 104, "column": 76 }, "end": { "line": 104, "column": 89 } } + ], + "line": 104 + } + }, + "s": { + "0": 14, + "1": 14, + "2": 14, + "3": 14, + "4": 14, + "5": 3, + "6": 14, + "7": 4, + "8": 4, + "9": 4, + "10": 0, + "11": 4, + "12": 4, + "13": 4, + "14": 0, + "15": 0, + "16": 14, + "17": 1, + "18": 1, + "19": 0, + "20": 1, + "21": 1, + "22": 1, + "23": 0, + "24": 1, + "25": 1, + "26": 1, + "27": 0, + "28": 0, + "29": 14, + "30": 1, + "31": 0, + "32": 1, + "33": 1, + "34": 1, + "35": 0, + "36": 1, + "37": 1, + "38": 1, + "39": 0, + "40": 0, + "41": 14, + "42": 1, + "43": 5, + "44": 1 + }, + "f": { + "0": 14, + "1": 3, + "2": 4, + "3": 4, + "4": 4, + "5": 0, + "6": 1, + "7": 1, + "8": 1, + "9": 0, + "10": 1, + "11": 1, + "12": 1, + "13": 0, + "14": 1, + "15": 5, + "16": 1 + }, + "b": { + "0": [0, 4], + "1": [0, 1], + "2": [0, 1], + "3": [0, 1], + "4": [0, 1], + "5": [14, 0], + "6": [14, 5], + "7": [5, 0] + }, + "meta": { + "lastBranch": 8, + "lastFunction": 17, + "lastStatement": 45, + "seen": { + "f:5:24:5:35": 0, + "s:6:26:6:Infinity": 0, + "s:7:36:7:Infinity": 1, + "s:8:30:8:Infinity": 2, + "s:9:26:9:Infinity": 3, + "s:11:4:13:Infinity": 4, + "f:11:14:11:20": 1, + "s:12:8:12:Infinity": 5, + "s:15:23:30:Infinity": 6, + "f:15:23:15:29": 2, + "s:16:8:16:Infinity": 7, + "s:17:8:29:Infinity": 8, + "f:18:18:18:19": 3, + "b:19:16:19:Infinity:undefined:undefined:undefined:undefined": 0, + "s:19:16:19:Infinity": 9, + "s:19:29:19:Infinity": 10, + "s:20:16:20:Infinity": 11, + "f:22:18:22:19": 4, + "s:23:16:23:Infinity": 12, + "s:24:16:24:Infinity": 13, + "f:26:19:26:20": 5, + "s:27:16:27:Infinity": 14, + "s:28:16:28:Infinity": 15, + "s:32:26:54:Infinity": 16, + "f:32:26:32:27": 6, + "s:33:8:33:Infinity": 17, + "b:34:8:34:Infinity:undefined:undefined:undefined:undefined": 1, + "s:34:8:34:Infinity": 18, + "s:34:25:34:Infinity": 19, + "s:36:8:36:Infinity": 20, + "s:37:8:53:Infinity": 21, + "f:42:18:42:19": 7, + "b:43:16:43:Infinity:undefined:undefined:undefined:undefined": 2, + "s:43:16:43:Infinity": 22, + "s:43:29:43:Infinity": 23, + "s:44:16:44:Infinity": 24, + "f:46:18:46:24": 8, + "s:47:16:47:Infinity": 25, + "s:48:16:48:Infinity": 26, + "f:50:19:50:20": 9, + "s:51:16:51:Infinity": 27, + "s:52:16:52:Infinity": 28, + "s:56:29:72:Infinity": 29, + "f:56:29:56:30": 10, + "b:57:8:57:Infinity:undefined:undefined:undefined:undefined": 3, + "s:57:8:57:Infinity": 30, + "s:57:79:57:Infinity": 31, + "s:59:8:59:Infinity": 32, + "s:60:8:71:Infinity": 33, + "f:63:18:63:19": 11, + "b:64:16:64:Infinity:undefined:undefined:undefined:undefined": 4, + "s:64:16:64:Infinity": 34, + "s:64:29:64:Infinity": 35, + "s:65:16:65:Infinity": 36, + "f:65:38:65:39": 12, + "s:65:45:65:57": 37, + "s:66:16:66:Infinity": 38, + "f:68:19:68:20": 13, + "s:69:16:69:Infinity": 39, + "s:70:16:70:Infinity": 40, + "s:74:4:119:Infinity": 41, + "f:84:34:84:35": 14, + "s:84:41:84:Infinity": 42, + "b:94:17:94:26:94:26:94:Infinity": 5, + "b:99:17:99:28:99:28:99:Infinity": 6, + "f:101:31:101:32": 15, + "s:102:24:115:Infinity": 43, + "b:104:62:104:76:104:76:104:89": 7, + "f:108:41:108:47": 16, + "s:108:47:108:Infinity": 44 + } + } + } } diff --git a/frontend/coverage/index.html b/frontend/coverage/index.html index 450975d..bd1150d 100644 --- a/frontend/coverage/index.html +++ b/frontend/coverage/index.html @@ -1,131 +1,141 @@ - - - + Code coverage report for All files - - - - -
-
+ + + +
+

All files

-
- -
- 86.17% - Statements - 212/246 -
- - -
- 76.76% - Branches - 109/142 -
- - -
- 86.3% - Functions - 63/73 -
- - -
- 88.39% - Lines - 198/224 -
- - +
+
+ 86.17% + Statements + 212/246 +
+ +
+ 76.76% + Branches + 109/142 +
+ +
+ 86.3% + Functions + 63/73 +
+ +
+ 88.39% + Lines + 198/224 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+
+
FileStatementsBranchesFunctionsLines
src -
-
78.94%15/1966.66%4/677.77%7/978.94%15/19
src/components -
-
86.78%197/22777.2%105/13687.5%56/6489.26%183/205
+ + + + + + + + + + + + + + + + + + + + + + + + + + + - -
File + Statements + BranchesFunctionsLines
src +
+
+
+
+
78.94%15/1966.66%4/677.77%7/978.94%15/19
-
-
-
- - - - - - + + + src/components + + +
+
+
+
+ + 86.78% + 197/227 + 77.2% + 105/136 + 87.5% + 56/64 + 89.26% + 183/205 + + + +
+
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/prettify.css b/frontend/coverage/prettify.css index b317a7c..006492c 100644 --- a/frontend/coverage/prettify.css +++ b/frontend/coverage/prettify.css @@ -1 +1,101 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} +.pln { + color: #000; +} +@media screen { + .str { + color: #080; + } + .kwd { + color: #008; + } + .com { + color: #800; + } + .typ { + color: #606; + } + .lit { + color: #066; + } + .pun, + .opn, + .clo { + color: #660; + } + .tag { + color: #008; + } + .atn { + color: #606; + } + .atv { + color: #080; + } + .dec, + .var { + color: #606; + } + .fun { + color: red; + } +} +@media print, projection { + .str { + color: #060; + } + .kwd { + color: #006; + font-weight: bold; + } + .com { + color: #600; + font-style: italic; + } + .typ { + color: #404; + font-weight: bold; + } + .lit { + color: #044; + } + .pun, + .opn, + .clo { + color: #440; + } + .tag { + color: #006; + font-weight: bold; + } + .atn { + color: #404; + } + .atv { + color: #060; + } +} +pre.prettyprint { + padding: 2px; + border: 1px solid #888; +} +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} +li.L0, +li.L1, +li.L2, +li.L3, +li.L5, +li.L6, +li.L7, +li.L8 { + list-style-type: none; +} +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + background: #eee; +} diff --git a/frontend/coverage/prettify.js b/frontend/coverage/prettify.js index b322523..a318211 100644 --- a/frontend/coverage/prettify.js +++ b/frontend/coverage/prettify.js @@ -1,2 +1,937 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); + +window.PR_SHOULD_USE_CONTINUATION = true; +(function () { + var h = ['break,continue,do,else,for,if,return,while']; + var u = [ + h, + 'auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile', + ]; + var p = [ + u, + 'catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof', + ]; + var l = [ + p, + 'alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where', + ]; + var x = [ + p, + 'abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient', + ]; + var R = [ + x, + 'as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var', + ]; + var r = + 'all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes'; + var w = [p, 'debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN']; + var s = + 'caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END'; + var I = [ + h, + 'and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None', + ]; + var f = [ + h, + 'alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END', + ]; + var H = [h, 'case,done,elif,esac,eval,fi,function,in,local,set,then,until']; + var A = [l, R, w, s + I, f, H]; + var e = + /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/; + var C = 'str'; + var z = 'kwd'; + var j = 'com'; + var O = 'typ'; + var G = 'lit'; + var L = 'pun'; + var F = 'pln'; + var m = 'tag'; + var E = 'dec'; + var J = 'src'; + var P = 'atn'; + var n = 'atv'; + var N = 'nocode'; + var M = + '(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*'; + function k(Z) { + var ad = 0; + var S = false; + var ac = false; + for (var V = 0, U = Z.length; V < U; ++V) { + var ae = Z[V]; + if (ae.ignoreCase) { + ac = true; + } else { + if (/[a-z]/i.test(ae.source.replace(/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) { + S = true; + ac = false; + break; + } + } + } + var Y = { b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 }; + function ab(ah) { + var ag = ah.charCodeAt(0); + if (ag !== 92) { + return ag; + } + var af = ah.charAt(1); + ag = Y[af]; + if (ag) { + return ag; + } else { + if ('0' <= af && af <= '7') { + return parseInt(ah.substring(1), 8); + } else { + if (af === 'u' || af === 'x') { + return parseInt(ah.substring(2), 16); + } else { + return ah.charCodeAt(1); + } + } + } + } + function T(af) { + if (af < 32) { + return (af < 16 ? '\\x0' : '\\x') + af.toString(16); + } + var ag = String.fromCharCode(af); + if (ag === '\\' || ag === '-' || ag === '[' || ag === ']') { + ag = '\\' + ag; + } + return ag; + } + function X(am) { + var aq = am + .substring(1, am.length - 1) + .match( + new RegExp( + '\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]', + 'g' + ) + ); + var ak = []; + var af = []; + var ao = aq[0] === '^'; + for (var ar = ao ? 1 : 0, aj = aq.length; ar < aj; ++ar) { + var ah = aq[ar]; + if (/\\[bdsw]/i.test(ah)) { + ak.push(ah); + } else { + var ag = ab(ah); + var al; + if (ar + 2 < aj && '-' === aq[ar + 1]) { + al = ab(aq[ar + 2]); + ar += 2; + } else { + al = ag; + } + af.push([ag, al]); + if (!(al < 65 || ag > 122)) { + if (!(al < 65 || ag > 90)) { + af.push([Math.max(65, ag) | 32, Math.min(al, 90) | 32]); + } + if (!(al < 97 || ag > 122)) { + af.push([Math.max(97, ag) & ~32, Math.min(al, 122) & ~32]); + } + } + } + } + af.sort(function (av, au) { + return av[0] - au[0] || au[1] - av[1]; + }); + var ai = []; + var ap = [NaN, NaN]; + for (var ar = 0; ar < af.length; ++ar) { + var at = af[ar]; + if (at[0] <= ap[1] + 1) { + ap[1] = Math.max(ap[1], at[1]); + } else { + ai.push((ap = at)); + } + } + var an = ['[']; + if (ao) { + an.push('^'); + } + an.push.apply(an, ak); + for (var ar = 0; ar < ai.length; ++ar) { + var at = ai[ar]; + an.push(T(at[0])); + if (at[1] > at[0]) { + if (at[1] + 1 > at[0]) { + an.push('-'); + } + an.push(T(at[1])); + } + } + an.push(']'); + return an.join(''); + } + function W(al) { + var aj = al.source.match( + new RegExp( + '(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)', + 'g' + ) + ); + var ah = aj.length; + var an = []; + for (var ak = 0, am = 0; ak < ah; ++ak) { + var ag = aj[ak]; + if (ag === '(') { + ++am; + } else { + if ('\\' === ag.charAt(0)) { + var af = +ag.substring(1); + if (af && af <= am) { + an[af] = -1; + } + } + } + } + for (var ak = 1; ak < an.length; ++ak) { + if (-1 === an[ak]) { + an[ak] = ++ad; + } + } + for (var ak = 0, am = 0; ak < ah; ++ak) { + var ag = aj[ak]; + if (ag === '(') { + ++am; + if (an[am] === undefined) { + aj[ak] = '(?:'; + } + } else { + if ('\\' === ag.charAt(0)) { + var af = +ag.substring(1); + if (af && af <= am) { + aj[ak] = '\\' + an[am]; + } + } + } + } + for (var ak = 0, am = 0; ak < ah; ++ak) { + if ('^' === aj[ak] && '^' !== aj[ak + 1]) { + aj[ak] = ''; + } + } + if (al.ignoreCase && S) { + for (var ak = 0; ak < ah; ++ak) { + var ag = aj[ak]; + var ai = ag.charAt(0); + if (ag.length >= 2 && ai === '[') { + aj[ak] = X(ag); + } else { + if (ai !== '\\') { + aj[ak] = ag.replace(/[a-zA-Z]/g, function (ao) { + var ap = ao.charCodeAt(0); + return '[' + String.fromCharCode(ap & ~32, ap | 32) + ']'; + }); + } + } + } + } + return aj.join(''); + } + var aa = []; + for (var V = 0, U = Z.length; V < U; ++V) { + var ae = Z[V]; + if (ae.global || ae.multiline) { + throw new Error('' + ae); + } + aa.push('(?:' + W(ae) + ')'); + } + return new RegExp(aa.join('|'), ac ? 'gi' : 'g'); + } + function a(V) { + var U = /(?:^|\s)nocode(?:\s|$)/; + var X = []; + var T = 0; + var Z = []; + var W = 0; + var S; + if (V.currentStyle) { + S = V.currentStyle.whiteSpace; + } else { + if (window.getComputedStyle) { + S = document.defaultView.getComputedStyle(V, null).getPropertyValue('white-space'); + } + } + var Y = S && 'pre' === S.substring(0, 3); + function aa(ab) { + switch (ab.nodeType) { + case 1: + if (U.test(ab.className)) { + return; + } + for (var ae = ab.firstChild; ae; ae = ae.nextSibling) { + aa(ae); + } + var ad = ab.nodeName; + if ('BR' === ad || 'LI' === ad) { + X[W] = '\n'; + Z[W << 1] = T++; + Z[(W++ << 1) | 1] = ab; + } + break; + case 3: + case 4: + var ac = ab.nodeValue; + if (ac.length) { + if (!Y) { + ac = ac.replace(/[ \t\r\n]+/g, ' '); + } else { + ac = ac.replace(/\r\n?/g, '\n'); + } + X[W] = ac; + Z[W << 1] = T; + T += ac.length; + Z[(W++ << 1) | 1] = ab; + } + break; + } + } + aa(V); + return { sourceCode: X.join('').replace(/\n$/, ''), spans: Z }; + } + function B(S, U, W, T) { + if (!U) { + return; + } + var V = { sourceCode: U, basePos: S }; + W(V); + T.push.apply(T, V.decorations); + } + var v = /\S/; + function o(S) { + var V = undefined; + for (var U = S.firstChild; U; U = U.nextSibling) { + var T = U.nodeType; + V = T === 1 ? (V ? S : U) : T === 3 ? (v.test(U.nodeValue) ? S : V) : V; + } + return V === S ? undefined : V; + } + function g(U, T) { + var S = {}; + var V; + (function () { + var ad = U.concat(T); + var ah = []; + var ag = {}; + for (var ab = 0, Z = ad.length; ab < Z; ++ab) { + var Y = ad[ab]; + var ac = Y[3]; + if (ac) { + for (var ae = ac.length; --ae >= 0; ) { + S[ac.charAt(ae)] = Y; + } + } + var af = Y[1]; + var aa = '' + af; + if (!ag.hasOwnProperty(aa)) { + ah.push(af); + ag[aa] = null; + } + } + ah.push(/[\0-\uffff]/); + V = k(ah); + })(); + var X = T.length; + var W = function (ah) { + var Z = ah.sourceCode, + Y = ah.basePos; + var ad = [Y, F]; + var af = 0; + var an = Z.match(V) || []; + var aj = {}; + for (var ae = 0, aq = an.length; ae < aq; ++ae) { + var ag = an[ae]; + var ap = aj[ag]; + var ai = void 0; + var am; + if (typeof ap === 'string') { + am = false; + } else { + var aa = S[ag.charAt(0)]; + if (aa) { + ai = ag.match(aa[1]); + ap = aa[0]; + } else { + for (var ao = 0; ao < X; ++ao) { + aa = T[ao]; + ai = ag.match(aa[1]); + if (ai) { + ap = aa[0]; + break; + } + } + if (!ai) { + ap = F; + } + } + am = ap.length >= 5 && 'lang-' === ap.substring(0, 5); + if (am && !(ai && typeof ai[1] === 'string')) { + am = false; + ap = J; + } + if (!am) { + aj[ag] = ap; + } + } + var ab = af; + af += ag.length; + if (!am) { + ad.push(Y + ab, ap); + } else { + var al = ai[1]; + var ak = ag.indexOf(al); + var ac = ak + al.length; + if (ai[2]) { + ac = ag.length - ai[2].length; + ak = ac - al.length; + } + var ar = ap.substring(5); + B(Y + ab, ag.substring(0, ak), W, ad); + B(Y + ab + ak, al, q(ar, al), ad); + B(Y + ab + ac, ag.substring(ac), W, ad); + } + } + ah.decorations = ad; + }; + return W; + } + function i(T) { + var W = [], + S = []; + if (T.tripleQuotedStrings) { + W.push([ + C, + /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/, + null, + '\'"', + ]); + } else { + if (T.multiLineStrings) { + W.push([ + C, + /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/, + null, + '\'"`', + ]); + } else { + W.push([ + C, + /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/, + null, + '"\'', + ]); + } + } + if (T.verbatimStrings) { + S.push([C, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]); + } + var Y = T.hashComments; + if (Y) { + if (T.cStyleComments) { + if (Y > 1) { + W.push([j, /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, null, '#']); + } else { + W.push([ + j, + /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/, + null, + '#', + ]); + } + S.push([C, /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, null]); + } else { + W.push([j, /^#[^\r\n]*/, null, '#']); + } + } + if (T.cStyleComments) { + S.push([j, /^\/\/[^\r\n]*/, null]); + S.push([j, /^\/\*[\s\S]*?(?:\*\/|$)/, null]); + } + if (T.regexLiterals) { + var X = + '/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/'; + S.push(['lang-regex', new RegExp('^' + M + '(' + X + ')')]); + } + var V = T.types; + if (V) { + S.push([O, V]); + } + var U = ('' + T.keywords).replace(/^ | $/g, ''); + if (U.length) { + S.push([z, new RegExp('^(?:' + U.replace(/[\s,]+/g, '|') + ')\\b'), null]); + } + W.push([F, /^\s+/, null, ' \r\n\t\xA0']); + S.push( + [G, /^@[a-z_$][a-z_$@0-9]*/i, null], + [O, /^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/, null], + [F, /^[a-z_$][a-z_$@0-9]*/i, null], + [ + G, + new RegExp( + '^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*', + 'i' + ), + null, + '0123456789', + ], + [F, /^\\[\s\S]?/, null], + [L, /^.[^\s\w\.$@\'\"\`\/\#\\]*/, null] + ); + return g(W, S); + } + var K = i({ + keywords: A, + hashComments: true, + cStyleComments: true, + multiLineStrings: true, + regexLiterals: true, + }); + function Q(V, ag) { + var U = /(?:^|\s)nocode(?:\s|$)/; + var ab = /\r\n?|\n/; + var ac = V.ownerDocument; + var S; + if (V.currentStyle) { + S = V.currentStyle.whiteSpace; + } else { + if (window.getComputedStyle) { + S = ac.defaultView.getComputedStyle(V, null).getPropertyValue('white-space'); + } + } + var Z = S && 'pre' === S.substring(0, 3); + var af = ac.createElement('LI'); + while (V.firstChild) { + af.appendChild(V.firstChild); + } + var W = [af]; + function ae(al) { + switch (al.nodeType) { + case 1: + if (U.test(al.className)) { + break; + } + if ('BR' === al.nodeName) { + ad(al); + if (al.parentNode) { + al.parentNode.removeChild(al); + } + } else { + for (var an = al.firstChild; an; an = an.nextSibling) { + ae(an); + } + } + break; + case 3: + case 4: + if (Z) { + var am = al.nodeValue; + var aj = am.match(ab); + if (aj) { + var ai = am.substring(0, aj.index); + al.nodeValue = ai; + var ah = am.substring(aj.index + aj[0].length); + if (ah) { + var ak = al.parentNode; + ak.insertBefore(ac.createTextNode(ah), al.nextSibling); + } + ad(al); + if (!ai) { + al.parentNode.removeChild(al); + } + } + } + break; + } + } + function ad(ak) { + while (!ak.nextSibling) { + ak = ak.parentNode; + if (!ak) { + return; + } + } + function ai(al, ar) { + var aq = ar ? al.cloneNode(false) : al; + var ao = al.parentNode; + if (ao) { + var ap = ai(ao, 1); + var an = al.nextSibling; + ap.appendChild(aq); + for (var am = an; am; am = an) { + an = am.nextSibling; + ap.appendChild(am); + } + } + return aq; + } + var ah = ai(ak.nextSibling, 0); + for (var aj; (aj = ah.parentNode) && aj.nodeType === 1; ) { + ah = aj; + } + W.push(ah); + } + for (var Y = 0; Y < W.length; ++Y) { + ae(W[Y]); + } + if (ag === (ag | 0)) { + W[0].setAttribute('value', ag); + } + var aa = ac.createElement('OL'); + aa.className = 'linenums'; + var X = Math.max(0, (ag - 1) | 0) || 0; + for (var Y = 0, T = W.length; Y < T; ++Y) { + af = W[Y]; + af.className = 'L' + ((Y + X) % 10); + if (!af.firstChild) { + af.appendChild(ac.createTextNode('\xA0')); + } + aa.appendChild(af); + } + V.appendChild(aa); + } + function D(ac) { + var aj = /\bMSIE\b/.test(navigator.userAgent); + var am = /\n/g; + var al = ac.sourceCode; + var an = al.length; + var V = 0; + var aa = ac.spans; + var T = aa.length; + var ah = 0; + var X = ac.decorations; + var Y = X.length; + var Z = 0; + X[Y] = an; + var ar, aq; + for (aq = ar = 0; aq < Y; ) { + if (X[aq] !== X[aq + 2]) { + X[ar++] = X[aq++]; + X[ar++] = X[aq++]; + } else { + aq += 2; + } + } + Y = ar; + for (aq = ar = 0; aq < Y; ) { + var at = X[aq]; + var ab = X[aq + 1]; + var W = aq + 2; + while (W + 2 <= Y && X[W + 1] === ab) { + W += 2; + } + X[ar++] = at; + X[ar++] = ab; + aq = W; + } + Y = X.length = ar; + var ae = null; + while (ah < T) { + var af = aa[ah]; + var S = aa[ah + 2] || an; + var ag = X[Z]; + var ap = X[Z + 2] || an; + var W = Math.min(S, ap); + var ak = aa[ah + 1]; + var U; + if (ak.nodeType !== 1 && (U = al.substring(V, W))) { + if (aj) { + U = U.replace(am, '\r'); + } + ak.nodeValue = U; + var ai = ak.ownerDocument; + var ao = ai.createElement('SPAN'); + ao.className = X[Z + 1]; + var ad = ak.parentNode; + ad.replaceChild(ao, ak); + ao.appendChild(ak); + if (V < S) { + aa[ah + 1] = ak = ai.createTextNode(al.substring(W, S)); + ad.insertBefore(ak, ao.nextSibling); + } + } + V = W; + if (V >= S) { + ah += 2; + } + if (V >= ap) { + Z += 2; + } + } + } + var t = {}; + function c(U, V) { + for (var S = V.length; --S >= 0; ) { + var T = V[S]; + if (!t.hasOwnProperty(T)) { + t[T] = U; + } else { + if (window.console) { + console.warn('cannot override language handler %s', T); + } + } + } + } + function q(T, S) { + if (!(T && t.hasOwnProperty(T))) { + T = /^\s*]*(?:>|$)/], + [j, /^<\!--[\s\S]*?(?:-\->|$)/], + ['lang-', /^<\?([\s\S]+?)(?:\?>|$)/], + ['lang-', /^<%([\s\S]+?)(?:%>|$)/], + [L, /^(?:<[%?]|[%?]>)/], + ['lang-', /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i], + ['lang-js', /^]*>([\s\S]*?)(<\/script\b[^>]*>)/i], + ['lang-css', /^]*>([\s\S]*?)(<\/style\b[^>]*>)/i], + ['lang-in.tag', /^(<\/?[a-z][^<>]*>)/i], + ] + ), + ['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl'] + ); + c( + g( + [ + [F, /^[\s]+/, null, ' \t\r\n'], + [n, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, '"\''], + ], + [ + [m, /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i], + [P, /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i], + ['lang-uq.val', /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/], + [L, /^[=<>\/]+/], + ['lang-js', /^on\w+\s*=\s*\"([^\"]+)\"/i], + ['lang-js', /^on\w+\s*=\s*\'([^\']+)\'/i], + ['lang-js', /^on\w+\s*=\s*([^\"\'>\s]+)/i], + ['lang-css', /^style\s*=\s*\"([^\"]+)\"/i], + ['lang-css', /^style\s*=\s*\'([^\']+)\'/i], + ['lang-css', /^style\s*=\s*([^\"\'>\s]+)/i], + ] + ), + ['in.tag'] + ); + c(g([], [[n, /^[\s\S]+/]]), ['uq.val']); + c(i({ keywords: l, hashComments: true, cStyleComments: true, types: e }), [ + 'c', + 'cc', + 'cpp', + 'cxx', + 'cyc', + 'm', + ]); + c(i({ keywords: 'null,true,false' }), ['json']); + c(i({ keywords: R, hashComments: true, cStyleComments: true, verbatimStrings: true, types: e }), [ + 'cs', + ]); + c(i({ keywords: x, cStyleComments: true }), ['java']); + c(i({ keywords: H, hashComments: true, multiLineStrings: true }), ['bsh', 'csh', 'sh']); + c(i({ keywords: I, hashComments: true, multiLineStrings: true, tripleQuotedStrings: true }), [ + 'cv', + 'py', + ]); + c(i({ keywords: s, hashComments: true, multiLineStrings: true, regexLiterals: true }), [ + 'perl', + 'pl', + 'pm', + ]); + c(i({ keywords: f, hashComments: true, multiLineStrings: true, regexLiterals: true }), ['rb']); + c(i({ keywords: w, cStyleComments: true, regexLiterals: true }), ['js']); + c( + i({ + keywords: r, + hashComments: 3, + cStyleComments: true, + multilineStrings: true, + tripleQuotedStrings: true, + regexLiterals: true, + }), + ['coffee'] + ); + c(g([], [[C, /^[\s\S]+/]]), ['regex']); + function d(V) { + var U = V.langExtension; + try { + var S = a(V.sourceNode); + var T = S.sourceCode; + V.sourceCode = T; + V.spans = S.spans; + V.basePos = 0; + q(U, T)(V); + D(V); + } catch (W) { + if ('console' in window) { + console.log(W && W.stack ? W.stack : W); + } + } + } + function y(W, V, U) { + var S = document.createElement('PRE'); + S.innerHTML = W; + if (U) { + Q(S, U); + } + var T = { langExtension: V, numberLines: U, sourceNode: S }; + d(T); + return S.innerHTML; + } + function b(ad) { + function Y(af) { + return document.getElementsByTagName(af); + } + var ac = [Y('pre'), Y('code'), Y('xmp')]; + var T = []; + for (var aa = 0; aa < ac.length; ++aa) { + for (var Z = 0, V = ac[aa].length; Z < V; ++Z) { + T.push(ac[aa][Z]); + } + } + ac = null; + var W = Date; + if (!W.now) { + W = { + now: function () { + return +new Date(); + }, + }; + } + var X = 0; + var S; + var ab = /\blang(?:uage)?-([\w.]+)(?!\S)/; + var ae = /\bprettyprint\b/; + function U() { + var ag = window.PR_SHOULD_USE_CONTINUATION ? W.now() + 250 : Infinity; + for (; X < T.length && W.now() < ag; X++) { + var aj = T[X]; + var ai = aj.className; + if (ai.indexOf('prettyprint') >= 0) { + var ah = ai.match(ab); + var am; + if (!ah && (am = o(aj)) && 'CODE' === am.tagName) { + ah = am.className.match(ab); + } + if (ah) { + ah = ah[1]; + } + var al = false; + for (var ak = aj.parentNode; ak; ak = ak.parentNode) { + if ( + (ak.tagName === 'pre' || ak.tagName === 'code' || ak.tagName === 'xmp') && + ak.className && + ak.className.indexOf('prettyprint') >= 0 + ) { + al = true; + break; + } + } + if (!al) { + var af = aj.className.match(/\blinenums\b(?::(\d+))?/); + af = af ? (af[1] && af[1].length ? +af[1] : true) : false; + if (af) { + Q(aj, af); + } + S = { langExtension: ah, sourceNode: aj, numberLines: af }; + d(S); + } + } + } + if (X < T.length) { + setTimeout(U, 250); + } else { + if (ad) { + ad(); + } + } + } + U(); + } + window.prettyPrintOne = y; + window.prettyPrint = b; + window.PR = { + createSimpleLexer: g, + registerLangHandler: c, + sourceDecorator: i, + PR_ATTRIB_NAME: P, + PR_ATTRIB_VALUE: n, + PR_COMMENT: j, + PR_DECLARATION: E, + PR_KEYWORD: z, + PR_LITERAL: G, + PR_NOCODE: N, + PR_PLAIN: F, + PR_PUNCTUATION: L, + PR_SOURCE: J, + PR_STRING: C, + PR_TAG: m, + PR_TYPE: O, + }; +})(); +PR.registerLangHandler( + PR.createSimpleLexer( + [], + [ + [PR.PR_DECLARATION, /^]*(?:>|$)/], + [PR.PR_COMMENT, /^<\!--[\s\S]*?(?:-\->|$)/], + [PR.PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/], + ['lang-', /^<\?([\s\S]+?)(?:\?>|$)/], + ['lang-', /^<%([\s\S]+?)(?:%>|$)/], + ['lang-', /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i], + [ + 'lang-handlebars', + /^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i, + ], + ['lang-js', /^]*>([\s\S]*?)(<\/script\b[^>]*>)/i], + ['lang-css', /^]*>([\s\S]*?)(<\/style\b[^>]*>)/i], + ['lang-in.tag', /^(<\/?[a-z][^<>]*>)/i], + [PR.PR_DECLARATION, /^{{[#^>/]?\s*[\w.][^}]*}}/], + [PR.PR_DECLARATION, /^{{&?\s*[\w.][^}]*}}/], + [PR.PR_DECLARATION, /^{{{>?\s*[\w.][^}]*}}}/], + [PR.PR_COMMENT, /^{{![^}]*}}/], + ] + ), + ['handlebars', 'hbs'] +); +PR.registerLangHandler( + PR.createSimpleLexer( + [[PR.PR_PLAIN, /^[ \t\r\n\f]+/, null, ' \t\r\n\f']], + [ + [PR.PR_STRING, /^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/, null], + [PR.PR_STRING, /^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/, null], + ['lang-css-str', /^url\(([^\)\"\']*)\)/i], + [ + PR.PR_KEYWORD, + /^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i, + null, + ], + ['lang-css-kw', /^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i], + [PR.PR_COMMENT, /^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//], + [PR.PR_COMMENT, /^(?:)/], + [PR.PR_LITERAL, /^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i], + [PR.PR_LITERAL, /^#(?:[0-9a-f]{3}){1,2}/i], + [PR.PR_PLAIN, /^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i], + [PR.PR_PUNCTUATION, /^[^\s\w\'\"]+/], + ] + ), + ['css'] +); +PR.registerLangHandler( + PR.createSimpleLexer( + [], + [[PR.PR_KEYWORD, /^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]] + ), + ['css-kw'] +); +PR.registerLangHandler(PR.createSimpleLexer([], [[PR.PR_STRING, /^[^\)\"\']+/]]), ['css-str']); diff --git a/frontend/coverage/sorter.js b/frontend/coverage/sorter.js index 4ed70ae..c3fbef4 100644 --- a/frontend/coverage/sorter.js +++ b/frontend/coverage/sorter.js @@ -1,210 +1,205 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; + +var addSorting = (function () { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false, + }; - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; } - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent.toLowerCase().includes(searchValue.toLowerCase()); + } - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); + row.style.display = isMatch ? '' : 'none'; } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string', + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = colNode.innerHTML + ''; + } } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function (a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function (a, b) { + return -1 * sorter(a, b); + }; } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function () { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); } + } } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; + } + // adds sorting functionality to the UI + return function () { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; })(); window.addEventListener('load', addSorting); diff --git a/frontend/coverage/src/App.css.html b/frontend/coverage/src/App.css.html index 213109e..a5aa753 100644 --- a/frontend/coverage/src/App.css.html +++ b/frontend/coverage/src/App.css.html @@ -1,68 +1,61 @@ - - - + Code coverage report for src/App.css - - - - -
-
+ + + +
+

All files / src App.css

-
- -
- 0% - Statements - 0/0 -
- - -
- 0% - Branches - 0/0 -
- - -
- 0% - Functions - 0/0 -
- - -
- 0% - Lines - 0/0 -
- - +
+
+ 0% + Statements + 0/0 +
+ +
+ 0% + Branches + 0/0 +
+ +
+ 0% + Functions + 0/0 +
+ +
+ 0% + Lines + 0/0 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -374,21 +367,22 @@ body { border-color: rgba(255, 255, 255, 0.5); }
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/App.tsx.html b/frontend/coverage/src/App.tsx.html index 0d5195d..ed02366 100644 --- a/frontend/coverage/src/App.tsx.html +++ b/frontend/coverage/src/App.tsx.html @@ -1,68 +1,61 @@ - - - + Code coverage report for src/App.tsx - - - - -
-
+ + + +
+

All files / src App.tsx

-
- -
- 78.94% - Statements - 15/19 -
- - -
- 66.66% - Branches - 4/6 -
- - -
- 77.77% - Functions - 7/9 -
- - -
- 78.94% - Lines - 15/19 -
- - +
+
+ 78.94% + Statements + 15/19 +
+ +
+ 66.66% + Branches + 4/6 +
+ +
+ 77.77% + Functions + 7/9 +
+ +
+ 78.94% + Lines + 15/19 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -335,21 +328,22 @@ function App() { export default App;  
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/FeedItem.css.html b/frontend/coverage/src/components/FeedItem.css.html index dd239a3..f6fe1a3 100644 --- a/frontend/coverage/src/components/FeedItem.css.html +++ b/frontend/coverage/src/components/FeedItem.css.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/FeedItem.css - - - - -
-
-

All files / src/components FeedItem.css

-
- -
- 0% - Statements - 0/0 -
- - -
- 0% - Branches - 0/0 -
- - -
- 0% - Functions - 0/0 -
- - -
- 0% - Lines - 0/0 -
- - + + + +
+
+

+ All files / + src/components FeedItem.css +

+
+
+ 0% + Statements + 0/0 +
+ +
+ 0% + Branches + 0/0 +
+ +
+ 0% + Functions + 0/0 +
+ +
+ 0% + Lines + 0/0 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -413,21 +409,22 @@ margin-left: 0; }
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/FeedItem.tsx.html b/frontend/coverage/src/components/FeedItem.tsx.html index f6b08e5..5512b78 100644 --- a/frontend/coverage/src/components/FeedItem.tsx.html +++ b/frontend/coverage/src/components/FeedItem.tsx.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/FeedItem.tsx - - - - -
-
-

All files / src/components FeedItem.tsx

-
- -
- 78.94% - Statements - 15/19 -
- - -
- 88.88% - Branches - 16/18 -
- - -
- 85.71% - Functions - 6/7 -
- - -
- 78.94% - Lines - 15/19 -
- - + + + +
+
+

+ All files / + src/components FeedItem.tsx +

+
+
+ 78.94% + Statements + 15/19 +
+ +
+ 88.88% + Branches + 16/18 +
+ +
+ 85.71% + Functions + 6/7 +
+ +
+ 78.94% + Lines + 15/19 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -317,21 +313,22 @@ export default function FeedItem({ item: initialItem }: FeedItemProps) { }  
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/FeedItems.css.html b/frontend/coverage/src/components/FeedItems.css.html index b5d2c41..0b1c77d 100644 --- a/frontend/coverage/src/components/FeedItems.css.html +++ b/frontend/coverage/src/components/FeedItems.css.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/FeedItems.css - - - - -
-
-

All files / src/components FeedItems.css

-
- -
- 0% - Statements - 0/0 -
- - -
- 0% - Branches - 0/0 -
- - -
- 0% - Functions - 0/0 -
- - -
- 0% - Lines - 0/0 -
- - + + + +
+
+

+ All files / + src/components FeedItems.css +

+
+
+ 0% + Statements + 0/0 +
+ +
+ 0% + Branches + 0/0 +
+ +
+ 0% + Functions + 0/0 +
+ +
+ 0% + Lines + 0/0 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -155,21 +151,22 @@ min-height: 50px; }
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/FeedItems.tsx.html b/frontend/coverage/src/components/FeedItems.tsx.html index e0f73cf..e57acf9 100644 --- a/frontend/coverage/src/components/FeedItems.tsx.html +++ b/frontend/coverage/src/components/FeedItems.tsx.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/FeedItems.tsx - - - - -
-
-

All files / src/components FeedItems.tsx

-
- -
- 89.34% - Statements - 109/122 -
- - -
- 77.21% - Branches - 61/79 -
- - -
- 86.2% - Functions - 25/29 -
- - -
- 89.09% - Lines - 98/110 -
- - + + + +
+
+

+ All files / + src/components FeedItems.tsx +

+
+
+ 89.34% + Statements + 109/122 +
+ +
+ 77.21% + Branches + 61/79 +
+ +
+ 86.2% + Functions + 25/29 +
+ +
+ 89.09% + Lines + 98/110 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -734,21 +730,22 @@ export default function FeedItems() { }  
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/FeedList.css.html b/frontend/coverage/src/components/FeedList.css.html index ffaa1a9..fe60b9d 100644 --- a/frontend/coverage/src/components/FeedList.css.html +++ b/frontend/coverage/src/components/FeedList.css.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/FeedList.css - - - - -
-
-

All files / src/components FeedList.css

-
- -
- 0% - Statements - 0/0 -
- - -
- 0% - Branches - 0/0 -
- - -
- 0% - Functions - 0/0 -
- - -
- 0% - Lines - 0/0 -
- - + + + +
+
+

+ All files / + src/components FeedList.css +

+
+
+ 0% + Statements + 0/0 +
+ +
+ 0% + Branches + 0/0 +
+ +
+ 0% + Functions + 0/0 +
+ +
+ 0% + Lines + 0/0 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -356,21 +352,22 @@ background-color: transparent; }
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/FeedList.tsx.html b/frontend/coverage/src/components/FeedList.tsx.html index b1b0a27..ba7d81f 100644 --- a/frontend/coverage/src/components/FeedList.tsx.html +++ b/frontend/coverage/src/components/FeedList.tsx.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/FeedList.tsx - - - - -
-
-

All files / src/components FeedList.tsx

-
- -
- 91.66% - Statements - 22/24 -
- - -
- 82.35% - Branches - 14/17 -
- - -
- 100% - Functions - 8/8 -
- - -
- 100% - Lines - 20/20 -
- - + + + +
+
+

+ All files / + src/components FeedList.tsx +

+
+
+ 91.66% + Statements + 22/24 +
+ +
+ 82.35% + Branches + 14/17 +
+ +
+ 100% + Functions + 8/8 +
+ +
+ 100% + Lines + 20/20 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -305,21 +301,22 @@ export default function FeedList() { }  
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/Login.css.html b/frontend/coverage/src/components/Login.css.html index 2b2fe0d..bb3654e 100644 --- a/frontend/coverage/src/components/Login.css.html +++ b/frontend/coverage/src/components/Login.css.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/Login.css - - - - -
-
-

All files / src/components Login.css

-
- -
- 0% - Statements - 0/0 -
- - -
- 0% - Branches - 0/0 -
- - -
- 0% - Functions - 0/0 -
- - -
- 0% - Lines - 0/0 -
- - + + + +
+
+

+ All files / + src/components Login.css +

+
+
+ 0% + Statements + 0/0 +
+ +
+ 0% + Branches + 0/0 +
+ +
+ 0% + Functions + 0/0 +
+ +
+ 0% + Lines + 0/0 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -254,21 +250,22 @@ button[type="submit"]:hover { }  
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/Login.tsx.html b/frontend/coverage/src/components/Login.tsx.html index 263fe57..f29e3cb 100644 --- a/frontend/coverage/src/components/Login.tsx.html +++ b/frontend/coverage/src/components/Login.tsx.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/Login.tsx - - - - -
-
-

All files / src/components Login.tsx

-
- -
- 100% - Statements - 17/17 -
- - -
- 83.33% - Branches - 5/6 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 17/17 -
- - + + + +
+
+

+ All files / + src/components Login.tsx +

+
+
+ 100% + Statements + 17/17 +
+ +
+ 83.33% + Branches + 5/6 +
+ +
+ 100% + Functions + 3/3 +
+ +
+ 100% + Lines + 17/17 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -227,21 +223,22 @@ export default function Login() { }  
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/Settings.css.html b/frontend/coverage/src/components/Settings.css.html index 428c9d2..6a1155e 100644 --- a/frontend/coverage/src/components/Settings.css.html +++ b/frontend/coverage/src/components/Settings.css.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/Settings.css - - - - -
-
-

All files / src/components Settings.css

-
- -
- 0% - Statements - 0/0 -
- - -
- 0% - Branches - 0/0 -
- - -
- 0% - Functions - 0/0 -
- - -
- 0% - Lines - 0/0 -
- - + + + +
+
+

+ All files / + src/components Settings.css +

+
+
+ 0% + Statements + 0/0 +
+ +
+ 0% + Branches + 0/0 +
+ +
+ 0% + Functions + 0/0 +
+ +
+ 0% + Lines + 0/0 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -311,21 +307,22 @@ cursor: not-allowed; }
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/Settings.tsx.html b/frontend/coverage/src/components/Settings.tsx.html index 62ca241..df6d027 100644 --- a/frontend/coverage/src/components/Settings.tsx.html +++ b/frontend/coverage/src/components/Settings.tsx.html @@ -1,68 +1,64 @@ - - - + Code coverage report for src/components/Settings.tsx - - - - -
-
-

All files / src/components Settings.tsx

-
- -
- 75.55% - Statements - 34/45 -
- - -
- 56.25% - Branches - 9/16 -
- - -
- 82.35% - Functions - 14/17 -
- - -
- 84.61% - Lines - 33/39 -
- - + + + +
+
+

+ All files / + src/components Settings.tsx +

+
+
+ 75.55% + Statements + 34/45 +
+ +
+ 56.25% + Branches + 9/16 +
+ +
+ 82.35% + Functions + 14/17 +
+ +
+ 84.61% + Lines + 33/39 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-

+      
+      
+
1 2 3 @@ -428,21 +424,22 @@ export default function Settings() { }  
-
-
- - - - - - +
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/components/index.html b/frontend/coverage/src/components/index.html index 534e353..7e1a0b7 100644 --- a/frontend/coverage/src/components/index.html +++ b/frontend/coverage/src/components/index.html @@ -1,251 +1,303 @@ - - - + Code coverage report for src/components - - - - -
-
+ + + +
+

All files src/components

-
- -
- 86.78% - Statements - 197/227 -
- - -
- 77.2% - Branches - 105/136 -
- - -
- 87.5% - Functions - 56/64 -
- - -
- 89.26% - Lines - 183/205 -
- - +
+
+ 86.78% + Statements + 197/227 +
+ +
+ 77.2% + Branches + 105/136 +
+ +
+ 87.5% + Functions + 56/64 +
+ +
+ 89.26% + Lines + 183/205 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+
+
FileStatementsBranchesFunctionsLines
FeedItem.css -
-
0%0/00%0/00%0/00%0/0
FeedItem.tsx -
-
78.94%15/1988.88%16/1885.71%6/778.94%15/19
+ + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - -
File + Statements + BranchesFunctionsLines
+ FeedItem.css + +
+
+
+
+
0%0/00%0/00%0/00%0/0
FeedItems.css -
-
0%0/00%0/00%0/00%0/0
+ FeedItem.tsx + +
+
+
+
+
78.94%15/1988.88%16/1885.71%6/778.94%15/19
FeedItems.tsx -
-
89.34%109/12277.21%61/7986.2%25/2989.09%98/110
+ FeedItems.css + +
+
+
+
+
0%0/00%0/00%0/00%0/0
FeedList.css -
-
0%0/00%0/00%0/00%0/0
+ FeedItems.tsx + +
+
+
+
+
89.34%109/12277.21%61/7986.2%25/2989.09%98/110
FeedList.tsx -
-
91.66%22/2482.35%14/17100%8/8100%20/20
+ FeedList.css + +
+
+
+
+
0%0/00%0/00%0/00%0/0
Login.css -
-
0%0/00%0/00%0/00%0/0
+ FeedList.tsx + +
+
+
+
+
91.66%22/2482.35%14/17100%8/8100%20/20
Login.tsx -
-
100%17/1783.33%5/6100%3/3100%17/17
+ Login.css + +
+
+
+
+
0%0/00%0/00%0/00%0/0
Settings.css -
-
0%0/00%0/00%0/00%0/0
+ Login.tsx + +
+
+
+
+
100%17/1783.33%5/6100%3/3100%17/17
Settings.tsx -
-
75.55%34/4556.25%9/1682.35%14/1784.61%33/39
+ Settings.css + +
+
+
+
+
0%0/00%0/00%0/00%0/0
-
-
-
- - - - - - + + + Settings.tsx + + +
+
+
+
+ + 75.55% + 34/45 + 56.25% + 9/16 + 82.35% + 14/17 + 84.61% + 33/39 + + + +
+
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/coverage/src/index.html b/frontend/coverage/src/index.html index 023e9bb..14e9f51 100644 --- a/frontend/coverage/src/index.html +++ b/frontend/coverage/src/index.html @@ -1,131 +1,139 @@ - - - + Code coverage report for src - - - - -
-
+ + + +
+

All files src

-
- -
- 78.94% - Statements - 15/19 -
- - -
- 66.66% - Branches - 4/6 -
- - -
- 77.77% - Functions - 7/9 -
- - -
- 78.94% - Lines - 15/19 -
- - +
+
+ 78.94% + Statements + 15/19 +
+ +
+ 66.66% + Branches + 4/6 +
+ +
+ 77.77% + Functions + 7/9 +
+ +
+ 78.94% + Lines + 15/19 +

- Press n or j to go to the next uncovered block, b, p or k for the previous block. + Press n or j to go to the next uncovered block, b, + p or k for the previous block.

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+
+
FileStatementsBranchesFunctionsLines
App.css -
-
0%0/00%0/00%0/00%0/0
App.tsx -
-
78.94%15/1966.66%4/677.77%7/978.94%15/19
+ + + + + + + + + + + + + + + + + + + + + + + + + + + - -
File + Statements + BranchesFunctionsLines
App.css +
+
+
+
+
0%0/00%0/00%0/00%0/0
-
-
-
- - - - - - + + App.tsx + +
+
+
+
+ + 78.94% + 15/19 + 66.66% + 4/6 + 77.77% + 7/9 + 78.94% + 15/19 + + + +
+
+ +
+ + + + + + + - \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 5e6b472..043ab7a 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,23 +1,27 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import eslintConfigPrettier from 'eslint-config-prettier'; -export default defineConfig([ - globalIgnores(['dist']), +export default tseslint.config( + { ignores: ['dist'] }, { + extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, }, -]) + eslintConfigPrettier +); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9414557..fda0116 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,10 +23,13 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", "jsdom": "^28.0.0", + "prettier": "^3.8.1", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.3.1", @@ -1174,6 +1177,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -2651,6 +2666,51 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", @@ -2790,6 +2850,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3568,6 +3634,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -3872,6 +3965,21 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e2f7c1c..e5475dd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "eslint .", + "lint": "eslint . --max-warnings 0", + "format": "prettier --write .", "preview": "vite preview", "test": "vitest", "test:e2e": "playwright test", @@ -28,13 +29,16 @@ "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.18", "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", "jsdom": "^28.0.0", + "prettier": "^3.8.1", "typescript": "~5.9.3", "typescript-eslint": "^8.48.0", "vite": "^7.3.1", "vitest": "^4.0.18" } -} \ No newline at end of file +} diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html index 4095719..1d408fd 100644 --- a/frontend/playwright-report/index.html +++ b/frontend/playwright-report/index.html @@ -1,85 +1,21807 @@ - - - - + + - - - + + + Playwright Test Report - - +`.trimStart(); + async function zv({ + testInfo: l, + metadata: u, + errorContext: c, + errors: f, + buildCodeFrame: r, + stdout: o, + stderr: h, + }) { + var S; + const v = new Set( + f + .filter( + (O) => + O.message && + !O.message.includes(` +`) + ) + .map((O) => O.message) + ); + for (const O of f) + for (const X of v.keys()) (S = O.message) != null && S.includes(X) && v.delete(X); + const y = f.filter( + (O) => + !( + !O.message || + (!O.message.includes(` +`) && + !v.has(O.message)) + ) + ); + if (!y.length) return; + const A = [Qv, '# Test info', '', l]; + (o && A.push('', '# Stdout', '', '```', Jf(o), '```'), + h && A.push('', '# Stderr', '', '```', Jf(h), '```'), + A.push('', '# Error details')); + for (const O of y) A.push('', '```', Jf(O.message || ''), '```'); + c && A.push(c); + const E = await r(y[y.length - 1]); + return ( + E && A.push('', '# Test source', '', '```ts', E, '```'), + u != null && u.gitDiff && A.push('', '# Local changes', '', '```diff', u.gitDiff, '```'), + A.join(` +`) + ); + } + const Yv = new RegExp( + '([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', + 'g' + ); + function Jf(l) { + return l.replace(Yv, ''); + } + function Lv(l, u) { + var f; + const c = new Map(); + for (const r of l) { + const o = r.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/); + if (!o) continue; + const [, h, v, y = ''] = o, + A = h + y; + let E = c.get(A); + (E || ((E = { name: A, anchors: [`attachment-${h}`] }), c.set(A, E)), + E.anchors.push(`attachment-${u.attachments.indexOf(r)}`), + v === 'actual' && (E.actual = { attachment: r }), + v === 'expected' && (E.expected = { attachment: r, title: 'Expected' }), + v === 'previous' && (E.expected = { attachment: r, title: 'Previous' }), + v === 'diff' && (E.diff = { attachment: r })); + } + for (const [r, o] of c) + !o.actual || !o.expected + ? c.delete(r) + : (l.delete(o.actual.attachment), + l.delete(o.expected.attachment), + l.delete((f = o.diff) == null ? void 0 : f.attachment)); + return [...c.values()]; + } + const Gv = ({ test: l, result: u, testRunMetadata: c, options: f }) => { + const { + screenshots: r, + videos: o, + traces: h, + otherAttachments: v, + diffs: y, + errors: A, + otherAttachmentAnchors: E, + screenshotAnchors: S, + errorContext: O, + } = ct.useMemo(() => { + const B = u.attachments.filter((N) => !N.name.startsWith('_')), + b = new Set(B.filter((N) => N.contentType.startsWith('image/'))), + p = [...b].map((N) => `attachment-${B.indexOf(N)}`), + x = B.filter((N) => N.contentType.startsWith('video/')), + R = B.filter((N) => N.name === 'trace'), + U = B.find((N) => N.name === 'error-context'), + Z = new Set(B); + [...b, ...x, ...R].forEach((N) => Z.delete(N)); + const F = [...Z].map((N) => `attachment-${B.indexOf(N)}`), + j = Lv(b, u), + D = u.errors.map((N) => N.message); + return { + screenshots: [...b], + videos: x, + traces: R, + otherAttachments: Z, + diffs: j, + errors: D, + otherAttachmentAnchors: F, + screenshotAnchors: p, + errorContext: U, + }; + }, [u]), + X = P5( + async () => { + if (f != null && f.noCopyPrompt) return; + const B = u.attachments.find((R) => R.name === 'stdout'), + b = u.attachments.find((R) => R.name === 'stderr'), + p = B != null && B.body && B.contentType === 'text/plain' ? B.body : void 0, + x = b != null && b.body && b.contentType === 'text/plain' ? b.body : void 0; + return await zv({ + testInfo: [ + `- Name: ${l.path.join(' >> ')} >> ${l.title}`, + `- Location: ${l.location.file}:${l.location.line}:${l.location.column}`, + ].join(` +`), + metadata: c, + errorContext: + O != null && O.path + ? await fetch(O.path).then((R) => R.text()) + : O == null + ? void 0 + : O.body, + errors: u.errors, + buildCodeFrame: async (R) => R.codeframe, + stdout: p, + stderr: x, + }); + }, + [l, O, c, u], + void 0 + ); + return m.jsxs('div', { + className: 'test-result', + children: [ + !!A.length && + m.jsxs(ke, { + header: 'Errors', + children: [ + X && + m.jsx('div', { + style: { position: 'absolute', right: '16px', padding: '10px', zIndex: 1 }, + children: m.jsx(Nv, { prompt: X }), + }), + A.map((B, b) => { + const p = Xv(B, y); + return m.jsxs(m.Fragment, { + children: [ + m.jsx(wr, { code: B }, 'test-result-error-message-' + b), + p && m.jsx(Bv, { diff: p }), + ], + }); + }), + ], + }), + !!u.steps.length && + m.jsx(ke, { + header: 'Test Steps', + children: u.steps.map((B, b) => + m.jsx(cm, { step: B, result: u, test: l, depth: 0 }, `step-${b}`) + ), + }), + y.map((B, b) => + m.jsx( + Si, + { + id: B.anchors, + children: m.jsx(ke, { + dataTestId: 'test-results-image-diff', + header: `Image mismatch: ${B.name}`, + revealOnAnchorId: B.anchors, + children: m.jsx(um, { diff: B }), + }), + }, + `diff-${b}` + ) + ), + !!r.length && + m.jsx(ke, { + header: 'Screenshots', + revealOnAnchorId: S, + children: r.map((B, b) => + m.jsxs( + Si, + { + id: `attachment-${u.attachments.indexOf(B)}`, + children: [ + m.jsx('a', { + href: Ve(B.path), + children: m.jsx('img', { className: 'screenshot', src: Ve(B.path) }), + }), + m.jsx(nc, { attachment: B, result: u }), + ], + }, + `screenshot-${b}` + ) + ), + }), + !!h.length && + m.jsx(Si, { + id: 'attachment-trace', + children: m.jsx(ke, { + header: 'Traces', + revealOnAnchorId: 'attachment-trace', + children: m.jsxs('div', { + children: [ + m.jsx('a', { + href: Ve(nm(h)), + children: m.jsx('img', { + className: 'screenshot', + src: Cv, + style: { width: 192, height: 117, marginLeft: 20 }, + }), + }), + h.map((B, b) => + m.jsx( + nc, + { + attachment: B, + result: u, + linkName: h.length === 1 ? 'trace' : `trace-${b + 1}`, + }, + `trace-${b}` + ) + ), + ], + }), + }), + }), + !!o.length && + m.jsx(Si, { + id: 'attachment-video', + children: m.jsx(ke, { + header: 'Videos', + revealOnAnchorId: 'attachment-video', + children: o.map((B) => + m.jsxs( + 'div', + { + children: [ + m.jsx('video', { + controls: !0, + children: m.jsx('source', { src: Ve(B.path), type: B.contentType }), + }), + m.jsx(nc, { attachment: B, result: u }), + ], + }, + B.path + ) + ), + }), + }), + !!v.size && + m.jsx(ke, { + header: 'Attachments', + revealOnAnchorId: E, + dataTestId: 'attachments', + children: [...v].map((B, b) => + m.jsx( + Si, + { + id: `attachment-${u.attachments.indexOf(B)}`, + children: m.jsx(nc, { + attachment: B, + result: u, + openInNewTab: B.contentType.startsWith('text/html'), + }), + }, + `attachment-link-${b}` + ) + ), + }), + ], + }); + }; + function Xv(l, u) { + const c = l.split(` +`)[0]; + if (!(!c.includes('toHaveScreenshot') && !c.includes('toMatchSnapshot'))) + return u.find((f) => l.includes(f.name)); + } + const cm = ({ test: l, step: u, result: c, depth: f }) => { + const r = se(); + return m.jsx(Tv, { + title: m.jsxs('div', { + 'aria-label': u.title, + className: 'step-title-container', + children: [ + hc(u.error || u.duration === -1 ? 'failed' : u.skipped ? 'skipped' : 'passed'), + m.jsxs('span', { + className: 'step-title-text', + children: [ + m.jsx('span', { children: u.title }), + u.count > 1 && + m.jsxs(m.Fragment, { + children: [ + ' ✕ ', + m.jsx('span', { className: 'test-result-counter', children: u.count }), + ], + }), + u.location && + m.jsxs('span', { + className: 'test-result-path', + children: ['— ', u.location.file, ':', u.location.line], + }), + ], + }), + m.jsx('span', { className: 'step-spacer' }), + u.attachments.length > 0 && + m.jsx('a', { + className: 'step-attachment-link', + title: 'reveal attachment', + href: Ve( + Cn({ test: l, result: c, anchor: `attachment-${u.attachments[0]}` }, r) + ), + onClick: (o) => { + o.stopPropagation(); + }, + children: Ih(), + }), + m.jsx('span', { className: 'step-duration', children: Ol(u.duration) }), + ], + }), + loadChildren: + u.steps.length || u.snippet + ? () => { + const o = u.snippet + ? [m.jsx(wr, { testId: 'test-snippet', code: u.snippet }, 'line')] + : [], + h = u.steps.map((v, y) => + m.jsx(cm, { step: v, depth: f + 1, result: c, test: l }, y) + ); + return o.concat(h); + } + : void 0, + depth: f, + }); + }, + Vv = ({ + projectNames: l, + test: u, + testRunMetadata: c, + run: f, + next: r, + prev: o, + options: h, + }) => { + const [v, y] = ct.useState(f), + A = se(), + E = u.annotations.filter((S) => !S.type.startsWith('_')) ?? []; + return m.jsxs(m.Fragment, { + children: [ + m.jsx(Or, { + title: u.title, + leftSuperHeader: m.jsx('div', { + className: 'test-case-path', + children: u.path.join(' › '), + }), + rightSuperHeader: m.jsxs(m.Fragment, { + children: [ + m.jsx('div', { + className: Ze(!o && 'hidden'), + children: m.jsx(Tn, { href: Cn({ test: o }, A), children: '« previous' }), + }), + m.jsx('div', { style: { width: 10 } }), + m.jsx('div', { + className: Ze(!r && 'hidden'), + children: m.jsx(Tn, { href: Cn({ test: r }, A), children: 'next »' }), + }), + ], + }), + }), + m.jsxs('div', { + className: 'hbox', + style: { lineHeight: '24px' }, + children: [ + m.jsx('div', { + className: 'test-case-location', + children: m.jsxs(Sr, { + value: `${u.location.file}:${u.location.line}`, + children: [u.location.file, ':', u.location.line], + }), + }), + m.jsx('div', { style: { flex: 'auto' } }), + m.jsx(tm, { test: u, trailingSeparator: !0 }), + m.jsx('div', { className: 'test-case-duration', children: Ol(u.duration) }), + ], + }), + m.jsx($h, { + style: { marginLeft: '6px' }, + projectNames: l, + activeProjectName: u.projectName, + otherLabels: u.tags, + }), + u.results.length === 0 && + E.length !== 0 && + m.jsx(ke, { + header: 'Annotations', + dataTestId: 'test-case-annotations', + children: E.map((S, O) => m.jsx(z2, { annotation: S }, O)), + }), + m.jsx(Sv, { + tabs: + u.results.map((S, O) => ({ + id: String(O), + title: m.jsxs('div', { + style: { display: 'flex', alignItems: 'center' }, + children: [ + hc(S.status), + ' ', + Zv(O), + u.results.length > 1 && + m.jsx('span', { + className: 'test-case-run-duration', + children: Ol(S.duration), + }), + ], + }), + render: () => { + const X = S.annotations.filter((B) => !B.type.startsWith('_')); + return m.jsxs(m.Fragment, { + children: [ + !!X.length && + m.jsx(ke, { + header: 'Annotations', + dataTestId: 'test-case-annotations', + children: X.map((B, b) => m.jsx(z2, { annotation: B }, b)), + }), + m.jsx(Gv, { test: u, result: S, testRunMetadata: c, options: h }), + ], + }); + }, + })) || [], + selectedTab: String(v), + setSelectedTab: (S) => y(+S), + }), + ], + }); + }; + function z2({ annotation: { type: l, description: u } }) { + return m.jsxs('div', { + className: 'test-case-annotation', + children: [ + m.jsx('span', { style: { fontWeight: 'bold' }, children: l }), + u && m.jsxs(Sr, { value: u, children: [': ', Di(u)] }), + ], + }); + } + function Zv(l) { + return l ? `Retry #${l}` : 'Run'; + } + const sm = ({ + file: l, + projectNames: u, + isFileExpanded: c, + setFileExpanded: f, + footer: r, + }) => { + const o = se(); + return m.jsx(im, { + expanded: c ? c(l.fileId) : void 0, + noInsets: !0, + setExpanded: f ? (h) => f(l.fileId, h) : void 0, + header: m.jsx('span', { className: 'chip-header-allow-selection', children: l.fileName }), + footer: r, + children: l.tests.map((h) => + m.jsxs( + 'div', + { + className: Ze('test-file-test', 'test-file-test-outcome-' + h.outcome), + children: [ + m.jsxs('div', { + className: 'hbox', + style: { alignItems: 'flex-start' }, + children: [ + m.jsxs('div', { + className: 'hbox', + children: [ + m.jsx('span', { + className: 'test-file-test-status-icon', + children: hc(h.outcome), + }), + m.jsxs('span', { + children: [ + m.jsx(Tn, { + href: Cn({ test: h }, o), + title: [...h.path, h.title].join(' › '), + children: m.jsx('span', { + className: 'test-file-title', + children: [...h.path, h.title].join(' › '), + }), + }), + m.jsx($h, { + style: { marginLeft: '6px' }, + projectNames: u, + activeProjectName: h.projectName, + otherLabels: h.tags, + }), + ], + }), + ], + }), + m.jsx('span', { + 'data-testid': 'test-duration', + style: { minWidth: '50px', textAlign: 'right' }, + children: Ol(h.duration), + }), + ], + }), + m.jsx('div', { + className: 'test-file-details-row', + children: m.jsxs('div', { + className: 'test-file-details-row-items', + children: [ + m.jsx(Tn, { + href: Cn({ test: h }, o), + title: [...h.path, h.title].join(' › '), + className: 'test-file-path-link', + children: m.jsxs('span', { + className: 'test-file-path', + children: [h.location.file, ':', h.location.line], + }), + }), + m.jsx(qv, { test: h }), + m.jsx(Iv, { test: h }), + m.jsx(tm, { test: h, dim: !0 }), + ], + }), + }), + ], + }, + `test-${h.testId}` + ) + ), + }); + }; + function qv({ test: l }) { + const u = se(); + for (const c of l.results) + for (const f of c.attachments) + if (f.contentType.startsWith('image/') && f.name.match(/-(expected|actual|diff)/)) + return m.jsx(Tr, { + href: Cn( + { test: l, result: c, anchor: `attachment-${c.attachments.indexOf(f)}` }, + u + ), + title: 'View images', + dim: !0, + children: k5(), + }); + } + function Iv({ test: l }) { + const u = se(), + c = l.results.find((f) => f.attachments.some((r) => r.name === 'video')); + return c + ? m.jsx(Tr, { + href: Cn({ test: l, result: c, anchor: 'attachment-video' }, u), + title: 'View video', + dim: !0, + children: J5(), + }) + : void 0; + } + class Kv extends ct.Component { + constructor() { + super(...arguments); + yn(this, 'state', { error: null, errorInfo: null }); + } + componentDidCatch(c, f) { + this.setState({ error: c, errorInfo: f }); + } + render() { + var c, f, r; + return this.state.error || this.state.errorInfo + ? m.jsxs('div', { + className: 'metadata-view p-3', + children: [ + m.jsx('p', { + children: 'An error was encountered when trying to render metadata.', + }), + m.jsx('p', { + children: m.jsxs('pre', { + style: { overflow: 'scroll' }, + children: [ + (c = this.state.error) == null ? void 0 : c.message, + m.jsx('br', {}), + (f = this.state.error) == null ? void 0 : f.stack, + m.jsx('br', {}), + (r = this.state.errorInfo) == null ? void 0 : r.componentStack, + ], + }), + }), + ], + }) + : this.props.children; + } + } + const kv = (l) => m.jsx(Kv, { children: m.jsx(Jv, { metadata: l.metadata }) }), + Jv = (l) => { + const u = l.metadata, + c = se().has('show-metadata-other') + ? Object.entries(l.metadata).filter(([r]) => !fm.has(r)) + : []; + if (u.ci || u.gitCommit || c.length > 0) + return m.jsxs('div', { + className: 'metadata-view', + children: [ + u.ci && !u.gitCommit && m.jsx(Fv, { info: u.ci }), + u.gitCommit && m.jsx(Wv, { ci: u.ci, commit: u.gitCommit }), + c.length > 0 && + m.jsxs(m.Fragment, { + children: [ + (u.gitCommit || u.ci) && m.jsx('div', { className: 'metadata-separator' }), + m.jsx('div', { + className: 'metadata-section metadata-properties', + role: 'list', + children: c.map(([r, o]) => { + const h = + typeof o != 'object' || o === null || o === void 0 + ? String(o) + : JSON.stringify(o), + v = h.length > 1e3 ? h.slice(0, 1e3) + '…' : h; + return m.jsx( + 'div', + { + className: 'copyable-property', + role: 'listitem', + children: m.jsxs(Sr, { + value: h, + children: [ + m.jsx('span', { + style: { fontWeight: 'bold' }, + title: r, + children: r, + }), + ': ', + m.jsx('span', { title: v, children: Di(v) }), + ], + }), + }, + r + ); + }), + }), + ], + }), + ], + }); + }, + Fv = ({ info: l }) => { + const u = l.prTitle || `Commit ${l.commitHash}`, + c = l.prHref || l.commitHref; + return m.jsx('div', { + className: 'metadata-section', + role: 'list', + children: m.jsx('div', { + role: 'listitem', + children: m.jsx('a', { + href: Ve(c), + target: '_blank', + rel: 'noopener noreferrer', + title: u, + children: u, + }), + }), + }); + }, + Wv = ({ ci: l, commit: u }) => { + const c = (l == null ? void 0 : l.prTitle) || u.subject, + f = (l == null ? void 0 : l.prHref) || (l == null ? void 0 : l.commitHref), + r = ` <${u.author.email}>`, + o = `${u.author.name}${r}`, + h = Intl.DateTimeFormat(void 0, { dateStyle: 'medium' }).format(u.committer.time), + v = Intl.DateTimeFormat(void 0, { dateStyle: 'full', timeStyle: 'long' }).format( + u.committer.time + ); + return m.jsxs('div', { + className: 'metadata-section', + role: 'list', + children: [ + m.jsxs('div', { + role: 'listitem', + children: [ + f && + m.jsx('a', { + href: Ve(f), + target: '_blank', + rel: 'noopener noreferrer', + title: c, + children: c, + }), + !f && m.jsx('span', { title: c, children: c }), + ], + }), + m.jsxs('div', { + role: 'listitem', + className: 'hbox', + children: [ + m.jsx('span', { className: 'mr-1', children: o }), + m.jsxs('span', { title: v, children: [' on ', h] }), + ], + }), + ], + }); + }, + fm = new Set(['ci', 'gitCommit', 'gitDiff', 'actualWorkers']), + _v = (l) => { + const u = Object.entries(l).filter(([c]) => !fm.has(c)); + return !l.ci && !l.gitCommit && !u.length; + }, + Pv = ({ files: l, expandedFiles: u, setExpandedFiles: c, projectNames: f }) => { + const r = ct.useMemo(() => { + const o = []; + let h = 0; + for (const v of l) + ((h += v.tests.length), o.push({ file: v, defaultExpanded: h < 200 })); + return o; + }, [l]); + return m.jsx(m.Fragment, { + children: + r.length > 0 + ? r.map(({ file: o, defaultExpanded: h }) => + m.jsx( + sm, + { + file: o, + projectNames: f, + isFileExpanded: (v) => { + const y = u.get(v); + return y === void 0 ? h : !!y; + }, + setFileExpanded: (v, y) => { + const A = new Map(u); + (A.set(v, y), c(A)); + }, + }, + `file-${o.fileId}` + ) + ) + : m.jsx('div', { + className: 'chip-header test-file-no-files', + children: 'No tests found', + }), + }); + }, + Y2 = ({ report: l, filteredStats: u, metadataVisible: c, toggleMetadataVisible: f }) => { + if (!l) return null; + const r = l.projectNames.length === 1 && !!l.projectNames[0], + o = !r && !u, + h = + !_v(l.metadata) && + m.jsxs('div', { + className: Ze('metadata-toggle', !o && 'metadata-toggle-second-line'), + role: 'button', + onClick: f, + title: c ? 'Hide metadata' : 'Show metadata', + children: [c ? Ni() : Cl(), 'Metadata'], + }), + v = m.jsxs('div', { + className: 'test-file-header-info', + children: [ + r && + m.jsxs('div', { + 'data-testid': 'project-name', + children: ['Project: ', l.projectNames[0]], + }), + u && + m.jsxs('div', { + 'data-testid': 'filtered-tests-count', + children: ['Filtered: ', u.total, ' ', !!u.total && '(' + Ol(u.duration) + ')'], + }), + o && h, + ], + }), + y = m.jsxs(m.Fragment, { + children: [ + m.jsx('div', { + 'data-testid': 'overall-time', + style: { marginRight: '10px' }, + children: l ? new Date(l.startTime).toLocaleString() : '', + }), + m.jsxs('div', { + 'data-testid': 'overall-duration', + children: ['Total time: ', Ol(l.duration ?? 0)], + }), + ], + }); + return m.jsxs(m.Fragment, { + children: [ + m.jsx(Or, { title: l.options.title, leftSuperHeader: v, rightSuperHeader: y }), + !o && h, + c && m.jsx(kv, { metadata: l.metadata }), + !!l.errors.length && + m.jsx(ke, { + header: 'Errors', + dataTestId: 'report-errors', + children: l.errors.map((A, E) => + m.jsx(wr, { code: A }, 'test-report-error-message-' + E) + ), + }), + ], + }); + }, + rm = (l) => { + const u = Math.round(l / 1e3), + c = Math.floor(u / 60), + f = u % 60; + return c === 0 ? `${f}s` : `${c}m ${f}s`; + }, + $v = ({ entries: l }) => { + const f = Math.max(...l.map((D) => D.label.length)) * 10, + o = { top: 20, right: 20, bottom: 40, left: Math.min(800 * 0.5, Math.max(50, f)) }, + h = 800 - o.left - o.right, + v = Math.min(...l.map((D) => D.startTime)), + y = Math.max(...l.map((D) => D.startTime + D.duration)); + let A, E; + const S = y - v; + S < 60 * 1e3 + ? ((A = 10 * 1e3), (E = !0)) + : S < 300 * 1e3 + ? ((A = 30 * 1e3), (E = !0)) + : S < 1800 * 1e3 + ? ((A = 300 * 1e3), (E = !1)) + : ((A = 600 * 1e3), (E = !1)); + const O = Math.ceil(v / A) * A, + X = (D, N) => { + const K = new Date(D).toLocaleTimeString(void 0, { + hour: '2-digit', + minute: '2-digit', + second: E ? '2-digit' : void 0, + }); + if (N) return K; + if (K.endsWith(' AM') || K.endsWith(' PM')) return K.slice(0, -3); + }, + b = (y - v) * 1.1, + p = Math.ceil(b / A) * A, + x = h / p, + R = 20, + U = 8, + Z = l.length * (R + U), + F = []; + for (let D = O; D <= v + p; D += A) { + const N = D - v; + F.push({ x: N * x, label: X(D, D === O) }); + } + const j = Z + o.top + o.bottom; + return m.jsx('svg', { + viewBox: `0 0 800 ${j}`, + preserveAspectRatio: 'xMidYMid meet', + style: { width: '100%', height: 'auto' }, + role: 'img', + children: m.jsxs('g', { + transform: `translate(${o.left}, ${o.top})`, + role: 'presentation', + children: [ + F.map(({ x: D, label: N }, K) => + m.jsxs( + 'g', + { + 'aria-hidden': 'true', + children: [ + m.jsx('line', { + x1: D, + y1: 0, + x2: D, + y2: Z, + stroke: 'var(--color-border-muted)', + strokeWidth: '1', + }), + m.jsx('text', { + x: D, + y: Z + 20, + textAnchor: 'middle', + dominantBaseline: 'middle', + fontSize: '12', + fill: 'var(--color-fg-muted)', + children: N, + }), + ], + }, + K + ) + ), + l.map((D, N) => { + const K = D.startTime - v, + J = D.duration * x, + k = K * x, + nt = N * (R + U), + P = [ + 'var(--color-scale-blue-2)', + 'var(--color-scale-blue-3)', + 'var(--color-scale-blue-4)', + ], + st = P[N % P.length]; + return m.jsxs( + 'g', + { + role: 'listitem', + 'aria-label': D.tooltip, + children: [ + m.jsx('rect', { + className: 'gantt-bar', + x: k, + y: nt, + width: J, + height: R, + fill: st, + rx: '2', + tabIndex: 0, + children: m.jsx('title', { children: D.tooltip }), + }), + m.jsx('text', { + x: k + J + 6, + y: nt + R / 2, + dominantBaseline: 'middle', + fontSize: '12', + fill: 'var(--color-fg-muted)', + 'aria-hidden': 'true', + children: rm(D.duration), + }), + m.jsx('text', { + x: -10, + y: nt + R / 2, + textAnchor: 'end', + dominantBaseline: 'middle', + fontSize: '12', + fill: 'var(--color-fg-muted)', + 'aria-hidden': 'true', + children: D.label, + }), + ], + }, + N + ); + }), + m.jsx('line', { + x1: 0, + y1: 0, + x2: 0, + y2: Z, + stroke: 'var(--color-fg-muted)', + strokeWidth: '1', + 'aria-hidden': 'true', + }), + m.jsx('line', { + x1: 0, + y1: Z, + x2: h, + y2: Z, + stroke: 'var(--color-fg-muted)', + strokeWidth: '1', + 'aria-hidden': 'true', + }), + ], + }), + }); + }; + function ty({ report: l, tests: u }) { + return m.jsxs(m.Fragment, { + children: [m.jsx(ny, { report: l }), m.jsx(ey, { report: l, tests: u })], + }); + } + function ey({ report: l, tests: u }) { + const [c, f] = ue.useState(50); + return m.jsx(sm, { + file: { fileId: 'slowest', fileName: 'Slowest Tests', tests: u.slice(0, c), stats: null }, + projectNames: l.json().projectNames, + footer: + c < u.length + ? m.jsxs('button', { + className: 'link-badge fullwidth-link', + style: { padding: '8px 5px' }, + onClick: () => f((r) => r + 50), + children: [Ni(), 'Show 50 more'], + }) + : void 0, + }); + } + function ny({ report: l }) { + const u = l.json().machines; + if (u.length === 0) return null; + const c = u + .map((f) => { + const r = f.tag.join(' '), + o = new Date(f.startTime).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZoneName: 'short', + }); + let h = `${r} started at ${o}, runs ${rm(f.duration)}`; + return ( + f.shardIndex && (h += ` (shard ${f.shardIndex})`), + { + label: r, + tooltip: h, + startTime: f.startTime, + duration: f.duration, + shardIndex: f.shardIndex ?? 1, + } + ); + }) + .sort((f, r) => f.label.localeCompare(r.label) || f.shardIndex - r.shardIndex); + return m.jsx(ke, { header: 'Timeline', children: m.jsx($v, { entries: c }) }); + } + const ay = (l) => !l.has('testId') && !l.has('speedboard'), + ly = (l) => l.has('testId'), + iy = (l) => l.has('speedboard') && !l.has('testId'), + uy = ({ report: l }) => { + var Z, F; + const u = se(), + [c, f] = ct.useState(new Map()), + [r, o] = ct.useState(u.get('q') || ''), + [h, v] = ct.useState(!1), + y = u.has('speedboard'), + [A] = _h('mergeFiles', !1), + E = u.get('testId'), + S = ((Z = u.get('q')) == null ? void 0 : Z.toString()) || '', + O = S ? '&q=' + S : '', + X = (F = l == null ? void 0 : l.json()) == null ? void 0 : F.options.title, + B = ct.useMemo(() => { + const j = new Map(); + for (const D of (l == null ? void 0 : l.json().files) || []) + for (const N of D.tests) j.set(N.testId, D.fileId); + return j; + }, [l]), + b = ct.useMemo(() => rc.parse(r), [r]), + p = ct.useMemo( + () => (b.empty() ? void 0 : sy((l == null ? void 0 : l.json().files) || [], b)), + [l, b] + ), + x = ct.useMemo(() => (y ? oy(l, b) : A ? ry(l, b) : fy(l, b)), [l, b, A, y]), + { prev: R, next: U } = ct.useMemo(() => { + const j = x.tests.findIndex((K) => K.testId === E), + D = j > 0 ? x.tests[j - 1] : void 0, + N = j < x.tests.length - 1 ? x.tests[j + 1] : void 0; + return { prev: D, next: N }; + }, [E, x]); + return ( + ct.useEffect(() => { + const j = (D) => { + if ( + D.target instanceof HTMLInputElement || + D.target instanceof HTMLTextAreaElement || + D.shiftKey || + D.ctrlKey || + D.metaKey || + D.altKey + ) + return; + const N = new URLSearchParams(u); + switch (D.key) { + case 'a': + (D.preventDefault(), ca('#?')); + break; + case 'p': + (D.preventDefault(), + N.delete('testId'), + N.delete('speedboard'), + ca(Na(N, 's:passed', !1))); + break; + case 'f': + (D.preventDefault(), + N.delete('testId'), + N.delete('speedboard'), + ca(Na(N, 's:failed', !1))); + break; + case 'ArrowLeft': + R && (D.preventDefault(), N.delete('testId'), ca(Cn({ test: R }, N) + O)); + break; + case 'ArrowRight': + U && (D.preventDefault(), N.delete('testId'), ca(Cn({ test: U }, N) + O)); + break; + } + }; + return ( + document.addEventListener('keydown', j), + () => document.removeEventListener('keydown', j) + ); + }, [R, U, O, S, u]), + ct.useEffect(() => { + X ? (document.title = X) : (document.title = 'Playwright Test Report'); + }, [X]), + m.jsx('div', { + className: 'htmlreport vbox px-4 pb-4', + children: m.jsxs('main', { + children: [ + l && m.jsx(Ev, { stats: l.json().stats, filterText: r, setFilterText: o }), + m.jsxs(Kf, { + predicate: ay, + children: [ + m.jsx(Y2, { + report: l == null ? void 0 : l.json(), + filteredStats: p, + metadataVisible: h, + toggleMetadataVisible: () => v((j) => !j), + }), + m.jsx(Pv, { + files: x.files, + expandedFiles: c, + setExpandedFiles: f, + projectNames: (l == null ? void 0 : l.json().projectNames) || [], + }), + ], + }), + m.jsxs(Kf, { + predicate: iy, + children: [ + m.jsx(Y2, { + report: l == null ? void 0 : l.json(), + filteredStats: p, + metadataVisible: h, + toggleMetadataVisible: () => v((j) => !j), + }), + l && m.jsx(ty, { report: l, tests: x.tests }), + ], + }), + m.jsx(Kf, { + predicate: ly, + children: + l && + m.jsx(cy, { report: l, next: U, prev: R, testId: E, testIdToFileIdMap: B }), + }), + ], + }), + }) + ); + }, + cy = ({ report: l, testIdToFileIdMap: u, next: c, prev: f, testId: r }) => { + const [o, h] = ct.useState('loading'), + v = +(se().get('run') || '0'); + if ( + (ct.useEffect(() => { + (async () => { + if (!r || (typeof o == 'object' && r === o.testId)) return; + const S = u.get(r); + if (!S) { + h('not-found'); + return; + } + const O = await l.entry(`${S}.json`); + h((O == null ? void 0 : O.tests.find((X) => X.testId === r)) || 'not-found'); + })(); + }, [o, l, r, u]), + o === 'loading') + ) + return m.jsx('div', { className: 'test-case-column' }); + if (o === 'not-found') + return m.jsxs('div', { + className: 'test-case-column', + children: [ + m.jsx(Or, { title: 'Test not found' }), + m.jsxs('div', { className: 'test-case-location', children: ['Test ID: ', r] }), + ], + }); + const { projectNames: y, metadata: A, options: E } = l.json(); + return m.jsx('div', { + className: 'test-case-column', + children: m.jsx(Vv, { + projectNames: y, + testRunMetadata: A, + options: E, + next: c, + prev: f, + test: o, + run: v, + }), + }); + }; + function sy(l, u) { + const c = { total: 0, duration: 0 }; + for (const f of l) { + const r = f.tests.filter((o) => u.matches(o)); + c.total += r.length; + for (const o of r) c.duration += o.duration; + } + return c; + } + function fy(l, u) { + const c = { files: [], tests: [] }; + for (const f of (l == null ? void 0 : l.json().files) || []) { + const r = f.tests.filter((o) => u.matches(o)); + (r.length && c.files.push({ ...f, tests: r }), c.tests.push(...r)); + } + return c; + } + function ry(l, u) { + const c = [], + f = new Map(); + for (const o of (l == null ? void 0 : l.json().files) || []) { + const h = o.tests.filter((v) => u.matches(v)); + for (const v of h) { + const y = v.path[0] ?? ''; + let A = f.get(y); + A || + ((A = { + fileId: y, + fileName: y, + tests: [], + stats: { total: 0, expected: 0, unexpected: 0, flaky: 0, skipped: 0, ok: !0 }, + }), + f.set(y, A), + c.push(A)); + const E = { ...v, path: v.path.slice(1) }; + A.tests.push(E); + } + } + c.sort((o, h) => o.fileName.localeCompare(h.fileName)); + const r = { files: c, tests: [] }; + for (const o of c) r.tests.push(...o.tests); + return r; + } + function oy(l, u) { + const f = ((l == null ? void 0 : l.json().files) || []) + .flatMap((r) => r.tests) + .filter((r) => u.matches(r)); + return (f.sort((r, o) => o.duration - r.duration), { files: [], tests: f }); + } + const dy = + "data:image/svg+xml,%3csvg%20width='400'%20height='400'%20viewBox='0%200%20400%20400'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='M136.444%20221.556C123.558%20225.213%20115.104%20231.625%20109.535%20238.032C114.869%20233.364%20122.014%20229.08%20131.652%20226.348C141.51%20223.554%20149.92%20223.574%20156.869%20224.915V219.481C150.941%20218.939%20144.145%20219.371%20136.444%20221.556ZM108.946%20175.876L61.0895%20188.484C61.0895%20188.484%2061.9617%20189.716%2063.5767%20191.36L104.153%20180.668C104.153%20180.668%20103.578%20188.077%2098.5847%20194.705C108.03%20187.559%20108.946%20175.876%20108.946%20175.876ZM149.005%20288.347C81.6582%20306.486%2046.0272%20228.438%2035.2396%20187.928C30.2556%20169.229%2028.0799%20155.067%2027.5%20145.928C27.4377%20144.979%2027.4665%20144.179%2027.5336%20143.446C24.04%20143.657%2022.3674%20145.473%2022.7077%20150.721C23.2876%20159.855%2025.4633%20174.016%2030.4473%20192.721C41.2301%20233.225%2076.8659%20311.273%20144.213%20293.134C158.872%20289.185%20169.885%20281.992%20178.152%20272.81C170.532%20279.692%20160.995%20285.112%20149.005%20288.347ZM161.661%20128.11V132.903H188.077C187.535%20131.206%20186.989%20129.677%20186.447%20128.11H161.661Z'%20fill='%232D4552'/%3e%3cpath%20d='M193.981%20167.584C205.861%20170.958%20212.144%20179.287%20215.465%20186.658L228.711%20190.42C228.711%20190.42%20226.904%20164.623%20203.57%20157.995C181.741%20151.793%20168.308%20170.124%20166.674%20172.496C173.024%20167.972%20182.297%20164.268%20193.981%20167.584ZM299.422%20186.777C277.573%20180.547%20264.145%20198.916%20262.535%20201.255C268.89%20196.736%20278.158%20193.031%20289.837%20196.362C301.698%20199.741%20307.976%20208.06%20311.307%20215.436L324.572%20219.212C324.572%20219.212%20322.736%20193.41%20299.422%20186.777ZM286.262%20254.795L176.072%20223.99C176.072%20223.99%20177.265%20230.038%20181.842%20237.869L274.617%20263.805C282.255%20259.386%20286.262%20254.795%20286.262%20254.795ZM209.867%20321.102C122.618%20297.71%20133.166%20186.543%20147.284%20133.865C153.097%20112.156%20159.073%2096.0203%20164.029%2085.204C161.072%2084.5953%20158.623%2086.1529%20156.203%2091.0746C150.941%20101.747%20144.212%20119.124%20137.7%20143.45C123.586%20196.127%20113.038%20307.29%20200.283%20330.682C241.406%20341.699%20273.442%20324.955%20297.323%20298.659C274.655%20319.19%20245.714%20330.701%20209.867%20321.102Z'%20fill='%232D4552'/%3e%3cpath%20d='M161.661%20262.296V239.863L99.3324%20257.537C99.3324%20257.537%20103.938%20230.777%20136.444%20221.556C146.302%20218.762%20154.713%20218.781%20161.661%20220.123V128.11H192.869C189.471%20117.61%20186.184%20109.526%20183.423%20103.909C178.856%2094.612%20174.174%20100.775%20163.545%20109.665C156.059%20115.919%20137.139%20129.261%20108.668%20136.933C80.1966%20144.61%2057.179%20142.574%2047.5752%20140.911C33.9601%20138.562%2026.8387%20135.572%2027.5049%20145.928C28.0847%20155.062%2030.2605%20169.224%2035.2445%20187.928C46.0272%20228.433%2081.663%20306.481%20149.01%20288.342C166.602%20283.602%20179.019%20274.233%20187.626%20262.291H161.661V262.296ZM61.0848%20188.484L108.946%20175.876C108.946%20175.876%20107.551%20194.288%2089.6087%20199.018C71.6614%20203.743%2061.0848%20188.484%2061.0848%20188.484Z'%20fill='%23E2574C'/%3e%3cpath%20d='M341.786%20129.174C329.345%20131.355%20299.498%20134.072%20262.612%20124.185C225.716%20114.304%20201.236%2097.0224%20191.537%2088.8994C177.788%2077.3834%20171.74%2069.3802%20165.788%2081.4857C160.526%2092.163%20153.797%20109.54%20147.284%20133.866C133.171%20186.543%20122.623%20297.706%20209.867%20321.098C297.093%20344.47%20343.53%20242.92%20357.644%20190.238C364.157%20165.917%20367.013%20147.5%20367.799%20135.625C368.695%20122.173%20359.455%20126.078%20341.786%20129.174ZM166.497%20172.756C166.497%20172.756%20180.246%20151.372%20203.565%20158C226.899%20164.628%20228.706%20190.425%20228.706%20190.425L166.497%20172.756ZM223.42%20268.713C182.403%20256.698%20176.077%20223.99%20176.077%20223.99L286.262%20254.796C286.262%20254.791%20264.021%20280.578%20223.42%20268.713ZM262.377%20201.495C262.377%20201.495%20276.107%20180.126%20299.422%20186.773C322.736%20193.411%20324.572%20219.208%20324.572%20219.208L262.377%20201.495Z'%20fill='%232EAD33'/%3e%3cpath%20d='M139.88%20246.04L99.3324%20257.532C99.3324%20257.532%20103.737%20232.44%20133.607%20222.496L110.647%20136.33L108.663%20136.933C80.1918%20144.611%2057.1742%20142.574%2047.5704%20140.911C33.9554%20138.563%2026.834%20135.572%2027.5001%20145.929C28.08%20155.063%2030.2557%20169.224%2035.2397%20187.929C46.0225%20228.433%2081.6583%20306.481%20149.005%20288.342L150.989%20287.719L139.88%20246.04ZM61.0848%20188.485L108.946%20175.876C108.946%20175.876%20107.551%20194.288%2089.6087%20199.018C71.6615%20203.743%2061.0848%20188.485%2061.0848%20188.485Z'%20fill='%23D65348'/%3e%3cpath%20d='M225.27%20269.163L223.415%20268.712C182.398%20256.698%20176.072%20223.99%20176.072%20223.99L232.89%20239.872L262.971%20124.281L262.607%20124.185C225.711%20114.304%20201.232%2097.0224%20191.532%2088.8994C177.783%2077.3834%20171.735%2069.3802%20165.783%2081.4857C160.526%2092.163%20153.797%20109.54%20147.284%20133.866C133.171%20186.543%20122.623%20297.706%20209.867%20321.097L211.655%20321.5L225.27%20269.163ZM166.497%20172.756C166.497%20172.756%20180.246%20151.372%20203.565%20158C226.899%20164.628%20228.706%20190.425%20228.706%20190.425L166.497%20172.756Z'%20fill='%231D8D22'/%3e%3cpath%20d='M141.946%20245.451L131.072%20248.537C133.641%20263.019%20138.169%20276.917%20145.276%20289.195C146.513%20288.922%20147.74%20288.687%20149%20288.342C152.302%20287.451%20155.364%20286.348%20158.312%20285.145C150.371%20273.361%20145.118%20259.789%20141.946%20245.451ZM137.7%20143.451C132.112%20164.307%20127.113%20194.326%20128.489%20224.436C130.952%20223.367%20133.554%20222.371%20136.444%20221.551L138.457%20221.101C136.003%20188.939%20141.308%20156.165%20147.284%20133.866C148.799%20128.225%20150.318%20122.978%20151.832%20118.085C149.393%20119.637%20146.767%20121.228%20143.776%20122.867C141.759%20129.093%20139.722%20135.898%20137.7%20143.451Z'%20fill='%23C04B41'/%3e%3c/svg%3e", + Ff = N5, + Rr = document.createElement('link'); + Rr.rel = 'shortcut icon'; + Rr.href = dy; + document.head.appendChild(Rr); + const hy = () => { + const [l, u] = ct.useState(); + return ( + ct.useEffect(() => { + const c = new my(); + c.load().then(() => { + var f; + ((f = document.getElementById('playwrightReportBase64')) == null || f.remove(), u(c)); + }); + }, []), + m.jsx(cv, { children: m.jsx(uy, { report: l }) }) + ); + }; + window.onload = () => { + (gv(), X5.createRoot(document.querySelector('#root')).render(m.jsx(hy, {}))); + }; + class my { + constructor() { + yn(this, '_entries', new Map()); + yn(this, '_json'); + } + async load() { + const u = document.getElementById('playwrightReportBase64').textContent, + c = new Ff.ZipReader(new Ff.Data64URIReader(u), { useWebWorkers: !1 }); + for (const f of await c.getEntries()) this._entries.set(f.filename, f); + this._json = await this.entry('report.json'); + } + json() { + return this._json; + } + async entry(u) { + const c = this._entries.get(u), + f = new Ff.TextWriter(); + return (await c.getData(f), JSON.parse(await f.getData())); + } + } + + -
+
- \ No newline at end of file + diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index a44a03e..26e0b59 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,25 +1,25 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: './tests', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: 1, // Avoid VM crash - reporter: 'html', - use: { - baseURL: 'http://localhost:5173', // Vite dev server - trace: 'on-first-retry', - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], - webServer: { - command: 'npm run dev', - url: 'http://localhost:5173', - reuseExistingServer: !process.env.CI, + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, // Avoid VM crash + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', // Vite dev server + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, }, + ], + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, }); diff --git a/frontend/src/App.css b/frontend/src/App.css index 3463f5d..09d1408 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -5,8 +5,6 @@ body { margin: 0; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } /* Dashboard Layout */ @@ -89,7 +87,7 @@ body { } .dashboard-main>* { - max-width: 600px; + max-width: 35em; margin: 0; } diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 303ac7e..196f32a 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -5,52 +5,55 @@ import App from './App'; import { describe, it, expect, vi, beforeEach } from 'vitest'; describe('App', () => { - beforeEach(() => { - vi.resetAllMocks(); - global.fetch = vi.fn(); + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + }); + + it('renders login on initial load (unauthenticated)', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: false, }); - - it('renders login on initial load (unauthenticated)', async () => { - (global.fetch as any).mockResolvedValueOnce({ - ok: false, - }); - window.history.pushState({}, 'Test page', '/v2/login'); - render(); - expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); + window.history.pushState({}, 'Test page', '/v2/login'); + render(); + expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); + }); + + it('renders dashboard when authenticated', async () => { + (global.fetch as any).mockImplementation((url: string) => { + if (url.includes('/api/auth')) return Promise.resolve({ ok: true }); + if (url.includes('/api/feed/')) return Promise.resolve({ ok: true, json: async () => [] }); + if (url.includes('/api/tag')) return Promise.resolve({ ok: true, json: async () => [] }); + return Promise.resolve({ ok: true }); // Fallback }); - it('renders dashboard when authenticated', async () => { - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/auth')) return Promise.resolve({ ok: true }); - if (url.includes('/api/feed/')) return Promise.resolve({ ok: true, json: async () => [] }); - if (url.includes('/api/tag')) return Promise.resolve({ ok: true, json: async () => [] }); - return Promise.resolve({ ok: true }); // Fallback - }); - - window.history.pushState({}, 'Test page', '/v2/'); - render(); + window.history.pushState({}, 'Test page', '/v2/'); + render(); - await waitFor(() => { - expect(screen.getByText('🐱')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText('🐱')).toBeInTheDocument(); + }); - // Test Logout - const logoutBtn = screen.getByText(/logout/i); - expect(logoutBtn).toBeInTheDocument(); + // Test Logout + const logoutBtn = screen.getByText(/logout/i); + expect(logoutBtn).toBeInTheDocument(); - // Mock window.location - Object.defineProperty(window, 'location', { - configurable: true, - value: { href: '' }, - }); + // Mock window.location + Object.defineProperty(window, 'location', { + configurable: true, + value: { href: '' }, + }); - (global.fetch as any).mockResolvedValueOnce({ ok: true }); + (global.fetch as any).mockResolvedValueOnce({ ok: true }); - fireEvent.click(logoutBtn); + fireEvent.click(logoutBtn); - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('/api/logout', expect.objectContaining({ method: 'POST' })); - expect(window.location.href).toBe('/v2/login'); - }); + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + '/api/logout', + expect.objectContaining({ method: 'POST' }) + ); + expect(window.location.href).toBe('/v2/login'); }); + }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9f53ace..4835cd3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,21 +35,47 @@ import FeedList from './components/FeedList'; import FeedItems from './components/FeedItems'; import Settings from './components/Settings'; -function Dashboard({ theme, setTheme }: { theme: string, setTheme: (t: string) => void }) { +function Dashboard({ theme, setTheme }: { theme: string; setTheme: (t: string) => void }) { const navigate = useNavigate(); const [sidebarVisible, setSidebarVisible] = useState(true); return ( -
+
-

setSidebarVisible(!sidebarVisible)} style={{ cursor: 'pointer' }}>🐱

+

setSidebarVisible(!sidebarVisible)} + style={{ cursor: 'pointer' }} + > + 🐱 +

diff --git a/frontend/src/components/FeedItem.css b/frontend/src/components/FeedItem.css index 1261737..1736032 100644 --- a/frontend/src/components/FeedItem.css +++ b/frontend/src/components/FeedItem.css @@ -1,114 +1,108 @@ .feed-item { - padding: 1rem; - margin-top: 5rem; - list-style: none; - border-bottom: none; + padding: 1rem; + margin-top: 5rem; + list-style: none; + border-bottom: none; } -.feed-item.read .item-title { - font-weight: normal; -} - -.feed-item.unread .item-title { - font-weight: bold; -} +/* removed read/unread specific font-weight to keep it always bold as requested */ .item-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 0.5rem; + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; } .item-title { - font-size: 1.35rem; - /* approx 24px */ - font-weight: bold; - text-decoration: none; - color: var(--link-color); - display: block; - flex: 1; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 1.8rem; + font-weight: bold; + text-decoration: none; + color: var(--link-color); + display: block; + flex: 1; } .item-title:hover { - text-decoration: none; - color: var(--link-color); + text-decoration: none; + color: var(--link-color); } .item-actions { - display: flex; - gap: 0.5rem; - margin-left: 1rem; + display: flex; + gap: 0.5rem; + margin-left: 1rem; } /* Legacy controls were simple text/links, but buttons are fine if minimal */ .star-btn { - background: none; - border: none; - cursor: pointer; - font-size: 1.25rem; - padding: 0 0 0 0.5rem; - vertical-align: middle; - transition: color 0.2s; - line-height: 1; + background: none; + border: none; + cursor: pointer; + font-size: 1.25rem; + padding: 0 0 0 0.5rem; + vertical-align: middle; + transition: color 0.2s; + line-height: 1; } .star-btn.is-starred { - color: blue; + color: blue; } .star-btn.is-unstarred { - color: black; + color: black; } .star-btn:hover { - color: blue; + color: blue; } .action-btn { - background: whitesmoke; - border: none; - cursor: pointer; - padding: 2px 6px; - font-size: 1rem; - color: blue; - font-weight: bold; + background: whitesmoke; + border: none; + cursor: pointer; + padding: 2px 6px; + font-size: 1rem; + color: blue; + font-weight: bold; } .action-btn:hover { - background-color: #eee; + background-color: #eee; } .dateline { - margin-top: 0; - font-weight: normal; - font-size: .75em; - color: #ccc; - margin-bottom: 1rem; + margin-top: 0; + font-weight: normal; + font-size: 0.75em; + color: #ccc; + margin-bottom: 1rem; } .dateline a { - color: #ccc; - text-decoration: none; + color: #ccc; + text-decoration: none; } .item-description { - color: #000; - line-height: 1.5; - font-size: 1rem; - margin-top: 1rem; + color: var(--text-color); + line-height: 1.5; + font-size: 1rem; + margin-top: 1rem; } .item-description img { - max-width: 100%; - height: auto; - display: block; - margin: 1rem 0; + max-width: 100%; + height: auto; + display: block; + margin: 1rem 0; } .item-description blockquote { - padding: 1rem 1rem 0 1rem; - border-left: 4px solid #ddd; - color: #666; - margin-left: 0; + padding: 1rem 1rem 0 1rem; + border-left: 4px solid #ddd; + color: #666; + margin-left: 0; } \ No newline at end of file diff --git a/frontend/src/components/FeedItem.test.tsx b/frontend/src/components/FeedItem.test.tsx index f0497c6..cb9aafa 100644 --- a/frontend/src/components/FeedItem.test.tsx +++ b/frontend/src/components/FeedItem.test.tsx @@ -6,66 +6,69 @@ import FeedItem from './FeedItem'; import type { Item } from '../types'; const mockItem: Item = { - _id: 1, - feed_id: 101, - title: 'Test Item', - url: 'http://example.com/item', - description: '

Description

', - publish_date: '2023-01-01', - read: false, - starred: false, - feed_title: 'Test Feed' + _id: 1, + feed_id: 101, + title: 'Test Item', + url: 'http://example.com/item', + description: '

Description

', + publish_date: '2023-01-01', + read: false, + starred: false, + feed_title: 'Test Feed', }; describe('FeedItem Component', () => { - beforeEach(() => { - vi.resetAllMocks(); - global.fetch = vi.fn(); - }); + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + }); - it('renders item details', () => { - render(); - expect(screen.getByText('Test Item')).toBeInTheDocument(); - expect(screen.getByText(/Test Feed/)).toBeInTheDocument(); - // Check for relative time or date formatting? For now just check it renders - }); + it('renders item details', () => { + render(); + expect(screen.getByText('Test Item')).toBeInTheDocument(); + expect(screen.getByText(/Test Feed/)).toBeInTheDocument(); + // Check for relative time or date formatting? For now just check it renders + }); - it('toggles star status', async () => { - (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => ({}) }); + it('toggles star status', async () => { + (global.fetch as any).mockResolvedValueOnce({ ok: true, json: async () => ({}) }); - render(); + render(); - const starBtn = screen.getByTitle('Star'); - expect(starBtn).toHaveTextContent('★'); - fireEvent.click(starBtn); + const starBtn = screen.getByTitle('Star'); + expect(starBtn).toHaveTextContent('★'); + fireEvent.click(starBtn); - // Optimistic update - expect(await screen.findByTitle('Unstar')).toHaveTextContent('★'); + // Optimistic update + expect(await screen.findByTitle('Unstar')).toHaveTextContent('★'); - expect(global.fetch).toHaveBeenCalledWith('/api/item/1', expect.objectContaining({ - method: 'PUT', - body: JSON.stringify({ - _id: 1, - read: false, - starred: true - }) - })); - }); + expect(global.fetch).toHaveBeenCalledWith( + '/api/item/1', + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({ + _id: 1, + read: false, + starred: true, + }), + }) + ); + }); - it('updates styling when read state changes', () => { - const { rerender } = render(); - const link = screen.getByText('Test Item'); - // Initial state: unread (bold) - // Note: checking computed style might be flaky in jsdom, but we can check the class on the parent - const listItem = link.closest('li'); - expect(listItem).toHaveClass('unread'); - expect(listItem).not.toHaveClass('read'); + it('updates styling when read state changes', () => { + const { rerender } = render(); + const link = screen.getByText('Test Item'); + // Initial state: unread (bold) + // Note: checking computed style might be flaky in jsdom, but we can check the class on the parent + const listItem = link.closest('li'); + expect(listItem).toHaveClass('unread'); + expect(listItem).not.toHaveClass('read'); - // Update prop to read - rerender(); + // Update prop to read + rerender(); - // Should now be read - expect(listItem).toHaveClass('read'); - expect(listItem).not.toHaveClass('unread'); - }); + // Should now be read + expect(listItem).toHaveClass('read'); + expect(listItem).not.toHaveClass('unread'); + }); }); diff --git a/frontend/src/components/FeedItem.tsx b/frontend/src/components/FeedItem.tsx index b86e60c..9b40114 100644 --- a/frontend/src/components/FeedItem.tsx +++ b/frontend/src/components/FeedItem.tsx @@ -3,86 +3,84 @@ import type { Item } from '../types'; import './FeedItem.css'; interface FeedItemProps { - item: Item; + item: Item; } export default function FeedItem({ item: initialItem }: FeedItemProps) { - const [item, setItem] = useState(initialItem); - const [loading, setLoading] = useState(false); + const [item, setItem] = useState(initialItem); + const [loading, setLoading] = useState(false); - useEffect(() => { - setItem(initialItem); - }, [initialItem]); + useEffect(() => { + setItem(initialItem); + }, [initialItem]); + const toggleStar = () => { + updateItem({ ...item, starred: !item.starred }); + }; - const toggleStar = () => { - updateItem({ ...item, starred: !item.starred }); - }; + const updateItem = (newItem: Item) => { + setLoading(true); + // Optimistic update + const previousItem = item; + setItem(newItem); - const updateItem = (newItem: Item) => { - setLoading(true); - // Optimistic update - const previousItem = item; - setItem(newItem); + fetch(`/api/item/${newItem._id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + _id: newItem._id, + read: newItem.read, + starred: newItem.starred, + }), + }) + .then((res) => { + if (!res.ok) { + throw new Error('Failed to update item'); + } + return res.json(); + }) + .then(() => { + // Confirm with server response if needed, but for now we trust the optimistic update + // or we could setItem(updated) if the server returns the full object + setLoading(false); + }) + .catch((err) => { + console.error('Error updating item:', err); + // Revert on error + setItem(previousItem); + setLoading(false); + }); + }; - fetch(`/api/item/${newItem._id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - _id: newItem._id, - read: newItem.read, - starred: newItem.starred, - }), - }) - .then((res) => { - if (!res.ok) { - throw new Error('Failed to update item'); - } - return res.json(); - }) - .then(() => { - // Confirm with server response if needed, but for now we trust the optimistic update - // or we could setItem(updated) if the server returns the full object - setLoading(false); - }) - .catch((err) => { - console.error('Error updating item:', err); - // Revert on error - setItem(previousItem); - setLoading(false); - }); - }; - - return ( -
  • -
    - - {item.title || '(No Title)'} - - -
    - - {item.description && ( -
    - )} -
  • - ); + return ( +
  • +
    + + {item.title || '(No Title)'} + + +
    + + {item.description && ( +
    + )} +
  • + ); } diff --git a/frontend/src/components/FeedItems.css b/frontend/src/components/FeedItems.css index 31394a4..02323a9 100644 --- a/frontend/src/components/FeedItems.css +++ b/frontend/src/components/FeedItems.css @@ -1,22 +1,23 @@ .feed-items { - padding: 1rem; + padding: 1rem 0; + /* Removing horizontal padding to avoid double-padding with FeedItem */ } .feed-items h2 { - margin-top: 0; - border-bottom: 2px solid #eee; - padding-bottom: 0.5rem; + margin-top: 0; + border-bottom: 2px solid #eee; + padding-bottom: 0.5rem; } .item-list { - list-style: none; - padding: 0; + list-style: none; + padding: 0; } .loading-more { - padding: 2rem; - text-align: center; - color: #888; - font-size: 0.9rem; - min-height: 50px; + padding: 2rem; + text-align: center; + color: #888; + font-size: 0.9rem; + min-height: 50px; } \ No newline at end of file diff --git a/frontend/src/components/FeedItems.test.tsx b/frontend/src/components/FeedItems.test.tsx index ea68a7c..4d96da9 100644 --- a/frontend/src/components/FeedItems.test.tsx +++ b/frontend/src/components/FeedItems.test.tsx @@ -6,220 +6,241 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import FeedItems from './FeedItems'; describe('FeedItems Component', () => { - beforeEach(() => { - vi.resetAllMocks(); - global.fetch = vi.fn(); - window.HTMLElement.prototype.scrollIntoView = vi.fn(); - - // Mock IntersectionObserver - class MockIntersectionObserver { - observe = vi.fn(); - unobserve = vi.fn(); - disconnect = vi.fn(); - } - window.IntersectionObserver = MockIntersectionObserver as any; + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + window.HTMLElement.prototype.scrollIntoView = vi.fn(); + + // Mock IntersectionObserver + class MockIntersectionObserver { + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); + } + window.IntersectionObserver = MockIntersectionObserver as any; + }); + + it('renders loading state', () => { + (global.fetch as any).mockImplementation(() => new Promise(() => {})); + render( + + + } /> + + + ); + expect(screen.getByText(/loading items/i)).toBeInTheDocument(); + }); + + it('renders items for a feed', async () => { + const mockItems = [ + { + _id: 101, + title: 'Item One', + url: 'http://example.com/1', + publish_date: '2023-01-01', + read: false, + }, + { + _id: 102, + title: 'Item Two', + url: 'http://example.com/2', + publish_date: '2023-01-02', + read: true, + }, + ]; + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockItems, }); - it('renders loading state', () => { - (global.fetch as any).mockImplementation(() => new Promise(() => { })); - render( - - - } /> - - - ); - expect(screen.getByText(/loading items/i)).toBeInTheDocument(); + render( + + + } /> + + + ); + + await waitFor(() => { + expect(screen.getByText('Item One')).toBeInTheDocument(); + }); + + const params = new URLSearchParams(); + params.append('feed_id', '1'); + params.append('read_filter', 'unread'); + expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`); + }); + + it('handles keyboard shortcuts', async () => { + const mockItems = [ + { _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }, + { _id: 102, title: 'Item 2', url: 'u2', read: true, starred: false }, + ]; + + (global.fetch as any).mockResolvedValue({ + ok: true, + json: async () => mockItems, + }); + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Item 1')).toBeVisible(); + }); + + // Press 'j' to select first item (index 0 -> 1 because it starts at -1... wait logic says min(prev+1)) + // init -1. j -> 0. + fireEvent.keyDown(window, { key: 'j' }); + + // Item 1 (index 0) should be selected. + // It's unread, so it should be marked read. + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + '/api/item/101', + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({ read: true, starred: false }), + }) + ); + }); + + // Press 'j' again -> index 1 (Item 2) + fireEvent.keyDown(window, { key: 'j' }); + + // Item 2 is already read, so no markRead call expected for it (mocks clear? no). + // let's check selection class if possible, but testing library doesn't easily check class on div wrapper unless we query it. + + // Press 's' to star Item 2 + fireEvent.keyDown(window, { key: 's' }); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + '/api/item/102', + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({ read: true, starred: true }), // toggled to true + }) + ); }); + }); - it('renders items for a feed', async () => { - const mockItems = [ - { _id: 101, title: 'Item One', url: 'http://example.com/1', publish_date: '2023-01-01', read: false }, - { _id: 102, title: 'Item Two', url: 'http://example.com/2', publish_date: '2023-01-02', read: true }, - ]; - - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => mockItems, - }); - - render( - - - } /> - - - ); - - await waitFor(() => { - expect(screen.getByText('Item One')).toBeInTheDocument(); - }); - - const params = new URLSearchParams(); - params.append('feed_id', '1'); - params.append('read_filter', 'unread'); - expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`); + it('marks items as read when scrolled past', async () => { + const mockItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }]; + (global.fetch as any).mockResolvedValue({ + ok: true, + json: async () => mockItems, }); - it('handles keyboard shortcuts', async () => { - const mockItems = [ - { _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }, - { _id: 102, title: 'Item 2', url: 'u2', read: true, starred: false }, - ]; - - (global.fetch as any).mockResolvedValue({ - ok: true, - json: async () => mockItems, - }); - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByText('Item 1')).toBeVisible(); - }); - - // Press 'j' to select first item (index 0 -> 1 because it starts at -1... wait logic says min(prev+1)) - // init -1. j -> 0. - fireEvent.keyDown(window, { key: 'j' }); - - // Item 1 (index 0) should be selected. - // It's unread, so it should be marked read. - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('/api/item/101', expect.objectContaining({ - method: 'PUT', - body: JSON.stringify({ read: true, starred: false }), - })); - }); - - // Press 'j' again -> index 1 (Item 2) - fireEvent.keyDown(window, { key: 'j' }); - - // Item 2 is already read, so no markRead call expected for it (mocks clear? no). - // let's check selection class if possible, but testing library doesn't easily check class on div wrapper unless we query it. - - // Press 's' to star Item 2 - fireEvent.keyDown(window, { key: 's' }); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('/api/item/102', expect.objectContaining({ - method: 'PUT', - body: JSON.stringify({ read: true, starred: true }), // toggled to true - })); - }); + // Capture the callback + let observerCallback: IntersectionObserverCallback = () => {}; + + // Override the mock to capture callback + class MockIntersectionObserver { + constructor(callback: IntersectionObserverCallback) { + observerCallback = callback; + } + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); + } + window.IntersectionObserver = MockIntersectionObserver as any; + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Item 1')).toBeVisible(); + }); + + // Simulate item leaving viewport at the top + // Element index is 0 + const entry = { + isIntersecting: false, + boundingClientRect: { top: -50 } as DOMRectReadOnly, + target: { getAttribute: () => '0' } as unknown as Element, + intersectionRatio: 0, + time: 0, + rootBounds: null, + intersectionRect: {} as DOMRectReadOnly, + } as IntersectionObserverEntry; + + // Use vi.waitUntil to wait for callback to be assigned if needed, + // though strictly synchronous render + effect should do it. + // Direct call: + act(() => { + observerCallback([entry], {} as IntersectionObserver); + }); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + '/api/item/101', + expect.objectContaining({ + method: 'PUT', + body: JSON.stringify({ read: true, starred: false }), + }) + ); + }); + }); + + it('loads more items when sentinel becomes visible', async () => { + const initialItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: true, starred: false }]; + const moreItems = [{ _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }]; + + (global.fetch as any) + .mockResolvedValueOnce({ ok: true, json: async () => initialItems }) + .mockResolvedValueOnce({ ok: true, json: async () => moreItems }); + + let observerCallback: IntersectionObserverCallback = () => {}; + class MockIntersectionObserver { + constructor(callback: IntersectionObserverCallback) { + observerCallback = callback; + } + observe = vi.fn(); + unobserve = vi.fn(); + disconnect = vi.fn(); + } + window.IntersectionObserver = MockIntersectionObserver as any; + + render( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Item 1')).toBeInTheDocument(); }); - it('marks items as read when scrolled past', async () => { - const mockItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: false, starred: false }]; - (global.fetch as any).mockResolvedValue({ - ok: true, - json: async () => mockItems, - }); - - // Capture the callback - let observerCallback: IntersectionObserverCallback = () => { }; - - // Override the mock to capture callback - class MockIntersectionObserver { - constructor(callback: IntersectionObserverCallback) { - observerCallback = callback; - } - observe = vi.fn(); - unobserve = vi.fn(); - disconnect = vi.fn(); - } - window.IntersectionObserver = MockIntersectionObserver as any; - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByText('Item 1')).toBeVisible(); - }); - - // Simulate item leaving viewport at the top - // Element index is 0 - const entry = { - isIntersecting: false, - boundingClientRect: { top: -50 } as DOMRectReadOnly, - target: { getAttribute: () => '0' } as unknown as Element, - intersectionRatio: 0, - time: 0, - rootBounds: null, - intersectionRect: {} as DOMRectReadOnly, - } as IntersectionObserverEntry; - - // Use vi.waitUntil to wait for callback to be assigned if needed, - // though strictly synchronous render + effect should do it. - // Direct call: - act(() => { - observerCallback([entry], {} as IntersectionObserver); - }); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('/api/item/101', expect.objectContaining({ - method: 'PUT', - body: JSON.stringify({ read: true, starred: false }), - })); - }); + // Simulate sentinel becoming visible + const entry = { + isIntersecting: true, + target: { id: 'load-more-sentinel' } as unknown as Element, + boundingClientRect: {} as DOMRectReadOnly, + intersectionRatio: 1, + time: 0, + rootBounds: null, + intersectionRect: {} as DOMRectReadOnly, + } as IntersectionObserverEntry; + + act(() => { + observerCallback([entry], {} as IntersectionObserver); }); - it('loads more items when sentinel becomes visible', async () => { - const initialItems = [{ _id: 101, title: 'Item 1', url: 'u1', read: true, starred: false }]; - const moreItems = [{ _id: 100, title: 'Item 0', url: 'u0', read: true, starred: false }]; - - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => initialItems }) - .mockResolvedValueOnce({ ok: true, json: async () => moreItems }); - - let observerCallback: IntersectionObserverCallback = () => { }; - class MockIntersectionObserver { - constructor(callback: IntersectionObserverCallback) { - observerCallback = callback; - } - observe = vi.fn(); - unobserve = vi.fn(); - disconnect = vi.fn(); - } - window.IntersectionObserver = MockIntersectionObserver as any; - - render( - - - - ); - - await waitFor(() => { - expect(screen.getByText('Item 1')).toBeInTheDocument(); - }); - - // Simulate sentinel becoming visible - const entry = { - isIntersecting: true, - target: { id: 'load-more-sentinel' } as unknown as Element, - boundingClientRect: {} as DOMRectReadOnly, - intersectionRatio: 1, - time: 0, - rootBounds: null, - intersectionRect: {} as DOMRectReadOnly, - } as IntersectionObserverEntry; - - act(() => { - observerCallback([entry], {} as IntersectionObserver); - }); - - await waitFor(() => { - expect(screen.getByText('Item 0')).toBeInTheDocument(); - const params = new URLSearchParams(); - params.append('max_id', '101'); - params.append('read_filter', 'unread'); - expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`); - }); + await waitFor(() => { + expect(screen.getByText('Item 0')).toBeInTheDocument(); + const params = new URLSearchParams(); + params.append('max_id', '101'); + params.append('read_filter', 'unread'); + expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`); }); + }); }); diff --git a/frontend/src/components/FeedItems.tsx b/frontend/src/components/FeedItems.tsx index bcee3b0..81c9139 100644 --- a/frontend/src/components/FeedItems.tsx +++ b/frontend/src/components/FeedItems.tsx @@ -5,227 +5,228 @@ import FeedItem from './FeedItem'; import './FeedItems.css'; export default function FeedItems() { - const { feedId, tagName } = useParams<{ feedId: string; tagName: string }>(); - const [searchParams] = useSearchParams(); - const filterFn = searchParams.get('filter') || 'unread'; - - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [error, setError] = useState(''); - - const fetchItems = (maxId?: string) => { - if (maxId) { - setLoadingMore(true); - } else { - setLoading(true); - setItems([]); + const { feedId, tagName } = useParams<{ feedId: string; tagName: string }>(); + const [searchParams] = useSearchParams(); + const filterFn = searchParams.get('filter') || 'unread'; + + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [error, setError] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(-1); + + const fetchItems = (maxId?: string) => { + if (maxId) { + setLoadingMore(true); + } else { + setLoading(true); + setItems([]); + } + setError(''); + + let url = '/api/stream'; + const params = new URLSearchParams(); + + if (feedId) { + params.append('feed_id', feedId); + } else if (tagName) { + params.append('tag', tagName); + } + + if (maxId) { + params.append('max_id', maxId); + } + + // Apply filters + const searchQuery = searchParams.get('q'); + if (searchQuery) { + params.append('q', searchQuery); + } + + if (filterFn === 'all') { + params.append('read_filter', 'all'); + } else if (filterFn === 'starred') { + params.append('starred', 'true'); + params.append('read_filter', 'all'); + } else { + // default to unread + if (!searchQuery) { + params.append('read_filter', 'unread'); + } + } + + const queryString = params.toString(); + if (queryString) { + url += `?${queryString}`; + } + + fetch(url) + .then((res) => { + if (!res.ok) { + throw new Error('Failed to fetch items'); } - setError(''); - - let url = '/api/stream'; - const params = new URLSearchParams(); - - if (feedId) { - params.append('feed_id', feedId); - } else if (tagName) { - params.append('tag', tagName); - } - + return res.json(); + }) + .then((data) => { if (maxId) { - params.append('max_id', maxId); - } - - // Apply filters - const searchQuery = searchParams.get('q'); - if (searchQuery) { - params.append('q', searchQuery); - } - - if (filterFn === 'all') { - params.append('read_filter', 'all'); - } else if (filterFn === 'starred') { - params.append('starred', 'true'); - params.append('read_filter', 'all'); + setItems((prev) => [...prev, ...data]); } else { - // default to unread - if (!searchQuery) { - params.append('read_filter', 'unread'); - } - } - - const queryString = params.toString(); - if (queryString) { - url += `?${queryString}`; + setItems(data); } - - fetch(url) - .then((res) => { - if (!res.ok) { - throw new Error('Failed to fetch items'); - } - return res.json(); - }) - .then((data) => { - if (maxId) { - setItems((prev) => [...prev, ...data]); - } else { - setItems(data); - } - setHasMore(data.length > 0); - setLoading(false); - setLoadingMore(false); - }) - .catch((err) => { - setError(err.message); - setLoading(false); - setLoadingMore(false); - }); - }; - - useEffect(() => { - fetchItems(); - setSelectedIndex(-1); - }, [feedId, tagName, filterFn, searchParams]); - - const [selectedIndex, setSelectedIndex] = useState(-1); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (items.length === 0) return; - - if (e.key === 'j') { - setSelectedIndex((prev) => { - const nextIndex = Math.min(prev + 1, items.length - 1); - if (nextIndex !== prev) { - const item = items[nextIndex]; - if (!item.read) { - markAsRead(item); - } - scrollToItem(nextIndex); - } - return nextIndex; - }); - } else if (e.key === 'k') { - setSelectedIndex((prev) => { - const nextIndex = Math.max(prev - 1, 0); - if (nextIndex !== prev) { - scrollToItem(nextIndex); - } - return nextIndex; - }); - } else if (e.key === 's') { - setSelectedIndex((currentIndex) => { - if (currentIndex >= 0 && currentIndex < items.length) { - toggleStar(items[currentIndex]); - } - return currentIndex; - }); + setHasMore(data.length > 0); + setLoading(false); + setLoadingMore(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + setLoadingMore(false); + }); + }; + + useEffect(() => { + fetchItems(); + setSelectedIndex(-1); + }, [feedId, tagName, filterFn, searchParams]); + + + const scrollToItem = (index: number) => { + const element = document.getElementById(`item-${index}`); + if (element) { + element.scrollIntoView({ behavior: 'auto', block: 'start' }); + } + }; + + const markAsRead = (item: Item) => { + const updatedItem = { ...item, read: true }; + // Optimistic update + setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i))); + + fetch(`/api/item/${item._id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ read: true, starred: item.starred }), + }).catch((err) => console.error('Failed to mark read', err)); + }; + + const toggleStar = (item: Item) => { + const updatedItem = { ...item, starred: !item.starred }; + // Optimistic update + setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i))); + + fetch(`/api/item/${item._id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ read: item.read, starred: !item.starred }), + }).catch((err) => console.error('Failed to toggle star', err)); + }; + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (items.length === 0) return; + + if (e.key === 'j') { + setSelectedIndex((prev) => { + const nextIndex = Math.min(prev + 1, items.length - 1); + if (nextIndex !== prev) { + const item = items[nextIndex]; + if (!item.read) { + markAsRead(item); } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [items]); - - const scrollToItem = (index: number) => { - const element = document.getElementById(`item-${index}`); - if (element) { - element.scrollIntoView({ behavior: 'auto', block: 'start' }); - } + scrollToItem(nextIndex); + } + return nextIndex; + }); + } else if (e.key === 'k') { + setSelectedIndex((prev) => { + const nextIndex = Math.max(prev - 1, 0); + if (nextIndex !== prev) { + scrollToItem(nextIndex); + } + return nextIndex; + }); + } else if (e.key === 's') { + setSelectedIndex((currentIndex) => { + if (currentIndex >= 0 && currentIndex < items.length) { + toggleStar(items[currentIndex]); + } + return currentIndex; + }); + } }; - const markAsRead = (item: Item) => { - const updatedItem = { ...item, read: true }; - // Optimistic update - setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i))); + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [items]); - fetch(`/api/item/${item._id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ read: true, starred: item.starred }), - }).catch((err) => console.error('Failed to mark read', err)); - }; - - const toggleStar = (item: Item) => { - const updatedItem = { ...item, starred: !item.starred }; - // Optimistic update - setItems((prevItems) => prevItems.map((i) => (i._id === item._id ? updatedItem : i))); - fetch(`/api/item/${item._id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ read: item.read, starred: !item.starred }), - }).catch((err) => console.error('Failed to toggle star', err)); - }; - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - // Infinity scroll sentinel - if (entry.target.id === 'load-more-sentinel') { - if (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) { - fetchItems(String(items[items.length - 1]._id)); - } - return; - } - - // If item is not intersecting and is above the viewport, it's been scrolled past - if (!entry.isIntersecting && entry.boundingClientRect.top < 0) { - const index = Number(entry.target.getAttribute('data-index')); - if (!isNaN(index) && index >= 0 && index < items.length) { - const item = items[index]; - if (!item.read) { - markAsRead(item); - } - } - } - }); - }, - { root: null, threshold: 0 } - ); - - items.forEach((_, index) => { - const el = document.getElementById(`item-${index}`); - if (el) observer.observe(el); + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + // Infinity scroll sentinel + if (entry.target.id === 'load-more-sentinel') { + if (entry.isIntersecting && !loadingMore && hasMore && items.length > 0) { + fetchItems(String(items[items.length - 1]._id)); + } + return; + } + + // If item is not intersecting and is above the viewport, it's been scrolled past + if (!entry.isIntersecting && entry.boundingClientRect.top < 0) { + const index = Number(entry.target.getAttribute('data-index')); + if (!isNaN(index) && index >= 0 && index < items.length) { + const item = items[index]; + if (!item.read) { + markAsRead(item); + } + } + } }); - - const sentinel = document.getElementById('load-more-sentinel'); - if (sentinel) observer.observe(sentinel); - - return () => observer.disconnect(); - }, [items, loadingMore, hasMore]); - - if (loading) return
    Loading items...
    ; - if (error) return
    Error: {error}
    ; - - - return ( -
    - {items.length === 0 ? ( -

    No items found.

    - ) : ( -
      - {items.map((item, index) => ( -
      setSelectedIndex(index)} - > - -
      - ))} - {hasMore && ( -
      - {loadingMore ? 'Loading more...' : ''} -
      - )} -
    - )} -
    + }, + { root: null, threshold: 0 } ); + + items.forEach((_, index) => { + const el = document.getElementById(`item-${index}`); + if (el) observer.observe(el); + }); + + const sentinel = document.getElementById('load-more-sentinel'); + if (sentinel) observer.observe(sentinel); + + return () => observer.disconnect(); + }, [items, loadingMore, hasMore]); + + if (loading) return
    Loading items...
    ; + if (error) return
    Error: {error}
    ; + + return ( +
    + {items.length === 0 ? ( +

    No items found.

    + ) : ( +
      + {items.map((item, index) => ( +
      setSelectedIndex(index)} + > + +
      + ))} + {hasMore && ( +
      + {loadingMore ? 'Loading more...' : ''} +
      + )} +
    + )} +
    + ); } diff --git a/frontend/src/components/FeedList.css b/frontend/src/components/FeedList.css index 0d6d26d..ff0f41b 100644 --- a/frontend/src/components/FeedList.css +++ b/frontend/src/components/FeedList.css @@ -1,170 +1,170 @@ .feed-list { - /* Removed card styling */ - padding: 0; - background: transparent; + /* Removed card styling */ + padding: 0; + background: transparent; } .search-section { - margin-bottom: 1.5rem; + margin-bottom: 1.5rem; } .search-form { - display: flex; + display: flex; } .search-input { - width: 100%; - padding: 0.5rem; - border: 1px solid #999; - background: #eee; - font-size: 1rem; - font-family: inherit; + width: 100%; + padding: 0.5rem; + border: 1px solid #999; + background: #eee; + font-size: 1rem; + font-family: inherit; } .search-input:focus { - outline: none; - background: white; - border-color: #000; + outline: none; + background: white; + border-color: #000; } .feed-list h2, .feed-section-header { - font-size: 1.2rem; - margin-bottom: 0.5rem; - border-bottom: 1px solid #999; - padding-bottom: 0.25rem; - text-transform: uppercase; - letter-spacing: 1px; - cursor: pointer; - user-select: none; - display: flex; - align-items: center; + font-size: 1.2rem; + margin-bottom: 0.5rem; + border-bottom: 1px solid #999; + padding-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 1px; + cursor: pointer; + user-select: none; + display: flex; + align-items: center; } .toggle-indicator { - font-size: 0.8rem; - margin-right: 0.5rem; - display: inline-block; - width: 1rem; - text-align: center; + font-size: 0.8rem; + margin-right: 0.5rem; + display: inline-block; + width: 1rem; + text-align: center; } .feed-list-items, .tag-list-items, .filter-list { - list-style: none; - padding: 0; - margin: 0; + list-style: none; + padding: 0; + margin: 0; } .sidebar-feed-item { - padding: 0.25rem 0; - border-bottom: none; - /* Clean look */ - display: flex; - justify-content: space-between; - align-items: center; + padding: 0.25rem 0; + border-bottom: none; + /* Clean look */ + display: flex; + justify-content: space-between; + align-items: center; } .feed-title { - color: var(--link-color); - text-decoration: none; - font-size: 0.9rem; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: var(--link-color); + text-decoration: none; + font-size: 0.9rem; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .feed-title:hover { - text-decoration: underline; - color: var(--link-color); + text-decoration: underline; + color: var(--link-color); } .feed-category { - display: none; - /* Hide category in sidebar list to save space */ + display: none; + /* Hide category in sidebar list to save space */ } .tag-section { - margin-top: 2rem; + margin-top: 2rem; } .tag-link { - color: var(--link-color); - text-decoration: none; - font-size: 0.9rem; - display: block; - padding: 0.1rem 0; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: var(--link-color); + text-decoration: none; + font-size: 0.9rem; + display: block; + padding: 0.1rem 0; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .tag-link:hover { - text-decoration: underline; - background: transparent; - color: var(--link-color); + text-decoration: underline; + background: transparent; + color: var(--link-color); } .filter-section { - margin-bottom: 2rem; + margin-bottom: 2rem; } .filter-list { - display: block; - list-style: none; - padding: 0; - margin: 0; + display: block; + list-style: none; + padding: 0; + margin: 0; } .filter-list li a { - text-decoration: none; - color: #333; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-variant: small-caps; - text-transform: lowercase; - font-size: 1.1rem; - display: block; - margin-bottom: 0.5rem; + text-decoration: none; + color: var(--text-color); + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-variant: small-caps; + text-transform: lowercase; + font-size: 1.1rem; + display: block; + margin-bottom: 0.5rem; } .filter-list li a:hover { - color: blue; - background-color: transparent; - text-decoration: underline; + color: blue; + background-color: transparent; + text-decoration: underline; } .feed-title.active, .tag-link.active, .filter-list li a.active, .theme-selector button.active { - font-weight: bold !important; + font-weight: bold !important; } .theme-section { - margin-top: 2rem; - padding-bottom: 2rem; + margin-top: 2rem; + padding-bottom: 2rem; } .theme-selector { - display: flex; - justify-content: space-between; - gap: 5px; + display: flex; + justify-content: space-between; + gap: 5px; } .theme-selector button { - font-size: 0.8rem; - padding: 0.2rem 0.5rem; - width: 30%; - background: whitesmoke; - color: blue; - border: 1px solid #ccc; - border-radius: 4px; - font-variant: small-caps; - text-transform: lowercase; + font-size: 0.8rem; + padding: 0.2rem 0.5rem; + width: 30%; + background: whitesmoke; + color: blue; + border: 1px solid #ccc; + border-radius: 4px; + font-variant: small-caps; + text-transform: lowercase; } .theme-selector button:hover { - background: #eee; + background: #eee; } .theme-selector button.active { - color: black; - border-color: #000; -} \ No newline at end of file + color: black; + border-color: #000; +} diff --git a/frontend/src/components/FeedList.test.tsx b/frontend/src/components/FeedList.test.tsx index d5f49b7..daa4d69 100644 --- a/frontend/src/components/FeedList.test.tsx +++ b/frontend/src/components/FeedList.test.tsx @@ -7,114 +7,126 @@ import FeedList from './FeedList'; import { BrowserRouter } from 'react-router-dom'; describe('FeedList Component', () => { - beforeEach(() => { - vi.resetAllMocks(); - global.fetch = vi.fn(); - }); - - it('renders loading state initially', () => { - (global.fetch as any).mockImplementation(() => new Promise(() => { })); - render( - - {/* @ts-ignore */} - { }} /> - - ); - expect(screen.getByText(/loading feeds/i)).toBeInTheDocument(); - }); - - it('renders list of feeds', async () => { - const mockFeeds = [ - { _id: 1, title: 'Feed One', url: 'http://example.com/rss', web_url: 'http://example.com', category: 'Tech' }, - { _id: 2, title: 'Feed Two', url: 'http://test.com/rss', web_url: 'http://test.com', category: 'News' }, - ]; - - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { - return Promise.resolve({ - ok: true, - json: async () => mockFeeds, - }); - } - if (url.includes('/api/tag')) { - return Promise.resolve({ - ok: true, - json: async () => [{ title: 'Tech' }], - }); - } - return Promise.reject(new Error(`Unknown URL: ${url}`)); + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + }); + + it('renders loading state initially', () => { + (global.fetch as any).mockImplementation(() => new Promise(() => {})); + render( + + {/* @ts-ignore */} + {}} /> + + ); + expect(screen.getByText(/loading feeds/i)).toBeInTheDocument(); + }); + + it('renders list of feeds', async () => { + const mockFeeds = [ + { + _id: 1, + title: 'Feed One', + url: 'http://example.com/rss', + web_url: 'http://example.com', + category: 'Tech', + }, + { + _id: 2, + title: 'Feed Two', + url: 'http://test.com/rss', + web_url: 'http://test.com', + category: 'News', + }, + ]; + + (global.fetch as any).mockImplementation((url: string) => { + if (url.includes('/api/feed/')) { + return Promise.resolve({ + ok: true, + json: async () => mockFeeds, }); + } + if (url.includes('/api/tag')) { + return Promise.resolve({ + ok: true, + json: async () => [{ title: 'Tech' }], + }); + } + return Promise.reject(new Error(`Unknown URL: ${url}`)); + }); - render( - - {/* @ts-ignore */} - { }} /> - - ); + render( + + {/* @ts-ignore */} + {}} /> + + ); - await waitFor(() => { - expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); + }); - // Expand feeds - fireEvent.click(screen.getByText(/feeds/i, { selector: 'h2' })); + // Expand feeds + fireEvent.click(screen.getByText(/feeds/i, { selector: 'h2' })); - await waitFor(() => { - expect(screen.getByText('Feed One')).toBeInTheDocument(); - expect(screen.getByText('Feed Two')).toBeInTheDocument(); - const techElements = screen.getAllByText('Tech'); - expect(techElements.length).toBeGreaterThan(0); - }); + await waitFor(() => { + expect(screen.getByText('Feed One')).toBeInTheDocument(); + expect(screen.getByText('Feed Two')).toBeInTheDocument(); + const techElements = screen.getAllByText('Tech'); + expect(techElements.length).toBeGreaterThan(0); }); + }); - it('handles fetch error', async () => { - (global.fetch as any).mockImplementation(() => Promise.reject(new Error('API Error'))); + it('handles fetch error', async () => { + (global.fetch as any).mockImplementation(() => Promise.reject(new Error('API Error'))); - render( - - {/* @ts-ignore */} - { }} /> - - ); + render( + + {/* @ts-ignore */} + {}} /> + + ); - await waitFor(() => { - expect(screen.getByText(/error: api error/i)).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText(/error: api error/i)).toBeInTheDocument(); }); - - it('handles empty feed list', async () => { - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { - return Promise.resolve({ - ok: true, - json: async () => [], - }); - } - if (url.includes('/api/tag')) { - return Promise.resolve({ - ok: true, - json: async () => [], - }); - } - return Promise.reject(new Error(`Unknown URL: ${url}`)); + }); + + it('handles empty feed list', async () => { + (global.fetch as any).mockImplementation((url: string) => { + if (url.includes('/api/feed/')) { + return Promise.resolve({ + ok: true, + json: async () => [], + }); + } + if (url.includes('/api/tag')) { + return Promise.resolve({ + ok: true, + json: async () => [], }); + } + return Promise.reject(new Error(`Unknown URL: ${url}`)); + }); - render( - - {/* @ts-ignore */} - { }} /> - - ); + render( + + {/* @ts-ignore */} + {}} /> + + ); - await waitFor(() => { - expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.queryByText(/loading feeds/i)).not.toBeInTheDocument(); + }); - // Expand feeds - fireEvent.click(screen.getByText(/feeds/i, { selector: 'h2' })); + // Expand feeds + fireEvent.click(screen.getByText(/feeds/i, { selector: 'h2' })); - await waitFor(() => { - expect(screen.getByText(/no feeds found/i)).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText(/no feeds found/i)).toBeInTheDocument(); }); + }); }); diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx index 56c96cd..497baf8 100644 --- a/frontend/src/components/FeedList.tsx +++ b/frontend/src/components/FeedList.tsx @@ -3,121 +3,151 @@ import { Link, useNavigate, useSearchParams, useLocation, useParams } from 'reac import type { Feed, Category } from '../types'; import './FeedList.css'; -export default function FeedList({ theme, setTheme }: { theme: string, setTheme: (t: string) => void }) { - const [feeds, setFeeds] = useState([]); - const [tags, setTags] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); - const [feedsExpanded, setFeedsExpanded] = useState(false); - const [searchQuery, setSearchQuery] = useState(''); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const location = useLocation(); - const { feedId, tagName } = useParams(); +export default function FeedList({ + theme, + setTheme, +}: { + theme: string; + setTheme: (t: string) => void; +}) { + const [feeds, setFeeds] = useState([]); + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [feedsExpanded, setFeedsExpanded] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const location = useLocation(); + const { feedId, tagName } = useParams(); - const currentFilter = searchParams.get('filter') || (location.pathname === '/' && !feedId && !tagName ? 'unread' : ''); + const currentFilter = + searchParams.get('filter') || + (location.pathname === '/' && !feedId && !tagName ? 'unread' : ''); - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - if (searchQuery.trim()) { - navigate(`/?q=${encodeURIComponent(searchQuery.trim())}`); - } - }; + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + if (searchQuery.trim()) { + navigate(`/?q=${encodeURIComponent(searchQuery.trim())}`); + } + }; - const toggleFeeds = () => { - setFeedsExpanded(!feedsExpanded); - }; + const toggleFeeds = () => { + setFeedsExpanded(!feedsExpanded); + }; - useEffect(() => { - Promise.all([ - fetch('/api/feed/').then(res => { - if (!res.ok) throw new Error('Failed to fetch feeds'); - return res.json(); - }), - fetch('/api/tag').then(res => { - if (!res.ok) throw new Error('Failed to fetch tags'); - return res.json(); - }) - ]) - .then(([feedsData, tagsData]) => { - setFeeds(feedsData); - setTags(tagsData); - setLoading(false); - }) - .catch((err) => { - setError(err.message); - setLoading(false); - }); - }, []); + useEffect(() => { + Promise.all([ + fetch('/api/feed/').then((res) => { + if (!res.ok) throw new Error('Failed to fetch feeds'); + return res.json(); + }), + fetch('/api/tag').then((res) => { + if (!res.ok) throw new Error('Failed to fetch tags'); + return res.json(); + }), + ]) + .then(([feedsData, tagsData]) => { + setFeeds(feedsData); + setTags(tagsData); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }, []); - if (loading) return
    Loading feeds...
    ; - if (error) return
    Error: {error}
    ; + if (loading) return
    Loading feeds...
    ; + if (error) return
    Error: {error}
    ; - return ( -
    -
    -
    - setSearchQuery(e.target.value)} - className="search-input" - /> -
    -
    -
    -
      -
    • Unread
    • -
    • All
    • -
    • Starred
    • -
    -
    -
    -

    - {feedsExpanded ? '▼' : '▶'} Feeds -

    - {feedsExpanded && ( - feeds.length === 0 ? ( -

    No feeds found.

    - ) : ( -
      - {feeds.map((feed) => ( -
    • - - {feed.title || feed.url} - - {feed.category && {feed.category}} -
    • - ))} -
    - ) - )} -
    + return ( +
    +
    +
    + setSearchQuery(e.target.value)} + className="search-input" + /> +
    +
    +
    +
      +
    • + + Unread + +
    • +
    • + + All + +
    • +
    • + + Starred + +
    • +
    +
    +
    +

    + {feedsExpanded ? '▼' : '▶'} Feeds +

    + {feedsExpanded && + (feeds.length === 0 ? ( +

    No feeds found.

    + ) : ( +
      + {feeds.map((feed) => ( +
    • + + {feed.title || feed.url} + + {feed.category && {feed.category}} +
    • + ))} +
    + ))} +
    - {tags && tags.length > 0 && ( -
    -

    Tags

    -
      - {tags.map((tag) => ( -
    • - - {tag.title} - -
    • - ))} -
    -
    - )} + {tags && tags.length > 0 && ( +
    +

    Tags

    +
      + {tags.map((tag) => ( +
    • + + {tag.title} + +
    • + ))} +
    +
    + )} -
    -

    Themes

    -
    - - - -
    -
    +
    +
    + + +
    - ); +
    +
    + ); } diff --git a/frontend/src/components/Login.css b/frontend/src/components/Login.css index f1ca976..6f40731 100644 --- a/frontend/src/components/Login.css +++ b/frontend/src/components/Login.css @@ -46,7 +46,7 @@ text-align: center; } -button[type="submit"] { +button[type='submit'] { width: 100%; padding: 0.75rem; background-color: #007bff; @@ -58,6 +58,6 @@ button[type="submit"] { transition: background-color 0.2s; } -button[type="submit"]:hover { +button[type='submit']:hover { background-color: #0056b3; } diff --git a/frontend/src/components/Login.test.tsx b/frontend/src/components/Login.test.tsx index ef946e2..aea7042 100644 --- a/frontend/src/components/Login.test.tsx +++ b/frontend/src/components/Login.test.tsx @@ -9,70 +9,73 @@ import Login from './Login'; global.fetch = vi.fn(); const renderLogin = () => { - render( - - - - ); + render( + + + + ); }; describe('Login Component', () => { - beforeEach(() => { - vi.resetAllMocks(); + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('renders login form', () => { + renderLogin(); + expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); + }); + + it('handles successful login', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: true, }); - it('renders login form', () => { - renderLogin(); - expect(screen.getByLabelText(/password/i)).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); - }); - - it('handles successful login', async () => { - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - }); - - renderLogin(); + renderLogin(); - fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'secret' } }); - fireEvent.click(screen.getByRole('button', { name: /login/i })); + fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'secret' } }); + fireEvent.click(screen.getByRole('button', { name: /login/i })); - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('/api/login', expect.objectContaining({ - method: 'POST', - })); - }); - // Navigation assertion is tricky without mocking useNavigate, - // but if no error is shown, we assume success path was taken - expect(screen.queryByText(/login failed/i)).not.toBeInTheDocument(); + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + '/api/login', + expect.objectContaining({ + method: 'POST', + }) + ); + }); + // Navigation assertion is tricky without mocking useNavigate, + // but if no error is shown, we assume success path was taken + expect(screen.queryByText(/login failed/i)).not.toBeInTheDocument(); + }); + + it('handles failed login', async () => { + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + json: async () => ({ message: 'Bad credentials' }), }); - it('handles failed login', async () => { - (global.fetch as any).mockResolvedValueOnce({ - ok: false, - json: async () => ({ message: 'Bad credentials' }), - }); - - renderLogin(); + renderLogin(); - fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'wrong' } }); - fireEvent.click(screen.getByRole('button', { name: /login/i })); + fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'wrong' } }); + fireEvent.click(screen.getByRole('button', { name: /login/i })); - await waitFor(() => { - expect(screen.getByText(/bad credentials/i)).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText(/bad credentials/i)).toBeInTheDocument(); }); + }); - it('handles network error', async () => { - (global.fetch as any).mockRejectedValueOnce(new Error('Network error')); + it('handles network error', async () => { + (global.fetch as any).mockRejectedValueOnce(new Error('Network error')); - renderLogin(); + renderLogin(); - fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'secret' } }); - fireEvent.click(screen.getByRole('button', { name: /login/i })); + fireEvent.change(screen.getByLabelText(/password/i), { target: { value: 'secret' } }); + fireEvent.click(screen.getByRole('button', { name: /login/i })); - await waitFor(() => { - expect(screen.getByText(/network error/i)).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText(/network error/i)).toBeInTheDocument(); }); + }); }); diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index 2e8bbf7..5f63248 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -3,52 +3,52 @@ import { useNavigate } from 'react-router-dom'; import './Login.css'; export default function Login() { - const [password, setPassword] = useState(''); - const [error, setError] = useState(''); - const navigate = useNavigate(); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); - const handleSubmit = async (e: FormEvent) => { - e.preventDefault(); - setError(''); + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(''); - try { - // Use URLSearchParams to send as form-urlencoded, matching backend expectation - const params = new URLSearchParams(); - params.append('password', password); + try { + // Use URLSearchParams to send as form-urlencoded, matching backend expectation + const params = new URLSearchParams(); + params.append('password', password); - const res = await fetch('/api/login', { - method: 'POST', - body: params, - }); + const res = await fetch('/api/login', { + method: 'POST', + body: params, + }); - if (res.ok) { - navigate('/'); - } else { - const data = await res.json(); - setError(data.message || 'Login failed'); - } - } catch (err) { - setError('Network error'); - } - }; + if (res.ok) { + navigate('/'); + } else { + const data = await res.json(); + setError(data.message || 'Login failed'); + } + } catch (err) { + setError('Network error'); + } + }; - return ( -
    -
    -

    neko rss mode

    -
    - - setPassword(e.target.value)} - autoFocus - /> -
    - {error &&
    {error}
    } - -
    + return ( +
    +
    +

    neko rss mode

    +
    + + setPassword(e.target.value)} + autoFocus + />
    - ); + {error &&
    {error}
    } + +
    +
    + ); } diff --git a/frontend/src/components/Settings.css b/frontend/src/components/Settings.css index 4065e88..6e74475 100644 --- a/frontend/src/components/Settings.css +++ b/frontend/src/components/Settings.css @@ -1,83 +1,84 @@ .settings-page { - padding: 2rem; - max-width: 800px; - margin: 0 auto; + padding: 2rem; + max-width: 800px; + margin: 0 auto; } .add-feed-section { - background: #f9f9f9; - padding: 1.5rem; - border-radius: 8px; - margin-bottom: 2rem; - border: 1px solid #eee; + background: #f9f9f9; + padding: 1.5rem; + border-radius: 8px; + margin-bottom: 2rem; + border: 1px solid #eee; } .add-feed-form { - display: flex; - gap: 1rem; + display: flex; + gap: 1rem; } .feed-input { - flex: 1; - padding: 0.5rem; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 1rem; + flex: 1; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; } .error-message { - color: #d32f2f; - margin-top: 1rem; + color: #d32f2f; + margin-top: 1rem; } .settings-feed-list { - list-style: none; - padding: 0; - border: 1px solid #eee; - border-radius: 8px; + list-style: none; + padding: 0; + border: 1px solid #eee; + border-radius: 8px; } .settings-feed-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - border-bottom: 1px solid #eee; + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #eee; } .settings-feed-item:last-child { - border-bottom: none; + border-bottom: none; } .feed-info { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .feed-title { - font-weight: bold; - font-size: 1.1rem; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: bold; + font-size: 1.1rem; } .feed-url { - color: #666; - font-size: 0.9rem; + color: #666; + font-size: 0.9rem; } .delete-btn { - background: #ff5252; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; + background: #ff5252; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; } .delete-btn:hover { - background: #ff1744; + background: #ff1744; } .delete-btn:disabled { - background: #ffcdd2; - cursor: not-allowed; -} \ No newline at end of file + background: #ffcdd2; + cursor: not-allowed; +} diff --git a/frontend/src/components/Settings.test.tsx b/frontend/src/components/Settings.test.tsx index a15192d..f46ce6f 100644 --- a/frontend/src/components/Settings.test.tsx +++ b/frontend/src/components/Settings.test.tsx @@ -5,88 +5,97 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import Settings from './Settings'; describe('Settings Component', () => { - beforeEach(() => { - vi.resetAllMocks(); - global.fetch = vi.fn(); - // Mock confirm - global.confirm = vi.fn(() => true); + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + // Mock confirm + global.confirm = vi.fn(() => true); + }); + + it('renders feed list', async () => { + const mockFeeds = [ + { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, + { _id: 2, title: 'Gaming', url: 'http://gaming.com/rss', category: 'gaming' }, + ]; + + (global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => mockFeeds, }); - it('renders feed list', async () => { - const mockFeeds = [ - { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, - { _id: 2, title: 'Gaming', url: 'http://gaming.com/rss', category: 'gaming' }, - ]; + render(); - (global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => mockFeeds, - }); - - render(); - - await waitFor(() => { - expect(screen.getByText('Tech News')).toBeInTheDocument(); - expect(screen.getByText('http://tech.com/rss')).toBeInTheDocument(); - expect(screen.getByText('Gaming')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText('Tech News')).toBeInTheDocument(); + expect(screen.getByText('http://tech.com/rss')).toBeInTheDocument(); + expect(screen.getByText('Gaming')).toBeInTheDocument(); + }); + }); + + it('adds a new feed', async () => { + (global.fetch as any) + .mockResolvedValueOnce({ ok: true, json: async () => [] }) // Initial load + .mockResolvedValueOnce({ ok: true, json: async () => ({}) }) // Add feed + .mockResolvedValueOnce({ + ok: true, + json: async () => [{ _id: 3, title: 'New Feed', url: 'http://new.com/rss' }], + }); // Refresh load + + render(); + + // Wait for initial load to finish + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); }); - it('adds a new feed', async () => { - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => [] }) // Initial load - .mockResolvedValueOnce({ ok: true, json: async () => ({}) }) // Add feed - .mockResolvedValueOnce({ ok: true, json: async () => [{ _id: 3, title: 'New Feed', url: 'http://new.com/rss' }] }); // Refresh load - - render(); - - // Wait for initial load to finish - await waitFor(() => { - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - }); - - const input = screen.getByPlaceholderText('https://example.com/feed.xml'); - const button = screen.getByText('Add Feed'); + const input = screen.getByPlaceholderText('https://example.com/feed.xml'); + const button = screen.getByText('Add Feed'); - fireEvent.change(input, { target: { value: 'http://new.com/rss' } }); - fireEvent.click(button); + fireEvent.change(input, { target: { value: 'http://new.com/rss' } }); + fireEvent.click(button); - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('/api/feed/', expect.objectContaining({ - method: 'POST', - body: JSON.stringify({ url: 'http://new.com/rss' }), - })); - }); + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith( + '/api/feed/', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ url: 'http://new.com/rss' }), + }) + ); + }); - // Wait for refresh - await waitFor(() => { - expect(screen.getByText('New Feed')).toBeInTheDocument(); - }); + // Wait for refresh + await waitFor(() => { + expect(screen.getByText('New Feed')).toBeInTheDocument(); }); + }); - it('deletes a feed', async () => { - const mockFeeds = [ - { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, - ]; + it('deletes a feed', async () => { + const mockFeeds = [ + { _id: 1, title: 'Tech News', url: 'http://tech.com/rss', category: 'tech' }, + ]; - (global.fetch as any) - .mockResolvedValueOnce({ ok: true, json: async () => mockFeeds }) // Initial load - .mockResolvedValueOnce({ ok: true }); // Delete + (global.fetch as any) + .mockResolvedValueOnce({ ok: true, json: async () => mockFeeds }) // Initial load + .mockResolvedValueOnce({ ok: true }); // Delete - render(); + render(); - await waitFor(() => { - expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); - expect(screen.getByText('Tech News')).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); + expect(screen.getByText('Tech News')).toBeInTheDocument(); + }); - const deleteBtn = screen.getByTitle('Delete Feed'); - fireEvent.click(deleteBtn); + const deleteBtn = screen.getByTitle('Delete Feed'); + fireEvent.click(deleteBtn); - await waitFor(() => { - expect(global.confirm).toHaveBeenCalled(); - expect(global.fetch).toHaveBeenCalledWith('/api/feed/1', expect.objectContaining({ method: 'DELETE' })); - expect(screen.queryByText('Tech News')).not.toBeInTheDocument(); - }); + await waitFor(() => { + expect(global.confirm).toHaveBeenCalled(); + expect(global.fetch).toHaveBeenCalledWith( + '/api/feed/1', + expect.objectContaining({ method: 'DELETE' }) + ); + expect(screen.queryByText('Tech News')).not.toBeInTheDocument(); }); + }); }); diff --git a/frontend/src/components/Settings.tsx b/frontend/src/components/Settings.tsx index def8ffe..b4f6a3b 100644 --- a/frontend/src/components/Settings.tsx +++ b/frontend/src/components/Settings.tsx @@ -3,119 +3,121 @@ import type { Feed } from '../types'; import './Settings.css'; export default function Settings() { - const [feeds, setFeeds] = useState([]); - const [newFeedUrl, setNewFeedUrl] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [feeds, setFeeds] = useState([]); + const [newFeedUrl, setNewFeedUrl] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); - useEffect(() => { - fetchFeeds(); - }, []); + const fetchFeeds = () => { + setLoading(true); + fetch('/api/feed/') + .then((res) => { + if (!res.ok) throw new Error('Failed to fetch feeds'); + return res.json(); + }) + .then((data) => { + setFeeds(data); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }; - const fetchFeeds = () => { - setLoading(true); - fetch('/api/feed/') - .then((res) => { - if (!res.ok) throw new Error('Failed to fetch feeds'); - return res.json(); - }) - .then((data) => { - setFeeds(data); - setLoading(false); - }) - .catch((err) => { - setError(err.message); - setLoading(false); - }); - }; + useEffect(() => { + fetchFeeds(); + }, []); - const handleAddFeed = (e: React.FormEvent) => { - e.preventDefault(); - if (!newFeedUrl) return; - setLoading(true); - fetch('/api/feed/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: newFeedUrl }), - }) - .then((res) => { - if (!res.ok) throw new Error('Failed to add feed'); - return res.json(); - }) - .then(() => { - setNewFeedUrl(''); - fetchFeeds(); // Refresh list (or we could append if server returns full feed object) - }) - .catch((err) => { - setError(err.message); - setLoading(false); - }); - }; - const handleDeleteFeed = (id: number) => { - if (!globalThis.confirm('Are you sure you want to delete this feed?')) return; + const handleAddFeed = (e: React.FormEvent) => { + e.preventDefault(); + if (!newFeedUrl) return; - setLoading(true); - fetch(`/api/feed/${id}`, { - method: 'DELETE', - }) - .then((res) => { - if (!res.ok) throw new Error('Failed to delete feed'); - setFeeds(feeds.filter((f) => f._id !== id)); - setLoading(false); - }) - .catch((err) => { - setError(err.message); - setLoading(false); - }); - }; + setLoading(true); + fetch('/api/feed/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: newFeedUrl }), + }) + .then((res) => { + if (!res.ok) throw new Error('Failed to add feed'); + return res.json(); + }) + .then(() => { + setNewFeedUrl(''); + fetchFeeds(); // Refresh list (or we could append if server returns full feed object) + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }; - return ( -
    -

    Settings

    + const handleDeleteFeed = (id: number) => { + if (!globalThis.confirm('Are you sure you want to delete this feed?')) return; -
    -

    Add New Feed

    -
    - setNewFeedUrl(e.target.value)} - placeholder="https://example.com/feed.xml" - required - className="feed-input" - disabled={loading} - /> - -
    - {error &&

    {error}

    } -
    + setLoading(true); + fetch(`/api/feed/${id}`, { + method: 'DELETE', + }) + .then((res) => { + if (!res.ok) throw new Error('Failed to delete feed'); + setFeeds(feeds.filter((f) => f._id !== id)); + setLoading(false); + }) + .catch((err) => { + setError(err.message); + setLoading(false); + }); + }; -
    -

    Manage Feeds

    - {loading &&

    Loading...

    } -
      - {feeds.map((feed) => ( -
    • -
      - {feed.title || '(No Title)'} - {feed.url} -
      - -
    • - ))} -
    -
    -
    - ); + return ( +
    +

    Settings

    + +
    +

    Add New Feed

    +
    + setNewFeedUrl(e.target.value)} + placeholder="https://example.com/feed.xml" + required + className="feed-input" + disabled={loading} + /> + +
    + {error &&

    {error}

    } +
    + +
    +

    Manage Feeds

    + {loading &&

    Loading...

    } +
      + {feeds.map((feed) => ( +
    • +
      + {feed.title || '(No Title)'} + {feed.url} +
      + +
    • + ))} +
    +
    +
    + ); } diff --git a/frontend/src/components/TagView.test.tsx b/frontend/src/components/TagView.test.tsx index d19d4bb..10872bc 100644 --- a/frontend/src/components/TagView.test.tsx +++ b/frontend/src/components/TagView.test.tsx @@ -6,79 +6,81 @@ import FeedList from './FeedList'; import FeedItems from './FeedItems'; describe('Tag View Integration', () => { - beforeEach(() => { - vi.resetAllMocks(); - global.fetch = vi.fn(); - }); + beforeEach(() => { + vi.resetAllMocks(); + global.fetch = vi.fn(); + }); - it('renders tags in FeedList and navigates to tag view', async () => { - const mockFeeds = [{ _id: 1, title: 'Feed 1', url: 'http://example.com/rss', category: 'Tech' }]; - const mockTags = [{ title: 'Tech' }, { title: 'News' }]; + it('renders tags in FeedList and navigates to tag view', async () => { + const mockFeeds = [ + { _id: 1, title: 'Feed 1', url: 'http://example.com/rss', category: 'Tech' }, + ]; + const mockTags = [{ title: 'Tech' }, { title: 'News' }]; - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/feed/')) { - return Promise.resolve({ - ok: true, - json: async () => mockFeeds, - }); - } - if (url.includes('/api/tag')) { - return Promise.resolve({ - ok: true, - json: async () => mockTags, - }); - } - return Promise.reject(new Error(`Unknown URL: ${url}`)); + (global.fetch as any).mockImplementation((url: string) => { + if (url.includes('/api/feed/')) { + return Promise.resolve({ + ok: true, + json: async () => mockFeeds, }); - - render( - - - - ); - - await waitFor(() => { - const techTags = screen.getAllByText('Tech'); - expect(techTags.length).toBeGreaterThan(0); - expect(screen.getByText('News')).toBeInTheDocument(); + } + if (url.includes('/api/tag')) { + return Promise.resolve({ + ok: true, + json: async () => mockTags, }); - - // Verify structure - const techTag = screen.getByText('News').closest('a'); - expect(techTag).toHaveAttribute('href', '/tag/News'); + } + return Promise.reject(new Error(`Unknown URL: ${url}`)); }); - it('fetches items by tag in FeedItems', async () => { - const mockItems = [ - { _id: 101, title: 'Tag Item 1', url: 'http://example.com/1', feed_title: 'Feed 1' } - ]; + render( + + + + ); - (global.fetch as any).mockImplementation((url: string) => { - if (url.includes('/api/stream')) { - return Promise.resolve({ - ok: true, - json: async () => mockItems, - }); - } - return Promise.reject(new Error(`Unknown URL: ${url}`)); - }); + await waitFor(() => { + const techTags = screen.getAllByText('Tech'); + expect(techTags.length).toBeGreaterThan(0); + expect(screen.getByText('News')).toBeInTheDocument(); + }); + + // Verify structure + const techTag = screen.getByText('News').closest('a'); + expect(techTag).toHaveAttribute('href', '/tag/News'); + }); - render( - - - } /> - - - ); + it('fetches items by tag in FeedItems', async () => { + const mockItems = [ + { _id: 101, title: 'Tag Item 1', url: 'http://example.com/1', feed_title: 'Feed 1' }, + ]; - await waitFor(() => { - // expect(screen.getByText('Tag: Tech')).toBeInTheDocument(); - expect(screen.getByText('Tag Item 1')).toBeInTheDocument(); + (global.fetch as any).mockImplementation((url: string) => { + if (url.includes('/api/stream')) { + return Promise.resolve({ + ok: true, + json: async () => mockItems, }); + } + return Promise.reject(new Error(`Unknown URL: ${url}`)); + }); - const params = new URLSearchParams(); - params.append('tag', 'Tech'); - params.append('read_filter', 'unread'); - expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`); + render( + + + } /> + + + ); + + await waitFor(() => { + // expect(screen.getByText('Tag: Tech')).toBeInTheDocument(); + expect(screen.getByText('Tag Item 1')).toBeInTheDocument(); }); + + const params = new URLSearchParams(); + params.append('tag', 'Tech'); + params.append('read_filter', 'unread'); + expect(global.fetch).toHaveBeenCalledWith(`/api/stream?${params.toString()}`); + }); }); diff --git a/frontend/src/index.css b/frontend/src/index.css index aca76c6..209a30a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -16,24 +16,18 @@ h5, :root { line-height: 1.5; - font-weight: 400; font-size: 18px; /* Light Mode Defaults */ --bg-color: #ffffff; --text-color: rgba(0, 0, 0, 0.87); --sidebar-bg: #ccc; - --link-color: #0000EE; + --link-color: #0000ee; /* Standard blue link */ color-scheme: light dark; color: var(--text-color); background-color: var(--bg-color); - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } @media (prefers-color-scheme: dark) { @@ -88,7 +82,7 @@ button { border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; - font-weight: 500; + font-weight: bold; font-family: inherit; background-color: #1a1a1a; cursor: pointer; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..df655ea 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App.tsx'; createRoot(document.getElementById('root')!).render( - , -) + +); diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 052d18e..5781184 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -2,39 +2,39 @@ import '@testing-library/jest-dom'; // Mock IntersectionObserver class IntersectionObserver { - readonly root: Element | null = null; - readonly rootMargin: string = ''; - readonly thresholds: ReadonlyArray = []; + readonly root: Element | null = null; + readonly rootMargin: string = ''; + readonly thresholds: ReadonlyArray = []; - constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) { - // nothing - } + constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) { + // nothing + } - observe(_target: Element): void { - // nothing - } + observe(_target: Element): void { + // nothing + } - unobserve(_target: Element): void { - // nothing - } + unobserve(_target: Element): void { + // nothing + } - disconnect(): void { - // nothing - } + disconnect(): void { + // nothing + } - takeRecords(): IntersectionObserverEntry[] { - return []; - } + takeRecords(): IntersectionObserverEntry[] { + return []; + } } Object.defineProperty(window, 'IntersectionObserver', { - writable: true, - configurable: true, - value: IntersectionObserver, + writable: true, + configurable: true, + value: IntersectionObserver, }); Object.defineProperty(globalThis, 'IntersectionObserver', { - writable: true, - configurable: true, - value: IntersectionObserver, + writable: true, + configurable: true, + value: IntersectionObserver, }); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 4c1110f..1feea1f 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,24 +1,24 @@ export interface Feed { - _id: number; - url: string; - web_url: string; - title: string; - category: string; + _id: number; + url: string; + web_url: string; + title: string; + category: string; } export interface Item { - _id: number; - feed_id: number; - title: string; - url: string; - description: string; - publish_date: string; - read: boolean; - starred: boolean; - full_content?: string; - header_image?: string; - feed_title?: string; + _id: number; + feed_id: number; + title: string; + url: string; + description: string; + publish_date: string; + read: boolean; + starred: boolean; + full_content?: string; + header_image?: string; + feed_title?: string; } export interface Category { - title: string; + title: string; } diff --git a/frontend/test-results/.last-run.json b/frontend/test-results/.last-run.json index cbcc1fb..f740f7c 100644 --- a/frontend/test-results/.last-run.json +++ b/frontend/test-results/.last-run.json @@ -1,4 +1,4 @@ { "status": "passed", "failedTests": [] -} \ No newline at end of file +} diff --git a/frontend/tests/e2e.spec.ts b/frontend/tests/e2e.spec.ts index ca4b4ad..3f4898a 100644 --- a/frontend/tests/e2e.spec.ts +++ b/frontend/tests/e2e.spec.ts @@ -1,56 +1,66 @@ import { test, expect } from '@playwright/test'; test.describe('Neko Reader E2E', () => { - test('should allow login, viewing feeds, and logout', async ({ page }) => { - // 1. Go to Login - await page.goto('/v2/login'); - await expect(page).toHaveTitle(/Neko/); - - // 2. Login - // 2. Login - // Password is empty by default in test env - await page.click('button[type="submit"]'); - - // Check for error message if login failed (optional, for debugging) - // await expect(page.locator('.error-message')).toBeVisible({ timeout: 2000 }).catch(() => {}); - - // 3. Verify Dashboard - // Keep checking for /v2/ or /v2 - await expect(page).toHaveURL(/.*\/v2\/?$/); - await expect(page.locator('h1.logo')).toContainText('🐱'); - await expect(page.getByText('Logout')).toBeVisible(); - - // 4. Verify Feed List - await page.click('text=Settings'); - await expect(page).toHaveURL(/.*\/v2\/settings/); - - // Add a feed - const feedUrl = 'http://rss.cnn.com/rss/cnn_topstories.rss'; - await page.fill('input[type="url"]', feedUrl); - await page.click('text=Add Feed'); - - // Wait for it to appear - await expect(page.getByText(feedUrl)).toBeVisible(); - - // 5. Navigate to Feed - await page.goto('/v2/'); - // Default view is now the stream. - // It should NOT show "Select a feed" anymore. - // Wait for items or "No items found" or loading state - await expect(page.locator('.feed-items').or(page.locator('.feed-items-loading')).or(page.getByText('No items found'))).toBeVisible({ timeout: 10000 }); - - // 6. Verify Tag View - // Go to a tag URL (simulated, since we can't easily add tags via UI in this test yet without setup) - // But we can check if the route loads without crashing - await page.goto('/v2/tag/Tech'); - // The TagView component might show "Category: Tech" or "Tag: Tech" or just items. - // In the current FeedItems.tsx it doesn't show a header, but it should load. - // The TagView component might show "Category: Tech" or "Tag: Tech" or just items. - // In the current FeedItems.tsx it doesn't show a header, but it should load. - await expect(page.locator('.feed-items').or(page.locator('.feed-items-loading')).or(page.getByText('No items found'))).toBeVisible({ timeout: 10000 }); - - // 7. Logout - await page.click('text=Logout'); - await expect(page).toHaveURL(/.*\/v2\/login/); - }); + test('should allow login, viewing feeds, and logout', async ({ page }) => { + // 1. Go to Login + await page.goto('/v2/login'); + await expect(page).toHaveTitle(/Neko/); + + // 2. Login + // 2. Login + // Password is empty by default in test env + await page.click('button[type="submit"]'); + + // Check for error message if login failed (optional, for debugging) + // await expect(page.locator('.error-message')).toBeVisible({ timeout: 2000 }).catch(() => {}); + + // 3. Verify Dashboard + // Keep checking for /v2/ or /v2 + await expect(page).toHaveURL(/.*\/v2\/?$/); + await expect(page.locator('h1.logo')).toContainText('🐱'); + await expect(page.getByText('Logout')).toBeVisible(); + + // 4. Verify Feed List + await page.click('text=Settings'); + await expect(page).toHaveURL(/.*\/v2\/settings/); + + // Add a feed + const feedUrl = 'http://rss.cnn.com/rss/cnn_topstories.rss'; + await page.fill('input[type="url"]', feedUrl); + await page.click('text=Add Feed'); + + // Wait for it to appear + await expect(page.getByText(feedUrl)).toBeVisible(); + + // 5. Navigate to Feed + await page.goto('/v2/'); + // Default view is now the stream. + // It should NOT show "Select a feed" anymore. + // Wait for items or "No items found" or loading state + await expect( + page + .locator('.feed-items') + .or(page.locator('.feed-items-loading')) + .or(page.getByText('No items found')) + ).toBeVisible({ timeout: 10000 }); + + // 6. Verify Tag View + // Go to a tag URL (simulated, since we can't easily add tags via UI in this test yet without setup) + // But we can check if the route loads without crashing + await page.goto('/v2/tag/Tech'); + // The TagView component might show "Category: Tech" or "Tag: Tech" or just items. + // In the current FeedItems.tsx it doesn't show a header, but it should load. + // The TagView component might show "Category: Tech" or "Tag: Tech" or just items. + // In the current FeedItems.tsx it doesn't show a header, but it should load. + await expect( + page + .locator('.feed-items') + .or(page.locator('.feed-items-loading')) + .or(page.getByText('No items found')) + ).toBeVisible({ timeout: 10000 }); + + // 7. Logout + await page.click('text=Logout'); + await expect(page).toHaveURL(/.*\/v2\/login/); + }); }); diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index f03834c..3fcc60b 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -3,15 +3,9 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", - "types": [ - "vite/client" - ], + "types": ["vite/client"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", @@ -28,11 +22,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": [ - "src" - ], - "exclude": [ - "**/*.test.tsx", - "**/*.test.ts" - ] -} \ No newline at end of file + "include": ["src"], + "exclude": ["**/*.test.tsx", "**/*.test.ts"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ffef60..d32ff68 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,7 +1,4 @@ { "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 61d6465..50145d1 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -2,14 +2,9 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2023", - "lib": [ - "ES2023" - ], + "lib": ["ES2023"], "module": "ESNext", - "types": [ - "node", - "vitest" - ], + "types": ["node", "vitest"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", @@ -25,7 +20,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": [ - "vite.config.ts" - ] -} \ No newline at end of file + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e04aec1..025cbb3 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,6 +1,6 @@ /// -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ @@ -10,6 +10,6 @@ export default defineConfig({ proxy: { '/api': 'http://127.0.0.1:4994', '/image': 'http://127.0.0.1:4994', - } + }, }, -}) +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index eee2cfa..9cb79ae 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -1,11 +1,11 @@ /// -import { defineConfig } from 'vite' +import { defineConfig } from 'vite'; export default defineConfig({ - test: { - globals: true, - environment: 'jsdom', - setupFiles: './src/setupTests.ts', - exclude: ['**/node_modules/**', '**/dist/**', '**/tests/**'], - }, -}) + test: { + globals: true, + environment: 'jsdom', + setupFiles: './src/setupTests.ts', + exclude: ['**/node_modules/**', '**/dist/**', '**/tests/**'], + }, +}); diff --git a/importer/importer.go b/importer/importer.go deleted file mode 100644 index 73a2cd8..0000000 --- a/importer/importer.go +++ /dev/null @@ -1,89 +0,0 @@ -package importer - -import ( - // "bufio" - "encoding/json" - //"fmt" - "io" - "log" - "os" - - "adammathes.com/neko/models/feed" - "adammathes.com/neko/models/item" -) - -type IItem struct { - Title string `json:"title"` - Url string `json:"url"` - Description string `json:"description"` - ReadState bool `json:"read"` - Starred bool `json:"starred"` - Date *IDate `json:"date"` - Feed *IFeed `json:"feed"` -} - -type IFeed struct { - Url string `json:"url"` - Title string `json:"title"` - WebUrl string `json:"web_url"` -} - -type IDate struct { - Date string `json:"$date"` -} - -func ImportJSON(filename string) error { - - f, err := os.Open(filename) - if err != nil { - return err - } - defer f.Close() - - dec := json.NewDecoder(f) - for { - var ii IItem - if err := dec.Decode(&ii); err == io.EOF { - break - } else if err != nil { - return err - } else { - err := InsertIItem(&ii) - if err != nil { - log.Println(err) - } - } - } - return nil -} - -func InsertIItem(ii *IItem) error { - var f feed.Feed - - if ii.Feed == nil { - return nil - } - err := f.ByUrl(ii.Feed.Url) - if err != nil { - f.Url = ii.Feed.Url - f.Title = ii.Feed.Title - err = f.Create() - if err != nil { - return err - } - } - - var i item.Item - i.FeedId = f.Id - i.Title = ii.Title - i.Url = ii.Url - i.Description = ii.Description - - if ii.Date != nil { - i.PublishDate = ii.Date.Date - } - - err = i.Create() - log.Printf("inserted %s\n", i.Url) - return err -} diff --git a/importer/importer_test.go b/importer/importer_test.go deleted file mode 100644 index 59f06f1..0000000 --- a/importer/importer_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package importer - -import ( - "os" - "path/filepath" - "testing" - - "adammathes.com/neko/config" - "adammathes.com/neko/models" -) - -func setupTestDB(t *testing.T) { - t.Helper() - config.Config.DBFile = filepath.Join(t.TempDir(), "test.db") - models.InitDB() - t.Cleanup(func() { - if models.DB != nil { - models.DB.Close() - } - }) -} - -func TestInsertIItem(t *testing.T) { - setupTestDB(t) - - ii := &IItem{ - Title: "Test Article", - Url: "https://example.com/article", - Description: "A test article description", - ReadState: false, - Starred: true, - Date: &IDate{Date: "2024-01-15 10:00:00"}, - Feed: &IFeed{ - Url: "https://example.com/feed", - Title: "Example Feed", - }, - } - - InsertIItem(ii) - - // Verify the feed was created - var feedCount int - models.DB.QueryRow("SELECT COUNT(*) FROM feed").Scan(&feedCount) - if feedCount != 1 { - t.Errorf("Expected 1 feed, got %d", feedCount) - } - - // Verify the item was created - var itemCount int - models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&itemCount) - if itemCount != 1 { - t.Errorf("Expected 1 item, got %d", itemCount) - } -} - -func TestInsertIItemNilFeed(t *testing.T) { - setupTestDB(t) - - ii := &IItem{ - Title: "No Feed Item", - Url: "https://example.com/nofeed", - Feed: nil, - } - - // Should not panic - InsertIItem(ii) - - var itemCount int - models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&itemCount) - if itemCount != 0 { - t.Errorf("Expected 0 items (nil feed should be skipped), got %d", itemCount) - } -} - -func TestInsertIItemExistingFeed(t *testing.T) { - setupTestDB(t) - - // Insert feed first - models.DB.Exec("INSERT INTO feed(url, title) VALUES(?, ?)", "https://example.com/feed", "Existing Feed") - - ii := &IItem{ - Title: "New Article", - Url: "https://example.com/new-article", - Description: "New article desc", - Date: &IDate{Date: "2024-01-15"}, - Feed: &IFeed{ - Url: "https://example.com/feed", - Title: "Existing Feed", - }, - } - - InsertIItem(ii) - - // Should still be just 1 feed - var feedCount int - models.DB.QueryRow("SELECT COUNT(*) FROM feed").Scan(&feedCount) - if feedCount != 1 { - t.Errorf("Expected 1 feed (reuse existing), got %d", feedCount) - } -} - -func TestImportJSON(t *testing.T) { - setupTestDB(t) - - dir := t.TempDir() - jsonFile := filepath.Join(dir, "import.json") - - content := `{"title":"Article 1","url":"https://example.com/1","description":"desc1","read":false,"starred":false,"date":{"$date":"2024-01-01"},"feed":{"url":"https://example.com/feed","title":"Feed 1"}} -{"title":"Article 2","url":"https://example.com/2","description":"desc2","read":true,"starred":true,"date":{"$date":"2024-01-02"},"feed":{"url":"https://example.com/feed","title":"Feed 1"}}` - - err := os.WriteFile(jsonFile, []byte(content), 0644) - if err != nil { - t.Fatal(err) - } - - ImportJSON(jsonFile) - - var itemCount int - models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&itemCount) - if itemCount != 2 { - t.Errorf("Expected 2 items after import, got %d", itemCount) - } - - var feedCount int - models.DB.QueryRow("SELECT COUNT(*) FROM feed").Scan(&feedCount) - if feedCount != 1 { - t.Errorf("Expected 1 feed after import, got %d", feedCount) - } -} - -func TestImportJSONInvalid(t *testing.T) { - setupTestDB(t) - dir := t.TempDir() - jsonFile := filepath.Join(dir, "invalid.json") - os.WriteFile(jsonFile, []byte("not json"), 0644) - - err := ImportJSON(jsonFile) - if err == nil { - t.Error("ImportJSON should error on invalid JSON") - } -} - -func TestImportJSONNonexistent(t *testing.T) { - setupTestDB(t) - err := ImportJSON("/nonexistent/file.json") - if err == nil { - t.Error("ImportJSON should error on nonexistent file") - } -} diff --git a/internal/crawler/crawler.go b/internal/crawler/crawler.go new file mode 100644 index 0000000..10253d8 --- /dev/null +++ b/internal/crawler/crawler.go @@ -0,0 +1,161 @@ +package crawler + +import ( + "io/ioutil" + "log" + "net/http" + "time" + + "adammathes.com/neko/internal/vlog" + "adammathes.com/neko/models/feed" + "adammathes.com/neko/models/item" + "github.com/mmcdole/gofeed" +) + +const MAX_CRAWLERS = 5 + +func Crawl() { + crawlJobs := make(chan *feed.Feed, 100) + results := make(chan string, 100) + + feeds, err := feed.All() + if err != nil { + log.Fatal(err) + } + + for i := 0; i < MAX_CRAWLERS; i++ { + vlog.Printf("spawning crawl worker %d\n", i) + go CrawlWorker(crawlJobs, results) + } + + for _, f := range feeds { + vlog.Printf("sending crawl job %s\n", f.Url) + crawlJobs <- f + } + close(crawlJobs) + + for i := 0; i < len(feeds); i++ { + vlog.Println(<-results) + } + close(results) +} + +func CrawlWorker(feeds <-chan *feed.Feed, results chan<- string) { + + for f := range feeds { + vlog.Printf("crawl job received %s\n", f.Url) + CrawlFeed(f, results) + vlog.Printf("crawl job finished %s\n", f.Url) + } +} + +/* +Simple HTTP Get fnx with custom user agent header +*/ +func GetFeedContent(feedURL string) string { + + // introduce delays for testing + // n := time.Duration(rand.Int63n(3)) + // time.Sleep(n * time.Second) + + c := &http.Client{ + // give up after 5 seconds + Timeout: 5 * time.Second, + } + + request, err := http.NewRequest("GET", feedURL, nil) + if err != nil { + log.Fatalln(err) + } + + userAgent := "neko RSS Crawler +https://github.com/adammathes/neko" + request.Header.Set("User-Agent", userAgent) + resp, err := c.Do(request) + + if err != nil { + return "" + } + + if resp != nil { + defer func() { + ce := resp.Body.Close() + if ce != nil { + err = ce + } + }() + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return "" + } + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "" + } + return string(bodyBytes) +} + +/* +TODO: sanitize input on crawl +*/ +func CrawlFeed(f *feed.Feed, ch chan<- string) { + c := &http.Client{ + // give up after 5 seconds + Timeout: 5 * time.Second, + } + + fp := gofeed.NewParser() + fp.Client = c + + content := GetFeedContent(f.Url) + feed, err := fp.ParseString(content) + if err != nil { + vlog.Println(err) + ch <- "failed parse for " + f.Url + "\n" + return + } + + f.Title = feed.Title + f.WebUrl = feed.Link + f.Update() + + for _, i := range feed.Items { + vlog.Printf("storing item: %s\n", i.Link) + var item item.Item + item.Title = i.Title + item.Url = i.Link + + item.Description = i.Description + if len(i.Content) > len(item.Description) { + item.Description = i.Content + } + + // a lot of RSS2.0 generated by wordpress and others + // uses + e, ok := i.Extensions["content"]["encoded"] + var encoded = "" + if ok { + encoded = e[0].Value + } + if len(encoded) > len(item.Description) { + item.Description = encoded + } + + if i.PublishedParsed != nil { + item.PublishDate = i.PublishedParsed.Format("2006-01-02 15:04:05") + } else { + item.PublishDate = time.Now().Format("2006-01-02 15:04:05") + } + + item.FeedId = f.Id + err := item.Create() + if err != nil { + vlog.Println(err) + } + // else { + // item.GetFullContent() + //} + } + ch <- "successfully crawled " + f.Url + "\n" +} diff --git a/internal/crawler/crawler_test.go b/internal/crawler/crawler_test.go new file mode 100644 index 0000000..e0c4c6b --- /dev/null +++ b/internal/crawler/crawler_test.go @@ -0,0 +1,278 @@ +package crawler + +import ( + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "adammathes.com/neko/config" + "adammathes.com/neko/models" + "adammathes.com/neko/models/feed" +) + +func setupTestDB(t *testing.T) { + t.Helper() + config.Config.DBFile = ":memory:" + models.InitDB() + t.Cleanup(func() { + if models.DB != nil { + models.DB.Close() + } + }) +} + +func TestGetFeedContentSuccess(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ua := r.Header.Get("User-Agent") + if ua == "" { + t.Error("Request should include User-Agent") + } + w.WriteHeader(200) + w.Write([]byte("Test")) + })) + defer ts.Close() + + content := GetFeedContent(ts.URL) + if content == "" { + t.Error("GetFeedContent should return content for valid URL") + } + if content != "Test" { + t.Errorf("Unexpected content: %q", content) + } +} + +func TestGetFeedContentBadURL(t *testing.T) { + content := GetFeedContent("http://invalid.invalid.invalid:99999/feed") + if content != "" { + t.Errorf("GetFeedContent should return empty string for bad URL, got %q", content) + } +} + +func TestGetFeedContent404(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(404) + })) + defer ts.Close() + + content := GetFeedContent(ts.URL) + if content != "" { + t.Errorf("GetFeedContent should return empty for 404, got %q", content) + } +} + +func TestGetFeedContent500(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + defer ts.Close() + + content := GetFeedContent(ts.URL) + if content != "" { + t.Errorf("GetFeedContent should return empty for 500, got %q", content) + } +} + +func TestGetFeedContentUserAgent(t *testing.T) { + var receivedUA string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedUA = r.Header.Get("User-Agent") + w.WriteHeader(200) + w.Write([]byte("ok")) + })) + defer ts.Close() + + GetFeedContent(ts.URL) + expected := "neko RSS Crawler +https://github.com/adammathes/neko" + if receivedUA != expected { + t.Errorf("Expected UA %q, got %q", expected, receivedUA) + } +} + +func TestCrawlFeedWithTestServer(t *testing.T) { + setupTestDB(t) + + rssContent := ` + + + Test Feed + https://example.com + + Article 1 + https://example.com/article1 + First article + Mon, 01 Jan 2024 00:00:00 GMT + + + Article 2 + https://example.com/article2 + Second article + Tue, 02 Jan 2024 00:00:00 GMT + + +` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/rss+xml") + w.WriteHeader(200) + w.Write([]byte(rssContent)) + })) + defer ts.Close() + + // Create a feed pointing to the test server + f := &feed.Feed{Url: ts.URL, Title: "Test"} + f.Create() + + ch := make(chan string, 1) + CrawlFeed(f, ch) + result := <-ch + + if result == "" { + t.Error("CrawlFeed should send a result") + } + + // Verify items were created + var count int + models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&count) + if count != 2 { + t.Errorf("Expected 2 items, got %d", count) + } +} + +func TestCrawlFeedBadContent(t *testing.T) { + setupTestDB(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("not xml at all")) + })) + defer ts.Close() + + f := &feed.Feed{Url: ts.URL, Title: "Bad"} + f.Create() + + ch := make(chan string, 1) + CrawlFeed(f, ch) + result := <-ch + + if result == "" { + t.Error("CrawlFeed should send a result even on failure") + } +} + +func TestCrawlWorker(t *testing.T) { + setupTestDB(t) + + rssContent := ` + + + Worker Feed + https://example.com + + Worker Article + https://example.com/worker-article + An article + + +` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte(rssContent)) + })) + defer ts.Close() + + f := &feed.Feed{Url: ts.URL, Title: "Worker Test"} + f.Create() + + feeds := make(chan *feed.Feed, 1) + results := make(chan string, 1) + + feeds <- f + close(feeds) + + CrawlWorker(feeds, results) + result := <-results + + if result == "" { + t.Error("CrawlWorker should produce a result") + } +} + +func TestCrawl(t *testing.T) { + setupTestDB(t) + + rssContent := ` + + + Crawl Feed + https://example.com + + Crawl Article + https://example.com/crawl-article + Article for crawl test + + +` + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte(rssContent)) + })) + defer ts.Close() + + f := &feed.Feed{Url: ts.URL, Title: "Full Crawl"} + f.Create() + + // Should not panic + Crawl() + + var count int + models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&count) + if count != 1 { + t.Errorf("Expected 1 item after crawl, got %d", count) + } +} + +func TestCrawlFeedWithExtensions(t *testing.T) { + setupTestDB(t) + + rssContent := ` + + + Extension Feed + + Extension Article + https://example.com/ext + Short description + + + +` + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte(rssContent)) + })) + defer ts.Close() + + f := &feed.Feed{Url: ts.URL, Title: "Extension Test"} + f.Create() + + ch := make(chan string, 1) + CrawlFeed(f, ch) + <-ch + + var itemTitle, itemDesc string + err := models.DB.QueryRow("SELECT title, description FROM item WHERE feed_id = ?", f.Id).Scan(&itemTitle, &itemDesc) + if err != nil { + log.Fatal(err) + } + + if itemTitle != "Extension Article" { + t.Errorf("Expected title 'Extension Article', got %q", itemTitle) + } + if !strings.Contains(itemDesc, "Much longer content") { + t.Errorf("Expected description to contain encoded content, got %q", itemDesc) + } +} diff --git a/internal/crawler/integration_test.go b/internal/crawler/integration_test.go new file mode 100644 index 0000000..633b60f --- /dev/null +++ b/internal/crawler/integration_test.go @@ -0,0 +1,67 @@ +package crawler + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "adammathes.com/neko/models/feed" + "adammathes.com/neko/models/item" +) + +func TestCrawlIntegration(t *testing.T) { + setupTestDB(t) + + // Mock RSS feed server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/rss+xml") + os.Stdout.Write([]byte("serving mock rss\n")) + fmt.Fprint(w, ` + + + Test Feed + http://example.com/ + Test Description + + Test Item 1 + http://example.com/item1 + Item 1 Description + Mon, 01 Jan 2024 00:00:00 +0000 + + +`) + })) + defer ts.Close() + + // Add the feed + f := &feed.Feed{Url: ts.URL} + err := f.Create() + if err != nil { + t.Fatalf("Failed to create feed: %v", err) + } + + // Crawl + ch := make(chan string, 1) + CrawlFeed(f, ch) + + res := <-ch + if res == "" { + t.Fatal("CrawlFeed returned empty result") + } + + // Verify items were stored + items, err := item.Filter(0, f.Id, "", false, false, 0, "") + if err != nil { + t.Fatalf("Failed to filter items: %v", err) + } + + if len(items) != 1 { + t.Fatalf("Expected 1 item, got %d", len(items)) + } + + if items[0].Title != "Test Item 1" { + t.Errorf("Expected 'Test Item 1', got %q", items[0].Title) + } +} diff --git a/internal/exporter/exporter.go b/internal/exporter/exporter.go new file mode 100644 index 0000000..9172fec --- /dev/null +++ b/internal/exporter/exporter.go @@ -0,0 +1,61 @@ +package exporter + +import ( + "adammathes.com/neko/models/feed" + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "html/template" +) + +func ExportFeeds(format string) string { + feeds, err := feed.All() + if err != nil { + panic(err) + } + + s := "" + switch format { + case "text": + for _, f := range feeds { + s = s + fmt.Sprintf("%s\n", f.Url) + } + + case "opml": + s = s + fmt.Sprintf(`neko feeds`) + s = s + fmt.Sprintf("\n") + for _, f := range feeds { + b, _ := xml.Marshal(f) + s = s + fmt.Sprintf("%s\n", string(b)) + } + s = s + fmt.Sprintf(``) + + case "json": + js, _ := json.Marshal(feeds) + s = fmt.Sprintf("%s\n", js) + + case "html": + htmlTemplateString := ` + +feeds + + + + +` + var bts bytes.Buffer + htmlTemplate, err := template.New("feeds").Parse(htmlTemplateString) + err = htmlTemplate.Execute(&bts, feeds) + if err != nil { + panic(err) + } + s = bts.String() + } + + return s +} diff --git a/internal/exporter/exporter_test.go b/internal/exporter/exporter_test.go new file mode 100644 index 0000000..d4cc994 --- /dev/null +++ b/internal/exporter/exporter_test.go @@ -0,0 +1,111 @@ +package exporter + +import ( + "encoding/json" + "strings" + "testing" + + "adammathes.com/neko/config" + "adammathes.com/neko/models" +) + +func setupTestDB(t *testing.T) { + t.Helper() + config.Config.DBFile = ":memory:" + models.InitDB() + t.Cleanup(func() { + if models.DB != nil { + models.DB.Close() + } + }) +} + +func seedFeeds(t *testing.T) { + t.Helper() + _, err := models.DB.Exec("INSERT INTO feed(url, web_url, title, category) VALUES(?, ?, ?, ?)", + "https://a.com/feed", "https://a.com", "Alpha Feed", "tech") + if err != nil { + t.Fatal(err) + } + _, err = models.DB.Exec("INSERT INTO feed(url, web_url, title, category) VALUES(?, ?, ?, ?)", + "https://b.com/feed", "https://b.com", "Beta Feed", "news") + if err != nil { + t.Fatal(err) + } +} + +func TestExportText(t *testing.T) { + setupTestDB(t) + seedFeeds(t) + + result := ExportFeeds("text") + if !strings.Contains(result, "https://a.com/feed") { + t.Error("text export should contain feed URL a") + } + if !strings.Contains(result, "https://b.com/feed") { + t.Error("text export should contain feed URL b") + } +} + +func TestExportJSON(t *testing.T) { + setupTestDB(t) + seedFeeds(t) + + result := ExportFeeds("json") + var feeds []interface{} + err := json.Unmarshal([]byte(result), &feeds) + if err != nil { + t.Fatalf("JSON export should be valid JSON: %v", err) + } + if len(feeds) != 2 { + t.Errorf("JSON export should contain 2 feeds, got %d", len(feeds)) + } +} + +func TestExportOPML(t *testing.T) { + setupTestDB(t) + seedFeeds(t) + + result := ExportFeeds("opml") + if !strings.Contains(result, "") { + t.Error("OPML export should close opml tag") + } +} + +func TestExportHTML(t *testing.T) { + setupTestDB(t) + seedFeeds(t) + + result := ExportFeeds("html") + if !strings.Contains(result, "") { + t.Error("HTML export should contain html tag") + } + if !strings.Contains(result, "Alpha Feed") { + t.Error("HTML export should contain feed title") + } +} + +func TestExportUnknownFormat(t *testing.T) { + setupTestDB(t) + seedFeeds(t) + + result := ExportFeeds("unknown") + if result != "" { + t.Errorf("Unknown format should return empty string, got %q", result) + } +} + +func TestExportEmpty(t *testing.T) { + setupTestDB(t) + + result := ExportFeeds("text") + if result != "" { + t.Errorf("Export with no feeds should be empty, got %q", result) + } +} diff --git a/internal/importer/importer.go b/internal/importer/importer.go new file mode 100644 index 0000000..73a2cd8 --- /dev/null +++ b/internal/importer/importer.go @@ -0,0 +1,89 @@ +package importer + +import ( + // "bufio" + "encoding/json" + //"fmt" + "io" + "log" + "os" + + "adammathes.com/neko/models/feed" + "adammathes.com/neko/models/item" +) + +type IItem struct { + Title string `json:"title"` + Url string `json:"url"` + Description string `json:"description"` + ReadState bool `json:"read"` + Starred bool `json:"starred"` + Date *IDate `json:"date"` + Feed *IFeed `json:"feed"` +} + +type IFeed struct { + Url string `json:"url"` + Title string `json:"title"` + WebUrl string `json:"web_url"` +} + +type IDate struct { + Date string `json:"$date"` +} + +func ImportJSON(filename string) error { + + f, err := os.Open(filename) + if err != nil { + return err + } + defer f.Close() + + dec := json.NewDecoder(f) + for { + var ii IItem + if err := dec.Decode(&ii); err == io.EOF { + break + } else if err != nil { + return err + } else { + err := InsertIItem(&ii) + if err != nil { + log.Println(err) + } + } + } + return nil +} + +func InsertIItem(ii *IItem) error { + var f feed.Feed + + if ii.Feed == nil { + return nil + } + err := f.ByUrl(ii.Feed.Url) + if err != nil { + f.Url = ii.Feed.Url + f.Title = ii.Feed.Title + err = f.Create() + if err != nil { + return err + } + } + + var i item.Item + i.FeedId = f.Id + i.Title = ii.Title + i.Url = ii.Url + i.Description = ii.Description + + if ii.Date != nil { + i.PublishDate = ii.Date.Date + } + + err = i.Create() + log.Printf("inserted %s\n", i.Url) + return err +} diff --git a/internal/importer/importer_test.go b/internal/importer/importer_test.go new file mode 100644 index 0000000..59f06f1 --- /dev/null +++ b/internal/importer/importer_test.go @@ -0,0 +1,149 @@ +package importer + +import ( + "os" + "path/filepath" + "testing" + + "adammathes.com/neko/config" + "adammathes.com/neko/models" +) + +func setupTestDB(t *testing.T) { + t.Helper() + config.Config.DBFile = filepath.Join(t.TempDir(), "test.db") + models.InitDB() + t.Cleanup(func() { + if models.DB != nil { + models.DB.Close() + } + }) +} + +func TestInsertIItem(t *testing.T) { + setupTestDB(t) + + ii := &IItem{ + Title: "Test Article", + Url: "https://example.com/article", + Description: "A test article description", + ReadState: false, + Starred: true, + Date: &IDate{Date: "2024-01-15 10:00:00"}, + Feed: &IFeed{ + Url: "https://example.com/feed", + Title: "Example Feed", + }, + } + + InsertIItem(ii) + + // Verify the feed was created + var feedCount int + models.DB.QueryRow("SELECT COUNT(*) FROM feed").Scan(&feedCount) + if feedCount != 1 { + t.Errorf("Expected 1 feed, got %d", feedCount) + } + + // Verify the item was created + var itemCount int + models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&itemCount) + if itemCount != 1 { + t.Errorf("Expected 1 item, got %d", itemCount) + } +} + +func TestInsertIItemNilFeed(t *testing.T) { + setupTestDB(t) + + ii := &IItem{ + Title: "No Feed Item", + Url: "https://example.com/nofeed", + Feed: nil, + } + + // Should not panic + InsertIItem(ii) + + var itemCount int + models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&itemCount) + if itemCount != 0 { + t.Errorf("Expected 0 items (nil feed should be skipped), got %d", itemCount) + } +} + +func TestInsertIItemExistingFeed(t *testing.T) { + setupTestDB(t) + + // Insert feed first + models.DB.Exec("INSERT INTO feed(url, title) VALUES(?, ?)", "https://example.com/feed", "Existing Feed") + + ii := &IItem{ + Title: "New Article", + Url: "https://example.com/new-article", + Description: "New article desc", + Date: &IDate{Date: "2024-01-15"}, + Feed: &IFeed{ + Url: "https://example.com/feed", + Title: "Existing Feed", + }, + } + + InsertIItem(ii) + + // Should still be just 1 feed + var feedCount int + models.DB.QueryRow("SELECT COUNT(*) FROM feed").Scan(&feedCount) + if feedCount != 1 { + t.Errorf("Expected 1 feed (reuse existing), got %d", feedCount) + } +} + +func TestImportJSON(t *testing.T) { + setupTestDB(t) + + dir := t.TempDir() + jsonFile := filepath.Join(dir, "import.json") + + content := `{"title":"Article 1","url":"https://example.com/1","description":"desc1","read":false,"starred":false,"date":{"$date":"2024-01-01"},"feed":{"url":"https://example.com/feed","title":"Feed 1"}} +{"title":"Article 2","url":"https://example.com/2","description":"desc2","read":true,"starred":true,"date":{"$date":"2024-01-02"},"feed":{"url":"https://example.com/feed","title":"Feed 1"}}` + + err := os.WriteFile(jsonFile, []byte(content), 0644) + if err != nil { + t.Fatal(err) + } + + ImportJSON(jsonFile) + + var itemCount int + models.DB.QueryRow("SELECT COUNT(*) FROM item").Scan(&itemCount) + if itemCount != 2 { + t.Errorf("Expected 2 items after import, got %d", itemCount) + } + + var feedCount int + models.DB.QueryRow("SELECT COUNT(*) FROM feed").Scan(&feedCount) + if feedCount != 1 { + t.Errorf("Expected 1 feed after import, got %d", feedCount) + } +} + +func TestImportJSONInvalid(t *testing.T) { + setupTestDB(t) + dir := t.TempDir() + jsonFile := filepath.Join(dir, "invalid.json") + os.WriteFile(jsonFile, []byte("not json"), 0644) + + err := ImportJSON(jsonFile) + if err == nil { + t.Error("ImportJSON should error on invalid JSON") + } +} + +func TestImportJSONNonexistent(t *testing.T) { + setupTestDB(t) + err := ImportJSON("/nonexistent/file.json") + if err == nil { + t.Error("ImportJSON should error on nonexistent file") + } +} diff --git a/internal/vlog/vlog.go b/internal/vlog/vlog.go new file mode 100644 index 0000000..ab48478 --- /dev/null +++ b/internal/vlog/vlog.go @@ -0,0 +1,25 @@ +// vlog -- verbose logger -- wraps log functions and only performs them if "verbose" +package vlog + +import ( + "fmt" +) + +var VERBOSE bool + +func init() { + VERBOSE=false +} + +func Printf(format string, v ...interface{}) { + if VERBOSE { + fmt.Printf(format, v...) + } +} + +func Println(v ...interface{}) { + if VERBOSE { + fmt.Println(v...) + } +} + diff --git a/internal/vlog/vlog_test.go b/internal/vlog/vlog_test.go new file mode 100644 index 0000000..9def0f0 --- /dev/null +++ b/internal/vlog/vlog_test.go @@ -0,0 +1,78 @@ +package vlog + +import ( + "bytes" + "fmt" + "os" + "testing" +) + +func captureStdout(f func()) string { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + f() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + buf.ReadFrom(r) + return buf.String() +} + +func TestPrintfVerbose(t *testing.T) { + VERBOSE = true + defer func() { VERBOSE = false }() + + output := captureStdout(func() { + Printf("hello %s", "world") + }) + expected := fmt.Sprintf("hello %s", "world") + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestPrintfSilent(t *testing.T) { + VERBOSE = false + + output := captureStdout(func() { + Printf("hello %s", "world") + }) + if output != "" { + t.Errorf("expected empty output when not verbose, got %q", output) + } +} + +func TestPrintlnVerbose(t *testing.T) { + VERBOSE = true + defer func() { VERBOSE = false }() + + output := captureStdout(func() { + Println("hello", "world") + }) + expected := fmt.Sprintln("hello", "world") + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestPrintlnSilent(t *testing.T) { + VERBOSE = false + + output := captureStdout(func() { + Println("hello", "world") + }) + if output != "" { + t.Errorf("expected empty output when not verbose, got %q", output) + } +} + +func TestInit(t *testing.T) { + // init() sets VERBOSE to false + if VERBOSE != false { + t.Error("VERBOSE should default to false") + } +} diff --git a/main.go b/main.go deleted file mode 100644 index 3dfe7ec..0000000 --- a/main.go +++ /dev/null @@ -1,152 +0,0 @@ -package main - -import ( - "fmt" - "os" - "time" - - "adammathes.com/neko/config" - "adammathes.com/neko/crawler" - "adammathes.com/neko/exporter" - "adammathes.com/neko/models" - "adammathes.com/neko/models/feed" - - "flag" - - "adammathes.com/neko/vlog" - "adammathes.com/neko/web" -) - -var Version, Build string - -func main() { - if err := Run(os.Args[1:]); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } -} - -func Run(args []string) error { - var help, update, verbose, proxyImages bool - var configFile, dbfile, newFeed, export, password string - var port, minutes int - - f := flag.NewFlagSet("neko", flag.ContinueOnError) - - // config file - f.StringVar(&configFile, "config", "", "read configuration from file") - f.StringVar(&configFile, "c", "", "read configuration from file (short)") - - // commands - f.BoolVar(&help, "help", false, "display help") - f.BoolVar(&help, "h", false, "display help (short)") - - f.BoolVar(&update, "update", false, "fetch feeds and store new items") - f.BoolVar(&update, "u", false, "fetch feeds and store new items (short)") - - f.StringVar(&newFeed, "add", "", "add the feed at URL") - f.StringVar(&newFeed, "a", "", "add the feed at URL (short)") - - f.StringVar(&export, "export", "", "export feed: text, opml, html, json") - f.StringVar(&export, "x", "", "export feed (short)") - - // options - f.StringVar(&dbfile, "database", "", "sqlite database file") - f.StringVar(&dbfile, "d", "", "sqlite database file (short)") - - f.IntVar(&port, "http", 0, "HTTP port to serve on") - f.IntVar(&port, "s", 0, "HTTP port to serve on (short)") - - f.IntVar(&minutes, "minutes", 0, "minutes between crawling feeds") - f.IntVar(&minutes, "m", 0, "minutes between crawling feeds (short)") - - f.BoolVar(&proxyImages, "imageproxy", false, "rewrite and proxy all image requests") - f.BoolVar(&proxyImages, "i", false, "rewrite and proxy all image requests (short)") - - f.BoolVar(&verbose, "verbose", false, "verbose output") - f.BoolVar(&verbose, "v", false, "verbose output (short)") - - f.StringVar(&password, "password", "", "password for web interface") - f.StringVar(&password, "p", "", "password for web interface (short)") - - f.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) - f.PrintDefaults() - } - - if err := f.Parse(args); err != nil { - return err - } - - if help { - fmt.Printf("neko v%s | build %s\n", Version, Build) - f.Usage() - return nil - } - // reads config if present and sets defaults - if err := config.Init(configFile); err != nil { - return fmt.Errorf("config error: %v", err) - } - - // override config file with flags if present - vlog.VERBOSE = verbose - if dbfile != "" { - config.Config.DBFile = dbfile - } - - if port != 0 { - config.Config.Port = port - } - - if password != "" { - config.Config.DigestPassword = password - } - - if minutes != 0 { - config.Config.CrawlMinutes = minutes - } - - if proxyImages != false { - config.Config.ProxyImages = proxyImages - } - - models.InitDB() - - if update { - vlog.Printf("starting crawl\n") - crawler.Crawl() - return nil - } - if newFeed != "" { - vlog.Printf("creating new feed\n") - feed.NewFeed(newFeed) - return nil - } - if export != "" { - vlog.Printf("exporting feeds in format %s\n", export) - fmt.Printf("%s", exporter.ExportFeeds(export)) - return nil - } - - // For testing, we might want to avoid starting a web server - if config.Config.Port == -1 { - return nil - } - - go backgroundCrawl(config.Config.CrawlMinutes) - vlog.Printf("starting web server at 127.0.0.1:%d\n", - config.Config.Port) - web.Serve() - return nil -} - -func backgroundCrawl(minutes int) { - if minutes < 1 { - return - } - vlog.Printf("starting background crawl every %d minutes\n", minutes) - for { - time.Sleep(time.Minute * time.Duration(minutes)) - crawler.Crawl() - } -} diff --git a/main_test.go b/main_test.go deleted file mode 100644 index fd36fdd..0000000 --- a/main_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "testing" - - "adammathes.com/neko/config" - "adammathes.com/neko/models" -) - -func TestRunHelp(t *testing.T) { - err := Run([]string{"--help"}) - if err != nil { - t.Errorf("Run(--help) should not error, got %v", err) - } -} - -func TestRunInvalidFlag(t *testing.T) { - err := Run([]string{"--invalid"}) - if err == nil { - t.Error("Run(--invalid) should error") - } -} - -func TestRunCrawl(t *testing.T) { - // Setup test DB - config.Config.DBFile = filepath.Join(t.TempDir(), "test_main.db") - models.InitDB() - defer models.DB.Close() - - // Use --update flag - err := Run([]string{"-u", "-d", config.Config.DBFile}) - if err != nil { - t.Errorf("Run(-u) should not error, got %v", err) - } -} - -func TestBackgroundCrawlZero(t *testing.T) { - backgroundCrawl(0) // Should return immediately -} - -func TestRunServerConfig(t *testing.T) { - // Setup test DB - config.Config.DBFile = filepath.Join(t.TempDir(), "test_main_server.db") - models.InitDB() - defer models.DB.Close() - - // Use config.Config.Port = -1 to signal Run to exit instead of starting server - config.Config.Port = -1 - err := Run([]string{"-d", config.Config.DBFile}) - if err != nil { - t.Errorf("Run should not error with Port=-1, got %v", err) - } -} - -func TestRunAdd(t *testing.T) { - dbPath := filepath.Join(t.TempDir(), "test_add.db") - err := Run([]string{"-d", dbPath, "-a", "http://example.com/rss"}) - if err != nil { - t.Errorf("Run -a failed: %v", err) - } -} - -func TestRunExport(t *testing.T) { - dbPath := filepath.Join(t.TempDir(), "test_export.db") - err := Run([]string{"-d", dbPath, "-x", "text"}) - if err != nil { - t.Errorf("Run -x failed: %v", err) - } -} - -func TestRunOptions(t *testing.T) { - dbPath := filepath.Join(t.TempDir(), "test_options.db") - err := Run([]string{"-d", dbPath, "-v", "-i", "-s", "-1"}) - if err != nil { - t.Errorf("Run with options failed: %v", err) - } -} - -func TestRunSetPassword(t *testing.T) { - dbPath := filepath.Join(t.TempDir(), "test_pass.db") - err := Run([]string{"-d", dbPath, "-p", "newpassword"}) - if err != nil { - t.Errorf("Run -p should succeed, got %v", err) - } - if config.Config.DigestPassword != "newpassword" { - t.Errorf("Expected password to be updated") - } -} - -func TestRunConfigError(t *testing.T) { - err := Run([]string{"-c", "/nonexistent/config.yaml"}) - if err == nil { - t.Error("Run should error for nonexistent config file") - } -} - -func TestRunExportFormat(t *testing.T) { - dbPath := filepath.Join(t.TempDir(), "test_export_format.db") - err := Run([]string{"-d", dbPath, "-x", "json"}) - if err != nil { - t.Errorf("Run -x json failed: %v", err) - } -} - -func TestRunConfigInvalidContent(t *testing.T) { - tmpDir := t.TempDir() - confPath := filepath.Join(tmpDir, "bad.yaml") - os.WriteFile(confPath, []byte("invalid: : yaml"), 0644) - err := Run([]string{"-c", confPath}) - if err == nil { - t.Error("Run should error for malformed config file") - } -} - -func TestRunNoArgs(t *testing.T) { - dbPath := filepath.Join(t.TempDir(), "test_noargs.db") - config.Config.Port = -1 - err := Run([]string{"-d", dbPath}) - if err != nil { - t.Errorf("Run with no args failed: %v", err) - } -} diff --git a/models/db.go b/models/db.go index d5bc7dc..67f7751 100644 --- a/models/db.go +++ b/models/db.go @@ -4,11 +4,12 @@ Package neko/models implements behavior for the entities necessary for the subsc package models import ( - "adammathes.com/neko/config" - "adammathes.com/neko/vlog" "database/sql" - _ "github.com/mattn/go-sqlite3" "log" + + "adammathes.com/neko/config" + "adammathes.com/neko/internal/vlog" + _ "github.com/mattn/go-sqlite3" ) var DB *sql.DB diff --git a/models/item/item.go b/models/item/item.go index c39f623..792fb4a 100644 --- a/models/item/item.go +++ b/models/item/item.go @@ -6,8 +6,8 @@ import ( "strings" "adammathes.com/neko/config" + "adammathes.com/neko/internal/vlog" "adammathes.com/neko/models" - "adammathes.com/neko/vlog" "github.com/PuerkitoBio/goquery" goose "github.com/advancedlogic/GoOse" "github.com/microcosm-cc/bluemonday" diff --git a/vlog/vlog.go b/vlog/vlog.go deleted file mode 100644 index ab48478..0000000 --- a/vlog/vlog.go +++ /dev/null @@ -1,25 +0,0 @@ -// vlog -- verbose logger -- wraps log functions and only performs them if "verbose" -package vlog - -import ( - "fmt" -) - -var VERBOSE bool - -func init() { - VERBOSE=false -} - -func Printf(format string, v ...interface{}) { - if VERBOSE { - fmt.Printf(format, v...) - } -} - -func Println(v ...interface{}) { - if VERBOSE { - fmt.Println(v...) - } -} - diff --git a/vlog/vlog_test.go b/vlog/vlog_test.go deleted file mode 100644 index 9def0f0..0000000 --- a/vlog/vlog_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package vlog - -import ( - "bytes" - "fmt" - "os" - "testing" -) - -func captureStdout(f func()) string { - old := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - f() - - w.Close() - os.Stdout = old - - var buf bytes.Buffer - buf.ReadFrom(r) - return buf.String() -} - -func TestPrintfVerbose(t *testing.T) { - VERBOSE = true - defer func() { VERBOSE = false }() - - output := captureStdout(func() { - Printf("hello %s", "world") - }) - expected := fmt.Sprintf("hello %s", "world") - if output != expected { - t.Errorf("expected %q, got %q", expected, output) - } -} - -func TestPrintfSilent(t *testing.T) { - VERBOSE = false - - output := captureStdout(func() { - Printf("hello %s", "world") - }) - if output != "" { - t.Errorf("expected empty output when not verbose, got %q", output) - } -} - -func TestPrintlnVerbose(t *testing.T) { - VERBOSE = true - defer func() { VERBOSE = false }() - - output := captureStdout(func() { - Println("hello", "world") - }) - expected := fmt.Sprintln("hello", "world") - if output != expected { - t.Errorf("expected %q, got %q", expected, output) - } -} - -func TestPrintlnSilent(t *testing.T) { - VERBOSE = false - - output := captureStdout(func() { - Println("hello", "world") - }) - if output != "" { - t.Errorf("expected empty output when not verbose, got %q", output) - } -} - -func TestInit(t *testing.T) { - // init() sets VERBOSE to false - if VERBOSE != false { - t.Error("VERBOSE should default to false") - } -} diff --git a/web/dist/v2/assets/index-B3U-SMew.css b/web/dist/v2/assets/index-B3U-SMew.css deleted file mode 100644 index 28223c2..0000000 --- a/web/dist/v2/assets/index-B3U-SMew.css +++ /dev/null @@ -1 +0,0 @@ -body{font-family:Palatino,Palatino Linotype,Palatino LT STD,Book Antiqua,Georgia,serif}h1,h2,h3,h4,h5,.logo,.nav-link,.logout-btn{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:700}:root{line-height:1.5;font-weight:400;font-size:18px;--bg-color: #ffffff;--text-color: rgba(0, 0, 0, .87);--sidebar-bg: #ccc;--link-color: #0000EE;color-scheme:light dark;color:var(--text-color);background-color:var(--bg-color);font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media(prefers-color-scheme:dark){:root{--bg-color: #24292e;--text-color: #ffffff;--sidebar-bg: #1b1f23;--link-color: rgb(90, 200, 250)}}.theme-dark{--bg-color: #24292e;--text-color: #ffffff;--sidebar-bg: #1b1f23;--link-color: rgb(90, 200, 250);background-color:var(--bg-color);color:var(--text-color)}.theme-black{--bg-color: #000000;--text-color: #ffffff;--sidebar-bg: #111111;--link-color: rgb(90, 200, 250);background-color:var(--bg-color);color:var(--text-color)}.theme-dark button,.theme-black button{background-color:#333;color:#fff}body{margin:0;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}a{color:var(--link-color);text-decoration:none}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#00f;text-decoration:underline}button{background-color:#f9f9f9}}.login-container{display:flex;justify-content:center;align-items:center;height:100vh;background-color:#f5f5f5}.login-form{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 4px 6px #0000001a;width:100%;max-width:400px}.login-form h1{margin-bottom:2rem;text-align:center;color:#333}.form-group{margin-bottom:1.5rem}.form-group label{display:block;margin-bottom:.5rem;font-weight:700;color:#555}.form-group input{width:100%;padding:.75rem;border:1px solid #ddd;border-radius:4px;font-size:1rem}.error-message{color:#dc3545;margin-bottom:1rem;text-align:center}button[type=submit]{width:100%;padding:.75rem;background-color:#007bff;color:#fff;border:none;border-radius:4px;font-size:1rem;cursor:pointer;transition:background-color .2s}button[type=submit]:hover{background-color:#0056b3}*{box-sizing:border-box}body{margin:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.dashboard{display:flex;flex-direction:column;height:100vh;overflow:hidden}.dashboard-header{background:#222;color:#fff;padding:.5rem 1rem;display:flex;justify-content:space-between;align-items:center;font-weight:700}.dashboard-header .logo{margin:0;font-size:2rem;cursor:pointer;line-height:1}.nav-link,.logout-btn{font-weight:700;font-variant:small-caps;text-transform:lowercase;font-size:1rem;background:transparent;border:none;color:#ccc;cursor:pointer;margin-left:1rem}.nav-link:hover,.logout-btn:hover{color:#fff;text-decoration:underline}.dashboard-content{display:flex;flex:1;overflow:hidden;position:relative}.dashboard-sidebar{width:15rem;background:var(--sidebar-bg);border-right:1px solid #999;display:flex;flex-direction:column;overflow-y:auto;padding:1rem;transition:margin-left .4s ease}.dashboard-sidebar.hidden{margin-left:-15rem}.dashboard-main{flex:1;padding:2rem;overflow-y:auto;background:var(--bg-color);margin-left:0}.dashboard-main>*{max-width:600px;margin:0}.logout-btn{background:transparent;border:1px solid rgba(255,255,255,.3);color:#fff;padding:.5rem 1rem;border-radius:4px;cursor:pointer;transition:all .2s;font-size:.9rem}.logout-btn:hover{background:#ffffff1a;border-color:#ffffff80}.feed-list{padding:0;background:transparent}.search-section{margin-bottom:1.5rem}.search-form{display:flex}.search-input{width:100%;padding:.5rem;border:1px solid #999;background:#eee;font-size:1rem;font-family:inherit}.search-input:focus{outline:none;background:#fff;border-color:#000}.feed-list h2,.feed-section-header{font-size:1.2rem;margin-bottom:.5rem;border-bottom:1px solid #999;padding-bottom:.25rem;text-transform:uppercase;letter-spacing:1px;cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;align-items:center}.toggle-indicator{font-size:.8rem;margin-right:.5rem;display:inline-block;width:1rem;text-align:center}.feed-list-items,.tag-list-items,.filter-list{list-style:none;padding:0;margin:0}.sidebar-feed-item{padding:.25rem 0;border-bottom:none;display:flex;justify-content:space-between;align-items:center}.feed-title{color:var(--link-color);text-decoration:none;font-size:.9rem;font-family:Helvetica Neue,Helvetica,Arial,sans-serif}.feed-title:hover{text-decoration:underline;color:var(--link-color)}.feed-category{display:none}.tag-section{margin-top:2rem}.tag-link{color:var(--link-color);text-decoration:none;font-size:.9rem;display:block;padding:.1rem 0;font-family:Helvetica Neue,Helvetica,Arial,sans-serif}.tag-link:hover{text-decoration:underline;background:transparent;color:var(--link-color)}.filter-section{margin-bottom:2rem}.filter-list{display:block;list-style:none;padding:0;margin:0}.filter-list li a{text-decoration:none;color:#333;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-variant:small-caps;text-transform:lowercase;font-size:1.1rem;display:block;margin-bottom:.5rem}.filter-list li a:hover{color:#00f;background-color:transparent;text-decoration:underline}.feed-title.active,.tag-link.active,.filter-list li a.active,.theme-selector button.active{font-weight:700!important}.theme-section{margin-top:2rem;padding-bottom:2rem}.theme-selector{display:flex;justify-content:space-between;gap:5px}.theme-selector button{font-size:.8rem;padding:.2rem .5rem;width:30%;background:#f5f5f5;color:#00f;border:1px solid #ccc;border-radius:4px;font-variant:small-caps;text-transform:lowercase}.theme-selector button:hover{background:#eee}.theme-selector button.active{color:#000;border-color:#000}.feed-item{padding:1rem;margin-top:5rem;list-style:none;border-bottom:none}.feed-item.read .item-title{font-weight:400}.feed-item.unread .item-title{font-weight:700}.item-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.5rem}.item-title{font-size:1.35rem;font-weight:700;text-decoration:none;color:var(--link-color);display:block;flex:1}.item-title:hover{text-decoration:none;color:var(--link-color)}.item-actions{display:flex;gap:.5rem;margin-left:1rem}.star-btn{background:none;border:none;cursor:pointer;font-size:1.25rem;padding:0 0 0 .5rem;vertical-align:middle;transition:color .2s;line-height:1}.star-btn.is-starred{color:#00f}.star-btn.is-unstarred{color:#000}.star-btn:hover{color:#00f}.action-btn{background:#f5f5f5;border:none;cursor:pointer;padding:2px 6px;font-size:1rem;color:#00f;font-weight:700}.action-btn:hover{background-color:#eee}.dateline{margin-top:0;font-weight:400;font-size:.75em;color:#ccc;margin-bottom:1rem}.dateline a{color:#ccc;text-decoration:none}.item-description{color:#000;line-height:1.5;font-size:1rem;margin-top:1rem}.item-description img{max-width:100%;height:auto;display:block;margin:1rem 0}.item-description blockquote{padding:1rem 1rem 0;border-left:4px solid #ddd;color:#666;margin-left:0}.feed-items{padding:1rem}.feed-items h2{margin-top:0;border-bottom:2px solid #eee;padding-bottom:.5rem}.item-list{list-style:none;padding:0}.loading-more{padding:2rem;text-align:center;color:#888;font-size:.9rem;min-height:50px}.settings-page{padding:2rem;max-width:800px;margin:0 auto}.add-feed-section{background:#f9f9f9;padding:1.5rem;border-radius:8px;margin-bottom:2rem;border:1px solid #eee}.add-feed-form{display:flex;gap:1rem}.feed-input{flex:1;padding:.5rem;border:1px solid #ccc;border-radius:4px;font-size:1rem}.error-message{color:#d32f2f;margin-top:1rem}.settings-feed-list{list-style:none;padding:0;border:1px solid #eee;border-radius:8px}.settings-feed-item{display:flex;justify-content:space-between;align-items:center;padding:1rem;border-bottom:1px solid #eee}.settings-feed-item:last-child{border-bottom:none}.feed-info{display:flex;flex-direction:column}.feed-title{font-weight:700;font-size:1.1rem}.feed-url{color:#666;font-size:.9rem}.delete-btn{background:#ff5252;color:#fff;border:none;padding:.5rem 1rem;border-radius:4px;cursor:pointer}.delete-btn:hover{background:#ff1744}.delete-btn:disabled{background:#ffcdd2;cursor:not-allowed} diff --git a/web/dist/v2/assets/index-DHAgSqjC.js b/web/dist/v2/assets/index-DHAgSqjC.js new file mode 100644 index 0000000..3f0fd50 --- /dev/null +++ b/web/dist/v2/assets/index-DHAgSqjC.js @@ -0,0 +1,11 @@ +(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const d of document.querySelectorAll('link[rel="modulepreload"]'))f(d);new MutationObserver(d=>{for(const m of d)if(m.type==="childList")for(const S of m.addedNodes)S.tagName==="LINK"&&S.rel==="modulepreload"&&f(S)}).observe(document,{childList:!0,subtree:!0});function o(d){const m={};return d.integrity&&(m.integrity=d.integrity),d.referrerPolicy&&(m.referrerPolicy=d.referrerPolicy),d.crossOrigin==="use-credentials"?m.credentials="include":d.crossOrigin==="anonymous"?m.credentials="omit":m.credentials="same-origin",m}function f(d){if(d.ep)return;d.ep=!0;const m=o(d);fetch(d.href,m)}})();var Mf={exports:{}},Cu={};var Fd;function lv(){if(Fd)return Cu;Fd=1;var c=Symbol.for("react.transitional.element"),s=Symbol.for("react.fragment");function o(f,d,m){var S=null;if(m!==void 0&&(S=""+m),d.key!==void 0&&(S=""+d.key),"key"in d){m={};for(var R in d)R!=="key"&&(m[R]=d[R])}else m=d;return d=m.ref,{$$typeof:c,type:f,key:S,ref:d!==void 0?d:null,props:m}}return Cu.Fragment=s,Cu.jsx=o,Cu.jsxs=o,Cu}var Id;function av(){return Id||(Id=1,Mf.exports=lv()),Mf.exports}var j=av(),Df={exports:{}},et={};var Pd;function uv(){if(Pd)return et;Pd=1;var c=Symbol.for("react.transitional.element"),s=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),f=Symbol.for("react.strict_mode"),d=Symbol.for("react.profiler"),m=Symbol.for("react.consumer"),S=Symbol.for("react.context"),R=Symbol.for("react.forward_ref"),b=Symbol.for("react.suspense"),y=Symbol.for("react.memo"),N=Symbol.for("react.lazy"),z=Symbol.for("react.activity"),H=Symbol.iterator;function Q(g){return g===null||typeof g!="object"?null:(g=H&&g[H]||g["@@iterator"],typeof g=="function"?g:null)}var F={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},X=Object.assign,Z={};function J(g,x,L){this.props=g,this.context=x,this.refs=Z,this.updater=L||F}J.prototype.isReactComponent={},J.prototype.setState=function(g,x){if(typeof g!="object"&&typeof g!="function"&&g!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,g,x,"setState")},J.prototype.forceUpdate=function(g){this.updater.enqueueForceUpdate(this,g,"forceUpdate")};function G(){}G.prototype=J.prototype;function W(g,x,L){this.props=g,this.context=x,this.refs=Z,this.updater=L||F}var $=W.prototype=new G;$.constructor=W,X($,J.prototype),$.isPureReactComponent=!0;var P=Array.isArray;function B(){}var Y={H:null,A:null,T:null,S:null},st=Object.prototype.hasOwnProperty;function vt(g,x,L){var K=L.ref;return{$$typeof:c,type:g,key:x,ref:K!==void 0?K:null,props:L}}function Wt(g,x){return vt(g.type,x,g.props)}function Ce(g){return typeof g=="object"&&g!==null&&g.$$typeof===c}function kt(g){var x={"=":"=0",":":"=2"};return"$"+g.replace(/[=:]/g,function(L){return x[L]})}var Dl=/\/+/g;function qe(g,x){return typeof g=="object"&&g!==null&&g.key!=null?kt(""+g.key):x.toString(36)}function Re(g){switch(g.status){case"fulfilled":return g.value;case"rejected":throw g.reason;default:switch(typeof g.status=="string"?g.then(B,B):(g.status="pending",g.then(function(x){g.status==="pending"&&(g.status="fulfilled",g.value=x)},function(x){g.status==="pending"&&(g.status="rejected",g.reason=x)})),g.status){case"fulfilled":return g.value;case"rejected":throw g.reason}}throw g}function D(g,x,L,K,lt){var nt=typeof g;(nt==="undefined"||nt==="boolean")&&(g=null);var yt=!1;if(g===null)yt=!0;else switch(nt){case"bigint":case"string":case"number":yt=!0;break;case"object":switch(g.$$typeof){case c:case s:yt=!0;break;case N:return yt=g._init,D(yt(g._payload),x,L,K,lt)}}if(yt)return lt=lt(g),yt=K===""?"."+qe(g,0):K,P(lt)?(L="",yt!=null&&(L=yt.replace(Dl,"$&/")+"/"),D(lt,x,L,"",function(Ya){return Ya})):lt!=null&&(Ce(lt)&&(lt=Wt(lt,L+(lt.key==null||g&&g.key===lt.key?"":(""+lt.key).replace(Dl,"$&/")+"/")+yt)),x.push(lt)),1;yt=0;var Jt=K===""?".":K+":";if(P(g))for(var Ut=0;Ut>>1,zt=D[St];if(0>>1;Std(L,tt))Kd(lt,L)?(D[St]=lt,D[K]=tt,St=K):(D[St]=L,D[x]=tt,St=x);else if(Kd(lt,tt))D[St]=lt,D[K]=tt,St=K;else break t}}return q}function d(D,q){var tt=D.sortIndex-q.sortIndex;return tt!==0?tt:D.id-q.id}if(c.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var m=performance;c.unstable_now=function(){return m.now()}}else{var S=Date,R=S.now();c.unstable_now=function(){return S.now()-R}}var b=[],y=[],N=1,z=null,H=3,Q=!1,F=!1,X=!1,Z=!1,J=typeof setTimeout=="function"?setTimeout:null,G=typeof clearTimeout=="function"?clearTimeout:null,W=typeof setImmediate<"u"?setImmediate:null;function $(D){for(var q=o(y);q!==null;){if(q.callback===null)f(y);else if(q.startTime<=D)f(y),q.sortIndex=q.expirationTime,s(b,q);else break;q=o(y)}}function P(D){if(X=!1,$(D),!F)if(o(b)!==null)F=!0,B||(B=!0,kt());else{var q=o(y);q!==null&&Re(P,q.startTime-D)}}var B=!1,Y=-1,st=5,vt=-1;function Wt(){return Z?!0:!(c.unstable_now()-vtD&&Wt());){var St=z.callback;if(typeof St=="function"){z.callback=null,H=z.priorityLevel;var zt=St(z.expirationTime<=D);if(D=c.unstable_now(),typeof zt=="function"){z.callback=zt,$(D),q=!0;break e}z===o(b)&&f(b),$(D)}else f(b);z=o(b)}if(z!==null)q=!0;else{var g=o(y);g!==null&&Re(P,g.startTime-D),q=!1}}break t}finally{z=null,H=tt,Q=!1}q=void 0}}finally{q?kt():B=!1}}}var kt;if(typeof W=="function")kt=function(){W(Ce)};else if(typeof MessageChannel<"u"){var Dl=new MessageChannel,qe=Dl.port2;Dl.port1.onmessage=Ce,kt=function(){qe.postMessage(null)}}else kt=function(){J(Ce,0)};function Re(D,q){Y=J(function(){D(c.unstable_now())},q)}c.unstable_IdlePriority=5,c.unstable_ImmediatePriority=1,c.unstable_LowPriority=4,c.unstable_NormalPriority=3,c.unstable_Profiling=null,c.unstable_UserBlockingPriority=2,c.unstable_cancelCallback=function(D){D.callback=null},c.unstable_forceFrameRate=function(D){0>D||125St?(D.sortIndex=tt,s(y,D),o(b)===null&&D===o(y)&&(X?(G(Y),Y=-1):X=!0,Re(P,tt-St))):(D.sortIndex=zt,s(b,D),F||Q||(F=!0,B||(B=!0,kt()))),D},c.unstable_shouldYield=Wt,c.unstable_wrapCallback=function(D){var q=H;return function(){var tt=H;H=q;try{return D.apply(this,arguments)}finally{H=tt}}}})(Cf)),Cf}var lh;function iv(){return lh||(lh=1,Uf.exports=nv()),Uf.exports}var xf={exports:{}},wt={};var ah;function cv(){if(ah)return wt;ah=1;var c=Xf();function s(b){var y="https://react.dev/errors/"+b;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(c)}catch(s){console.error(s)}}return c(),xf.exports=cv(),xf.exports}var nh;function rv(){if(nh)return xu;nh=1;var c=iv(),s=Xf(),o=fv();function f(t){var e="https://react.dev/errors/"+t;if(1zt||(t.current=St[zt],St[zt]=null,zt--)}function L(t,e){zt++,St[zt]=t.current,t.current=e}var K=g(null),lt=g(null),nt=g(null),yt=g(null);function Jt(t,e){switch(L(nt,e),L(lt,t),L(K,null),e.nodeType){case 9:case 11:t=(t=e.documentElement)&&(t=t.namespaceURI)?bd(t):0;break;default:if(t=e.tagName,e=e.namespaceURI)e=bd(e),t=Ed(e,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}x(K),L(K,t)}function Ut(){x(K),x(lt),x(nt)}function Ya(t){t.memoizedState!==null&&L(yt,t);var e=K.current,l=Ed(e,t.type);e!==l&&(L(lt,t),L(K,l))}function Lu(t){lt.current===t&&(x(K),x(lt)),yt.current===t&&(x(yt),Mu._currentValue=tt)}var ri,Wf;function Nl(t){if(ri===void 0)try{throw Error()}catch(l){var e=l.stack.trim().match(/\n( *(at )?)/);ri=e&&e[1]||"",Wf=-1)":-1u||h[a]!==T[u]){var M=` +`+h[a].replace(" at new "," at ");return t.displayName&&M.includes("")&&(M=M.replace("",t.displayName)),M}while(1<=a&&0<=u);break}}}finally{si=!1,Error.prepareStackTrace=l}return(l=t?t.displayName||t.name:"")?Nl(l):""}function xh(t,e){switch(t.tag){case 26:case 27:case 5:return Nl(t.type);case 16:return Nl("Lazy");case 13:return t.child!==e&&e!==null?Nl("Suspense Fallback"):Nl("Suspense");case 19:return Nl("SuspenseList");case 0:case 15:return oi(t.type,!1);case 11:return oi(t.type.render,!1);case 1:return oi(t.type,!0);case 31:return Nl("Activity");default:return""}}function kf(t){try{var e="",l=null;do e+=xh(t,l),l=t,t=t.return;while(t);return e}catch(a){return` +Error generating stack: `+a.message+` +`+a.stack}}var di=Object.prototype.hasOwnProperty,hi=c.unstable_scheduleCallback,mi=c.unstable_cancelCallback,jh=c.unstable_shouldYield,Hh=c.unstable_requestPaint,ue=c.unstable_now,Bh=c.unstable_getCurrentPriorityLevel,Ff=c.unstable_ImmediatePriority,If=c.unstable_UserBlockingPriority,Yu=c.unstable_NormalPriority,qh=c.unstable_LowPriority,Pf=c.unstable_IdlePriority,Lh=c.log,Yh=c.unstable_setDisableYieldValue,Ga=null,ne=null;function ul(t){if(typeof Lh=="function"&&Yh(t),ne&&typeof ne.setStrictMode=="function")try{ne.setStrictMode(Ga,t)}catch{}}var ie=Math.clz32?Math.clz32:Qh,Gh=Math.log,Xh=Math.LN2;function Qh(t){return t>>>=0,t===0?32:31-(Gh(t)/Xh|0)|0}var Gu=256,Xu=262144,Qu=4194304;function Ul(t){var e=t&42;if(e!==0)return e;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Zu(t,e,l){var a=t.pendingLanes;if(a===0)return 0;var u=0,n=t.suspendedLanes,i=t.pingedLanes;t=t.warmLanes;var r=a&134217727;return r!==0?(a=r&~n,a!==0?u=Ul(a):(i&=r,i!==0?u=Ul(i):l||(l=r&~t,l!==0&&(u=Ul(l))))):(r=a&~n,r!==0?u=Ul(r):i!==0?u=Ul(i):l||(l=a&~t,l!==0&&(u=Ul(l)))),u===0?0:e!==0&&e!==u&&(e&n)===0&&(n=u&-u,l=e&-e,n>=l||n===32&&(l&4194048)!==0)?e:u}function Xa(t,e){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&e)===0}function Zh(t,e){switch(t){case 1:case 2:case 4:case 8:case 64:return e+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function tr(){var t=Qu;return Qu<<=1,(Qu&62914560)===0&&(Qu=4194304),t}function yi(t){for(var e=[],l=0;31>l;l++)e.push(t);return e}function Qa(t,e){t.pendingLanes|=e,e!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Vh(t,e,l,a,u,n){var i=t.pendingLanes;t.pendingLanes=l,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=l,t.entangledLanes&=l,t.errorRecoveryDisabledLanes&=l,t.shellSuspendCounter=0;var r=t.entanglements,h=t.expirationTimes,T=t.hiddenUpdates;for(l=i&~l;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var kh=/[\n"\\]/g;function ve(t){return t.replace(kh,function(e){return"\\"+e.charCodeAt(0).toString(16)+" "})}function Ei(t,e,l,a,u,n,i,r){t.name="",i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"?t.type=i:t.removeAttribute("type"),e!=null?i==="number"?(e===0&&t.value===""||t.value!=e)&&(t.value=""+ye(e)):t.value!==""+ye(e)&&(t.value=""+ye(e)):i!=="submit"&&i!=="reset"||t.removeAttribute("value"),e!=null?Ti(t,i,ye(e)):l!=null?Ti(t,i,ye(l)):a!=null&&t.removeAttribute("value"),u==null&&n!=null&&(t.defaultChecked=!!n),u!=null&&(t.checked=u&&typeof u!="function"&&typeof u!="symbol"),r!=null&&typeof r!="function"&&typeof r!="symbol"&&typeof r!="boolean"?t.name=""+ye(r):t.removeAttribute("name")}function hr(t,e,l,a,u,n,i,r){if(n!=null&&typeof n!="function"&&typeof n!="symbol"&&typeof n!="boolean"&&(t.type=n),e!=null||l!=null){if(!(n!=="submit"&&n!=="reset"||e!=null)){bi(t);return}l=l!=null?""+ye(l):"",e=e!=null?""+ye(e):l,r||e===t.value||(t.value=e),t.defaultValue=e}a=a??u,a=typeof a!="function"&&typeof a!="symbol"&&!!a,t.checked=r?t.checked:!!a,t.defaultChecked=!!a,i!=null&&typeof i!="function"&&typeof i!="symbol"&&typeof i!="boolean"&&(t.name=i),bi(t)}function Ti(t,e,l){e==="number"&&wu(t.ownerDocument)===t||t.defaultValue===""+l||(t.defaultValue=""+l)}function la(t,e,l,a){if(t=t.options,e){e={};for(var u=0;u"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Oi=!1;if(Ge)try{var wa={};Object.defineProperty(wa,"passive",{get:function(){Oi=!0}}),window.addEventListener("test",wa,wa),window.removeEventListener("test",wa,wa)}catch{Oi=!1}var il=null,Mi=null,$u=null;function br(){if($u)return $u;var t,e=Mi,l=e.length,a,u="value"in il?il.value:il.textContent,n=u.length;for(t=0;t=Wa),Rr=" ",Or=!1;function Mr(t,e){switch(t){case"keyup":return Am.indexOf(e.keyCode)!==-1;case"keydown":return e.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Dr(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var ia=!1;function Rm(t,e){switch(t){case"compositionend":return Dr(e);case"keypress":return e.which!==32?null:(Or=!0,Rr);case"textInput":return t=e.data,t===Rr&&Or?null:t;default:return null}}function Om(t,e){if(ia)return t==="compositionend"||!xi&&Mr(t,e)?(t=br(),$u=Mi=il=null,ia=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(e.ctrlKey||e.altKey||e.metaKey)||e.ctrlKey&&e.altKey){if(e.char&&1=e)return{node:l,offset:e-t};t=a}t:{for(;l;){if(l.nextSibling){l=l.nextSibling;break t}l=l.parentNode}l=void 0}l=qr(l)}}function Yr(t,e){return t&&e?t===e?!0:t&&t.nodeType===3?!1:e&&e.nodeType===3?Yr(t,e.parentNode):"contains"in t?t.contains(e):t.compareDocumentPosition?!!(t.compareDocumentPosition(e)&16):!1:!1}function Gr(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var e=wu(t.document);e instanceof t.HTMLIFrameElement;){try{var l=typeof e.contentWindow.location.href=="string"}catch{l=!1}if(l)t=e.contentWindow;else break;e=wu(t.document)}return e}function Bi(t){var e=t&&t.nodeName&&t.nodeName.toLowerCase();return e&&(e==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||e==="textarea"||t.contentEditable==="true")}var Hm=Ge&&"documentMode"in document&&11>=document.documentMode,ca=null,qi=null,Pa=null,Li=!1;function Xr(t,e,l){var a=l.window===l?l.document:l.nodeType===9?l:l.ownerDocument;Li||ca==null||ca!==wu(a)||(a=ca,"selectionStart"in a&&Bi(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),Pa&&Ia(Pa,a)||(Pa=a,a=Qn(qi,"onSelect"),0>=i,u-=i,xe=1<<32-ie(e)+u|l<ut?(rt=w,w=null):rt=w.sibling;var ht=_(p,w,E[ut],U);if(ht===null){w===null&&(w=rt);break}t&&w&&ht.alternate===null&&e(p,w),v=n(ht,v,ut),dt===null?k=ht:dt.sibling=ht,dt=ht,w=rt}if(ut===E.length)return l(p,w),ot&&Qe(p,ut),k;if(w===null){for(;utut?(rt=w,w=null):rt=w.sibling;var Ml=_(p,w,ht.value,U);if(Ml===null){w===null&&(w=rt);break}t&&w&&Ml.alternate===null&&e(p,w),v=n(Ml,v,ut),dt===null?k=Ml:dt.sibling=Ml,dt=Ml,w=rt}if(ht.done)return l(p,w),ot&&Qe(p,ut),k;if(w===null){for(;!ht.done;ut++,ht=E.next())ht=C(p,ht.value,U),ht!==null&&(v=n(ht,v,ut),dt===null?k=ht:dt.sibling=ht,dt=ht);return ot&&Qe(p,ut),k}for(w=a(w);!ht.done;ut++,ht=E.next())ht=O(w,p,ut,ht.value,U),ht!==null&&(t&&ht.alternate!==null&&w.delete(ht.key===null?ut:ht.key),v=n(ht,v,ut),dt===null?k=ht:dt.sibling=ht,dt=ht);return t&&w.forEach(function(ev){return e(p,ev)}),ot&&Qe(p,ut),k}function Tt(p,v,E,U){if(typeof E=="object"&&E!==null&&E.type===X&&E.key===null&&(E=E.props.children),typeof E=="object"&&E!==null){switch(E.$$typeof){case Q:t:{for(var k=E.key;v!==null;){if(v.key===k){if(k=E.type,k===X){if(v.tag===7){l(p,v.sibling),U=u(v,E.props.children),U.return=p,p=U;break t}}else if(v.elementType===k||typeof k=="object"&&k!==null&&k.$$typeof===st&&Ql(k)===v.type){l(p,v.sibling),U=u(v,E.props),nu(U,E),U.return=p,p=U;break t}l(p,v);break}else e(p,v);v=v.sibling}E.type===X?(U=ql(E.props.children,p.mode,U,E.key),U.return=p,p=U):(U=un(E.type,E.key,E.props,null,p.mode,U),nu(U,E),U.return=p,p=U)}return i(p);case F:t:{for(k=E.key;v!==null;){if(v.key===k)if(v.tag===4&&v.stateNode.containerInfo===E.containerInfo&&v.stateNode.implementation===E.implementation){l(p,v.sibling),U=u(v,E.children||[]),U.return=p,p=U;break t}else{l(p,v);break}else e(p,v);v=v.sibling}U=Ki(E,p.mode,U),U.return=p,p=U}return i(p);case st:return E=Ql(E),Tt(p,v,E,U)}if(Re(E))return V(p,v,E,U);if(kt(E)){if(k=kt(E),typeof k!="function")throw Error(f(150));return E=k.call(E),I(p,v,E,U)}if(typeof E.then=="function")return Tt(p,v,dn(E),U);if(E.$$typeof===W)return Tt(p,v,fn(p,E),U);hn(p,E)}return typeof E=="string"&&E!==""||typeof E=="number"||typeof E=="bigint"?(E=""+E,v!==null&&v.tag===6?(l(p,v.sibling),U=u(v,E),U.return=p,p=U):(l(p,v),U=Vi(E,p.mode,U),U.return=p,p=U),i(p)):l(p,v)}return function(p,v,E,U){try{uu=0;var k=Tt(p,v,E,U);return pa=null,k}catch(w){if(w===ga||w===sn)throw w;var dt=fe(29,w,null,p.mode);return dt.lanes=U,dt.return=p,dt}}}var Vl=ss(!0),os=ss(!1),ol=!1;function ac(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function uc(t,e){t=t.updateQueue,e.updateQueue===t&&(e.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function dl(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function hl(t,e,l){var a=t.updateQueue;if(a===null)return null;if(a=a.shared,(mt&2)!==0){var u=a.pending;return u===null?e.next=e:(e.next=u.next,u.next=e),a.pending=e,e=an(t),$r(t,null,l),e}return ln(t,a,e,l),an(t)}function iu(t,e,l){if(e=e.updateQueue,e!==null&&(e=e.shared,(l&4194048)!==0)){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,lr(t,l)}}function nc(t,e){var l=t.updateQueue,a=t.alternate;if(a!==null&&(a=a.updateQueue,l===a)){var u=null,n=null;if(l=l.firstBaseUpdate,l!==null){do{var i={lane:l.lane,tag:l.tag,payload:l.payload,callback:null,next:null};n===null?u=n=i:n=n.next=i,l=l.next}while(l!==null);n===null?u=n=e:n=n.next=e}else u=n=e;l={baseState:a.baseState,firstBaseUpdate:u,lastBaseUpdate:n,shared:a.shared,callbacks:a.callbacks},t.updateQueue=l;return}t=l.lastBaseUpdate,t===null?l.firstBaseUpdate=e:t.next=e,l.lastBaseUpdate=e}var ic=!1;function cu(){if(ic){var t=va;if(t!==null)throw t}}function fu(t,e,l,a){ic=!1;var u=t.updateQueue;ol=!1;var n=u.firstBaseUpdate,i=u.lastBaseUpdate,r=u.shared.pending;if(r!==null){u.shared.pending=null;var h=r,T=h.next;h.next=null,i===null?n=T:i.next=T,i=h;var M=t.alternate;M!==null&&(M=M.updateQueue,r=M.lastBaseUpdate,r!==i&&(r===null?M.firstBaseUpdate=T:r.next=T,M.lastBaseUpdate=h))}if(n!==null){var C=u.baseState;i=0,M=T=h=null,r=n;do{var _=r.lane&-536870913,O=_!==r.lane;if(O?(ft&_)===_:(a&_)===_){_!==0&&_===ya&&(ic=!0),M!==null&&(M=M.next={lane:0,tag:r.tag,payload:r.payload,callback:null,next:null});t:{var V=t,I=r;_=e;var Tt=l;switch(I.tag){case 1:if(V=I.payload,typeof V=="function"){C=V.call(Tt,C,_);break t}C=V;break t;case 3:V.flags=V.flags&-65537|128;case 0:if(V=I.payload,_=typeof V=="function"?V.call(Tt,C,_):V,_==null)break t;C=z({},C,_);break t;case 2:ol=!0}}_=r.callback,_!==null&&(t.flags|=64,O&&(t.flags|=8192),O=u.callbacks,O===null?u.callbacks=[_]:O.push(_))}else O={lane:_,tag:r.tag,payload:r.payload,callback:r.callback,next:null},M===null?(T=M=O,h=C):M=M.next=O,i|=_;if(r=r.next,r===null){if(r=u.shared.pending,r===null)break;O=r,r=O.next,O.next=null,u.lastBaseUpdate=O,u.shared.pending=null}}while(!0);M===null&&(h=C),u.baseState=h,u.firstBaseUpdate=T,u.lastBaseUpdate=M,n===null&&(u.shared.lanes=0),pl|=i,t.lanes=i,t.memoizedState=C}}function ds(t,e){if(typeof t!="function")throw Error(f(191,t));t.call(e)}function hs(t,e){var l=t.callbacks;if(l!==null)for(t.callbacks=null,t=0;tn?n:8;var i=D.T,r={};D.T=r,_c(t,!1,e,l);try{var h=u(),T=D.S;if(T!==null&&T(r,h),h!==null&&typeof h=="object"&&typeof h.then=="function"){var M=Vm(h,a);ou(t,e,M,he(t))}else ou(t,e,a,he(t))}catch(C){ou(t,e,{then:function(){},status:"rejected",reason:C},he())}finally{q.p=n,i!==null&&r.types!==null&&(i.types=r.types),D.T=i}}function km(){}function zc(t,e,l,a){if(t.tag!==5)throw Error(f(476));var u=Ks(t).queue;Vs(t,u,e,tt,l===null?km:function(){return ws(t),l(a)})}function Ks(t){var e=t.memoizedState;if(e!==null)return e;e={memoizedState:tt,baseState:tt,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:we,lastRenderedState:tt},next:null};var l={};return e.next={memoizedState:l,baseState:l,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:we,lastRenderedState:l},next:null},t.memoizedState=e,t=t.alternate,t!==null&&(t.memoizedState=e),e}function ws(t){var e=Ks(t);e.next===null&&(e=t.alternate.memoizedState),ou(t,e.next.queue,{},he())}function Ac(){return Zt(Mu)}function Js(){return xt().memoizedState}function $s(){return xt().memoizedState}function Fm(t){for(var e=t.return;e!==null;){switch(e.tag){case 24:case 3:var l=he();t=dl(l);var a=hl(e,t,l);a!==null&&(ae(a,e,l),iu(a,e,l)),e={cache:Pi()},t.payload=e;return}e=e.return}}function Im(t,e,l){var a=he();l={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null},zn(t)?ks(e,l):(l=Qi(t,e,l,a),l!==null&&(ae(l,t,a),Fs(l,e,a)))}function Ws(t,e,l){var a=he();ou(t,e,l,a)}function ou(t,e,l,a){var u={lane:a,revertLane:0,gesture:null,action:l,hasEagerState:!1,eagerState:null,next:null};if(zn(t))ks(e,u);else{var n=t.alternate;if(t.lanes===0&&(n===null||n.lanes===0)&&(n=e.lastRenderedReducer,n!==null))try{var i=e.lastRenderedState,r=n(i,l);if(u.hasEagerState=!0,u.eagerState=r,ce(r,i))return ln(t,e,u,0),At===null&&en(),!1}catch{}if(l=Qi(t,e,u,a),l!==null)return ae(l,t,a),Fs(l,e,a),!0}return!1}function _c(t,e,l,a){if(a={lane:2,revertLane:af(),gesture:null,action:a,hasEagerState:!1,eagerState:null,next:null},zn(t)){if(e)throw Error(f(479))}else e=Qi(t,l,a,2),e!==null&&ae(e,t,2)}function zn(t){var e=t.alternate;return t===at||e!==null&&e===at}function ks(t,e){ba=vn=!0;var l=t.pending;l===null?e.next=e:(e.next=l.next,l.next=e),t.pending=e}function Fs(t,e,l){if((l&4194048)!==0){var a=e.lanes;a&=t.pendingLanes,l|=a,e.lanes=l,lr(t,l)}}var du={readContext:Zt,use:Sn,useCallback:Dt,useContext:Dt,useEffect:Dt,useImperativeHandle:Dt,useLayoutEffect:Dt,useInsertionEffect:Dt,useMemo:Dt,useReducer:Dt,useRef:Dt,useState:Dt,useDebugValue:Dt,useDeferredValue:Dt,useTransition:Dt,useSyncExternalStore:Dt,useId:Dt,useHostTransitionStatus:Dt,useFormState:Dt,useActionState:Dt,useOptimistic:Dt,useMemoCache:Dt,useCacheRefresh:Dt};du.useEffectEvent=Dt;var Is={readContext:Zt,use:Sn,useCallback:function(t,e){return $t().memoizedState=[t,e===void 0?null:e],t},useContext:Zt,useEffect:Hs,useImperativeHandle:function(t,e,l){l=l!=null?l.concat([t]):null,En(4194308,4,Ys.bind(null,e,t),l)},useLayoutEffect:function(t,e){return En(4194308,4,t,e)},useInsertionEffect:function(t,e){En(4,2,t,e)},useMemo:function(t,e){var l=$t();e=e===void 0?null:e;var a=t();if(Kl){ul(!0);try{t()}finally{ul(!1)}}return l.memoizedState=[a,e],a},useReducer:function(t,e,l){var a=$t();if(l!==void 0){var u=l(e);if(Kl){ul(!0);try{l(e)}finally{ul(!1)}}}else u=e;return a.memoizedState=a.baseState=u,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:u},a.queue=t,t=t.dispatch=Im.bind(null,at,t),[a.memoizedState,t]},useRef:function(t){var e=$t();return t={current:t},e.memoizedState=t},useState:function(t){t=pc(t);var e=t.queue,l=Ws.bind(null,at,e);return e.dispatch=l,[t.memoizedState,l]},useDebugValue:Ec,useDeferredValue:function(t,e){var l=$t();return Tc(l,t,e)},useTransition:function(){var t=pc(!1);return t=Vs.bind(null,at,t.queue,!0,!1),$t().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,e,l){var a=at,u=$t();if(ot){if(l===void 0)throw Error(f(407));l=l()}else{if(l=e(),At===null)throw Error(f(349));(ft&127)!==0||Ss(a,e,l)}u.memoizedState=l;var n={value:l,getSnapshot:e};return u.queue=n,Hs(Es.bind(null,a,n,t),[t]),a.flags|=2048,Ta(9,{destroy:void 0},bs.bind(null,a,n,l,e),null),l},useId:function(){var t=$t(),e=At.identifierPrefix;if(ot){var l=je,a=xe;l=(a&~(1<<32-ie(a)-1)).toString(32)+l,e="_"+e+"R_"+l,l=gn++,0<\/script>",n=n.removeChild(n.firstChild);break;case"select":n=typeof a.is=="string"?i.createElement("select",{is:a.is}):i.createElement("select"),a.multiple?n.multiple=!0:a.size&&(n.size=a.size);break;default:n=typeof a.is=="string"?i.createElement(u,{is:a.is}):i.createElement(u)}}n[Xt]=e,n[Ft]=a;t:for(i=e.child;i!==null;){if(i.tag===5||i.tag===6)n.appendChild(i.stateNode);else if(i.tag!==4&&i.tag!==27&&i.child!==null){i.child.return=i,i=i.child;continue}if(i===e)break t;for(;i.sibling===null;){if(i.return===null||i.return===e)break t;i=i.return}i.sibling.return=i.return,i=i.sibling}e.stateNode=n;t:switch(Kt(n,u,a),u){case"button":case"input":case"select":case"textarea":a=!!a.autoFocus;break t;case"img":a=!0;break t;default:a=!1}a&&$e(e)}}return Rt(e),Yc(e,e.type,t===null?null:t.memoizedProps,e.pendingProps,l),null;case 6:if(t&&e.stateNode!=null)t.memoizedProps!==a&&$e(e);else{if(typeof a!="string"&&e.stateNode===null)throw Error(f(166));if(t=nt.current,ha(e)){if(t=e.stateNode,l=e.memoizedProps,a=null,u=Qt,u!==null)switch(u.tag){case 27:case 5:a=u.memoizedProps}t[Xt]=e,t=!!(t.nodeValue===l||a!==null&&a.suppressHydrationWarning===!0||pd(t.nodeValue,l)),t||rl(e,!0)}else t=Zn(t).createTextNode(a),t[Xt]=e,e.stateNode=t}return Rt(e),null;case 31:if(l=e.memoizedState,t===null||t.memoizedState!==null){if(a=ha(e),l!==null){if(t===null){if(!a)throw Error(f(318));if(t=e.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(f(557));t[Xt]=e}else Ll(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Rt(e),t=!1}else l=Wi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=l),t=!0;if(!t)return e.flags&256?(se(e),e):(se(e),null);if((e.flags&128)!==0)throw Error(f(558))}return Rt(e),null;case 13:if(a=e.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(u=ha(e),a!==null&&a.dehydrated!==null){if(t===null){if(!u)throw Error(f(318));if(u=e.memoizedState,u=u!==null?u.dehydrated:null,!u)throw Error(f(317));u[Xt]=e}else Ll(),(e.flags&128)===0&&(e.memoizedState=null),e.flags|=4;Rt(e),u=!1}else u=Wi(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=u),u=!0;if(!u)return e.flags&256?(se(e),e):(se(e),null)}return se(e),(e.flags&128)!==0?(e.lanes=l,e):(l=a!==null,t=t!==null&&t.memoizedState!==null,l&&(a=e.child,u=null,a.alternate!==null&&a.alternate.memoizedState!==null&&a.alternate.memoizedState.cachePool!==null&&(u=a.alternate.memoizedState.cachePool.pool),n=null,a.memoizedState!==null&&a.memoizedState.cachePool!==null&&(n=a.memoizedState.cachePool.pool),n!==u&&(a.flags|=2048)),l!==t&&l&&(e.child.flags|=8192),Mn(e,e.updateQueue),Rt(e),null);case 4:return Ut(),t===null&&ff(e.stateNode.containerInfo),Rt(e),null;case 10:return Ve(e.type),Rt(e),null;case 19:if(x(Ct),a=e.memoizedState,a===null)return Rt(e),null;if(u=(e.flags&128)!==0,n=a.rendering,n===null)if(u)mu(a,!1);else{if(Nt!==0||t!==null&&(t.flags&128)!==0)for(t=e.child;t!==null;){if(n=yn(t),n!==null){for(e.flags|=128,mu(a,!1),t=n.updateQueue,e.updateQueue=t,Mn(e,t),e.subtreeFlags=0,t=l,l=e.child;l!==null;)Wr(l,t),l=l.sibling;return L(Ct,Ct.current&1|2),ot&&Qe(e,a.treeForkCount),e.child}t=t.sibling}a.tail!==null&&ue()>xn&&(e.flags|=128,u=!0,mu(a,!1),e.lanes=4194304)}else{if(!u)if(t=yn(n),t!==null){if(e.flags|=128,u=!0,t=t.updateQueue,e.updateQueue=t,Mn(e,t),mu(a,!0),a.tail===null&&a.tailMode==="hidden"&&!n.alternate&&!ot)return Rt(e),null}else 2*ue()-a.renderingStartTime>xn&&l!==536870912&&(e.flags|=128,u=!0,mu(a,!1),e.lanes=4194304);a.isBackwards?(n.sibling=e.child,e.child=n):(t=a.last,t!==null?t.sibling=n:e.child=n,a.last=n)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=ue(),t.sibling=null,l=Ct.current,L(Ct,u?l&1|2:l&1),ot&&Qe(e,a.treeForkCount),t):(Rt(e),null);case 22:case 23:return se(e),fc(),a=e.memoizedState!==null,t!==null?t.memoizedState!==null!==a&&(e.flags|=8192):a&&(e.flags|=8192),a?(l&536870912)!==0&&(e.flags&128)===0&&(Rt(e),e.subtreeFlags&6&&(e.flags|=8192)):Rt(e),l=e.updateQueue,l!==null&&Mn(e,l.retryQueue),l=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(l=t.memoizedState.cachePool.pool),a=null,e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(a=e.memoizedState.cachePool.pool),a!==l&&(e.flags|=2048),t!==null&&x(Xl),null;case 24:return l=null,t!==null&&(l=t.memoizedState.cache),e.memoizedState.cache!==l&&(e.flags|=2048),Ve(jt),Rt(e),null;case 25:return null;case 30:return null}throw Error(f(156,e.tag))}function ay(t,e){switch(Ji(e),e.tag){case 1:return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 3:return Ve(jt),Ut(),t=e.flags,(t&65536)!==0&&(t&128)===0?(e.flags=t&-65537|128,e):null;case 26:case 27:case 5:return Lu(e),null;case 31:if(e.memoizedState!==null){if(se(e),e.alternate===null)throw Error(f(340));Ll()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 13:if(se(e),t=e.memoizedState,t!==null&&t.dehydrated!==null){if(e.alternate===null)throw Error(f(340));Ll()}return t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 19:return x(Ct),null;case 4:return Ut(),null;case 10:return Ve(e.type),null;case 22:case 23:return se(e),fc(),t!==null&&x(Xl),t=e.flags,t&65536?(e.flags=t&-65537|128,e):null;case 24:return Ve(jt),null;case 25:return null;default:return null}}function zo(t,e){switch(Ji(e),e.tag){case 3:Ve(jt),Ut();break;case 26:case 27:case 5:Lu(e);break;case 4:Ut();break;case 31:e.memoizedState!==null&&se(e);break;case 13:se(e);break;case 19:x(Ct);break;case 10:Ve(e.type);break;case 22:case 23:se(e),fc(),t!==null&&x(Xl);break;case 24:Ve(jt)}}function yu(t,e){try{var l=e.updateQueue,a=l!==null?l.lastEffect:null;if(a!==null){var u=a.next;l=u;do{if((l.tag&t)===t){a=void 0;var n=l.create,i=l.inst;a=n(),i.destroy=a}l=l.next}while(l!==u)}}catch(r){pt(e,e.return,r)}}function vl(t,e,l){try{var a=e.updateQueue,u=a!==null?a.lastEffect:null;if(u!==null){var n=u.next;a=n;do{if((a.tag&t)===t){var i=a.inst,r=i.destroy;if(r!==void 0){i.destroy=void 0,u=e;var h=l,T=r;try{T()}catch(M){pt(u,h,M)}}}a=a.next}while(a!==n)}}catch(M){pt(e,e.return,M)}}function Ao(t){var e=t.updateQueue;if(e!==null){var l=t.stateNode;try{hs(e,l)}catch(a){pt(t,t.return,a)}}}function _o(t,e,l){l.props=wl(t.type,t.memoizedProps),l.state=t.memoizedState;try{l.componentWillUnmount()}catch(a){pt(t,e,a)}}function vu(t,e){try{var l=t.ref;if(l!==null){switch(t.tag){case 26:case 27:case 5:var a=t.stateNode;break;case 30:a=t.stateNode;break;default:a=t.stateNode}typeof l=="function"?t.refCleanup=l(a):l.current=a}}catch(u){pt(t,e,u)}}function He(t,e){var l=t.ref,a=t.refCleanup;if(l!==null)if(typeof a=="function")try{a()}catch(u){pt(t,e,u)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof l=="function")try{l(null)}catch(u){pt(t,e,u)}else l.current=null}function Ro(t){var e=t.type,l=t.memoizedProps,a=t.stateNode;try{t:switch(e){case"button":case"input":case"select":case"textarea":l.autoFocus&&a.focus();break t;case"img":l.src?a.src=l.src:l.srcSet&&(a.srcset=l.srcSet)}}catch(u){pt(t,t.return,u)}}function Gc(t,e,l){try{var a=t.stateNode;_y(a,t.type,l,e),a[Ft]=e}catch(u){pt(t,t.return,u)}}function Oo(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&zl(t.type)||t.tag===4}function Xc(t){t:for(;;){for(;t.sibling===null;){if(t.return===null||Oo(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&zl(t.type)||t.flags&2||t.child===null||t.tag===4)continue t;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Qc(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?(l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l).insertBefore(t,e):(e=l.nodeType===9?l.body:l.nodeName==="HTML"?l.ownerDocument.body:l,e.appendChild(t),l=l._reactRootContainer,l!=null||e.onclick!==null||(e.onclick=Ye));else if(a!==4&&(a===27&&zl(t.type)&&(l=t.stateNode,e=null),t=t.child,t!==null))for(Qc(t,e,l),t=t.sibling;t!==null;)Qc(t,e,l),t=t.sibling}function Dn(t,e,l){var a=t.tag;if(a===5||a===6)t=t.stateNode,e?l.insertBefore(t,e):l.appendChild(t);else if(a!==4&&(a===27&&zl(t.type)&&(l=t.stateNode),t=t.child,t!==null))for(Dn(t,e,l),t=t.sibling;t!==null;)Dn(t,e,l),t=t.sibling}function Mo(t){var e=t.stateNode,l=t.memoizedProps;try{for(var a=t.type,u=e.attributes;u.length;)e.removeAttributeNode(u[0]);Kt(e,a,l),e[Xt]=t,e[Ft]=l}catch(n){pt(t,t.return,n)}}var We=!1,qt=!1,Zc=!1,Do=typeof WeakSet=="function"?WeakSet:Set,Gt=null;function uy(t,e){if(t=t.containerInfo,of=kn,t=Gr(t),Bi(t)){if("selectionStart"in t)var l={start:t.selectionStart,end:t.selectionEnd};else t:{l=(l=t.ownerDocument)&&l.defaultView||window;var a=l.getSelection&&l.getSelection();if(a&&a.rangeCount!==0){l=a.anchorNode;var u=a.anchorOffset,n=a.focusNode;a=a.focusOffset;try{l.nodeType,n.nodeType}catch{l=null;break t}var i=0,r=-1,h=-1,T=0,M=0,C=t,_=null;e:for(;;){for(var O;C!==l||u!==0&&C.nodeType!==3||(r=i+u),C!==n||a!==0&&C.nodeType!==3||(h=i+a),C.nodeType===3&&(i+=C.nodeValue.length),(O=C.firstChild)!==null;)_=C,C=O;for(;;){if(C===t)break e;if(_===l&&++T===u&&(r=i),_===n&&++M===a&&(h=i),(O=C.nextSibling)!==null)break;C=_,_=C.parentNode}C=O}l=r===-1||h===-1?null:{start:r,end:h}}else l=null}l=l||{start:0,end:0}}else l=null;for(df={focusedElem:t,selectionRange:l},kn=!1,Gt=e;Gt!==null;)if(e=Gt,t=e.child,(e.subtreeFlags&1028)!==0&&t!==null)t.return=e,Gt=t;else for(;Gt!==null;){switch(e=Gt,n=e.alternate,t=e.flags,e.tag){case 0:if((t&4)!==0&&(t=e.updateQueue,t=t!==null?t.events:null,t!==null))for(l=0;l title"))),Kt(n,a,l),n[Xt]=t,Yt(n),a=n;break t;case"link":var i=Hd("link","href",u).get(a+(l.href||""));if(i){for(var r=0;rTt&&(i=Tt,Tt=I,I=i);var p=Lr(r,I),v=Lr(r,Tt);if(p&&v&&(O.rangeCount!==1||O.anchorNode!==p.node||O.anchorOffset!==p.offset||O.focusNode!==v.node||O.focusOffset!==v.offset)){var E=C.createRange();E.setStart(p.node,p.offset),O.removeAllRanges(),I>Tt?(O.addRange(E),O.extend(v.node,v.offset)):(E.setEnd(v.node,v.offset),O.addRange(E))}}}}for(C=[],O=r;O=O.parentNode;)O.nodeType===1&&C.push({element:O,left:O.scrollLeft,top:O.scrollTop});for(typeof r.focus=="function"&&r.focus(),r=0;rl?32:l,D.T=null,l=kc,kc=null;var n=bl,i=tl;if(Lt=0,Oa=bl=null,tl=0,(mt&6)!==0)throw Error(f(331));var r=mt;if(mt|=4,Go(n.current),qo(n,n.current,i,l),mt=r,Tu(0,!1),ne&&typeof ne.onPostCommitFiberRoot=="function")try{ne.onPostCommitFiberRoot(Ga,n)}catch{}return!0}finally{q.p=u,D.T=a,ud(t,e)}}function id(t,e,l){e=pe(l,e),e=Dc(t.stateNode,e,2),t=hl(t,e,2),t!==null&&(Qa(t,2),Be(t))}function pt(t,e,l){if(t.tag===3)id(t,t,l);else for(;e!==null;){if(e.tag===3){id(e,t,l);break}else if(e.tag===1){var a=e.stateNode;if(typeof e.type.getDerivedStateFromError=="function"||typeof a.componentDidCatch=="function"&&(Sl===null||!Sl.has(a))){t=pe(l,t),l=io(2),a=hl(e,l,2),a!==null&&(co(l,a,e,t),Qa(a,2),Be(a));break}}e=e.return}}function tf(t,e,l){var a=t.pingCache;if(a===null){a=t.pingCache=new cy;var u=new Set;a.set(e,u)}else u=a.get(e),u===void 0&&(u=new Set,a.set(e,u));u.has(l)||(wc=!0,u.add(l),t=dy.bind(null,t,e,l),e.then(t,t))}function dy(t,e,l){var a=t.pingCache;a!==null&&a.delete(e),t.pingedLanes|=t.suspendedLanes&l,t.warmLanes&=~l,At===t&&(ft&l)===l&&(Nt===4||Nt===3&&(ft&62914560)===ft&&300>ue()-Cn?(mt&2)===0&&Ma(t,0):Jc|=l,Ra===ft&&(Ra=0)),Be(t)}function cd(t,e){e===0&&(e=tr()),t=Bl(t,e),t!==null&&(Qa(t,e),Be(t))}function hy(t){var e=t.memoizedState,l=0;e!==null&&(l=e.retryLane),cd(t,l)}function my(t,e){var l=0;switch(t.tag){case 31:case 13:var a=t.stateNode,u=t.memoizedState;u!==null&&(l=u.retryLane);break;case 19:a=t.stateNode;break;case 22:a=t.stateNode._retryCache;break;default:throw Error(f(314))}a!==null&&a.delete(e),cd(t,l)}function yy(t,e){return hi(t,e)}var Yn=null,Na=null,ef=!1,Gn=!1,lf=!1,Tl=0;function Be(t){t!==Na&&t.next===null&&(Na===null?Yn=Na=t:Na=Na.next=t),Gn=!0,ef||(ef=!0,gy())}function Tu(t,e){if(!lf&&Gn){lf=!0;do for(var l=!1,a=Yn;a!==null;){if(t!==0){var u=a.pendingLanes;if(u===0)var n=0;else{var i=a.suspendedLanes,r=a.pingedLanes;n=(1<<31-ie(42|t)+1)-1,n&=u&~(i&~r),n=n&201326741?n&201326741|1:n?n|2:0}n!==0&&(l=!0,od(a,n))}else n=ft,n=Zu(a,a===At?n:0,a.cancelPendingCommit!==null||a.timeoutHandle!==-1),(n&3)===0||Xa(a,n)||(l=!0,od(a,n));a=a.next}while(l);lf=!1}}function vy(){fd()}function fd(){Gn=ef=!1;var t=0;Tl!==0&&Oy()&&(t=Tl);for(var e=ue(),l=null,a=Yn;a!==null;){var u=a.next,n=rd(a,e);n===0?(a.next=null,l===null?Yn=u:l.next=u,u===null&&(Na=l)):(l=a,(t!==0||(n&3)!==0)&&(Gn=!0)),a=u}Lt!==0&&Lt!==5||Tu(t),Tl!==0&&(Tl=0)}function rd(t,e){for(var l=t.suspendedLanes,a=t.pingedLanes,u=t.expirationTimes,n=t.pendingLanes&-62914561;0r)break;var M=h.transferSize,C=h.initiatorType;M&&Sd(C)&&(h=h.responseEnd,i+=M*(h"u"?null:document;function Ud(t,e,l){var a=Ua;if(a&&typeof e=="string"&&e){var u=ve(e);u='link[rel="'+t+'"][href="'+u+'"]',typeof l=="string"&&(u+='[crossorigin="'+l+'"]'),Nd.has(u)||(Nd.add(u),t={rel:t,crossOrigin:l,href:e},a.querySelector(u)===null&&(e=a.createElement("link"),Kt(e,"link",t),Yt(e),a.head.appendChild(e)))}}function By(t){el.D(t),Ud("dns-prefetch",t,null)}function qy(t,e){el.C(t,e),Ud("preconnect",t,e)}function Ly(t,e,l){el.L(t,e,l);var a=Ua;if(a&&t&&e){var u='link[rel="preload"][as="'+ve(e)+'"]';e==="image"&&l&&l.imageSrcSet?(u+='[imagesrcset="'+ve(l.imageSrcSet)+'"]',typeof l.imageSizes=="string"&&(u+='[imagesizes="'+ve(l.imageSizes)+'"]')):u+='[href="'+ve(t)+'"]';var n=u;switch(e){case"style":n=Ca(t);break;case"script":n=xa(t)}Ae.has(n)||(t=z({rel:"preload",href:e==="image"&&l&&l.imageSrcSet?void 0:t,as:e},l),Ae.set(n,t),a.querySelector(u)!==null||e==="style"&&a.querySelector(Ru(n))||e==="script"&&a.querySelector(Ou(n))||(e=a.createElement("link"),Kt(e,"link",t),Yt(e),a.head.appendChild(e)))}}function Yy(t,e){el.m(t,e);var l=Ua;if(l&&t){var a=e&&typeof e.as=="string"?e.as:"script",u='link[rel="modulepreload"][as="'+ve(a)+'"][href="'+ve(t)+'"]',n=u;switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":n=xa(t)}if(!Ae.has(n)&&(t=z({rel:"modulepreload",href:t},e),Ae.set(n,t),l.querySelector(u)===null)){switch(a){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(l.querySelector(Ou(n)))return}a=l.createElement("link"),Kt(a,"link",t),Yt(a),l.head.appendChild(a)}}}function Gy(t,e,l){el.S(t,e,l);var a=Ua;if(a&&t){var u=ta(a).hoistableStyles,n=Ca(t);e=e||"default";var i=u.get(n);if(!i){var r={loading:0,preload:null};if(i=a.querySelector(Ru(n)))r.loading=5;else{t=z({rel:"stylesheet",href:t,"data-precedence":e},l),(l=Ae.get(n))&&Sf(t,l);var h=i=a.createElement("link");Yt(h),Kt(h,"link",t),h._p=new Promise(function(T,M){h.onload=T,h.onerror=M}),h.addEventListener("load",function(){r.loading|=1}),h.addEventListener("error",function(){r.loading|=2}),r.loading|=4,Kn(i,e,a)}i={type:"stylesheet",instance:i,count:1,state:r},u.set(n,i)}}}function Xy(t,e){el.X(t,e);var l=Ua;if(l&&t){var a=ta(l).hoistableScripts,u=xa(t),n=a.get(u);n||(n=l.querySelector(Ou(u)),n||(t=z({src:t,async:!0},e),(e=Ae.get(u))&&bf(t,e),n=l.createElement("script"),Yt(n),Kt(n,"link",t),l.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(u,n))}}function Qy(t,e){el.M(t,e);var l=Ua;if(l&&t){var a=ta(l).hoistableScripts,u=xa(t),n=a.get(u);n||(n=l.querySelector(Ou(u)),n||(t=z({src:t,async:!0,type:"module"},e),(e=Ae.get(u))&&bf(t,e),n=l.createElement("script"),Yt(n),Kt(n,"link",t),l.head.appendChild(n)),n={type:"script",instance:n,count:1,state:null},a.set(u,n))}}function Cd(t,e,l,a){var u=(u=nt.current)?Vn(u):null;if(!u)throw Error(f(446));switch(t){case"meta":case"title":return null;case"style":return typeof l.precedence=="string"&&typeof l.href=="string"?(e=Ca(l.href),l=ta(u).hoistableStyles,a=l.get(e),a||(a={type:"style",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};case"link":if(l.rel==="stylesheet"&&typeof l.href=="string"&&typeof l.precedence=="string"){t=Ca(l.href);var n=ta(u).hoistableStyles,i=n.get(t);if(i||(u=u.ownerDocument||u,i={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},n.set(t,i),(n=u.querySelector(Ru(t)))&&!n._p&&(i.instance=n,i.state.loading=5),Ae.has(t)||(l={rel:"preload",as:"style",href:l.href,crossOrigin:l.crossOrigin,integrity:l.integrity,media:l.media,hrefLang:l.hrefLang,referrerPolicy:l.referrerPolicy},Ae.set(t,l),n||Zy(u,t,l,i.state))),e&&a===null)throw Error(f(528,""));return i}if(e&&a!==null)throw Error(f(529,""));return null;case"script":return e=l.async,l=l.src,typeof l=="string"&&e&&typeof e!="function"&&typeof e!="symbol"?(e=xa(l),l=ta(u).hoistableScripts,a=l.get(e),a||(a={type:"script",instance:null,count:0,state:null},l.set(e,a)),a):{type:"void",instance:null,count:0,state:null};default:throw Error(f(444,t))}}function Ca(t){return'href="'+ve(t)+'"'}function Ru(t){return'link[rel="stylesheet"]['+t+"]"}function xd(t){return z({},t,{"data-precedence":t.precedence,precedence:null})}function Zy(t,e,l,a){t.querySelector('link[rel="preload"][as="style"]['+e+"]")?a.loading=1:(e=t.createElement("link"),a.preload=e,e.addEventListener("load",function(){return a.loading|=1}),e.addEventListener("error",function(){return a.loading|=2}),Kt(e,"link",l),Yt(e),t.head.appendChild(e))}function xa(t){return'[src="'+ve(t)+'"]'}function Ou(t){return"script[async]"+t}function jd(t,e,l){if(e.count++,e.instance===null)switch(e.type){case"style":var a=t.querySelector('style[data-href~="'+ve(l.href)+'"]');if(a)return e.instance=a,Yt(a),a;var u=z({},l,{"data-href":l.href,"data-precedence":l.precedence,href:null,precedence:null});return a=(t.ownerDocument||t).createElement("style"),Yt(a),Kt(a,"style",u),Kn(a,l.precedence,t),e.instance=a;case"stylesheet":u=Ca(l.href);var n=t.querySelector(Ru(u));if(n)return e.state.loading|=4,e.instance=n,Yt(n),n;a=xd(l),(u=Ae.get(u))&&Sf(a,u),n=(t.ownerDocument||t).createElement("link"),Yt(n);var i=n;return i._p=new Promise(function(r,h){i.onload=r,i.onerror=h}),Kt(n,"link",a),e.state.loading|=4,Kn(n,l.precedence,t),e.instance=n;case"script":return n=xa(l.src),(u=t.querySelector(Ou(n)))?(e.instance=u,Yt(u),u):(a=l,(u=Ae.get(n))&&(a=z({},l),bf(a,u)),t=t.ownerDocument||t,u=t.createElement("script"),Yt(u),Kt(u,"link",a),t.head.appendChild(u),e.instance=u);case"void":return null;default:throw Error(f(443,e.type))}else e.type==="stylesheet"&&(e.state.loading&4)===0&&(a=e.instance,e.state.loading|=4,Kn(a,l.precedence,t));return e.instance}function Kn(t,e,l){for(var a=l.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),u=a.length?a[a.length-1]:null,n=u,i=0;i title"):null)}function Vy(t,e,l){if(l===1||e.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof e.precedence!="string"||typeof e.href!="string"||e.href==="")break;return!0;case"link":if(typeof e.rel!="string"||typeof e.href!="string"||e.href===""||e.onLoad||e.onError)break;return e.rel==="stylesheet"?(t=e.disabled,typeof e.precedence=="string"&&t==null):!0;case"script":if(e.async&&typeof e.async!="function"&&typeof e.async!="symbol"&&!e.onLoad&&!e.onError&&e.src&&typeof e.src=="string")return!0}return!1}function qd(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function Ky(t,e,l,a){if(l.type==="stylesheet"&&(typeof a.media!="string"||matchMedia(a.media).matches!==!1)&&(l.state.loading&4)===0){if(l.instance===null){var u=Ca(a.href),n=e.querySelector(Ru(u));if(n){e=n._p,e!==null&&typeof e=="object"&&typeof e.then=="function"&&(t.count++,t=Jn.bind(t),e.then(t,t)),l.state.loading|=4,l.instance=n,Yt(n);return}n=e.ownerDocument||e,a=xd(a),(u=Ae.get(u))&&Sf(a,u),n=n.createElement("link"),Yt(n);var i=n;i._p=new Promise(function(r,h){i.onload=r,i.onerror=h}),Kt(n,"link",a),l.instance=n}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(l,e),(e=l.state.preload)&&(l.state.loading&3)===0&&(t.count++,l=Jn.bind(t),e.addEventListener("load",l),e.addEventListener("error",l))}}var Ef=0;function wy(t,e){return t.stylesheets&&t.count===0&&Wn(t,t.stylesheets),0Ef?50:800)+e);return t.unsuspend=l,function(){t.unsuspend=null,clearTimeout(a),clearTimeout(u)}}:null}function Jn(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Wn(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var $n=null;function Wn(t,e){t.stylesheets=null,t.unsuspend!==null&&(t.count++,$n=new Map,e.forEach(Jy,t),$n=null,Jn.call(t))}function Jy(t,e){if(!(e.state.loading&4)){var l=$n.get(t);if(l)var a=l.get(null);else{l=new Map,$n.set(t,l);for(var u=t.querySelectorAll("link[data-precedence],style[data-precedence]"),n=0;n"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(c)}catch(s){console.error(s)}}return c(),Nf.exports=rv(),Nf.exports}var ov=sv();var ch="popstate";function dv(c={}){function s(f,d){let{pathname:m,search:S,hash:R}=f.location;return Lf("",{pathname:m,search:S,hash:R},d.state&&d.state.usr||null,d.state&&d.state.key||"default")}function o(f,d){return typeof d=="string"?d:Hu(d)}return mv(s,o,null,c)}function Mt(c,s){if(c===!1||c===null||typeof c>"u")throw new Error(s)}function _e(c,s){if(!c){typeof console<"u"&&console.warn(s);try{throw new Error(s)}catch{}}}function hv(){return Math.random().toString(36).substring(2,10)}function fh(c,s){return{usr:c.state,key:c.key,idx:s}}function Lf(c,s,o=null,f){return{pathname:typeof c=="string"?c:c.pathname,search:"",hash:"",...typeof s=="string"?Ha(s):s,state:o,key:s&&s.key||f||hv()}}function Hu({pathname:c="/",search:s="",hash:o=""}){return s&&s!=="?"&&(c+=s.charAt(0)==="?"?s:"?"+s),o&&o!=="#"&&(c+=o.charAt(0)==="#"?o:"#"+o),c}function Ha(c){let s={};if(c){let o=c.indexOf("#");o>=0&&(s.hash=c.substring(o),c=c.substring(0,o));let f=c.indexOf("?");f>=0&&(s.search=c.substring(f),c=c.substring(0,f)),c&&(s.pathname=c)}return s}function mv(c,s,o,f={}){let{window:d=document.defaultView,v5Compat:m=!1}=f,S=d.history,R="POP",b=null,y=N();y==null&&(y=0,S.replaceState({...S.state,idx:y},""));function N(){return(S.state||{idx:null}).idx}function z(){R="POP";let Z=N(),J=Z==null?null:Z-y;y=Z,b&&b({action:R,location:X.location,delta:J})}function H(Z,J){R="PUSH";let G=Lf(X.location,Z,J);y=N()+1;let W=fh(G,y),$=X.createHref(G);try{S.pushState(W,"",$)}catch(P){if(P instanceof DOMException&&P.name==="DataCloneError")throw P;d.location.assign($)}m&&b&&b({action:R,location:X.location,delta:1})}function Q(Z,J){R="REPLACE";let G=Lf(X.location,Z,J);y=N();let W=fh(G,y),$=X.createHref(G);S.replaceState(W,"",$),m&&b&&b({action:R,location:X.location,delta:0})}function F(Z){return yv(Z)}let X={get action(){return R},get location(){return c(d,S)},listen(Z){if(b)throw new Error("A history only accepts one active listener");return d.addEventListener(ch,z),b=Z,()=>{d.removeEventListener(ch,z),b=null}},createHref(Z){return s(d,Z)},createURL:F,encodeLocation(Z){let J=F(Z);return{pathname:J.pathname,search:J.search,hash:J.hash}},push:H,replace:Q,go(Z){return S.go(Z)}};return X}function yv(c,s=!1){let o="http://localhost";typeof window<"u"&&(o=window.location.origin!=="null"?window.location.origin:window.location.href),Mt(o,"No window.location.(origin|href) available to create URL");let f=typeof c=="string"?c:Hu(c);return f=f.replace(/ $/,"%20"),!s&&f.startsWith("//")&&(f=o+f),new URL(f,o)}function hh(c,s,o="/"){return vv(c,s,o,!1)}function vv(c,s,o,f){let d=typeof s=="string"?Ha(s):s,m=al(d.pathname||"/",o);if(m==null)return null;let S=mh(c);gv(S);let R=null;for(let b=0;R==null&&b{let N={relativePath:y===void 0?S.path||"":y,caseSensitive:S.caseSensitive===!0,childrenIndex:R,route:S};if(N.relativePath.startsWith("/")){if(!N.relativePath.startsWith(f)&&b)return;Mt(N.relativePath.startsWith(f),`Absolute route path "${N.relativePath}" nested under path "${f}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),N.relativePath=N.relativePath.slice(f.length)}let z=ll([f,N.relativePath]),H=o.concat(N);S.children&&S.children.length>0&&(Mt(S.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${z}".`),mh(S.children,s,H,z,b)),!(S.path==null&&!S.index)&&s.push({path:z,score:Av(z,S.index),routesMeta:H})};return c.forEach((S,R)=>{if(S.path===""||!S.path?.includes("?"))m(S,R);else for(let b of yh(S.path))m(S,R,!0,b)}),s}function yh(c){let s=c.split("/");if(s.length===0)return[];let[o,...f]=s,d=o.endsWith("?"),m=o.replace(/\?$/,"");if(f.length===0)return d?[m,""]:[m];let S=yh(f.join("/")),R=[];return R.push(...S.map(b=>b===""?m:[m,b].join("/"))),d&&R.push(...S),R.map(b=>c.startsWith("/")&&b===""?"/":b)}function gv(c){c.sort((s,o)=>s.score!==o.score?o.score-s.score:_v(s.routesMeta.map(f=>f.childrenIndex),o.routesMeta.map(f=>f.childrenIndex)))}var pv=/^:[\w-]+$/,Sv=3,bv=2,Ev=1,Tv=10,zv=-2,rh=c=>c==="*";function Av(c,s){let o=c.split("/"),f=o.length;return o.some(rh)&&(f+=zv),s&&(f+=bv),o.filter(d=>!rh(d)).reduce((d,m)=>d+(pv.test(m)?Sv:m===""?Ev:Tv),f)}function _v(c,s){return c.length===s.length&&c.slice(0,-1).every((f,d)=>f===s[d])?c[c.length-1]-s[s.length-1]:0}function Rv(c,s,o=!1){let{routesMeta:f}=c,d={},m="/",S=[];for(let R=0;R{if(N==="*"){let F=R[H]||"";S=m.slice(0,m.length-F.length).replace(/(.)\/+$/,"$1")}const Q=R[H];return z&&!Q?y[N]=void 0:y[N]=(Q||"").replace(/%2F/g,"/"),y},{}),pathname:m,pathnameBase:S,pattern:c}}function Ov(c,s=!1,o=!0){_e(c==="*"||!c.endsWith("*")||c.endsWith("/*"),`Route path "${c}" will be treated as if it were "${c.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${c.replace(/\*$/,"/*")}".`);let f=[],d="^"+c.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(S,R,b)=>(f.push({paramName:R,isOptional:b!=null}),b?"/?([^\\/]+)?":"/([^\\/]+)")).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return c.endsWith("*")?(f.push({paramName:"*"}),d+=c==="*"||c==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):o?d+="\\/*$":c!==""&&c!=="/"&&(d+="(?:(?=\\/|$))"),[new RegExp(d,s?void 0:"i"),f]}function Mv(c){try{return c.split("/").map(s=>decodeURIComponent(s).replace(/\//g,"%2F")).join("/")}catch(s){return _e(!1,`The URL path "${c}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${s}).`),c}}function al(c,s){if(s==="/")return c;if(!c.toLowerCase().startsWith(s.toLowerCase()))return null;let o=s.endsWith("/")?s.length-1:s.length,f=c.charAt(o);return f&&f!=="/"?null:c.slice(o)||"/"}var Dv=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function Nv(c,s="/"){let{pathname:o,search:f="",hash:d=""}=typeof c=="string"?Ha(c):c,m;return o?(o=o.replace(/\/\/+/g,"/"),o.startsWith("/")?m=sh(o.substring(1),"/"):m=sh(o,s)):m=s,{pathname:m,search:xv(f),hash:jv(d)}}function sh(c,s){let o=s.replace(/\/+$/,"").split("/");return c.split("/").forEach(d=>{d===".."?o.length>1&&o.pop():d!=="."&&o.push(d)}),o.length>1?o.join("/"):"/"}function jf(c,s,o,f){return`Cannot include a '${c}' character in a manually specified \`to.${s}\` field [${JSON.stringify(f)}]. Please separate it out to the \`to.${o}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Uv(c){return c.filter((s,o)=>o===0||s.route.path&&s.route.path.length>0)}function Qf(c){let s=Uv(c);return s.map((o,f)=>f===s.length-1?o.pathname:o.pathnameBase)}function Zf(c,s,o,f=!1){let d;typeof c=="string"?d=Ha(c):(d={...c},Mt(!d.pathname||!d.pathname.includes("?"),jf("?","pathname","search",d)),Mt(!d.pathname||!d.pathname.includes("#"),jf("#","pathname","hash",d)),Mt(!d.search||!d.search.includes("#"),jf("#","search","hash",d)));let m=c===""||d.pathname==="",S=m?"/":d.pathname,R;if(S==null)R=o;else{let z=s.length-1;if(!f&&S.startsWith("..")){let H=S.split("/");for(;H[0]==="..";)H.shift(),z-=1;d.pathname=H.join("/")}R=z>=0?s[z]:"/"}let b=Nv(d,R),y=S&&S!=="/"&&S.endsWith("/"),N=(m||S===".")&&o.endsWith("/");return!b.pathname.endsWith("/")&&(y||N)&&(b.pathname+="/"),b}var ll=c=>c.join("/").replace(/\/\/+/g,"/"),Cv=c=>c.replace(/\/+$/,"").replace(/^\/*/,"/"),xv=c=>!c||c==="?"?"":c.startsWith("?")?c:"?"+c,jv=c=>!c||c==="#"?"":c.startsWith("#")?c:"#"+c,Hv=class{constructor(c,s,o,f=!1){this.status=c,this.statusText=s||"",this.internal=f,o instanceof Error?(this.data=o.toString(),this.error=o):this.data=o}};function Bv(c){return c!=null&&typeof c.status=="number"&&typeof c.statusText=="string"&&typeof c.internal=="boolean"&&"data"in c}function qv(c){return c.map(s=>s.route.path).filter(Boolean).join("/").replace(/\/\/*/g,"/")||"/"}var vh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function gh(c,s){let o=c;if(typeof o!="string"||!Dv.test(o))return{absoluteURL:void 0,isExternal:!1,to:o};let f=o,d=!1;if(vh)try{let m=new URL(window.location.href),S=o.startsWith("//")?new URL(m.protocol+o):new URL(o),R=al(S.pathname,s);S.origin===m.origin&&R!=null?o=R+S.search+S.hash:d=!0}catch{_e(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:f,isExternal:d,to:o}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var ph=["POST","PUT","PATCH","DELETE"];new Set(ph);var Lv=["GET",...ph];new Set(Lv);var Ba=A.createContext(null);Ba.displayName="DataRouter";var ci=A.createContext(null);ci.displayName="DataRouterState";var Yv=A.createContext(!1),Sh=A.createContext({isTransitioning:!1});Sh.displayName="ViewTransition";var Gv=A.createContext(new Map);Gv.displayName="Fetchers";var Xv=A.createContext(null);Xv.displayName="Await";var me=A.createContext(null);me.displayName="Navigation";var Bu=A.createContext(null);Bu.displayName="Location";var Ne=A.createContext({outlet:null,matches:[],isDataRoute:!1});Ne.displayName="Route";var Vf=A.createContext(null);Vf.displayName="RouteError";var bh="REACT_ROUTER_ERROR",Qv="REDIRECT",Zv="ROUTE_ERROR_RESPONSE";function Vv(c){if(c.startsWith(`${bh}:${Qv}:{`))try{let s=JSON.parse(c.slice(28));if(typeof s=="object"&&s&&typeof s.status=="number"&&typeof s.statusText=="string"&&typeof s.location=="string"&&typeof s.reloadDocument=="boolean"&&typeof s.replace=="boolean")return s}catch{}}function Kv(c){if(c.startsWith(`${bh}:${Zv}:{`))try{let s=JSON.parse(c.slice(40));if(typeof s=="object"&&s&&typeof s.status=="number"&&typeof s.statusText=="string")return new Hv(s.status,s.statusText,s.data)}catch{}}function wv(c,{relative:s}={}){Mt(qa(),"useHref() may be used only in the context of a component.");let{basename:o,navigator:f}=A.useContext(me),{hash:d,pathname:m,search:S}=qu(c,{relative:s}),R=m;return o!=="/"&&(R=m==="/"?o:ll([o,m])),f.createHref({pathname:R,search:S,hash:d})}function qa(){return A.useContext(Bu)!=null}function Ue(){return Mt(qa(),"useLocation() may be used only in the context of a component."),A.useContext(Bu).location}var Eh="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function Th(c){A.useContext(me).static||A.useLayoutEffect(c)}function La(){let{isDataRoute:c}=A.useContext(Ne);return c?n0():Jv()}function Jv(){Mt(qa(),"useNavigate() may be used only in the context of a component.");let c=A.useContext(Ba),{basename:s,navigator:o}=A.useContext(me),{matches:f}=A.useContext(Ne),{pathname:d}=Ue(),m=JSON.stringify(Qf(f)),S=A.useRef(!1);return Th(()=>{S.current=!0}),A.useCallback((b,y={})=>{if(_e(S.current,Eh),!S.current)return;if(typeof b=="number"){o.go(b);return}let N=Zf(b,JSON.parse(m),d,y.relative==="path");c==null&&s!=="/"&&(N.pathname=N.pathname==="/"?s:ll([s,N.pathname])),(y.replace?o.replace:o.push)(N,y.state,y)},[s,o,m,d,c])}A.createContext(null);function zh(){let{matches:c}=A.useContext(Ne),s=c[c.length-1];return s?s.params:{}}function qu(c,{relative:s}={}){let{matches:o}=A.useContext(Ne),{pathname:f}=Ue(),d=JSON.stringify(Qf(o));return A.useMemo(()=>Zf(c,JSON.parse(d),f,s==="path"),[c,d,f,s])}function $v(c,s){return Ah(c,s)}function Ah(c,s,o,f,d){Mt(qa(),"useRoutes() may be used only in the context of a component.");let{navigator:m}=A.useContext(me),{matches:S}=A.useContext(Ne),R=S[S.length-1],b=R?R.params:{},y=R?R.pathname:"/",N=R?R.pathnameBase:"/",z=R&&R.route;{let G=z&&z.path||"";Rh(y,!z||G.endsWith("*")||G.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${y}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let H=Ue(),Q;if(s){let G=typeof s=="string"?Ha(s):s;Mt(N==="/"||G.pathname?.startsWith(N),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${N}" but pathname "${G.pathname}" was given in the \`location\` prop.`),Q=G}else Q=H;let F=Q.pathname||"/",X=F;if(N!=="/"){let G=N.replace(/^\//,"").split("/");X="/"+F.replace(/^\//,"").split("/").slice(G.length).join("/")}let Z=hh(c,{pathname:X});_e(z||Z!=null,`No routes matched location "${Q.pathname}${Q.search}${Q.hash}" `),_e(Z==null||Z[Z.length-1].route.element!==void 0||Z[Z.length-1].route.Component!==void 0||Z[Z.length-1].route.lazy!==void 0,`Matched leaf route at location "${Q.pathname}${Q.search}${Q.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let J=Pv(Z&&Z.map(G=>Object.assign({},G,{params:Object.assign({},b,G.params),pathname:ll([N,m.encodeLocation?m.encodeLocation(G.pathname.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:G.pathname]),pathnameBase:G.pathnameBase==="/"?N:ll([N,m.encodeLocation?m.encodeLocation(G.pathnameBase.replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:G.pathnameBase])})),S,o,f,d);return s&&J?A.createElement(Bu.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",...Q},navigationType:"POP"}},J):J}function Wv(){let c=u0(),s=Bv(c)?`${c.status} ${c.statusText}`:c instanceof Error?c.message:JSON.stringify(c),o=c instanceof Error?c.stack:null,f="rgba(200,200,200, 0.5)",d={padding:"0.5rem",backgroundColor:f},m={padding:"2px 4px",backgroundColor:f},S=null;return console.error("Error handled by React Router default ErrorBoundary:",c),S=A.createElement(A.Fragment,null,A.createElement("p",null,"💿 Hey developer 👋"),A.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",A.createElement("code",{style:m},"ErrorBoundary")," or"," ",A.createElement("code",{style:m},"errorElement")," prop on your route.")),A.createElement(A.Fragment,null,A.createElement("h2",null,"Unexpected Application Error!"),A.createElement("h3",{style:{fontStyle:"italic"}},s),o?A.createElement("pre",{style:d},o):null,S)}var kv=A.createElement(Wv,null),_h=class extends A.Component{constructor(c){super(c),this.state={location:c.location,revalidation:c.revalidation,error:c.error}}static getDerivedStateFromError(c){return{error:c}}static getDerivedStateFromProps(c,s){return s.location!==c.location||s.revalidation!=="idle"&&c.revalidation==="idle"?{error:c.error,location:c.location,revalidation:c.revalidation}:{error:c.error!==void 0?c.error:s.error,location:s.location,revalidation:c.revalidation||s.revalidation}}componentDidCatch(c,s){this.props.onError?this.props.onError(c,s):console.error("React Router caught the following error during render",c)}render(){let c=this.state.error;if(this.context&&typeof c=="object"&&c&&"digest"in c&&typeof c.digest=="string"){const o=Kv(c.digest);o&&(c=o)}let s=c!==void 0?A.createElement(Ne.Provider,{value:this.props.routeContext},A.createElement(Vf.Provider,{value:c,children:this.props.component})):this.props.children;return this.context?A.createElement(Fv,{error:c},s):s}};_h.contextType=Yv;var Hf=new WeakMap;function Fv({children:c,error:s}){let{basename:o}=A.useContext(me);if(typeof s=="object"&&s&&"digest"in s&&typeof s.digest=="string"){let f=Vv(s.digest);if(f){let d=Hf.get(s);if(d)throw d;let m=gh(f.location,o);if(vh&&!Hf.get(s))if(m.isExternal||f.reloadDocument)window.location.href=m.absoluteURL||m.to;else{const S=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(m.to,{replace:f.replace}));throw Hf.set(s,S),S}return A.createElement("meta",{httpEquiv:"refresh",content:`0;url=${m.absoluteURL||m.to}`})}}return c}function Iv({routeContext:c,match:s,children:o}){let f=A.useContext(Ba);return f&&f.static&&f.staticContext&&(s.route.errorElement||s.route.ErrorBoundary)&&(f.staticContext._deepestRenderedBoundaryId=s.route.id),A.createElement(Ne.Provider,{value:c},o)}function Pv(c,s=[],o=null,f=null,d=null){if(c==null){if(!o)return null;if(o.errors)c=o.matches;else if(s.length===0&&!o.initialized&&o.matches.length>0)c=o.matches;else return null}let m=c,S=o?.errors;if(S!=null){let N=m.findIndex(z=>z.route.id&&S?.[z.route.id]!==void 0);Mt(N>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(S).join(",")}`),m=m.slice(0,Math.min(m.length,N+1))}let R=!1,b=-1;if(o)for(let N=0;N=0?m=m.slice(0,b+1):m=[m[0]];break}}}let y=o&&f?(N,z)=>{f(N,{location:o.location,params:o.matches?.[0]?.params??{},unstable_pattern:qv(o.matches),errorInfo:z})}:void 0;return m.reduceRight((N,z,H)=>{let Q,F=!1,X=null,Z=null;o&&(Q=S&&z.route.id?S[z.route.id]:void 0,X=z.route.errorElement||kv,R&&(b<0&&H===0?(Rh("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),F=!0,Z=null):b===H&&(F=!0,Z=z.route.hydrateFallbackElement||null)));let J=s.concat(m.slice(0,H+1)),G=()=>{let W;return Q?W=X:F?W=Z:z.route.Component?W=A.createElement(z.route.Component,null):z.route.element?W=z.route.element:W=N,A.createElement(Iv,{match:z,routeContext:{outlet:N,matches:J,isDataRoute:o!=null},children:W})};return o&&(z.route.ErrorBoundary||z.route.errorElement||H===0)?A.createElement(_h,{location:o.location,revalidation:o.revalidation,component:X,error:Q,children:G(),routeContext:{outlet:null,matches:J,isDataRoute:!0},onError:y}):G()},null)}function Kf(c){return`${c} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function t0(c){let s=A.useContext(Ba);return Mt(s,Kf(c)),s}function e0(c){let s=A.useContext(ci);return Mt(s,Kf(c)),s}function l0(c){let s=A.useContext(Ne);return Mt(s,Kf(c)),s}function wf(c){let s=l0(c),o=s.matches[s.matches.length-1];return Mt(o.route.id,`${c} can only be used on routes that contain a unique "id"`),o.route.id}function a0(){return wf("useRouteId")}function u0(){let c=A.useContext(Vf),s=e0("useRouteError"),o=wf("useRouteError");return c!==void 0?c:s.errors?.[o]}function n0(){let{router:c}=t0("useNavigate"),s=wf("useNavigate"),o=A.useRef(!1);return Th(()=>{o.current=!0}),A.useCallback(async(d,m={})=>{_e(o.current,Eh),o.current&&(typeof d=="number"?await c.navigate(d):await c.navigate(d,{fromRouteId:s,...m}))},[c,s])}var oh={};function Rh(c,s,o){!s&&!oh[c]&&(oh[c]=!0,_e(!1,o))}A.memo(i0);function i0({routes:c,future:s,state:o,onError:f}){return Ah(c,void 0,o,f,s)}function c0({to:c,replace:s,state:o,relative:f}){Mt(qa()," may be used only in the context of a component.");let{static:d}=A.useContext(me);_e(!d," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:m}=A.useContext(Ne),{pathname:S}=Ue(),R=La(),b=Zf(c,Qf(m),S,f==="path"),y=JSON.stringify(b);return A.useEffect(()=>{R(JSON.parse(y),{replace:s,state:o,relative:f})},[R,y,f,s,o]),null}function kl(c){Mt(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function f0({basename:c="/",children:s=null,location:o,navigationType:f="POP",navigator:d,static:m=!1,unstable_useTransitions:S}){Mt(!qa(),"You cannot render a inside another . You should never have more than one in your app.");let R=c.replace(/^\/*/,"/"),b=A.useMemo(()=>({basename:R,navigator:d,static:m,unstable_useTransitions:S,future:{}}),[R,d,m,S]);typeof o=="string"&&(o=Ha(o));let{pathname:y="/",search:N="",hash:z="",state:H=null,key:Q="default"}=o,F=A.useMemo(()=>{let X=al(y,R);return X==null?null:{location:{pathname:X,search:N,hash:z,state:H,key:Q},navigationType:f}},[R,y,N,z,H,Q,f]);return _e(F!=null,` is not able to match the URL "${y}${N}${z}" because it does not start with the basename, so the won't render anything.`),F==null?null:A.createElement(me.Provider,{value:b},A.createElement(Bu.Provider,{children:s,value:F}))}function Oh({children:c,location:s}){return $v(Yf(c),s)}function Yf(c,s=[]){let o=[];return A.Children.forEach(c,(f,d)=>{if(!A.isValidElement(f))return;let m=[...s,d];if(f.type===A.Fragment){o.push.apply(o,Yf(f.props.children,m));return}Mt(f.type===kl,`[${typeof f.type=="string"?f.type:f.type.name}] is not a component. All component children of must be a or `),Mt(!f.props.index||!f.props.children,"An index route cannot have child routes.");let S={id:f.props.id||m.join("-"),caseSensitive:f.props.caseSensitive,element:f.props.element,Component:f.props.Component,index:f.props.index,path:f.props.path,middleware:f.props.middleware,loader:f.props.loader,action:f.props.action,hydrateFallbackElement:f.props.hydrateFallbackElement,HydrateFallback:f.props.HydrateFallback,errorElement:f.props.errorElement,ErrorBoundary:f.props.ErrorBoundary,hasErrorBoundary:f.props.hasErrorBoundary===!0||f.props.ErrorBoundary!=null||f.props.errorElement!=null,shouldRevalidate:f.props.shouldRevalidate,handle:f.props.handle,lazy:f.props.lazy};f.props.children&&(S.children=Yf(f.props.children,m)),o.push(S)}),o}var ui="get",ni="application/x-www-form-urlencoded";function fi(c){return typeof HTMLElement<"u"&&c instanceof HTMLElement}function r0(c){return fi(c)&&c.tagName.toLowerCase()==="button"}function s0(c){return fi(c)&&c.tagName.toLowerCase()==="form"}function o0(c){return fi(c)&&c.tagName.toLowerCase()==="input"}function d0(c){return!!(c.metaKey||c.altKey||c.ctrlKey||c.shiftKey)}function h0(c,s){return c.button===0&&(!s||s==="_self")&&!d0(c)}function Gf(c=""){return new URLSearchParams(typeof c=="string"||Array.isArray(c)||c instanceof URLSearchParams?c:Object.keys(c).reduce((s,o)=>{let f=c[o];return s.concat(Array.isArray(f)?f.map(d=>[o,d]):[[o,f]])},[]))}function m0(c,s){let o=Gf(c);return s&&s.forEach((f,d)=>{o.has(d)||s.getAll(d).forEach(m=>{o.append(d,m)})}),o}var ai=null;function y0(){if(ai===null)try{new FormData(document.createElement("form"),0),ai=!1}catch{ai=!0}return ai}var v0=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Bf(c){return c!=null&&!v0.has(c)?(_e(!1,`"${c}" is not a valid \`encType\` for \`
    \`/\`\` and will default to "${ni}"`),null):c}function g0(c,s){let o,f,d,m,S;if(s0(c)){let R=c.getAttribute("action");f=R?al(R,s):null,o=c.getAttribute("method")||ui,d=Bf(c.getAttribute("enctype"))||ni,m=new FormData(c)}else if(r0(c)||o0(c)&&(c.type==="submit"||c.type==="image")){let R=c.form;if(R==null)throw new Error('Cannot submit a