diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 5f7efb4..bd05693 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 - - 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/README.md b/README.md index 25c1c29..c03019e 100644 --- a/README.md +++ b/README.md @@ -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/kafka/main.go index 29ab620..cdc81b5 100644 --- a/cmd/kafka/main.go +++ b/cmd/kafka/main.go @@ -19,6 +19,7 @@ package main import ( "flag" "fmt" + "io/fs" "log" "log/slog" "net/http" @@ -30,7 +31,7 @@ import ( "github.com/metamorphosis-dev/kafka/internal/httpapi" "github.com/metamorphosis-dev/kafka/internal/middleware" "github.com/metamorphosis-dev/kafka/internal/search" - "github.com/metamorphosis-dev/kafka/internal/spa" + "github.com/metamorphosis-dev/kafka/internal/views" ) func main() { @@ -79,20 +80,22 @@ func main() { h := httpapi.NewHandler(svc, acSvc.Suggestions, cfg.Server.SourceURL) mux := http.NewServeMux() - - // API routes - handled by Go + mux.HandleFunc("/", h.Index) 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) + // 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)))) - // 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, @@ -104,7 +107,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, 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.

- - - -
- - 🌐 - All - - - 🖼️ - Images - - - 📰 - News - - - 🎬 - Videos - - - 🗺️ - Maps - - - 🛒 - Shopping - - - 🎵 - Music - - - 🌤️ - Weather - -
-
-
-{{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/specs/2026-03-22-brave-search-frontend-redesign.md b/docs/superpowers/specs/2026-03-22-brave-search-frontend-redesign.md deleted file mode 100644 index d30ab99..0000000 --- a/docs/superpowers/specs/2026-03-22-brave-search-frontend-redesign.md +++ /dev/null @@ -1,328 +0,0 @@ -# Brave Search Frontend Redesign — Design Specification - -## Overview - -Redesign the kafka frontend to match Brave Search's clean, functional aesthetic with emphasis on layout changes: three-column results page, category tiles on homepage, and a hybrid preferences system with full-page `/preferences` route. - -## Design Principles - -1. **Brave-like layout** — Three-column results, full-page preferences, homepage tiles -2. **Preserve existing design tokens** — Keep current CSS variables (colors, spacing, radii) -3. **CSS Grid for layout** — Three-column grid for results, flexible layouts elsewhere -4. **Hybrid preferences** — Quick popover for common settings (theme + engines), full `/preferences` page for all options -5. **Minimal HTML changes** — Restructure templates where needed for layout, reuse existing partials -6. **localStorage-only preferences** — No server-side persistence; all preferences stored in browser localStorage - ---- - -## 1. Homepage Redesign - -### Current State -- Centered hero with logo, tagline, and search box -- No visual categorization of search types - -### New Layout -``` -┌─────────────────────────────────────────────────────────────┐ -│ [Logo] [⚙ Preferences]│ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ [🔍 Search Box] │ -│ │ -│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ -│ │ News │ │ Images │ │ Videos │ │ Maps │ ... │ -│ └────────┘ └────────┘ └────────┘ └────────┘ │ -│ │ -│ "Search the web privately..." │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Implementation -- **File:** `internal/views/templates/index.html` -- **Structure:** Search hero + category tiles grid -- **Tiles:** Static links to `/search?q=...` with category parameter (e.g., `&category=images`) -- **Styling:** Grid of icon+label cards below search box, subtle hover effects - -### Category Tiles -| Category | Icon | Notes | -|----------|------|-------| -| All | 🌐 | Default, no category param | -| News | 📰 | | -| Images | 🖼️ | | -| Videos | 🎬 | | -| Maps | 🗺️ | | -| Shopping | 🛒 | Future: connect to shopping engine | -| Music | 🎵 | Future: connect to music engine | -| Weather | 🌤️ | Future: connect to weather API | -| Sports | ⚽ | Future | -| Cryptocurrency | ₿ | Future | - -Categories marked "Future" are included in the UI but may not have backend support yet. Category tiles that lack backend support display grayed out with a "Coming soon" tooltip. - ---- - -## 2. Results Page — Three-Column Layout - -### Current State -- Two columns: compact search bar spanning top, main results + right sidebar - -### New Layout -``` -┌─────────────────────────────────────────────────────────────┐ -│ [Logo] [⚙ Preferences]│ -├─────────────────────────────────────────────────────────────┤ -│ ┌─────────┐ ┌────────────────────────────┐ ┌──────────┐│ -│ │ Nav │ │ 🔍 [ Search Input ] │ │ Related ││ -│ │ ─────── │ └────────────────────────────┘ │ Searches ││ -│ │ All │ About 1,240 results (0.42s) │ ││ -│ │ Images │ ┌──────────────────────────┐ │ │ ─────── ││ -│ │ Videos │ │ Result Card │ │ │ Suggestions│ -│ │ News │ │ Title, URL, Description │ │ │ ││ -│ │ Maps │ └──────────────────────────┘ │ └──────────┘│ -│ │ Shopping│ ┌──────────────────────────┐ │ │ -│ │ ... │ │ Result Card │ │ │ -│ │ │ │ ... │ │ │ -│ │ ─────── │ └──────────────────────────┘ │ │ -│ │ Filters │ ... │ │ -│ │ Time │ │ │ -│ │ Type │ [Pagination] │ │ -│ └─────────┘ │ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Implementation -- **Files:** `internal/views/templates/results.html`, `internal/views/templates/base.html` -- **Left Sidebar (desktop, sticky):** - - Category navigation links (All, Images, Videos, News, Maps, Shopping, Music, Weather) - - Filters section (Time range, Result type) — collapsible - - Hidden on mobile (< 768px) - -- **Center Column:** - - Compact search bar - - Results count meta: "About {n} results ({time}s)" - - Result cards (unchanged markup) - - Pagination - -- **Right Sidebar:** - - Related searches (existing suggestions) - - Additional panels as needed - -### Filters -**Time Range Options:** -| Label | Query Param | -|-------|-------------| -| Any time | (none) | -| Past hour | `&time=h` | -| Past 24 hours | `&time=d` | -| Past week | `&time=w` | -| Past month | `&time=m` | -| Past year | `&time=y` | - -**Result Type Options:** -| Label | Query Param | -|-------|-------------| -| All results | (none) | -| News | `&type=news` | -| Videos | `&type=video` | -| Images | `&type=image` | - -Filter state persists in URL query params and is preserved across HTMX navigation via `hx-include`. - -### Mobile Behavior -| Breakpoint | Layout | -|------------|--------| -| < 768px | Single column, no left sidebar | -| 768px - 1024px | Two columns (center + right sidebar), no left nav | -| > 1024px | Full three columns | - -On mobile (< 768px): -- Category filters accessible via a horizontal scrollable chip row above results -- Both sidebars hidden -- Search bar full-width - ---- - -## 3. Preferences Page — Full-Page Hybrid - -### Current State -- Popover triggered by gear icon in header -- JavaScript-rendered from localStorage -- Sections: Appearance, Engines, Search Defaults - -### New Layout -``` -┌─────────────────────────────────────────────────────────────┐ -│ [Logo] [⚙ Preferences]│ -├─────────────────────────────────────────────────────────────┤ -│ ┌────────────────┐ ┌─────────────────────────────────────┐│ -│ │ Nav │ │ Content ││ -│ │ ───────────── │ │ ││ -│ │ Search │ │ [Section Content] ││ -│ │ Privacy │ │ ││ -│ │ Tabs │ │ ││ -│ │ Appearance │ │ ││ -│ │ Sidebar │ │ ││ -│ │ Content │ │ ││ -│ │ Languages │ │ ││ -│ │ Regional │ │ ││ -│ └────────────────┘ └─────────────────────────────────────┘│ -└─────────────────────────────────────────────────────────────┘ -``` - -### Sections (Brave-style) -1. **Search** — Default engine, safe search, language -2. **Privacy** — Tracking protection toggle (UI only, always on), request DNT header toggle -3. **Tabs** — New tab behavior (placeholder section) -4. **Appearance** — Theme (Light/Dark/System), results font size -5. **Sidebar** — Sidebar visibility toggle -6. **Content** — Filter explicit results (SafeSearch), auto-play media toggle -7. **Languages** — UI language (English only for now), search language -8. **Regional** — Region/Country, timezone (placeholder) - -### Implementation -- **Route:** Add `GET /preferences` and `POST /preferences` to `internal/httpapi/` -- **Template:** `internal/views/templates/preferences.html` -- **Storage:** localStorage-only. GET handler renders page shell, JavaScript populates form values from localStorage. POST handler receives form data, writes to localStorage, re-renders page. -- **Quick Settings Popover:** Keep existing popover for **theme toggle and engine toggles only** (lightweight, localStorage). SafeSearch and Format settings move exclusively to full preferences page. -- **Styling:** Match existing design tokens, section headers, form controls - -### Preferences Nav (Mobile) -- Horizontal scrollable nav on mobile (< 768px) -- Active section highlighted - ---- - -## 4. Component Changes - -### Header -- Logo + site name (unchanged) -- Preferences button (unchanged) - -### Search Box -- Homepage: Larger, prominent, centered -- Results page: Compact, full-width within center column - -### Result Cards -- Keep existing structure -- Consider subtle styling improvements (spacing, typography) - -### Category Tiles (Homepage) -- Icon + label per category -- Hover: slight scale + shadow - -### Left Sidebar (Results Page) -- Sticky positioning (`position: sticky; top: calc(var(--header-height) + 1rem)`) -- Category links with active state indicator -- Collapsible filter sections - -### Preferences Nav -- Vertical nav with section icons -- Active state indicator -- Mobile: horizontal scroll - ---- - -## 5. CSS Architecture - -### Existing (Retain) -- CSS custom properties (design tokens) -- Component-level styles -- Dark mode via `[data-theme="dark"]` - -### New - -**Layout Grid for three-column results:** -```css -.results-layout { - display: grid; - grid-template-columns: 200px 1fr 240px; - gap: 2rem; - align-items: start; -} -``` - -**Sticky Left Sidebar:** -```css -.results-layout .left-sidebar { - position: sticky; - top: calc(var(--header-height) + 1.5rem); - max-height: calc(100vh - var(--header-height) - 3rem); - overflow-y: auto; -} -``` - -**Preferences page layout:** -```css -.preferences-layout { - display: grid; - grid-template-columns: 200px 1fr; - gap: 2rem; -} -``` - -**Category tiles grid:** -```css -.category-tiles { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); - gap: 1rem; -} -``` - -**Mobile breakpoints:** -```css -@media (max-width: 768px) { - .results-layout { - grid-template-columns: 1fr; - } - .results-layout .left-sidebar, - .results-layout .right-sidebar { - display: none; - } -} - -@media (min-width: 769px) and (max-width: 1024px) { - .results-layout { - grid-template-columns: 1fr 220px; - } - .results-layout .left-sidebar { - display: none; - } -} -``` - ---- - -## 6. Files to Modify - -| File | Change | -|------|--------| -| `internal/views/templates/index.html` | Add category tiles | -| `internal/views/templates/results.html` | Add left sidebar, restructure for three columns | -| `internal/views/templates/base.html` | Minimal changes (no structural changes needed) | -| `internal/views/templates/preferences.html` | **New** — full preferences page | -| `internal/views/static/css/kafka.css` | Add layout grids, category tiles, sidebar styles, sticky positioning, mobile breakpoints | -| `internal/views/static/js/settings.js` | Reduce popover to theme + engines; add preferences page JS | -| `internal/httpapi/httpapi.go` | Add `/preferences` route (GET + POST) | -| `internal/views/views.go` | Add preferences template rendering | - ---- - -## 7. Priority Order - -1. **Phase 1:** CSS layout framework (three-column grid, new variables, breakpoints) -2. **Phase 2:** Results page three-column layout -3. **Phase 3:** Homepage category tiles -4. **Phase 4:** Preferences page (quick popover first, then full page) -5. **Phase 5:** Polish and mobile responsiveness - ---- - -## Out of Scope - -- Backend search logic changes -- New engine implementations (category tiles for future engines are UI placeholders only) -- Caching or performance improvements -- User authentication/account system -- Server-side preference storage diff --git a/docs/superpowers/specs/2026-03-22-frontend-replacement-design.md b/docs/superpowers/specs/2026-03-22-frontend-replacement-design.md deleted file mode 100644 index 65c294a..0000000 --- a/docs/superpowers/specs/2026-03-22-frontend-replacement-design.md +++ /dev/null @@ -1,74 +0,0 @@ -# Frontend Replacement: search-zen-50 Integration - -## Status -Approved - -## Overview - -Replace the current Go template-based frontend (HTMX + Go templates) with the search-zen-50 React SPA. The React app is built statically and embedded into the Go binary, serving as a single binary deployment. - -## Architecture - -- **Build**: React/Vite app builds to `dist/` directory -- **Embed**: Go's `//go:embed` embeds the dist folder into the binary -- **Serve**: Go HTTP server serves static files and handles API routes -- **SPA routing**: Non-API routes serve `index.html` for React Router - -## Changes - -### Go Side - -1. **Create `internal/spa/spa.go`** - - Embeds the React build (`dist/`) using `//go:embed` - - Serves static files (JS, CSS, images) - - Handles SPA fallback: serves `index.html` for all non-API routes - - Provides `SPAHandler` that wraps API routes - -2. **Modify `cmd/kafka/main.go`** - - Import the embedded SPA files - - Route `/`, `/preferences`, and unknown routes to SPA handler - - Keep existing API routes: `/search`, `/autocompleter`, `/healthz`, `/opensearch.xml` - -### React Side - -1. **Modify `use-search.ts`** - - Replace mock data with real API call: `fetch("/search?format=json&q=${encodeURIComponent(query)}")` - - Map response to existing `SearXNGResponse` type (already matches) - -2. **Add autocomplete** (optional enhancement) - - Call `/autocompleter?q=${encodeURIComponent(query)}` - - Display suggestions while typing - -3. **Keep unchanged** - - All UI components - - Preferences page (localStorage-based) - - Routing (React Router) - -## Data Flow - -``` -Browser → GET / → Go serves embedded index.html -Browser → GET /search?format=json&q=... → Go search handler → JSON -Browser → React renders results via use-search hook -``` - -## API Compatibility - -The existing kafka API (`/search?format=json`) already matches the expected `SearXNGResponse` interface in the React code: -- `query: string` -- `number_of_results: number` -- `results: SearchResult[]` -- `suggestions: string[]` -- `unresponsive_engines: string[][]` - -## File Changes - -- **New**: `internal/spa/spa.go` -- **Modified**: `cmd/kafka/main.go` (wire SPA handler) -- **Modified**: `src/hooks/use-search.ts` (use real API) -- **Build step**: `npm run build` or `bun run build` in search-zen-50 - -## Dependencies - -- React app uses `@tanstack/react-query` for API calls (already in package.json) -- No new Go dependencies needed diff --git a/flake.nix b/flake.nix index 3552728..d143495 100644 --- a/flake.nix +++ b/flake.nix @@ -21,16 +21,13 @@ version = "0.1.0"; src = ./.; - vendorHash = "sha256-8wlKD+33s97oorCJTfHKAgE2Xp1HKXV+bSr6z29KrKM="; + vendorHash = "sha256-NbAa4QM/TI3BTuZs4glx9k3ZjSl2/2LQfKlQ7izR8Ho="; # Run: nix build .#packages.x86_64-linux.default - # It will fail with the correct hash. Replace vendorHash with it. + # It will fail with the correct hash. Replace it here. # Embed the templates and static files at build time. ldflags = [ "-s" "-w" ]; - # Remove stale vendor directory before buildGoModule deletes it. - preConfigure = "rm -rf vendor || true"; - nativeCheckInputs = with pkgs; [ ]; # Tests require network; they run in CI instead. diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index a125fd6..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Welcome to your Lovable project - -TODO: Document your project here diff --git a/frontend/components.json b/frontend/components.json deleted file mode 100644 index 62e1011..0000000 --- a/frontend/components.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/index.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - } -} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 40f72cc..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,26 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; - -export default tseslint.config( - { ignores: ["dist"] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - "@typescript-eslint/no-unused-vars": "off", - }, - }, -); diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index c1ff5ee..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - kafka — Private Meta-Search - - - - - - - - - - - - - - - - - - -
- - - 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) =>