diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 5f7efb4..6b09ea7 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -11,15 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: https://github.com/actions/checkout@v5 + uses: https://github.com/actions/checkout@v4 - name: Set up Go uses: https://github.com/actions/setup-go@v5 with: - go-version-file: go.mod - - - name: Clean vendor - run: rm -rf vendor + go-version: '1.24' - name: Test run: go test -race -v ./... diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 47cc920..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Tests - -on: - pull_request: - branches: [main] - push: - branches: [main] - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Clean vendor - run: rm -rf vendor - - - name: Test - run: go test -race -v ./... diff --git a/.gitignore b/.gitignore index 6cea500..a5388c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,5 @@ node_modules/ .agent/ -internal/spa/dist/ -frontend/node_modules/ -frontend/dist/ -frontend/bun.lock -frontend/bun.lockb -frontend/package-lock.json *.exe *.exe~ *.dll diff --git a/CLAUDE.md b/CLAUDE.md index b7f254e..1ba6bdc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -kafka is a privacy-respecting metasearch engine written in Go. It provides a SearXNG-compatible `/search` API and an HTML frontend (HTMX + Go templates). 9 engines are implemented natively in Go; unlisted engines can be proxied to an upstream metasearch instance. Responses from multiple engines are merged into a single JSON/CSV/RSS/HTML response. +kafka is a privacy-respecting metasearch engine written in Go. It provides a SearXNG-compatible `/search` API and an HTML frontend (HTMX + Go templates). 9 engines are implemented natively in Go; unlisted engines can be proxied to an upstream SearXNG instance. Responses from multiple engines are merged into a single JSON/CSV/RSS/HTML response. ## Build & Run Commands @@ -22,7 +22,7 @@ go test -run TestWikipedia ./internal/engines/ go test -v ./internal/engines/ # Run the server (requires config.toml) -go run ./cmd/kafka -config config.toml +go run ./cmd/searxng-go -config config.toml ``` There is no Makefile. There is no linter configured. @@ -37,13 +37,13 @@ There is no Makefile. There is no linter configured. - `internal/config` — TOML-based configuration with env var fallbacks. `Load(path)` reads `config.toml`; env vars override zero-value fields. See `config.example.toml` for all settings. - `internal/engines` — `Engine` interface and all 9 Go-native implementations. `factory.go` registers engines via `NewDefaultPortedEngines()`. `planner.go` routes engines to local or upstream based on `LOCAL_PORTED_ENGINES` env var. - `internal/search` — `Service` orchestrates the pipeline: cache check, planning, parallel engine execution via goroutines/WaitGroup, upstream proxying, response merging. Individual engine failures are reported as `unresponsive_engines` rather than aborting the search. Qwant has fallback logic to upstream on empty results. -- `internal/autocomplete` — Fetches search suggestions. Proxies to upstream `/autocompleter` if configured, falls back to Wikipedia OpenSearch API otherwise. +- `internal/autocomplete` — Fetches search suggestions. Proxies to upstream SearXNG `/autocompleter` if configured, falls back to Wikipedia OpenSearch API otherwise. - `internal/httpapi` — HTTP handlers for `/`, `/search`, `/autocompleter`, `/healthz`, `/opensearch.xml`. Detects HTMX requests via `HX-Request` header to return fragments instead of full pages. -- `internal/upstream` — Client that proxies requests to an upstream metasearch instance via POST. +- `internal/upstream` — Client that proxies requests to an upstream SearXNG instance via POST. - `internal/cache` — Valkey/Redis-backed cache with SHA-256 cache keys. No-op if unconfigured. - `internal/middleware` — Three rate limiters (per-IP sliding window, burst+sustained, global) and CORS. All disabled by default. - `internal/views` — HTML templates and static files embedded via `//go:embed`. Renders full pages or HTMX fragments. Templates: `base.html`, `index.html`, `results.html`, `results_inner.html`, `result_item.html`. -- `cmd/kafka` — Entry point. Loads TOML config, seeds env vars for engine code, wires up middleware chain, starts HTTP server. +- `cmd/searxng-go` — Entry point. Loads TOML config, seeds env vars for engine code, wires up middleware chain, starts HTTP server. **Engine interface** (`internal/engines/engine.go`): ```go diff --git a/Dockerfile b/Dockerfile index e21960f..c41b5a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN go mod download # Copy source and build COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /kafka ./cmd/kafka +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /kafka ./cmd/searxng-go # Runtime stage FROM alpine:3.21 diff --git a/LICENSE b/LICENSE index cb069f5..4fdaf2a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,662 +1,21 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2026-present metamorphosis-dev - - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the Corresponding - Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - kafka — a privacy-respecting metasearch engine - Copyright (C) 2026-present metamorphosis-dev - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - - Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. +MIT License + +Copyright (c) 2026-present metamorphosis-dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 25c1c29..2f0868f 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ A privacy-respecting, open metasearch engine written in Go. SearXNG-compatible A ```bash git clone https://git.ashisgreat.xyz/penal-colony/gosearch.git cd kafka -go build ./cmd/kafka -./kafka -config config.toml +go build ./cmd/searxng-go +./searxng-go -config config.toml ``` ### Docker Compose @@ -76,7 +76,7 @@ sudo nixos-rebuild switch --flake .# ```bash nix develop go test ./... -go run ./cmd/kafka -config config.toml +go run ./cmd/searxng-go -config config.toml ``` ## Endpoints @@ -138,7 +138,7 @@ Copy `config.example.toml` to `config.toml` and edit. All settings can also be o ### Key Sections - **`[server]`** — port, timeout, public base URL for OpenSearch -- **`[upstream]`** — optional upstream metasearch proxy for unported engines +- **`[upstream]`** — optional upstream SearXNG proxy for unported engines - **`[engines]`** — which engines run locally, engine-specific settings - **`[cache]`** — Valkey/Redis address, password, TTL - **`[cors]`** — allowed origins and methods @@ -152,7 +152,7 @@ Copy `config.example.toml` to `config.toml` and edit. All settings can also be o |---|---| | `PORT` | Listen port (default: 8080) | | `BASE_URL` | Public URL for OpenSearch XML | -| `UPSTREAM_SEARXNG_URL` | Upstream instance URL | +| `UPSTREAM_SEARXNG_URL` | Upstream SearXNG instance URL | | `LOCAL_PORTED_ENGINES` | Comma-separated local engine list | | `HTTP_TIMEOUT` | Upstream request timeout | | `BRAVE_API_KEY` | Brave Search API key | @@ -177,7 +177,7 @@ See `config.example.toml` for the full list including rate limiting and CORS var | Reddit | Reddit JSON API | Discussions | | Bing | Bing RSS | General web | -Engines not listed in `engines.local_ported` are proxied to an upstream metasearch instance if `upstream.url` is configured. +Engines not listed in `engines.local_ported` are proxied to an upstream SearXNG instance if `upstream.url` is configured. ## Architecture @@ -221,4 +221,4 @@ Includes Valkey 8 with health checks out of the box. ## License -[AGPLv3](https://www.gnu.org/licenses/agpl-3.0.html) +MIT diff --git a/cmd/kafka/main.go b/cmd/searxng-go/main.go similarity index 72% rename from cmd/kafka/main.go rename to cmd/searxng-go/main.go index 68fa1dd..dac6258 100644 --- a/cmd/kafka/main.go +++ b/cmd/searxng-go/main.go @@ -1,19 +1,3 @@ -// kafka — a privacy-respecting metasearch engine -// Copyright (C) 2026-present metamorphosis-dev -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - package main import ( @@ -69,25 +53,19 @@ func main() { } svc := search.NewService(search.ServiceConfig{ - UpstreamURL: cfg.Upstream.URL, - HTTPTimeout: cfg.HTTPTimeout(), - Cache: searchCache, - EnginesConfig: cfg, + UpstreamURL: cfg.Upstream.URL, + HTTPTimeout: cfg.HTTPTimeout(), + Cache: searchCache, }) acSvc := autocomplete.NewService(cfg.Upstream.URL, cfg.HTTPTimeout()) - h := httpapi.NewHandler(svc, acSvc.Suggestions, cfg.Server.SourceURL) + h := httpapi.NewHandler(svc, acSvc.Suggestions) mux := http.NewServeMux() - - // HTML template routes mux.HandleFunc("/", h.Index) - mux.HandleFunc("/search", h.Search) - mux.HandleFunc("/preferences", h.Preferences) - - // API routes mux.HandleFunc("/healthz", h.Healthz) + mux.HandleFunc("/search", h.Search) mux.HandleFunc("/autocompleter", h.Autocompleter) mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL)) @@ -99,9 +77,8 @@ func main() { var subFS fs.FS = staticFS mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS)))) - // Apply middleware: global rate limit → burst rate limit → per-IP rate limit → CORS → security headers → handler. + // Apply middleware: global rate limit → burst rate limit → per-IP rate limit → CORS → handler. var handler http.Handler = mux - handler = middleware.SecurityHeaders(middleware.SecurityHeadersConfig{})(handler) handler = middleware.CORS(middleware.CORSConfig{ AllowedOrigins: cfg.CORS.AllowedOrigins, AllowedMethods: cfg.CORS.AllowedMethods, @@ -113,7 +90,6 @@ func main() { Requests: cfg.RateLimit.Requests, Window: cfg.RateLimitWindow(), CleanupInterval: cfg.RateLimitCleanupInterval(), - TrustedProxies: cfg.RateLimit.TrustedProxies, }, logger)(handler) handler = middleware.GlobalRateLimit(middleware.GlobalRateLimitConfig{ Requests: cfg.GlobalRateLimit.Requests, @@ -127,7 +103,7 @@ func main() { }, logger)(handler) addr := fmt.Sprintf(":%d", cfg.Server.Port) - logger.Info("kafka starting", + logger.Info("searxng-go starting", "addr", addr, "cache", searchCache.Enabled(), "rate_limit", cfg.RateLimit.Requests > 0, diff --git a/config.example.toml b/config.example.toml index 042bb63..df77184 100644 --- a/config.example.toml +++ b/config.example.toml @@ -14,20 +14,15 @@ http_timeout = "10s" # Example: "https://search.example.com" base_url = "" -# Link to the source code (shown in footer as "Source" link) -# Defaults to the upstream kafka repo if not set. -# Example: "https://git.example.com/my-kafka-fork" -source_url = "" - [upstream] -# URL of an upstream metasearch instance for unported engines (env: UPSTREAM_SEARXNG_URL) +# URL of an upstream SearXNG instance for unported engines (env: UPSTREAM_SEARXNG_URL) # Leave empty to run without an upstream proxy. url = "" [engines] # Comma-separated list of engines to execute locally in Go (env: LOCAL_PORTED_ENGINES) -# Engines not listed here will be proxied to the upstream instance. -local_ported = ["wikipedia", "arxiv", "crossref", "braveapi", "qwant", "duckduckgo", "github", "reddit", "bing", "google", "youtube"] +# Engines not listed here will be proxied to upstream SearXNG. +local_ported = ["wikipedia", "arxiv", "crossref", "braveapi", "qwant", "duckduckgo", "github", "reddit", "bing"] [engines.brave] # Brave Search API key (env: BRAVE_API_KEY) @@ -40,10 +35,6 @@ access_token = "" category = "web-lite" results_per_page = 10 -[engines.youtube] -# YouTube Data API v3 key (env: YOUTUBE_API_KEY) -api_key = "" - [cache] # Valkey/Redis cache for search results. # Leave address empty to disable caching entirely. diff --git a/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md b/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md deleted file mode 100644 index 28b98a1..0000000 --- a/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md +++ /dev/null @@ -1,1222 +0,0 @@ -# Brave Search Frontend Redesign — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Redesign the kafka frontend to match Brave Search's layout: three-column results page, category tiles on homepage, and a hybrid preferences system with full-page `/preferences` route. - -**Architecture:** CSS Grid for page-level layouts (three-column results, two-column preferences). JavaScript popover for quick settings (theme + engines only). Server-rendered full preferences page with localStorage persistence. Category tiles are static links with category query params. - -**Tech Stack:** Go (Go templates), CSS Grid/Flexbox, Vanilla JavaScript (HTMX for search), localStorage for preferences - ---- - -## File Map - -| File | Responsibility | -|------|----------------| -| `internal/views/static/css/kafka.css` | Add layout grids, category tiles, sidebar styles, mobile breakpoints | -| `internal/views/templates/index.html` | Add category tiles below search box | -| `internal/views/templates/results.html` | Add left sidebar, restructure for three-column grid | -| `internal/views/templates/preferences.html` | **New** — full preferences page with nav | -| `internal/views/templates/base.html` | No structural changes needed | -| `internal/views/static/js/settings.js` | Reduce popover to theme + engines; add preferences page JS | -| `internal/httpapi/handlers.go` | Add `GET /preferences` and `POST /preferences` handlers | -| `internal/views/views.go` | Add `RenderPreferences` and `tmplPreferences` template | - ---- - -## PHASE 1: CSS Layout Framework - -### Task 1: Add CSS Grid Layouts and Breakpoints - -**Files:** -- Modify: `internal/views/static/css/kafka.css` - -- [ ] **Step 1: Add three-column results layout CSS** - -Append to end of `kafka.css`, before the `@media print` block: - -```css -/* ============================================================ - Three-Column Results Layout - ============================================================ */ - -.results-layout { - display: grid; - grid-template-columns: 200px 1fr 240px; - gap: 2rem; - align-items: start; -} - -.results-layout .left-sidebar { - position: sticky; - top: calc(var(--header-height) + 1.5rem); - max-height: calc(100vh - var(--header-height) - 3rem); - overflow-y: auto; -} - -.results-layout .right-sidebar { - position: sticky; - top: calc(var(--header-height) + 1.5rem); - max-height: calc(100vh - var(--header-height) - 3rem); - overflow-y: auto; -} - -.results-layout .results-column { - min-width: 0; -} -``` - -- [ ] **Step 2: Add mobile breakpoints** - -```css -/* Tablet: hide left sidebar, two columns */ -@media (min-width: 769px) and (max-width: 1024px) { - .results-layout { - grid-template-columns: 1fr 220px; - } - .results-layout .left-sidebar { - display: none; - } -} - -/* Mobile: single column, no sidebars */ -@media (max-width: 768px) { - .results-layout { - grid-template-columns: 1fr; - } - .results-layout .left-sidebar, - .results-layout .right-sidebar { - display: none; - } -} -``` - -- [ ] **Step 3: Add preferences page layout CSS** - -```css -/* ============================================================ - Preferences Page Layout - ============================================================ */ - -.preferences-layout { - display: grid; - grid-template-columns: 200px 1fr; - gap: 2rem; - align-items: start; - padding: 2rem 0; -} - -.preferences-nav { - position: sticky; - top: calc(var(--header-height) + 1.5rem); -} - -.preferences-nav-item { - display: flex; - align-items: center; - gap: 0.6rem; - padding: 0.6rem 0.75rem; - border-radius: var(--radius-sm); - color: var(--text-secondary); - text-decoration: none; - font-size: 0.9rem; - transition: background 0.15s, color 0.15s; - cursor: pointer; -} - -.preferences-nav-item:hover { - background: var(--bg-tertiary); - color: var(--text-primary); -} - -.preferences-nav-item.active { - background: var(--accent-soft); - color: var(--accent); - font-weight: 500; -} - -.preferences-content { - background: var(--bg); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 1.5rem; -} - -@media (max-width: 768px) { - .preferences-layout { - grid-template-columns: 1fr; - } - .preferences-nav { - position: static; - display: flex; - overflow-x: auto; - gap: 0.5rem; - padding-bottom: 0.5rem; - } - .preferences-nav-item { - white-space: nowrap; - } -} -``` - -- [ ] **Step 4: Add category tiles CSS** - -```css -/* ============================================================ - Category Tiles - ============================================================ */ - -.category-tiles { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); - gap: 1rem; - margin-top: 2rem; -} - -.category-tile { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - padding: 1rem 0.5rem; - border-radius: var(--radius-md); - text-decoration: none; - color: var(--text-secondary); - font-size: 0.85rem; - transition: background 0.15s, color 0.15s, transform 0.15s, box-shadow 0.15s; -} - -.category-tile:hover { - background: var(--bg-tertiary); - color: var(--text-primary); - transform: translateY(-2px); - box-shadow: var(--shadow-sm); -} - -.category-tile-icon { - font-size: 1.5rem; - line-height: 1; -} - -.category-tile.disabled { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; -} - -@media (max-width: 768px) { - .category-tiles { - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); - gap: 0.75rem; - } - .category-tile { - padding: 0.75rem 0.25rem; - font-size: 0.75rem; - } - .category-tile-icon { - font-size: 1.25rem; - } -} -``` - -- [ ] **Step 5: Add left sidebar navigation styles** - -```css -/* ============================================================ - Left Sidebar (Results Page) - ============================================================ */ - -.left-sidebar { - padding: 0; -} - -.sidebar-nav { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.sidebar-nav-title { - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--text-muted); - padding: 0.5rem 0.75rem; - margin-top: 0.5rem; -} - -.sidebar-nav-item { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - border-radius: var(--radius-sm); - color: var(--text-secondary); - text-decoration: none; - font-size: 0.875rem; - transition: background 0.15s, color 0.15s; -} - -.sidebar-nav-item:hover { - background: var(--bg-tertiary); - color: var(--text-primary); -} - -.sidebar-nav-item.active { - background: var(--accent-soft); - color: var(--accent); - font-weight: 500; -} - -.sidebar-nav-item-icon { - font-size: 1rem; - width: 20px; - text-align: center; -} - -.sidebar-filters { - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid var(--border); -} - -.sidebar-filter-group { - margin-bottom: 0.75rem; -} - -.sidebar-filter-label { - font-size: 0.75rem; - font-weight: 500; - color: var(--text-muted); - padding: 0 0.75rem; - margin-bottom: 0.25rem; -} - -.sidebar-filter-option { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.35rem 0.75rem; - font-size: 0.8rem; - color: var(--text-secondary); - cursor: pointer; - border-radius: var(--radius-sm); - transition: background 0.15s; -} - -.sidebar-filter-option:hover { - background: var(--bg-tertiary); -} - -.sidebar-filter-option input[type="radio"] { - accent-color: var(--accent); -} - -/* Mobile filter chips */ -.mobile-filter-chips { - display: none; - overflow-x: auto; - gap: 0.5rem; - padding: 0.75rem 0; - -webkit-overflow-scrolling: touch; -} - -.mobile-filter-chips::-webkit-scrollbar { - display: none; -} - -.mobile-filter-chip { - display: inline-flex; - align-items: center; - padding: 0.4rem 0.75rem; - border: 1px solid var(--border); - border-radius: var(--radius-full); - font-size: 0.8rem; - color: var(--text-secondary); - white-space: nowrap; - text-decoration: none; - transition: background 0.15s, border-color 0.15s; -} - -.mobile-filter-chip:hover, -.mobile-filter-chip.active { - background: var(--accent-soft); - border-color: var(--accent); - color: var(--accent); -} - -@media (max-width: 768px) { - .mobile-filter-chips { - display: flex; - } -} -``` - -- [ ] **Step 6: Verify Go compilation** - -Run: `go build ./...` -Expected: No errors - -Note: CSS is embedded as static files and not processed by the Go compiler. CSS changes must be tested manually in a browser. - -- [ ] **Step 7: Commit** - -```bash -git add internal/views/static/css/kafka.css -git commit -m "feat(frontend): add CSS layout framework for three-column results and preferences page" -``` - ---- - -## PHASE 2: Results Page Three-Column Layout - -### Task 2: Restructure Results Template - -**Files:** -- Modify: `internal/views/templates/results.html` -- Modify: `internal/views/views.go` - -- [ ] **Step 1: Read current results.html to understand exact content** - -Current structure has `.results-layout` grid with `.search-compact` spanning full width, `.results-column`, and `.sidebar`. Need to add left sidebar and restructure grid. - -- [ ] **Step 2: Replace results.html content** - -Replace the entire file content: - -```html -{{define "title"}}{{if .Query}}{{.Query}} — {{end}}{{end}} -{{define "content"}} -
- - - - -
- -
- -
- - -
- All - {{range .Categories}} - {{.}} - {{end}} -
- - - {{template "results_inner" .}} -
- - - -
-{{end}} -``` - -- [ ] **Step 3: Add FilterOption struct and update PageData struct** - -Add `FilterOption` struct at package level in `views.go` (near `PageNumber` struct): - -```go -// FilterOption represents a filter radio option for the sidebar. -type FilterOption struct { - Label string - Value string -} -``` - -Then update `PageData` struct to include new fields: - -```go -type PageData struct { - // ... existing fields (SourceURL, Query, Pageno, etc.) ... - - // New fields for three-column layout - Categories []string - CategoryIcons map[string]string - DisabledCategories []string - ActiveCategory string - TimeFilters []FilterOption - TypeFilters []FilterOption - ActiveTime string - ActiveType string -} -``` - -- [ ] **Step 4: Update FromResponse signature and body** - -Update `FromResponse` signature to accept filter params and set defaults: - -```go -func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType string) PageData { - // Set defaults - if activeCategory == "" { - activeCategory = "all" - } - - pd := PageData{ - // ... existing initialization (NumberOfResults, Results, etc.) ... - - // New: categories with icons - Categories: []string{"all", "news", "images", "videos", "maps"}, - DisabledCategories: []string{"shopping", "music", "weather"}, - CategoryIcons: map[string]string{ - "all": "🌐", - "news": "📰", - "images": "🖼️", - "videos": "🎬", - "maps": "🗺️", - "shopping": "🛒", - "music": "🎵", - "weather": "🌤️", - }, - ActiveCategory: activeCategory, - - // Time filters - TimeFilters: []FilterOption{ - {Label: "Any time", Value: ""}, - {Label: "Past hour", Value: "h"}, - {Label: "Past 24 hours", Value: "d"}, - {Label: "Past week", Value: "w"}, - {Label: "Past month", Value: "m"}, - {Label: "Past year", Value: "y"}, - }, - ActiveTime: activeTime, - - // Type filters - TypeFilters: []FilterOption{ - {Label: "All results", Value: ""}, - {Label: "News", Value: "news"}, - {Label: "Videos", Value: "video"}, - {Label: "Images", Value: "image"}, - }, - ActiveType: activeType, - } - // ... rest of function ... -} -``` - -Update the `Search` handler in `handlers.go` to pass filter params: - -```go -pd := views.FromResponse(resp, req.Query, req.Pageno, - r.FormValue("category"), r.FormValue("time"), r.FormValue("type")) -``` - -- [ ] **Step 5: Update results.html sidebar to show disabled state** - -Update the sidebar category loop to conditionally apply `disabled` class: - -```html -{{range .Categories}} - - {{index $.CategoryIcons .}} - {{.}} - -{{end}} - -{{range .DisabledCategories}} - - {{index $.CategoryIcons .}} - {{.}} - -{{end}} -``` - -- [ ] **Step 6: Test compilation** - -Run: `go build ./...` -Expected: No errors - -- [ ] **Step 7: Commit** - -```bash -git add internal/views/views.go internal/views/templates/results.html -git commit -m "feat(frontend): add three-column results layout with left sidebar navigation" -``` - ---- - -## PHASE 3: Homepage Category Tiles - -### Task 3: Add Category Tiles to Homepage - -**Files:** -- Modify: `internal/views/templates/index.html` - -- [ ] **Step 1: Read current index.html** - -- [ ] **Step 2: Replace index.html with tiles** - -```html -{{define "title"}}{{end}} -{{define "content"}} -
- -

Search the web privately, without tracking or censorship.

- - - - -
-
-{{end}} -``` - -- [ ] **Step 3: Test compilation** - -Run: `go build ./...` -Expected: No errors - -- [ ] **Step 4: Commit** - -```bash -git add internal/views/templates/index.html -git commit -m "feat(frontend): add category tiles to homepage" -``` - ---- - -## PHASE 4: Preferences Page - -### Task 4: Create Preferences Template - -**Files:** -- Create: `internal/views/templates/preferences.html` - -- [ ] **Step 1: Create preferences.html** - -```html -{{define "title"}}Preferences{{end}} -{{define "content"}} -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - -
-
-{{end}} -``` - -- [ ] **Step 2: Add preferences section CSS styles** - -Append to `kafka.css`: - -```css -/* ============================================================ - Preferences Page Styles - ============================================================ */ - -.pref-section { - margin-bottom: 2rem; -} - -.pref-section:last-child { - margin-bottom: 0; -} - -.pref-section-title { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--border); -} - -.pref-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.75rem 0; - border-bottom: 1px solid var(--border); -} - -.pref-row:last-child { - border-bottom: none; -} - -.pref-row label { - font-size: 0.9rem; - color: var(--text-primary); -} - -.pref-row-info { - flex: 1; -} - -.pref-row-info label { - font-weight: 500; -} - -.pref-desc { - font-size: 0.8rem; - color: var(--text-muted); - margin-top: 0.25rem; -} - -.pref-row select { - padding: 0.5rem 0.75rem; - font-size: 0.85rem; - font-family: inherit; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg); - color: var(--text-primary); - cursor: pointer; - min-width: 150px; -} - -.pref-row select:focus { - outline: none; - border-color: var(--accent); -} - -.pref-row input[type="checkbox"] { - width: 18px; - height: 18px; - accent-color: var(--accent); - cursor: pointer; - flex-shrink: 0; -} - -.pref-row input[type="checkbox"]:disabled { - opacity: 0.6; - cursor: not-allowed; -} -``` - -- [ ] **Step 3: Register preferences template in views.go** - -Add `tmplPreferences` variable and initialize it in `init()`. Also add `RenderPreferences` function: - -```go -// In views.go, add to var block: -var ( - tmplFull *template.Template - tmplIndex *template.Template - tmplFragment *template.Template - tmplPreferences *template.Template -) - -// In init(), after existing template parsing, add: -tmplPreferences = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, - "base.html", "preferences.html", -)) - -// Add RenderPreferences function: -func RenderPreferences(w http.ResponseWriter, sourceURL string) error { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL}) -} -``` - -- [ ] **Step 4: Commit** - -```bash -git add internal/views/templates/preferences.html internal/views/static/css/kafka.css internal/views/views.go -git commit -m "feat(frontend): add preferences page template and styles" -``` - ---- - -### Task 5: Add Preferences Route - -**Files:** -- Modify: `internal/httpapi/handlers.go` -- Modify: `cmd/kafka/main.go` - -- [ ] **Step 1: Add GET and POST handlers for /preferences** - -Add to `handlers.go`: - -```go -// Preferences renders the preferences page. -func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/preferences" { - http.NotFound(w, r) - return - } - if err := views.RenderPreferences(w, h.sourceURL); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -// PreferencesPOST handles form submission from the preferences page. -// NOTE: This is a no-op. All preferences are stored in localStorage on the client -// via JavaScript. This handler exists only for form submission completeness (e.g., -// if a form POSTs without JS). The JavaScript in settings.js handles all saves. -func (h *Handler) PreferencesPOST(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/preferences" { - http.NotFound(w, r) - return - } - http.Redirect(w, r, "/preferences", http.StatusFound) -} -``` - -- [ ] **Step 2: Register the route in main** - -Find where routes are registered (likely in `cmd/kafka/main.go`) and add: - -```go -mux.HandleFunc("GET /preferences", handler.Preferences) -mux.HandleFunc("POST /preferences", handler.PreferencesPOST) -``` - -- [ ] **Step 3: Test compilation** - -Run: `go build ./...` -Expected: No errors - -- [ ] **Step 4: Commit** - -```bash -git add internal/httpapi/handlers.go cmd/kafka/main.go -git commit -m "feat: add GET and POST /preferences route" -``` - ---- - -### Task 6: Update Settings JavaScript - -**Files:** -- Modify: `internal/views/static/js/settings.js` - -- [ ] **Step 1: Reduce popover to theme + engines only** - -Update the `renderPanel` function to remove SafeSearch and Format options. Keep only theme buttons and engine toggles. - -- [ ] **Step 2: Add preferences page navigation JavaScript** - -Add to end of `settings.js`: - -```javascript -// Preferences page navigation -function initPreferences() { - var nav = document.getElementById('preferences-nav'); - if (!nav) return; - - var sections = document.querySelectorAll('.pref-section'); - var navItems = nav.querySelectorAll('.preferences-nav-item'); - - function showSection(id) { - sections.forEach(function(sec) { - sec.style.display = sec.id === 'section-' + id ? 'block' : 'none'; - }); - navItems.forEach(function(item) { - item.classList.toggle('active', item.getAttribute('data-section') === id); - }); - } - - navItems.forEach(function(item) { - item.addEventListener('click', function() { - showSection(item.getAttribute('data-section')); - }); - }); - - // Load saved preferences - var prefs = loadPrefs(); - var themeEl = document.getElementById('pref-theme'); - if (themeEl) themeEl.value = prefs.theme || 'system'; - - var ssEl = document.getElementById('pref-safesearch'); - if (ssEl) ssEl.value = prefs.safeSearch || 'moderate'; - - var fmtEl = document.getElementById('pref-format'); - if (fmtEl) fmtEl.value = prefs.format || 'html'; - - // Save handlers - if (themeEl) { - themeEl.addEventListener('change', function() { - prefs.theme = themeEl.value; - savePrefs(prefs); - applyTheme(prefs.theme); - }); - } - - if (ssEl) { - ssEl.addEventListener('change', function() { - prefs.safeSearch = ssEl.value; - savePrefs(prefs); - }); - } - - if (fmtEl) { - fmtEl.addEventListener('change', function() { - prefs.format = fmtEl.value; - savePrefs(prefs); - }); - } - - // Show first section by default - showSection('search'); -} - -document.addEventListener('DOMContentLoaded', initPreferences); -``` - -- [ ] **Step 3: Test with browser** - -Manual verification needed — cannot test browser JS with `go build` - -- [ ] **Step 4: Commit** - -```bash -git add internal/views/static/js/settings.js -git commit -m "feat(frontend): reduce popover to theme+engines, add preferences page JS" -``` - ---- - -## PHASE 5: Polish and Mobile Responsiveness - -### Task 7: Mobile Filter Chips Integration - -**Files:** -- Modify: `internal/views/templates/results.html` - -- [ ] **Step 1: Ensure mobile filter chips have working category links** - -The current results.html has mobile filter chips with category links. These should preserve existing query params for pagination/HTMX navigation. - -- [ ] **Step 2: Add filter form submission via HTMX** - -Update the filter radio buttons to submit via HTMX when changed. - -- [ ] **Step 3: Commit** - -```bash -git add internal/views/templates/results.html -git commit -m "fix(frontend): add HTMX filter submission" -``` - ---- - -### Task 8: Final Mobile Responsiveness Audit - -**Files:** -- Review: `internal/views/static/css/kafka.css` - -- [ ] **Step 1: Test all breakpoints manually** - -- [ ] **Step 2: Fix any layout issues found** - -- [ ] **Step 3: Commit any fixes** - -```bash -git add internal/views/static/css/kafka.css -git commit -m "fix(frontend): improve mobile responsiveness" -``` - ---- - -## Summary - -| Phase | Task | Files | -|-------|------|-------| -| 1 | CSS Layout Framework | `kafka.css` | -| 2 | Results Three-Column | `results.html`, `views.go` | -| 3 | Homepage Tiles | `index.html` | -| 4 | Preferences Page | `preferences.html` (new), `handlers.go`, `settings.js` | -| 5 | Polish | Various | - -**Total: 8 tasks across 5 phases** - -Run `go test ./...` after each phase to verify nothing is broken. diff --git a/docs/superpowers/plans/2026-03-22-frontend-replacement.md b/docs/superpowers/plans/2026-03-22-frontend-replacement.md deleted file mode 100644 index 1cb475e..0000000 --- a/docs/superpowers/plans/2026-03-22-frontend-replacement.md +++ /dev/null @@ -1,358 +0,0 @@ -# Frontend Replacement Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace the Go template-based frontend with the search-zen-50 React SPA, embedded in the Go binary as a single deployment. - -**Architecture:** Build React app → embed in Go binary via `//go:embed` → serve via Go HTTP server with SPA fallback routing. React calls `/search?format=json` and `/autocompleter?q=` APIs. - -**Tech Stack:** Go (embed), React 18, Vite, TailwindCSS, React Router, @tanstack/react-query - ---- - -## File Map - -| File | Action | -|------|--------| -| `cmd/kafka/main.go` | Modify - replace template handlers with SPA handler | -| `internal/spa/spa.go` | Create - embed React build, serve static files, SPA fallback | -| `internal/spa/dist/` | Build output - React build artifacts (gitignored) | -| `src/hooks/use-search.ts` | Modify - replace mock with real API calls | -| `src/lib/mock-data.ts` | Keep types, remove MOCK_RESPONSE usage | - ---- - -## Task 1: Build React App - -**Files:** -- Build: `/tmp/search-zen-50/dist/` (output directory) - -- [ ] **Step 1: Install dependencies and build** - -```bash -cd /tmp/search-zen-50 && bun install && bun run build -``` - -Expected: `dist/` directory created with `index.html`, `assets/` folder containing JS/CSS bundles - -- [ ] **Step 2: Verify dist contents** - -```bash -ls /tmp/search-zen-50/dist/ && ls /tmp/search-zen-50/dist/assets/ | head -10 -``` - -Expected: `index.html` exists, `assets/` contains `.js` and `.css` files - ---- - -## Task 2: Create SPA Go Package - -**Files:** -- Create: `internal/spa/spa.go` - -```go -package spa - -import ( - "embed" - "io/fs" - "net/http" - "path" -) - -//go:embed all:dist -var distFS embed.FS - -// DistFS returns the embedded dist directory as an fs.FS. -func DistFS() (fs.FS, error) { - return fs.Sub(distFS, "dist") -} - -// NewHandler returns an HTTP handler that: -// - Serves static files from the embedded dist/ directory -// - Falls back to index.html for SPA routing (any non-API path) -func NewHandler() http.Handler { - dist, err := DistFS() - if err != nil { - panic("spa: embedded dist not found: " + err.Error()) - } - return &spaHandler{dist: dist} -} - -type spaHandler struct { - dist fs.FS -} - -func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // API paths are handled by Go API handlers - this should never be reached - // since Go mux dispatches to specific handlers first. But if reached, - // pass through to FileServer which will return 404 for unknown paths. - - // Try to serve the requested file first - filePath := path.Clean(r.URL.Path) - f, err := h.dist.Open(filePath) - if err == nil { - f.Close() - // File exists - serve it via FileServer - http.FileServer(http.FS(h.dist)).ServeHTTP(w, r) - return - } - - // Fallback to index.html for SPA routing - indexFile, err := h.dist.Open("index.html") - if err != nil { - http.Error(w, "index.html not found in embedded files", http.StatusInternalServerError) - return - } - indexFile.Close() - http.FileServer(http.FS(h.dist)).ServeHTTP(w, r) -} - -``` - ---- - -## Task 3: Wire SPA Handler in main.go - -**Files:** -- Modify: `cmd/kafka/main.go` - -- [ ] **Step 1: Replace handlers with SPA** - -In `main.go`, find and replace the `mux.HandleFunc` section (lines 82-88) and the static file serving section (lines 90-96). - -Old code (lines 82-96): -```go -mux := http.NewServeMux() -mux.HandleFunc("/", h.Index) -mux.HandleFunc("/healthz", h.Healthz) -mux.HandleFunc("/search", h.Search) -mux.HandleFunc("/autocompleter", h.Autocompleter) -mux.HandleFunc("/preferences", h.Preferences) -mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL)) - -// Serve embedded static files (CSS, JS, images). -staticFS, err := views.StaticFS() -if err != nil { - log.Fatalf("failed to load static files: %v", err) -} -var subFS fs.FS = staticFS -mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(subFS)))) -``` - -New code: -```go -mux := http.NewServeMux() - -// API routes - handled by Go -mux.HandleFunc("/healthz", h.Healthz) -mux.HandleFunc("/search", h.Search) -mux.HandleFunc("/autocompleter", h.Autocompleter) -mux.HandleFunc("/opensearch.xml", h.OpenSearch(cfg.Server.BaseURL)) - -// SPA handler - serves React app for all other routes -spaHandler := spa.NewHandler() -mux.Handle("/", spaHandler) -``` - -- [ ] **Step 2: Add spa import** - -Add to imports (after `"github.com/metamorphosis-dev/kafka/internal/search"`): -```go -"github.com/metamorphosis-dev/kafka/internal/spa" -``` - -- [ ] **Step 3: Remove unused views import if needed** - -If `views` is only used for `StaticFS()`, remove the import. The template rendering functions (`RenderIndex`, etc.) won't be needed anymore. - -- [ ] **Step 4: Verify build** - -```bash -cd /home/ashie/git/kafka && go build ./cmd/kafka/ -``` - -Expected: Builds successfully (may fail on embed if dist not found - continue to next task) - ---- - -## Task 4: Wire React to Real API - -**Files:** -- Modify: `src/hooks/use-search.ts` in `/tmp/search-zen-50/` - -- [ ] **Step 1: Replace mock search with real API call** - -Replace the `search` function in `use-search.ts`: - -Old code (lines 23-36): -```typescript -const search = useCallback(async (query: string) => { - if (!query.trim()) return; - - setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true })); - - // Simulate network delay - await new Promise((r) => setTimeout(r, 800)); - - setState((prev) => ({ - ...prev, - isLoading: false, - results: { ...MOCK_RESPONSE, query }, - })); -}, []); -``` - -New code: -```typescript -const search = useCallback(async (query: string) => { - if (!query.trim()) return; - - setState((prev) => ({ ...prev, query, isLoading: true, error: null, hasSearched: true })); - - try { - const response = await fetch(`/search?format=json&q=${encodeURIComponent(query)}`); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - const data = await response.json(); - setState((prev) => ({ - ...prev, - isLoading: false, - results: data, - })); - } catch (err) { - setState((prev) => ({ - ...prev, - isLoading: false, - error: err instanceof Error ? err.message : "Search failed", - })); - } -}, []); -``` - -- [ ] **Step 2: Remove mock data import** - -Remove the mock import line (should be near line 2): -```typescript -import { MOCK_RESPONSE, type SearXNGResponse, type Category } from "@/lib/mock-data"; -``` - -Replace with: -```typescript -import type { SearXNGResponse, Category } from "@/lib/mock-data"; -``` - -- [ ] **Step 3: Keep the CATEGORIES export** - -Ensure `mock-data.ts` still exports `CATEGORIES` and `Category` type. The file should look like: - -```typescript -// Keep these exports - used by CategoryTabs and preferences -export const CATEGORIES = ["general", "it", "images", "news"] as const; -export type Category = typeof CATEGORIES[number]; - -// Keep interfaces -export interface SearchResult { - url: string; - title: string; - content: string; - engine: string; - parsed_url: [string, string, string, string, string]; - engines: string[]; - positions: number[]; - score: number; - category: string; - pretty_url: string; - img_src?: string; - thumbnail?: string; - publishedDate?: string; -} - -export interface SearXNGResponse { - query: string; - number_of_results: number; - results: SearchResult[]; - answers: string[]; - corrections: string[]; - infoboxes: any[]; - suggestions: string[]; - unresponsive_engines: string[]; -} -``` - ---- - -## Task 5: Rebuild React and Verify - -**Files:** -- Build: `/tmp/search-zen-50/dist/` - -- [ ] **Step 1: Rebuild with changes** - -```bash -cd /tmp/search-zen-50 && bun run build -``` - -- [ ] **Step 2: Copy dist to kafka** - -```bash -rm -rf /home/ashie/git/kafka/internal/spa/dist -cp -r /tmp/search-zen-50/dist /home/ashie/git/kafka/internal/spa/dist -``` - -- [ ] **Step 3: Verify Go build** - -```bash -cd /home/ashie/git/kafka && go build ./cmd/kafka/ && echo "Build successful" -``` - -Expected: "Build successful" - ---- - -## Task 6: Test the Integration - -- [ ] **Step 1: Start the server** - -```bash -cd /home/ashie/git/kafka && ./kafka -config config.toml & -sleep 2 -``` - -- [ ] **Step 2: Test homepage** - -```bash -curl -s http://localhost:8080/ | head -20 -``` - -Expected: HTML with `
` from React app - -- [ ] **Step 3: Test API** - -```bash -curl -s "http://localhost:8080/search?format=json&q=test" | head -50 -``` - -Expected: JSON search response - -- [ ] **Step 4: Clean up** - -```bash -pkill -f "./kafka" 2>/dev/null; echo "Done" -``` - ---- - -## Dependencies - -- Node.js/Bun for building React app -- Go 1.24+ for embed functionality -- No new Go dependencies - -## Notes - -- The `internal/spa/dist/` folder should be gitignored (build artifact) -- The `internal/spa/dist/` copy is needed for the embed to work at compile time -- Preferences page is entirely client-side (localStorage) - no backend needed -- Autocomplete can be added later by modifying `SearchInput.tsx` to call `/autocompleter` diff --git a/docs/superpowers/plans/2026-03-22-settings-ui.md b/docs/superpowers/plans/2026-03-22-settings-ui.md deleted file mode 100644 index cf34df5..0000000 --- a/docs/superpowers/plans/2026-03-22-settings-ui.md +++ /dev/null @@ -1,747 +0,0 @@ -# Settings UI Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** A preferences popover panel (top-right on desktop, bottom sheet on mobile) that lets users set theme, enabled engines, safe search, and default format. All changes auto-save to `localStorage` and apply immediately to the DOM. - -**Architecture:** Pure client-side JS + CSS added alongside existing templates. No Go changes. Settings persist via `localStorage` key `kafka_prefs`. Theme applies via `data-theme` attribute on ``. - -**Tech Stack:** Vanilla JS (no framework), existing `kafka.css` custom properties, HTMX for search. - ---- - -## File Map - -| Action | File | -|--------|------| -| Create | `internal/views/static/js/settings.js` | -| Modify | `internal/views/static/css/kafka.css` | -| Modify | `internal/views/templates/base.html` | -| Modify | `internal/views/templates/index.html` | -| Modify | `internal/views/templates/results.html` | -| Modify | `internal/views/views.go` | - -**Key insight on engine preferences:** `ParseSearchRequest` reads `engines` as a CSV form value (`r.FormValue("engines")`). The search forms in `index.html` and `results.html` will get a hidden `#engines-input` field that is kept in sync with localStorage. On submit, the engines preference is sent as a normal form field. HTMX `hx-include="this"` already includes the form element, so the hidden input is automatically included in the request. - ---- - -## Task 1: CSS — Popover, toggles, bottom sheet - -**Files:** -- Modify: `internal/views/static/css/kafka.css` - -- [ ] **Step 1: Add CSS for popover, triggers, toggles, bottom sheet** - -Append the following to `kafka.css`: - -```css -/* ============================================ - Settings Panel - ============================================ */ - -/* Header */ -.site-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.6rem 1rem; - background: var(--color-header-background); - border-bottom: 1px solid var(--color-header-border); -} -.site-title { - font-size: 1rem; - font-weight: 600; - color: var(--color-base-font); -} - -/* Gear trigger button */ -.settings-trigger { - background: none; - border: none; - font-size: 1.1rem; - cursor: pointer; - padding: 0.3rem 0.5rem; - border-radius: var(--radius); - color: var(--color-base-font); - opacity: 0.7; - transition: opacity 0.2s, background 0.2s; - line-height: 1; -} -.settings-trigger:hover, -.settings-trigger[aria-expanded="true"] { - opacity: 1; - background: var(--color-sidebar-background); -} - -/* Popover panel */ -.settings-popover { - position: absolute; - top: 100%; - right: 0; - width: 280px; - max-height: 420px; - overflow-y: auto; - background: var(--color-base-background); - border: 1px solid var(--color-sidebar-border); - border-radius: var(--radius); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); - z-index: 200; - display: none; - flex-direction: column; -} -.settings-popover[data-open="true"] { - display: flex; - animation: settings-slide-in 0.2s ease; -} -@keyframes settings-slide-in { - from { opacity: 0; transform: translateY(-8px); } - to { opacity: 1; transform: translateY(0); } -} - -.settings-popover-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1rem; - border-bottom: 1px solid var(--color-sidebar-border); - font-weight: 600; - font-size: 0.9rem; - flex-shrink: 0; -} -.settings-popover-close { - background: none; - border: none; - font-size: 1.2rem; - cursor: pointer; - color: var(--color-base-font); - opacity: 0.6; - padding: 0 0.25rem; - line-height: 1; -} -.settings-popover-close:hover { opacity: 1; } - -.settings-popover-body { - padding: 0.8rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.settings-section-title { - font-size: 0.7rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-suggestion); - margin-bottom: 0.5rem; -} - -/* Theme buttons */ -.theme-buttons { - display: flex; - gap: 0.4rem; -} -.theme-btn { - flex: 1; - padding: 0.35rem 0.5rem; - border: 1px solid var(--color-sidebar-border); - border-radius: var(--radius); - background: var(--color-btn-background); - color: var(--color-base-font); - cursor: pointer; - font-size: 0.75rem; - text-align: center; - transition: background 0.15s, border-color 0.15s; -} -.theme-btn:hover { background: var(--color-btn-hover); } -.theme-btn.active { - background: var(--color-link); - color: #fff; - border-color: var(--color-link); -} - -/* Engine toggles — 2-column grid */ -.engine-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.4rem; -} -.engine-toggle { - display: flex; - align-items: center; - gap: 0.4rem; - padding: 0.3rem 0.5rem; - border-radius: var(--radius); - background: var(--color-sidebar-background); - font-size: 0.78rem; - cursor: pointer; -} -.engine-toggle input[type="checkbox"] { - width: 15px; - height: 15px; - margin: 0; - cursor: pointer; - accent-color: var(--color-link); -} -.engine-toggle span { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* Search defaults */ -.setting-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - margin-top: 0.4rem; -} -.setting-row label { - font-size: 0.85rem; - flex: 1; -} -.setting-row select { - width: 110px; - padding: 0.3rem 0.4rem; - font-size: 0.8rem; - border: 1px solid var(--color-sidebar-border); - border-radius: var(--radius); - background: var(--color-base-background); - color: var(--color-base-font); - cursor: pointer; -} - -/* Mid-search notice */ -.settings-notice { - font-size: 0.72rem; - color: var(--color-suggestion); - margin-top: 0.3rem; - font-style: italic; -} - -/* Dark theme via data-theme attribute */ -html[data-theme="dark"] { - --color-base: #222; - --color-base-font: #dcdcdc; - --color-base-background: #2b2b2b; - --color-header-background: #333; - --color-header-border: #444; - --color-search-border: #555; - --color-search-focus: #5dade2; - --color-result-url: #8ab4f8; - --color-result-url-visited: #b39ddb; - --color-result-content: #b0b0b0; - --color-result-title: #8ab4f8; - --color-result-title-visited: #b39ddb; - --color-result-engine: #999; - --color-result-border: #3a3a3a; - --color-link: #5dade2; - --color-link-visited: #b39ddb; - --color-sidebar-background: #333; - --color-sidebar-border: #444; - --color-infobox-background: #333; - --color-infobox-border: #444; - --color-pagination-current: #5dade2; - --color-pagination-border: #444; - --color-error: #e74c3c; - --color-error-background: #3b1a1a; - --color-suggestion: #999; - --color-footer: #666; - --color-btn-background: #333; - --color-btn-border: #555; - --color-btn-hover: #444; -} - -/* Mobile: Bottom sheet + FAB trigger */ -@media (max-width: 768px) { - /* Hide desktop trigger, show FAB */ - .settings-trigger-desktop { - display: none; - } - .settings-trigger-mobile { - display: block; - } - .settings-popover { - position: fixed; - top: auto; - bottom: 0; - left: 0; - right: 0; - width: 100%; - max-height: 70vh; - border-radius: var(--radius) var(--radius) 0 0; - border-bottom: none; - } - /* FAB: fixed bottom-right button visible only on mobile */ - .settings-trigger-mobile { - display: block; - position: fixed; - bottom: 1.5rem; - right: 1.5rem; - width: 48px; - height: 48px; - border-radius: 50%; - background: var(--color-link); - color: #fff; - border: none; - box-shadow: 0 4px 12px rgba(0,0,0,0.2); - font-size: 1.2rem; - z-index: 199; - opacity: 1; - } -} -``` - -Note: The existing `:root` and `@media (prefers-color-scheme: dark)` blocks provide the "system" theme. `html[data-theme="dark"]` overrides only apply when the user explicitly picks dark mode. When `theme === 'system'`, the `data-theme` attribute is removed and the browser's `prefers-color-scheme` media query kicks in via the existing CSS. - -- [ ] **Step 2: Verify existing tests still pass** - -Run: `go test ./...` -Expected: all pass - -- [ ] **Step 3: Commit** - -```bash -git add internal/views/static/css/kafka.css -git commit -m "feat(settings): add popover, toggle, and bottom-sheet CSS" -``` - ---- - -## Task 2: JS — Settings logic - -**Files:** -- Create: `internal/views/static/js/settings.js` - -- [ ] **Step 1: Write the settings JS module** - -Create `internal/views/static/js/settings.js`: - -```javascript -'use strict'; - -var ALL_ENGINES = [ - 'wikipedia', 'arxiv', 'crossref', 'braveapi', - 'qwant', 'duckduckgo', 'github', 'reddit', 'bing' -]; - -var DEFAULT_PREFS = { - theme: 'system', - engines: ALL_ENGINES.slice(), - safeSearch: 'moderate', - format: 'html' -}; - -var STORAGE_KEY = 'kafka_prefs'; - -// ── Persistence ────────────────────────────────────────────────────────────── - -function loadPrefs() { - try { - var raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return { theme: DEFAULT_PREFS.theme, engines: DEFAULT_PREFS.engines.slice(), safeSearch: DEFAULT_PREFS.safeSearch, format: DEFAULT_PREFS.format }; - var saved = JSON.parse(raw); - return { theme: saved.theme || DEFAULT_PREFS.theme, engines: saved.engines || DEFAULT_PREFS.engines.slice(), safeSearch: saved.safeSearch || DEFAULT_PREFS.safeSearch, format: saved.format || DEFAULT_PREFS.format }; - } catch (e) { - return { theme: DEFAULT_PREFS.theme, engines: DEFAULT_PREFS.engines.slice(), safeSearch: DEFAULT_PREFS.safeSearch, format: DEFAULT_PREFS.format }; - } -} - -function savePrefs(prefs) { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: prefs.theme, engines: prefs.engines, safeSearch: prefs.safeSearch, format: prefs.format })); - } catch (e) { /* quota or private mode */ } -} - -// ── Theme application ──────────────────────────────────────────────────────── - -function applyTheme(theme) { - if (theme === 'system') { - document.documentElement.removeAttribute('data-theme'); - } else { - document.documentElement.setAttribute('data-theme', theme); - } -} - -// ── Engine input sync ───────────────────────────────────────────────────────── - -function syncEngineInput(prefs) { - var input = document.getElementById('engines-input'); - if (input) input.value = prefs.engines.join(','); -} - -// ── Panel open / close ──────────────────────────────────────────────────────── - -function closePanel() { - var panel = document.getElementById('settings-popover'); - var trigger = document.getElementById('settings-trigger'); - if (!panel) return; - panel.setAttribute('data-open', 'false'); - if (trigger) trigger.setAttribute('aria-expanded', 'false'); - if (trigger) trigger.focus(); -} - -function openPanel() { - var panel = document.getElementById('settings-popover'); - var trigger = document.getElementById('settings-trigger'); - if (!panel) return; - panel.setAttribute('data-open', 'true'); - if (trigger) trigger.setAttribute('aria-expanded', 'true'); - var focusable = panel.querySelector('button, input, select'); - if (focusable) focusable.focus(); -} - -// ── Escape key ─────────────────────────────────────────────────────────────── - -document.addEventListener('keydown', function(e) { - if (e.key !== 'Escape') return; - var panel = document.getElementById('settings-popover'); - if (!panel || panel.getAttribute('data-open') !== 'true') return; - closePanel(); -}); - -// ── Click outside ───────────────────────────────────────────────────────────── - -document.addEventListener('click', function(e) { - var panel = document.getElementById('settings-popover'); - var trigger = document.getElementById('settings-trigger'); - if (!panel || panel.getAttribute('data-open') !== 'true') return; - if (!panel.contains(e.target) && (!trigger || !trigger.contains(e.target))) { - closePanel(); - } -}); - -// ── Focus trap ──────────────────────────────────────────────────────────────── - -document.addEventListener('keydown', function(e) { - if (e.key !== 'Tab') return; - var panel = document.getElementById('settings-popover'); - if (!panel || panel.getAttribute('data-open') !== 'true') return; - var focusable = Array.prototype.slice.call(panel.querySelectorAll('button, input, select, [tabindex]:not([tabindex="-1"])')); - if (!focusable.length) return; - var first = focusable[0]; - var last = focusable[focusable.length - 1]; - if (e.shiftKey) { - if (document.activeElement === first) { e.preventDefault(); last.focus(); } - } else { - if (document.activeElement === last) { e.preventDefault(); first.focus(); } - } -}); - -// ── Render ──────────────────────────────────────────────────────────────────── - -function escapeHtml(str) { - return String(str).replace(/&/g, '&').replace(//g, '>'); -} - -function renderPanel(prefs) { - var panel = document.getElementById('settings-popover'); - if (!panel) return; - var body = panel.querySelector('.settings-popover-body'); - if (!body) return; - - var themeBtns = ''; - ['light', 'dark', 'system'].forEach(function(t) { - var icons = { light: '\u2600', dark: '\u263D', system: '\u2318' }; - var labels = { light: 'Light', dark: 'Dark', system: 'System' }; - var active = prefs.theme === t ? ' active' : ''; - themeBtns += ''; - }); - - var engineToggles = ''; - ALL_ENGINES.forEach(function(name) { - var checked = prefs.engines.indexOf(name) !== -1 ? ' checked' : ''; - engineToggles += ''; - }); - - var ssOptions = [ - { val: 'moderate', label: 'Moderate' }, - { val: 'strict', label: 'Strict' }, - { val: 'off', label: 'Off' } - ]; - var fmtOptions = [ - { val: 'html', label: 'HTML' }, - { val: 'json', label: 'JSON' }, - { val: 'csv', label: 'CSV' }, - { val: 'rss', label: 'RSS' } - ]; - var ssOptionsHtml = ''; - var fmtOptionsHtml = ''; - ssOptions.forEach(function(o) { - var sel = prefs.safeSearch === o.val ? ' selected' : ''; - ssOptionsHtml += ''; - }); - fmtOptions.forEach(function(o) { - var sel = prefs.format === o.val ? ' selected' : ''; - fmtOptionsHtml += ''; - }); - - body.innerHTML = - '
' + - '
Appearance
' + - '
' + themeBtns + '
' + - '
' + - '
' + - '
Engines
' + - '
' + engineToggles + '
' + - '

Engine changes apply to your next search.

' + - '
' + - '
' + - '
Search Defaults
' + - '
' + - '' + - '' + - '
' + - '
' + - '' + - '' + - '
' + - '
'; - - // Theme buttons - var themeBtnEls = panel.querySelectorAll('.theme-btn'); - for (var i = 0; i < themeBtnEls.length; i++) { - themeBtnEls[i].addEventListener('click', (function(btn) { - return function() { - prefs.theme = btn.getAttribute('data-theme'); - savePrefs(prefs); - applyTheme(prefs.theme); - syncEngineInput(prefs); - renderPanel(prefs); - }; - })(themeBtnEls[i])); - } - - // Engine checkboxes - var checkboxes = panel.querySelectorAll('.engine-toggle input[type="checkbox"]'); - for (var j = 0; j < checkboxes.length; j++) { - checkboxes[j].addEventListener('change', (function(cb) { - return function() { - var checked = Array.prototype.slice.call(panel.querySelectorAll('.engine-toggle input[type="checkbox"]:checked')).map(function(el) { return el.value; }); - if (checked.length === 0) { cb.checked = true; return; } - prefs.engines = checked; - savePrefs(prefs); - syncEngineInput(prefs); - }; - })(checkboxes[j])); - } - - // Safe search - var ssEl = panel.querySelector('#pref-safesearch'); - if (ssEl) { - ssEl.addEventListener('change', function() { - prefs.safeSearch = ssEl.value; - savePrefs(prefs); - }); - } - - // Format - var fmtEl = panel.querySelector('#pref-format'); - if (fmtEl) { - fmtEl.addEventListener('change', function() { - prefs.format = fmtEl.value; - savePrefs(prefs); - }); - } - - // Close button - var closeBtn = panel.querySelector('.settings-popover-close'); - if (closeBtn) closeBtn.addEventListener('click', closePanel); -} - -// ── Init ───────────────────────────────────────────────────────────────────── - -function initSettings() { - var prefs = loadPrefs(); - applyTheme(prefs.theme); - syncEngineInput(prefs); - - var panel = document.getElementById('settings-popover'); - var trigger = document.getElementById('settings-trigger'); - var mobileTrigger = document.getElementById('settings-trigger-mobile'); - - if (panel) { - renderPanel(prefs); - - function togglePanel() { - var isOpen = panel.getAttribute('data-open') === 'true'; - if (isOpen) closePanel(); else openPanel(); - } - - if (trigger) trigger.addEventListener('click', togglePanel); - if (mobileTrigger) mobileTrigger.addEventListener('click', togglePanel); - } -} - -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initSettings); -} else { - initSettings(); -} -``` - -- [ ] **Step 2: Verify JS syntax** - -Run: `node --check internal/views/static/js/settings.js` -Expected: no output (exit 0) - -- [ ] **Step 3: Commit** - -```bash -git add internal/views/static/js/settings.js -git commit -m "feat(settings): add JS module for localStorage preferences and panel" -``` - ---- - -## Task 3: HTML — Gear trigger, panel markup, header in base - -**Files:** -- Modify: `internal/views/templates/base.html` -- Modify: `internal/views/views.go` - -- [ ] **Step 1: Add ShowHeader to PageData** - -In `views.go`, add `ShowHeader bool` to `PageData` struct. - -- [ ] **Step 2: Set ShowHeader in render functions** - -In `RenderIndex` and `RenderSearch`, set `PageData.ShowHeader = true`. - -- [ ] **Step 3: Update base.html — add header and settings markup** - -In `base.html`, update the `` to: - -```html - - {{if .ShowHeader}} - - - - {{end}} -
- {{template "content" .}} -
-
-

Powered by kafka — a privacy-respecting, open metasearch engine

-
- - - - -``` - -**Note:** The existing autocomplete ` - - diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index e90cada..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "vite_react_shadcn_ts", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "build:dev": "vite build --mode development", - "lint": "eslint .", - "preview": "vite preview", - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "@hookform/resolvers": "^3.10.0", - "@radix-ui/react-accordion": "^1.2.11", - "@radix-ui/react-alert-dialog": "^1.1.14", - "@radix-ui/react-aspect-ratio": "^1.1.7", - "@radix-ui/react-avatar": "^1.1.10", - "@radix-ui/react-checkbox": "^1.3.2", - "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-context-menu": "^2.2.15", - "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-hover-card": "^1.1.14", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-menubar": "^1.1.15", - "@radix-ui/react-navigation-menu": "^1.2.13", - "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-radio-group": "^1.3.7", - "@radix-ui/react-scroll-area": "^1.2.9", - "@radix-ui/react-select": "^2.2.5", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slider": "^1.3.5", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toast": "^1.2.14", - "@radix-ui/react-toggle": "^1.1.9", - "@radix-ui/react-toggle-group": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.7", - "@tanstack/react-query": "^5.83.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "date-fns": "^3.6.0", - "embla-carousel-react": "^8.6.0", - "input-otp": "^1.4.2", - "lucide-react": "^0.462.0", - "next-themes": "^0.3.0", - "react": "^18.3.1", - "react-day-picker": "^8.10.1", - "react-dom": "^18.3.1", - "react-hook-form": "^7.61.1", - "react-resizable-panels": "^2.1.9", - "react-router-dom": "^6.30.1", - "recharts": "^2.15.4", - "sonner": "^1.7.4", - "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.9", - "zod": "^3.25.76" - }, - "devDependencies": { - "@eslint/js": "^9.32.0", - "@playwright/test": "^1.57.0", - "@tailwindcss/typography": "^0.5.16", - "@testing-library/jest-dom": "^6.6.0", - "@testing-library/react": "^16.0.0", - "@types/node": "^22.16.5", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@vitejs/plugin-react-swc": "^3.11.0", - "autoprefixer": "^10.4.21", - "eslint": "^9.32.0", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-react-refresh": "^0.4.20", - "globals": "^15.15.0", - "jsdom": "^20.0.3", - "lovable-tagger": "^1.1.13", - "postcss": "^8.5.6", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.3", - "typescript-eslint": "^8.38.0", - "vite": "^5.4.19", - "vitest": "^3.2.4" - } -} diff --git a/frontend/playwright-fixture.ts b/frontend/playwright-fixture.ts deleted file mode 100644 index 7d471c1..0000000 --- a/frontend/playwright-fixture.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Re-export the base fixture from the package -// Override or extend test/expect here if needed -export { test, expect } from "lovable-agent-playwright-config/fixture"; diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts deleted file mode 100644 index ec19e95..0000000 --- a/frontend/playwright.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createLovableConfig } from "lovable-agent-playwright-config/config"; - -export default createLovableConfig({ - // Add your custom playwright configuration overrides here - // Example: - // timeout: 60000, - // use: { - // baseURL: 'http://localhost:3000', - // }, -}); diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js deleted file mode 100644 index 2aa7205..0000000 --- a/frontend/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico deleted file mode 100644 index 3c01d69..0000000 Binary files a/frontend/public/favicon.ico and /dev/null differ diff --git a/frontend/public/placeholder.svg b/frontend/public/placeholder.svg deleted file mode 100644 index ea950de..0000000 --- a/frontend/public/placeholder.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt deleted file mode 100644 index 6018e70..0000000 --- a/frontend/public/robots.txt +++ /dev/null @@ -1,14 +0,0 @@ -User-agent: Googlebot -Allow: / - -User-agent: Bingbot -Allow: / - -User-agent: Twitterbot -Allow: / - -User-agent: facebookexternalhit -Allow: / - -User-agent: * -Allow: / diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index f1ed102..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter, Route, Routes } from "react-router-dom"; -import { Toaster as Sonner } from "@/components/ui/sonner"; -import { Toaster } from "@/components/ui/toaster"; -import { TooltipProvider } from "@/components/ui/tooltip"; -import { PreferencesProvider } from "@/contexts/PreferencesContext"; -import Index from "./pages/Index.tsx"; -import Preferences from "./pages/Preferences.tsx"; -import NotFound from "./pages/NotFound.tsx"; - -const queryClient = new QueryClient(); - -const App = () => ( - - - - - - - - } /> - } /> - } /> - - - - - -); - -export default App; diff --git a/frontend/src/components/CategoryTabs.tsx b/frontend/src/components/CategoryTabs.tsx deleted file mode 100644 index 1be1eda..0000000 --- a/frontend/src/components/CategoryTabs.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Globe, Code, Image, Newspaper } from "lucide-react"; -import { CATEGORIES, type Category } from "@/lib/mock-data"; - -const CATEGORY_META: Record = { - general: { label: "General", icon: Globe }, - it: { label: "IT", icon: Code }, - images: { label: "Images", icon: Image }, - news: { label: "News", icon: Newspaper }, -}; - -interface CategoryTabsProps { - active: Category; - onChange: (c: Category) => void; -} - -export function CategoryTabs({ active, onChange }: CategoryTabsProps) { - return ( -
- {CATEGORIES.map((cat) => { - const { label, icon: Icon } = CATEGORY_META[cat]; - const isActive = cat === active; - return ( - - ); - })} -
- ); -} diff --git a/frontend/src/components/NavLink.tsx b/frontend/src/components/NavLink.tsx deleted file mode 100644 index a561a95..0000000 --- a/frontend/src/components/NavLink.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom"; -import { forwardRef } from "react"; -import { cn } from "@/lib/utils"; - -interface NavLinkCompatProps extends Omit { - className?: string; - activeClassName?: string; - pendingClassName?: string; -} - -const NavLink = forwardRef( - ({ className, activeClassName, pendingClassName, to, ...props }, ref) => { - return ( - - cn(className, isActive && activeClassName, isPending && pendingClassName) - } - {...props} - /> - ); - }, -); - -NavLink.displayName = "NavLink"; - -export { NavLink }; diff --git a/frontend/src/components/ResultCard.tsx b/frontend/src/components/ResultCard.tsx deleted file mode 100644 index 62209eb..0000000 --- a/frontend/src/components/ResultCard.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { SearchResult } from "@/lib/mock-data"; - -interface ResultCardProps { - result: SearchResult; -} - -export function ResultCard({ result }: ResultCardProps) { - const domain = result.parsed_url[1]; - const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; - - return ( - -
- - {result.pretty_url} - {result.engines.length > 1 && ( - - {result.engines.length} engines - - )} -
-

- {result.title} -

-

- {result.content} -

- {result.publishedDate && ( - - {new Date(result.publishedDate).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })} - - )} -
- ); -} diff --git a/frontend/src/components/ResultSkeleton.tsx b/frontend/src/components/ResultSkeleton.tsx deleted file mode 100644 index 0119f4c..0000000 --- a/frontend/src/components/ResultSkeleton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -export function ResultSkeleton() { - return ( -
- {Array.from({ length: 5 }).map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
- ))} -
- ); -} diff --git a/frontend/src/components/SearchInput.tsx b/frontend/src/components/SearchInput.tsx deleted file mode 100644 index 9d1cfe5..0000000 --- a/frontend/src/components/SearchInput.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Search } from "lucide-react"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { FormEvent, useRef, useEffect } from "react"; - -interface SearchInputProps { - query: string; - onQueryChange: (q: string) => void; - onSearch: (q: string) => void; - compact?: boolean; - autoFocus?: boolean; -} - -export function SearchInput({ query, onQueryChange, onSearch, compact, autoFocus }: SearchInputProps) { - const inputRef = useRef(null); - - useEffect(() => { - if (autoFocus) inputRef.current?.focus(); - }, [autoFocus]); - - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - onSearch(query); - }; - - return ( -
-
- - onQueryChange(e.target.value)} - placeholder="Search the web privately..." - className={`pl-10 pr-4 border-input bg-background focus-visible:ring-ring ${compact ? "h-9 text-sm" : "h-12 text-base"}`} - /> -
- -
- ); -} diff --git a/frontend/src/components/ui/accordion.tsx b/frontend/src/components/ui/accordion.tsx deleted file mode 100644 index 1e7878c..0000000 --- a/frontend/src/components/ui/accordion.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDown } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Accordion = AccordionPrimitive.Root; - -const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AccordionItem.displayName = "AccordionItem"; - -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", - className, - )} - {...props} - > - {children} - - - -)); -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)); - -AccordionContent.displayName = AccordionPrimitive.Content.displayName; - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/frontend/src/components/ui/alert-dialog.tsx b/frontend/src/components/ui/alert-dialog.tsx deleted file mode 100644 index 6dfbfb4..0000000 --- a/frontend/src/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import * as React from "react"; -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; - -import { cn } from "@/lib/utils"; -import { buttonVariants } from "@/components/ui/button"; - -const AlertDialog = AlertDialogPrimitive.Root; - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger; - -const AlertDialogPortal = AlertDialogPrimitive.Portal; - -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - -)); -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; - -const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
-); -AlertDialogHeader.displayName = "AlertDialogHeader"; - -const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
-); -AlertDialogFooter.displayName = "AlertDialogFooter"; - -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; - -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; - -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; - -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -}; diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx deleted file mode 100644 index 2efc3c8..0000000 --- a/frontend/src/components/ui/alert.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)); -Alert.displayName = "Alert"; - -const AlertTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -); -AlertTitle.displayName = "AlertTitle"; - -const AlertDescription = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ), -); -AlertDescription.displayName = "AlertDescription"; - -export { Alert, AlertTitle, AlertDescription }; diff --git a/frontend/src/components/ui/aspect-ratio.tsx b/frontend/src/components/ui/aspect-ratio.tsx deleted file mode 100644 index c9e6f4b..0000000 --- a/frontend/src/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; - -const AspectRatio = AspectRatioPrimitive.Root; - -export { AspectRatio }; diff --git a/frontend/src/components/ui/avatar.tsx b/frontend/src/components/ui/avatar.tsx deleted file mode 100644 index 68d21bb..0000000 --- a/frontend/src/components/ui/avatar.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from "react"; -import * as AvatarPrimitive from "@radix-ui/react-avatar"; - -import { cn } from "@/lib/utils"; - -const Avatar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -Avatar.displayName = AvatarPrimitive.Root.displayName; - -const AvatarImage = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AvatarImage.displayName = AvatarPrimitive.Image.displayName; - -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; - -export { Avatar, AvatarImage, AvatarFallback }; diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx deleted file mode 100644 index 0853c44..0000000 --- a/frontend/src/components/ui/badge.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -export interface BadgeProps extends React.HTMLAttributes, VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return
; -} - -export { Badge, badgeVariants }; diff --git a/frontend/src/components/ui/breadcrumb.tsx b/frontend/src/components/ui/breadcrumb.tsx deleted file mode 100644 index ca91ff5..0000000 --- a/frontend/src/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { ChevronRight, MoreHorizontal } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode; - } ->(({ ...props }, ref) =>