refactor: remove category feature from UI, focus on tags
All checks were successful
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m22s
Basil CI/CD Pipeline / Code Linting (push) Successful in 1m30s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m37s
Basil CI/CD Pipeline / API Tests (push) Successful in 1m50s
Basil CI/CD Pipeline / Security Scanning (push) Successful in 1m16s
Basil CI/CD Pipeline / Build All Packages (push) Successful in 1m32s
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Basil CI/CD Pipeline / Build & Push Docker Images (push) Successful in 5m6s
Basil CI/CD Pipeline / Trigger Deployment (push) Successful in 12s

Removed all category-related UI elements from the web application to simplify recipe organization. Users will now use tags exclusively for categorizing recipes and cookbooks.

Changes:
- Remove category input fields from RecipeForm and UnifiedEditRecipe
- Remove category filters from CookbookDetail
- Remove category auto-add feature from Cookbooks and EditCookbook
- Preserve category data in database for backward compatibility
- Keep API category support for future use or migrations

This change reduces user confusion by having a single organizational method (tags) instead of overlapping categories and tags.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Paul R Kartchner
2026-01-16 17:25:06 -07:00
parent da085b7332
commit c41cb5723f
5 changed files with 2 additions and 291 deletions

View File

@@ -14,7 +14,6 @@ function CookbookDetail() {
// Filters
const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('');
const [selectedCuisine, setSelectedCuisine] = useState<string>('');
useEffect(() => {
@@ -71,7 +70,7 @@ function CookbookDetail() {
);
};
// Get all unique tags and categories from recipes
// Get all unique tags from recipes
const getAllTags = (): string[] => {
if (!cookbook) return [];
const tagSet = new Set<string>();
@@ -81,17 +80,6 @@ function CookbookDetail() {
return Array.from(tagSet).sort();
};
const getAllCategories = (): string[] => {
if (!cookbook) return [];
const categorySet = new Set<string>();
cookbook.recipes.forEach(recipe => {
if (recipe.categories) {
recipe.categories.forEach(cat => categorySet.add(cat));
}
});
return Array.from(categorySet).sort();
};
const getAllCuisines = (): string[] => {
if (!cookbook) return [];
const cuisineSet = new Set<string>();
@@ -121,14 +109,6 @@ function CookbookDetail() {
if (!hasAllTags) return false;
}
// Category filter
if (selectedCategory) {
const recipeCategories = recipe.categories || [];
if (!recipeCategories.includes(selectedCategory)) {
return false;
}
}
// Cuisine filter
if (selectedCuisine && recipe.cuisine !== selectedCuisine) {
return false;
@@ -141,7 +121,6 @@ function CookbookDetail() {
const clearFilters = () => {
setSearchQuery('');
setSelectedTags([]);
setSelectedCategory('');
setSelectedCuisine('');
};
@@ -164,9 +143,8 @@ function CookbookDetail() {
const filteredRecipes = getFilteredRecipes();
const allTags = getAllTags();
const allCategories = getAllCategories();
const allCuisines = getAllCuisines();
const hasActiveFilters = searchQuery || selectedTags.length > 0 || selectedCategory || selectedCuisine;
const hasActiveFilters = searchQuery || selectedTags.length > 0 || selectedCuisine;
return (
<div className="cookbook-detail-page">
@@ -220,22 +198,6 @@ function CookbookDetail() {
</div>
<div className="filter-row">
{allCategories.length > 0 && (
<div className="filter-group">
<label htmlFor="category-filter">Category:</label>
<select
id="category-filter"
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
>
<option value="">All Categories</option>
{allCategories.map(category => (
<option key={category} value={category}>{category}</option>
))}
</select>
</div>
)}
{allCuisines.length > 0 && (
<div className="filter-group">
<label htmlFor="cuisine-filter">Cuisine:</label>

View File

@@ -13,16 +13,13 @@ function Cookbooks() {
const [showCreateModal, setShowCreateModal] = useState(false);
const [newCookbookName, setNewCookbookName] = useState('');
const [newCookbookDescription, setNewCookbookDescription] = useState('');
const [autoFilterCategories, setAutoFilterCategories] = useState<string[]>([]);
const [autoFilterTags, setAutoFilterTags] = useState<string[]>([]);
const [autoFilterCookbookTags, setAutoFilterCookbookTags] = useState<string[]>([]);
const [cookbookTags, setCookbookTags] = useState<string[]>([]);
const [categoryInput, setCategoryInput] = useState('');
const [tagInput, setTagInput] = useState('');
const [cookbookTagInput, setCookbookTagInput] = useState('');
const [cookbookTagFilterInput, setCookbookTagFilterInput] = useState('');
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
useEffect(() => {
loadData();
@@ -41,15 +38,6 @@ function Cookbooks() {
setRecentRecipes(recipesResponse.data || []);
setAvailableTags(tagsResponse.data || []);
// Extract unique categories from recent recipes
const categories = new Set<string>();
(recipesResponse.data || []).forEach(recipe => {
if (recipe.categories) {
recipe.categories.forEach(cat => categories.add(cat));
}
});
setAvailableCategories(Array.from(categories).sort());
setError(null);
} catch (err) {
setError('Failed to load data');
@@ -71,7 +59,6 @@ function Cookbooks() {
await cookbooksApi.create({
name: newCookbookName,
description: newCookbookDescription || undefined,
autoFilterCategories: autoFilterCategories.length > 0 ? autoFilterCategories : undefined,
autoFilterTags: autoFilterTags.length > 0 ? autoFilterTags : undefined,
autoFilterCookbookTags: autoFilterCookbookTags.length > 0 ? autoFilterCookbookTags : undefined,
tags: cookbookTags.length > 0 ? cookbookTags : undefined
@@ -79,11 +66,9 @@ function Cookbooks() {
setNewCookbookName('');
setNewCookbookDescription('');
setAutoFilterCategories([]);
setAutoFilterTags([]);
setAutoFilterCookbookTags([]);
setCookbookTags([]);
setCategoryInput('');
setTagInput('');
setCookbookTagInput('');
setCookbookTagFilterInput('');
@@ -95,18 +80,6 @@ function Cookbooks() {
}
};
const handleAddCategory = () => {
const trimmed = categoryInput.trim();
if (trimmed && !autoFilterCategories.includes(trimmed)) {
setAutoFilterCategories([...autoFilterCategories, trimmed]);
setCategoryInput('');
}
};
const handleRemoveCategory = (category: string) => {
setAutoFilterCategories(autoFilterCategories.filter(c => c !== category));
};
const handleAddTag = () => {
const trimmed = tagInput.trim();
if (trimmed && !autoFilterTags.includes(trimmed)) {
@@ -293,35 +266,6 @@ function Cookbooks() {
/>
</div>
<div className="form-group">
<label>Auto-Add Categories (Optional)</label>
<p className="help-text">Recipes with these categories will be automatically added to this cookbook</p>
<div className="filter-chips">
{autoFilterCategories.map(category => (
<span key={category} className="filter-chip">
{category}
<button type="button" onClick={() => handleRemoveCategory(category)}>×</button>
</span>
))}
</div>
<div className="input-with-button">
<input
type="text"
value={categoryInput}
onChange={(e) => setCategoryInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddCategory())}
placeholder="Add category"
list="available-categories"
/>
<button type="button" onClick={handleAddCategory} className="btn-add-filter">+</button>
</div>
<datalist id="available-categories">
{availableCategories.map(category => (
<option key={category} value={category} />
))}
</datalist>
</div>
<div className="form-group">
<label>Auto-Add Tags (Optional)</label>
<p className="help-text">Recipes with these tags will be automatically added to this cookbook</p>

View File

@@ -18,17 +18,14 @@ function EditCookbook() {
const [coverImageUrl, setCoverImageUrl] = useState('');
const [imageUrlInput, setImageUrlInput] = useState('');
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [autoFilterCategories, setAutoFilterCategories] = useState<string[]>([]);
const [autoFilterTags, setAutoFilterTags] = useState<string[]>([]);
const [autoFilterCookbookTags, setAutoFilterCookbookTags] = useState<string[]>([]);
const [cookbookTags, setCookbookTags] = useState<string[]>([]);
const [categoryInput, setCategoryInput] = useState('');
const [tagInput, setTagInput] = useState('');
const [cookbookTagInput, setCookbookTagInput] = useState('');
const [cookbookTagFilterInput, setCookbookTagFilterInput] = useState('');
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
useEffect(() => {
if (id) {
@@ -49,7 +46,6 @@ function EditCookbook() {
setName(cookbook.name);
setDescription(cookbook.description || '');
setCoverImageUrl(cookbook.coverImageUrl || '');
setAutoFilterCategories(cookbook.autoFilterCategories || []);
setAutoFilterTags(cookbook.autoFilterTags || []);
setAutoFilterCookbookTags(cookbook.autoFilterCookbookTags || []);
setCookbookTags(cookbook.tags || []);
@@ -57,17 +53,6 @@ function EditCookbook() {
setAvailableTags(tagsResponse.data || []);
// Extract unique categories from cookbook's recipes
const categories = new Set<string>();
if (cookbook && 'recipes' in cookbook) {
(cookbook as any).recipes.forEach((recipe: any) => {
if (recipe.categories) {
recipe.categories.forEach((cat: string) => categories.add(cat));
}
});
}
setAvailableCategories(Array.from(categories).sort());
setError(null);
} catch (err) {
console.error('Failed to load cookbook:', err);
@@ -91,7 +76,6 @@ function EditCookbook() {
name,
description: description || undefined,
coverImageUrl: coverImageUrl === '' ? '' : (coverImageUrl || undefined),
autoFilterCategories,
autoFilterTags,
autoFilterCookbookTags,
tags: cookbookTags
@@ -106,18 +90,6 @@ function EditCookbook() {
}
};
const handleAddCategory = () => {
const trimmed = categoryInput.trim();
if (trimmed && !autoFilterCategories.includes(trimmed)) {
setAutoFilterCategories([...autoFilterCategories, trimmed]);
setCategoryInput('');
}
};
const handleRemoveCategory = (category: string) => {
setAutoFilterCategories(autoFilterCategories.filter(c => c !== category));
};
const handleAddTag = () => {
const trimmed = tagInput.trim();
if (trimmed && !autoFilterTags.includes(trimmed)) {
@@ -265,7 +237,6 @@ function EditCookbook() {
name,
description: description || undefined,
coverImageUrl: '',
autoFilterCategories,
autoFilterTags
});
setCoverImageUrl('');
@@ -339,39 +310,6 @@ function EditCookbook() {
</div>
</div>
<div className="form-group">
<label>Auto-Add Categories</label>
<p className="help-text">
Recipes with these categories will be automatically added to this cookbook
</p>
<div className="filter-chips">
{autoFilterCategories.map(category => (
<span key={category} className="filter-chip">
{category}
<button type="button" onClick={() => handleRemoveCategory(category)}>×</button>
</span>
))}
</div>
<div className="input-with-button">
<input
type="text"
value={categoryInput}
onChange={(e) => setCategoryInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddCategory())}
placeholder="Add category"
list="available-categories"
/>
<button type="button" onClick={handleAddCategory} className="btn-add-filter">
+
</button>
</div>
<datalist id="available-categories">
{availableCategories.map(category => (
<option key={category} value={category} />
))}
</datalist>
</div>
<div className="form-group">
<label>Auto-Add Tags</label>
<p className="help-text">

View File

@@ -17,8 +17,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
const [cookTime, setCookTime] = useState(initialRecipe?.cookTime?.toString() || '');
const [servings, setServings] = useState(initialRecipe?.servings?.toString() || '');
const [cuisine, setCuisine] = useState(initialRecipe?.cuisine || '');
const [categories, setCategories] = useState<string[]>(initialRecipe?.categories || []);
const [categoryInput, setCategoryInput] = useState('');
// Image handling
const [uploadingImage, setUploadingImage] = useState(false);
@@ -277,28 +275,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
};
// Category management
const handleAddCategory = () => {
const trimmedCategory = categoryInput.trim();
if (!trimmedCategory) return;
if (categories.includes(trimmedCategory)) {
setCategoryInput('');
return;
}
setCategories([...categories, trimmedCategory]);
setCategoryInput('');
};
const handleRemoveCategory = (categoryToRemove: string) => {
setCategories(categories.filter(cat => cat !== categoryToRemove));
};
const handleCategoryInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCategory();
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -309,7 +285,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
cookTime: cookTime ? parseInt(cookTime) : undefined,
servings: servings ? parseInt(servings) : undefined,
cuisine: cuisine || undefined,
categories: categories.length > 0 ? categories : undefined,
};
if (useSections) {
@@ -399,44 +374,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
/>
</div>
<div className="form-group">
<label htmlFor="categories">Categories</label>
<div className="tags-input-container">
<div className="tags-list">
{categories.map((category) => (
<span key={category} className="tag">
{category}
<button
type="button"
onClick={() => handleRemoveCategory(category)}
className="tag-remove"
title="Remove category"
>
×
</button>
</span>
))}
</div>
<div className="tag-input-row">
<input
type="text"
id="categories"
value={categoryInput}
onChange={(e) => setCategoryInput(e.target.value)}
onKeyDown={handleCategoryInputKeyDown}
placeholder="Add a category and press Enter"
/>
<button
type="button"
onClick={handleAddCategory}
className="btn-add-tag"
>
Add Category
</button>
</div>
</div>
</div>
{/* Image Upload (only for editing existing recipes) */}
{initialRecipe?.id && (
<div className="form-group image-upload-section">

View File

@@ -27,8 +27,6 @@ function UnifiedEditRecipe() {
const [cookTime, setCookTime] = useState('');
const [servings, setServings] = useState('');
const [cuisine, setCuisine] = useState('');
const [recipeCategories, setRecipeCategories] = useState<string[]>([]);
const [categoryInput, setCategoryInput] = useState('');
const [recipeTags, setRecipeTags] = useState<string[]>([]);
const [tagInput, setTagInput] = useState('');
const [availableTags, setAvailableTags] = useState<Tag[]>([]);
@@ -92,7 +90,6 @@ function UnifiedEditRecipe() {
setCookTime(loadedRecipe.cookTime?.toString() || '');
setServings(loadedRecipe.servings?.toString() || '');
setCuisine(loadedRecipe.cuisine || '');
setRecipeCategories(loadedRecipe.categories || []);
// Handle tags - API returns array of {tag: {id, name}} objects, we need string[]
const tagNames = (loadedRecipe.tags as any)?.map((t: any) => t.tag?.name || t).filter(Boolean) || [];
@@ -482,7 +479,6 @@ function UnifiedEditRecipe() {
cookTime: cookTime ? parseInt(cookTime) : undefined,
servings: servings ? parseInt(servings) : undefined,
cuisine: cuisine || undefined,
categories: recipeCategories.length > 0 ? recipeCategories : undefined,
tags: recipeTags,
};
@@ -602,33 +598,6 @@ function UnifiedEditRecipe() {
navigate(`/recipes/${id}`);
};
// Category management functions
const handleAddCategory = (categoryName: string) => {
const trimmedCategory = categoryName.trim();
if (!trimmedCategory) return;
if (recipeCategories.includes(trimmedCategory)) {
setCategoryInput('');
return; // Category already exists
}
setRecipeCategories([...recipeCategories, trimmedCategory]);
setCategoryInput('');
setHasChanges(true);
};
const handleRemoveCategory = (categoryToRemove: string) => {
setRecipeCategories(recipeCategories.filter(cat => cat !== categoryToRemove));
setHasChanges(true);
};
const handleCategoryInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCategory(categoryInput);
}
};
// Tag management functions
const handleAddTag = async (tagName: string) => {
const trimmedTag = tagName.trim();
@@ -840,45 +809,6 @@ function UnifiedEditRecipe() {
</div>
{/* Categories */}
<div className="form-group">
<label htmlFor="categories">Categories</label>
<div className="tags-input-container">
<div className="tags-list">
{recipeCategories.map((category) => (
<span key={category} className="tag">
{category}
<button
type="button"
onClick={() => handleRemoveCategory(category)}
className="tag-remove"
title="Remove category"
>
×
</button>
</span>
))}
</div>
<div className="tag-input-row">
<input
type="text"
id="categories"
value={categoryInput}
onChange={(e) => setCategoryInput(e.target.value)}
onKeyDown={handleCategoryInputKeyDown}
placeholder="Add a category and press Enter"
/>
<button
type="button"
onClick={() => handleAddCategory(categoryInput)}
className="btn-add-tag"
>
Add Category
</button>
</div>
</div>
</div>
{/* Tags */}
<div className="form-group">
<label htmlFor="tags">Tags</label>