Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/awesome-seo.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/output.css

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/AwesomeSEOApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class AwesomeSEOApp {
private state: AppState = {
tools: [],
searchQuery: '',
selectedCategory: '',
selectedCategory: [],
sortBy: 'name',
loading: true
};
Expand Down Expand Up @@ -41,7 +41,7 @@ export class AwesomeSEOApp {
this.setState({ searchQuery });
}

setSelectedCategory(selectedCategory: string): void {
setSelectedCategory(selectedCategory: string[]): void {
this.setState({ selectedCategory });
}

Expand All @@ -52,7 +52,7 @@ export class AwesomeSEOApp {
clearFilters(): void {
this.setState({
searchQuery: '',
selectedCategory: ''
selectedCategory: []
});
}

Expand Down
10 changes: 5 additions & 5 deletions src/__tests__/AwesomeSEOApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('AwesomeSEOApp', () => {
});

it('should filter by category', () => {
app.setSelectedCategory('Category 1');
app.setSelectedCategory(['Category 1']);
const filtered = app.getFilteredTools();

expect(filtered).toHaveLength(1);
Expand All @@ -93,13 +93,13 @@ describe('AwesomeSEOApp', () => {

it('should clear filters', () => {
app.setSearchQuery('test');
app.setSelectedCategory('Category 1');
app.setSelectedCategory(['Category 1']);

app.clearFilters();

const state = app.getState();
expect(state.searchQuery).toBe('');
expect(state.selectedCategory).toBe('');
expect(state.selectedCategory).toEqual([]);
});
});

Expand Down Expand Up @@ -147,9 +147,9 @@ describe('AwesomeSEOApp', () => {
});

it('should update selected category', () => {
app.setSelectedCategory('new category');
app.setSelectedCategory(['new category']);

expect(app.getState().selectedCategory).toBe('new category');
expect(app.getState().selectedCategory).toEqual(['new category']);
});

it('should update sort by', () => {
Expand Down
27 changes: 25 additions & 2 deletions src/__tests__/filters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('Filters', () => {
});

it('should throw error for invalid tools input', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => searchFilter.filter(null as any, 'test')).toThrow('Tools must be an array');
});
});
Expand All @@ -75,23 +76,45 @@ describe('Filters', () => {
categoryFilter = new CategoryFilter();
});

it('should return all tools when category is empty', () => {
it('should return all tools when category is empty string', () => {
const result = categoryFilter.filter(mockTools, '');
expect(result).toEqual(mockTools);
});

it('should filter tools by category', () => {
it('should return all tools when category array is empty', () => {
const result = categoryFilter.filter(mockTools, []);
expect(result).toEqual(mockTools);
});

it('should filter tools by single category (string)', () => {
const result = categoryFilter.filter(mockTools, 'Keyword and Competitor Research');
expect(result).toHaveLength(2);
expect(result.every(tool => tool.category === 'Keyword and Competitor Research')).toBe(true);
});

it('should filter tools by single category (array)', () => {
const result = categoryFilter.filter(mockTools, ['Keyword and Competitor Research']);
expect(result).toHaveLength(2);
expect(result.every(tool => tool.category === 'Keyword and Competitor Research')).toBe(true);
});

it('should filter tools by multiple categories', () => {
const result = categoryFilter.filter(mockTools, ['Keyword and Competitor Research', 'Analysis and Site Auditing']);
expect(result).toHaveLength(3);
});

it('should return empty array when category not found', () => {
const result = categoryFilter.filter(mockTools, 'Nonexistent Category');
expect(result).toHaveLength(0);
});

it('should return empty array when none of the categories match', () => {
const result = categoryFilter.filter(mockTools, ['Nonexistent Category', 'Another Nonexistent']);
expect(result).toHaveLength(0);
});

it('should throw error for invalid tools input', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(() => categoryFilter.filter(null as any, 'test')).toThrow('Tools must be an array');
});
});
Expand Down
70 changes: 64 additions & 6 deletions src/components/SearchAndFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { Card } from './ui/card';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Button } from './ui/button';
import { Search } from 'lucide-react';
import { Search, X } from 'lucide-react';

interface SearchAndFiltersProps {
searchQuery: string;
selectedCategory: string;
selectedCategory: string[];
sortBy: SortBy;
categories: string[];
onSearchChange: (query: string) => void;
onCategoryChange: (category: string) => void;
onCategoryChange: (categories: string[]) => void;
onSortByChange: (sortBy: SortBy) => void;
}

Expand All @@ -25,6 +25,22 @@ export function SearchAndFilters({
onCategoryChange,
onSortByChange,
}: SearchAndFiltersProps) {
const handleCategoryToggle = (category: string) => {
if (selectedCategory.includes(category)) {
onCategoryChange(selectedCategory.filter(c => c !== category));
} else {
onCategoryChange([...selectedCategory, category]);
}
};

const handleRemoveCategory = (category: string) => {
onCategoryChange(selectedCategory.filter(c => c !== category));
};

const handleClearAllCategories = () => {
onCategoryChange([]);
};

return (
<Card className="p-8 mb-12">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
Expand All @@ -51,19 +67,61 @@ export function SearchAndFilters({
<label htmlFor="categorySelect" className="block text-sm font-semibold mb-3">
Filter by category
</label>
<Select value={selectedCategory || "all"} onValueChange={(value) => onCategoryChange(value === "all" ? "" : value)}>
<Select
value={selectedCategory.length > 0 ? selectedCategory[0] : "all"}
onValueChange={(value) => {
if (value === "all") {
handleClearAllCategories();
} else {
handleCategoryToggle(value);
}
}}
>
<SelectTrigger id="categorySelect" className="h-11">
<SelectValue placeholder="All categories" />
<SelectValue>
{selectedCategory.length === 0
? "All categories"
: `${selectedCategory.length} selected`}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All categories</SelectItem>
{categories.map((category) => (
<SelectItem key={category} value={category}>
{category}
<div className="flex items-center justify-between w-full">
<span>{category}</span>
{selectedCategory.includes(category) && (
<span className="ml-2 text-primary">✓</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>

{/* Selected Categories Badges */}
{selectedCategory.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{selectedCategory.map((category) => (
<button
key={category}
onClick={() => handleRemoveCategory(category)}
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
{category}
<X className="h-3 w-3" />
</button>
))}
{selectedCategory.length > 1 && (
<button
onClick={handleClearAllCategories}
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium rounded-full bg-muted text-muted-foreground hover:bg-muted/80 transition-colors"
>
Clear all
</button>
)}
</div>
)}
</div>
</div>

Expand Down
9 changes: 6 additions & 3 deletions src/filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ export class SearchFilter extends BaseFilter {
}

export class CategoryFilter extends BaseFilter {
filter(tools: Tool[], selectedCategory: string): Tool[] {
filter(tools: Tool[], selectedCategories: string | string[]): Tool[] {
this.validateTools(tools);

if (!selectedCategory || selectedCategory.trim() === '') {
// Support both string and string[] for backwards compatibility
const categories = Array.isArray(selectedCategories) ? selectedCategories : [selectedCategories];

if (categories.length === 0 || (categories.length === 1 && !categories[0])) {
return tools;
}

return tools.filter(tool => tool.category === selectedCategory);
return tools.filter(tool => categories.includes(tool.category));
}
}
4 changes: 2 additions & 2 deletions src/hooks/useAwesomeSEO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function useAwesomeSEO(apiUrl?: string) {

const [tools, setTools] = useState<Tool[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string[]>([]);
const [sortBy, setSortBy] = useState<SortBy>('name');
const [loading, setLoading] = useState(true);

Expand All @@ -41,7 +41,7 @@ export function useAwesomeSEO(apiUrl?: string) {
// Clear filters
const clearFilters = useCallback(() => {
setSearchQuery('');
setSelectedCategory('');
setSelectedCategory([]);
}, []);

// Copy to clipboard
Expand Down
2 changes: 1 addition & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type SortBy = 'name' | 'category';
export interface AppState {
tools: Tool[];
searchQuery: string;
selectedCategory: string;
selectedCategory: string[];
sortBy: SortBy;
loading: boolean;
}
Loading