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

View File

@@ -13,16 +13,13 @@ function Cookbooks() {
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [newCookbookName, setNewCookbookName] = useState(''); const [newCookbookName, setNewCookbookName] = useState('');
const [newCookbookDescription, setNewCookbookDescription] = useState(''); const [newCookbookDescription, setNewCookbookDescription] = useState('');
const [autoFilterCategories, setAutoFilterCategories] = useState<string[]>([]);
const [autoFilterTags, setAutoFilterTags] = useState<string[]>([]); const [autoFilterTags, setAutoFilterTags] = useState<string[]>([]);
const [autoFilterCookbookTags, setAutoFilterCookbookTags] = useState<string[]>([]); const [autoFilterCookbookTags, setAutoFilterCookbookTags] = useState<string[]>([]);
const [cookbookTags, setCookbookTags] = useState<string[]>([]); const [cookbookTags, setCookbookTags] = useState<string[]>([]);
const [categoryInput, setCategoryInput] = useState('');
const [tagInput, setTagInput] = useState(''); const [tagInput, setTagInput] = useState('');
const [cookbookTagInput, setCookbookTagInput] = useState(''); const [cookbookTagInput, setCookbookTagInput] = useState('');
const [cookbookTagFilterInput, setCookbookTagFilterInput] = useState(''); const [cookbookTagFilterInput, setCookbookTagFilterInput] = useState('');
const [availableTags, setAvailableTags] = useState<Tag[]>([]); const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
loadData(); loadData();
@@ -41,15 +38,6 @@ function Cookbooks() {
setRecentRecipes(recipesResponse.data || []); setRecentRecipes(recipesResponse.data || []);
setAvailableTags(tagsResponse.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); setError(null);
} catch (err) { } catch (err) {
setError('Failed to load data'); setError('Failed to load data');
@@ -71,7 +59,6 @@ function Cookbooks() {
await cookbooksApi.create({ await cookbooksApi.create({
name: newCookbookName, name: newCookbookName,
description: newCookbookDescription || undefined, description: newCookbookDescription || undefined,
autoFilterCategories: autoFilterCategories.length > 0 ? autoFilterCategories : undefined,
autoFilterTags: autoFilterTags.length > 0 ? autoFilterTags : undefined, autoFilterTags: autoFilterTags.length > 0 ? autoFilterTags : undefined,
autoFilterCookbookTags: autoFilterCookbookTags.length > 0 ? autoFilterCookbookTags : undefined, autoFilterCookbookTags: autoFilterCookbookTags.length > 0 ? autoFilterCookbookTags : undefined,
tags: cookbookTags.length > 0 ? cookbookTags : undefined tags: cookbookTags.length > 0 ? cookbookTags : undefined
@@ -79,11 +66,9 @@ function Cookbooks() {
setNewCookbookName(''); setNewCookbookName('');
setNewCookbookDescription(''); setNewCookbookDescription('');
setAutoFilterCategories([]);
setAutoFilterTags([]); setAutoFilterTags([]);
setAutoFilterCookbookTags([]); setAutoFilterCookbookTags([]);
setCookbookTags([]); setCookbookTags([]);
setCategoryInput('');
setTagInput(''); setTagInput('');
setCookbookTagInput(''); setCookbookTagInput('');
setCookbookTagFilterInput(''); 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 handleAddTag = () => {
const trimmed = tagInput.trim(); const trimmed = tagInput.trim();
if (trimmed && !autoFilterTags.includes(trimmed)) { if (trimmed && !autoFilterTags.includes(trimmed)) {
@@ -293,35 +266,6 @@ function Cookbooks() {
/> />
</div> </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"> <div className="form-group">
<label>Auto-Add Tags (Optional)</label> <label>Auto-Add Tags (Optional)</label>
<p className="help-text">Recipes with these tags will be automatically added to this cookbook</p> <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 [coverImageUrl, setCoverImageUrl] = useState('');
const [imageUrlInput, setImageUrlInput] = useState(''); const [imageUrlInput, setImageUrlInput] = useState('');
const [selectedFile, setSelectedFile] = useState<File | null>(null); const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [autoFilterCategories, setAutoFilterCategories] = useState<string[]>([]);
const [autoFilterTags, setAutoFilterTags] = useState<string[]>([]); const [autoFilterTags, setAutoFilterTags] = useState<string[]>([]);
const [autoFilterCookbookTags, setAutoFilterCookbookTags] = useState<string[]>([]); const [autoFilterCookbookTags, setAutoFilterCookbookTags] = useState<string[]>([]);
const [cookbookTags, setCookbookTags] = useState<string[]>([]); const [cookbookTags, setCookbookTags] = useState<string[]>([]);
const [categoryInput, setCategoryInput] = useState('');
const [tagInput, setTagInput] = useState(''); const [tagInput, setTagInput] = useState('');
const [cookbookTagInput, setCookbookTagInput] = useState(''); const [cookbookTagInput, setCookbookTagInput] = useState('');
const [cookbookTagFilterInput, setCookbookTagFilterInput] = useState(''); const [cookbookTagFilterInput, setCookbookTagFilterInput] = useState('');
const [availableTags, setAvailableTags] = useState<Tag[]>([]); const [availableTags, setAvailableTags] = useState<Tag[]>([]);
const [availableCategories, setAvailableCategories] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
if (id) { if (id) {
@@ -49,7 +46,6 @@ function EditCookbook() {
setName(cookbook.name); setName(cookbook.name);
setDescription(cookbook.description || ''); setDescription(cookbook.description || '');
setCoverImageUrl(cookbook.coverImageUrl || ''); setCoverImageUrl(cookbook.coverImageUrl || '');
setAutoFilterCategories(cookbook.autoFilterCategories || []);
setAutoFilterTags(cookbook.autoFilterTags || []); setAutoFilterTags(cookbook.autoFilterTags || []);
setAutoFilterCookbookTags(cookbook.autoFilterCookbookTags || []); setAutoFilterCookbookTags(cookbook.autoFilterCookbookTags || []);
setCookbookTags(cookbook.tags || []); setCookbookTags(cookbook.tags || []);
@@ -57,17 +53,6 @@ function EditCookbook() {
setAvailableTags(tagsResponse.data || []); 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); setError(null);
} catch (err) { } catch (err) {
console.error('Failed to load cookbook:', err); console.error('Failed to load cookbook:', err);
@@ -91,7 +76,6 @@ function EditCookbook() {
name, name,
description: description || undefined, description: description || undefined,
coverImageUrl: coverImageUrl === '' ? '' : (coverImageUrl || undefined), coverImageUrl: coverImageUrl === '' ? '' : (coverImageUrl || undefined),
autoFilterCategories,
autoFilterTags, autoFilterTags,
autoFilterCookbookTags, autoFilterCookbookTags,
tags: cookbookTags 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 handleAddTag = () => {
const trimmed = tagInput.trim(); const trimmed = tagInput.trim();
if (trimmed && !autoFilterTags.includes(trimmed)) { if (trimmed && !autoFilterTags.includes(trimmed)) {
@@ -265,7 +237,6 @@ function EditCookbook() {
name, name,
description: description || undefined, description: description || undefined,
coverImageUrl: '', coverImageUrl: '',
autoFilterCategories,
autoFilterTags autoFilterTags
}); });
setCoverImageUrl(''); setCoverImageUrl('');
@@ -339,39 +310,6 @@ function EditCookbook() {
</div> </div>
</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"> <div className="form-group">
<label>Auto-Add Tags</label> <label>Auto-Add Tags</label>
<p className="help-text"> <p className="help-text">

View File

@@ -17,8 +17,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
const [cookTime, setCookTime] = useState(initialRecipe?.cookTime?.toString() || ''); const [cookTime, setCookTime] = useState(initialRecipe?.cookTime?.toString() || '');
const [servings, setServings] = useState(initialRecipe?.servings?.toString() || ''); const [servings, setServings] = useState(initialRecipe?.servings?.toString() || '');
const [cuisine, setCuisine] = useState(initialRecipe?.cuisine || ''); const [cuisine, setCuisine] = useState(initialRecipe?.cuisine || '');
const [categories, setCategories] = useState<string[]>(initialRecipe?.categories || []);
const [categoryInput, setCategoryInput] = useState('');
// Image handling // Image handling
const [uploadingImage, setUploadingImage] = useState(false); const [uploadingImage, setUploadingImage] = useState(false);
@@ -277,28 +275,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
}; };
// Category management // 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) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -309,7 +285,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
cookTime: cookTime ? parseInt(cookTime) : undefined, cookTime: cookTime ? parseInt(cookTime) : undefined,
servings: servings ? parseInt(servings) : undefined, servings: servings ? parseInt(servings) : undefined,
cuisine: cuisine || undefined, cuisine: cuisine || undefined,
categories: categories.length > 0 ? categories : undefined,
}; };
if (useSections) { if (useSections) {
@@ -399,44 +374,6 @@ function RecipeForm({ initialRecipe, onSubmit, onCancel }: RecipeFormProps) {
/> />
</div> </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) */} {/* Image Upload (only for editing existing recipes) */}
{initialRecipe?.id && ( {initialRecipe?.id && (
<div className="form-group image-upload-section"> <div className="form-group image-upload-section">

View File

@@ -27,8 +27,6 @@ function UnifiedEditRecipe() {
const [cookTime, setCookTime] = useState(''); const [cookTime, setCookTime] = useState('');
const [servings, setServings] = useState(''); const [servings, setServings] = useState('');
const [cuisine, setCuisine] = useState(''); const [cuisine, setCuisine] = useState('');
const [recipeCategories, setRecipeCategories] = useState<string[]>([]);
const [categoryInput, setCategoryInput] = useState('');
const [recipeTags, setRecipeTags] = useState<string[]>([]); const [recipeTags, setRecipeTags] = useState<string[]>([]);
const [tagInput, setTagInput] = useState(''); const [tagInput, setTagInput] = useState('');
const [availableTags, setAvailableTags] = useState<Tag[]>([]); const [availableTags, setAvailableTags] = useState<Tag[]>([]);
@@ -92,7 +90,6 @@ function UnifiedEditRecipe() {
setCookTime(loadedRecipe.cookTime?.toString() || ''); setCookTime(loadedRecipe.cookTime?.toString() || '');
setServings(loadedRecipe.servings?.toString() || ''); setServings(loadedRecipe.servings?.toString() || '');
setCuisine(loadedRecipe.cuisine || ''); setCuisine(loadedRecipe.cuisine || '');
setRecipeCategories(loadedRecipe.categories || []);
// Handle tags - API returns array of {tag: {id, name}} objects, we need string[] // 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) || []; 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, cookTime: cookTime ? parseInt(cookTime) : undefined,
servings: servings ? parseInt(servings) : undefined, servings: servings ? parseInt(servings) : undefined,
cuisine: cuisine || undefined, cuisine: cuisine || undefined,
categories: recipeCategories.length > 0 ? recipeCategories : undefined,
tags: recipeTags, tags: recipeTags,
}; };
@@ -602,33 +598,6 @@ function UnifiedEditRecipe() {
navigate(`/recipes/${id}`); 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 // Tag management functions
const handleAddTag = async (tagName: string) => { const handleAddTag = async (tagName: string) => {
const trimmedTag = tagName.trim(); const trimmedTag = tagName.trim();
@@ -840,45 +809,6 @@ function UnifiedEditRecipe() {
</div> </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 */} {/* Tags */}
<div className="form-group"> <div className="form-group">
<label htmlFor="tags">Tags</label> <label htmlFor="tags">Tags</label>