diff --git a/src/client/features/domain/DomainOverviewPage.tsx b/src/client/features/domain/DomainOverviewPage.tsx index e27bd2d..a4e632a 100644 --- a/src/client/features/domain/DomainOverviewPage.tsx +++ b/src/client/features/domain/DomainOverviewPage.tsx @@ -114,8 +114,18 @@ export function DomainOverviewPage({ visibleKeywords={state.visibleKeywords} filteredKeywords={state.filteredKeywords} filteredPages={state.filteredPages} + showFilters={state.showFilters} + setShowFilters={state.setShowFilters} + filtersForm={state.filtersForm} + activeFilterCount={state.activeFilterCount} + resetFilters={state.resetFilters} onTabChange={(tab) => { - if (tab === "pages" && searchState.sort === "rank") { + if ( + tab === "pages" && + (searchState.sort === "rank" || + searchState.sort === "score" || + searchState.sort === "cpc") + ) { state.applySort("traffic", getDefaultSortOrder("traffic")); } state.setSearchParams({ tab }); diff --git a/src/client/features/domain/components/DomainFilterPanel.tsx b/src/client/features/domain/components/DomainFilterPanel.tsx new file mode 100644 index 0000000..7efebc1 --- /dev/null +++ b/src/client/features/domain/components/DomainFilterPanel.tsx @@ -0,0 +1,167 @@ +import { RotateCcw } from "lucide-react"; +import type { useDomainFilters } from "@/client/features/domain/hooks/useDomainFilters"; +import type { DomainFilterValues } from "@/client/features/domain/types"; + +type FilterForm = ReturnType["filtersForm"]; + +type Props = { + filtersForm: FilterForm; + activeFilterCount: number; + resetFilters: () => void; +}; + +export function DomainFilterPanel({ + filtersForm, + activeFilterCount, + resetFilters, +}: Props) { + return ( +
+
+
+

Refine table results

+ {activeFilterCount > 0 ? ( + + {activeFilterCount} active + + ) : null} +
+ +
+ +
+ + +
+ +
+ + + + + +
+
+ ); +} + +function FilterTextInput({ + form, + name, + label, + placeholder, +}: { + form: FilterForm; + name: "include" | "exclude"; + label: string; + placeholder: string; +}) { + return ( + + ); +} + +function FilterRangeInputs({ + form, + title, + minName, + maxName, + step, +}: { + form: FilterForm; + title: string; + minName: keyof DomainFilterValues; + maxName: keyof DomainFilterValues; + step?: string; +}) { + return ( +
+

+ {title} +

+
+ + {(field) => ( + field.handleChange(event.target.value)} + /> + )} + + + {(field) => ( + field.handleChange(event.target.value)} + /> + )} + +
+
+ ); +} diff --git a/src/client/features/domain/components/DomainKeywordsTable.tsx b/src/client/features/domain/components/DomainKeywordsTable.tsx index 6319674..b2478c3 100644 --- a/src/client/features/domain/components/DomainKeywordsTable.tsx +++ b/src/client/features/domain/components/DomainKeywordsTable.tsx @@ -1,4 +1,3 @@ -import { HeaderHelpLabel } from "@/client/features/keywords/components"; import { DifficultyBadge } from "@/client/features/domain/components/DifficultyBadge"; import { SortableHeader } from "@/client/features/domain/components/SortableHeader"; import { formatFloat, formatNumber } from "@/client/features/domain/utils"; @@ -78,13 +77,22 @@ export function DomainKeywordsTable({ /> - + onSortClick("cpc")} + /> URL - onSortClick("score")} /> diff --git a/src/client/features/domain/components/DomainResultsCard.tsx b/src/client/features/domain/components/DomainResultsCard.tsx index 0cf63a8..ce89b36 100644 --- a/src/client/features/domain/components/DomainResultsCard.tsx +++ b/src/client/features/domain/components/DomainResultsCard.tsx @@ -1,3 +1,4 @@ +import { type Dispatch, type SetStateAction } from "react"; import { ChevronDown, Copy, @@ -5,10 +6,13 @@ import { FileSpreadsheet, Save, Search, + SlidersHorizontal, } from "lucide-react"; import { toast } from "sonner"; +import { DomainFilterPanel } from "@/client/features/domain/components/DomainFilterPanel"; import { DomainKeywordsTable } from "@/client/features/domain/components/DomainKeywordsTable"; import { DomainPagesTable } from "@/client/features/domain/components/DomainPagesTable"; +import type { useDomainFilters } from "@/client/features/domain/hooks/useDomainFilters"; import { downloadCsv, keywordsToCsv, @@ -33,6 +37,11 @@ type Props = { visibleKeywords: string[]; filteredKeywords: KeywordRow[]; filteredPages: PageRow[]; + showFilters: boolean; + setShowFilters: Dispatch>; + filtersForm: ReturnType["filtersForm"]; + activeFilterCount: number; + resetFilters: () => void; onTabChange: (tab: DomainActiveTab) => void; onSearchChange: (value: string) => void; onSaveKeywords: () => void; @@ -51,6 +60,11 @@ export function DomainResultsCard({ visibleKeywords, filteredKeywords, filteredPages, + showFilters, + setShowFilters, + filtersForm, + activeFilterCount, + resetFilters, onTabChange, onSearchChange, onSaveKeywords, @@ -78,28 +92,105 @@ export function DomainResultsCard({ const isKeywordsTab = activeTab === "keywords"; return ( -
-
- - -
- +
+
+
+ +
+
+ {activeTab === "keywords" ? ( + + ) : null} +
+
+ + Export + +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+ +
+ + + {filteredKeywords.length} keywords + +
+ +
+ + {showFilters ? ( + + ) : null} + +
{isKeywordsTab ? ( ); } - -function DomainResultsCardHeader({ - activeTab, - selectedKeywordsCount, - onTabChange, - onSaveKeywords, - onCopy, - onDownload, -}: { - activeTab: DomainActiveTab; - selectedKeywordsCount: number; - onTabChange: (tab: DomainActiveTab) => void; - onSaveKeywords: () => void; - onCopy: () => Promise; - onDownload: (extension: "csv" | "xls") => void; -}) { - return ( -
-
- - -
- -
- {activeTab === "keywords" ? ( - - ) : null} -
-
- - Export - -
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
-
-
- ); -} diff --git a/src/client/features/domain/components/DomainSearchCard.tsx b/src/client/features/domain/components/DomainSearchCard.tsx index f816af7..1f7214a 100644 --- a/src/client/features/domain/components/DomainSearchCard.tsx +++ b/src/client/features/domain/components/DomainSearchCard.tsx @@ -62,6 +62,8 @@ export function DomainSearchCard({ + + )} diff --git a/src/client/features/domain/components/SortableHeader.tsx b/src/client/features/domain/components/SortableHeader.tsx index 3230c90..9add4e2 100644 --- a/src/client/features/domain/components/SortableHeader.tsx +++ b/src/client/features/domain/components/SortableHeader.tsx @@ -1,14 +1,22 @@ import { ArrowDown, ArrowUp } from "lucide-react"; +import { HeaderHelpLabel } from "@/client/features/keywords/components"; import type { SortOrder } from "@/client/features/domain/types"; type Props = { label: string; + helpText?: string; isActive: boolean; order: SortOrder; onClick: () => void; }; -export function SortableHeader({ label, isActive, order, onClick }: Props) { +export function SortableHeader({ + label, + helpText, + isActive, + order, + onClick, +}: Props) { return (