feat: add collapsible sections to cooking mode
Some checks failed
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Test API Package (push) Has been cancelled
CI Pipeline / Test Web Package (push) Has been cancelled
CI Pipeline / Test Shared Package (push) Has been cancelled
CI Pipeline / Build All Packages (push) Has been cancelled
CI Pipeline / Generate Coverage Report (push) Has been cancelled
Docker Build & Deploy / Build Docker Images (push) Has been cancelled
Docker Build & Deploy / Push Docker Images (push) Has been cancelled
Docker Build & Deploy / Deploy to Staging (push) Has been cancelled
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
E2E Tests / End-to-End Tests (push) Has been cancelled
E2E Tests / E2E Tests (Mobile) (push) Has been cancelled
Security Scanning / NPM Audit (push) Has been cancelled
Security Scanning / Dependency License Check (push) Has been cancelled
Security Scanning / Code Quality Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled

Add collapsible functionality to cooking mode for better UX during cooking:

- Collapsible ingredients section: Click "Ingredients" heading to hide/show full list
- Collapsible instruction steps: Click step number to collapse/expand individual steps
- Auto-collapse toggle: Option to automatically collapse steps when marked complete
- Visual indicators: Triangle icons (▶/▼) show collapse state
- Reduced opacity for collapsed steps for visual distinction

This helps users focus on relevant information and reduces scrolling during cooking.

Changes:
- Added state management for collapsed ingredients and steps
- Added toggleStepCollapse function for manual step collapse
- Updated toggleStep to handle auto-collapse when enabled
- Added collapsible UI with click handlers and collapse icons
- Added CSS for collapsible headers and auto-collapse toggle
- Styled collapsed steps with opacity and spacing adjustments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-01 05:54:31 +00:00
parent e2c7693a8f
commit d6fceccba5
2 changed files with 211 additions and 89 deletions

View File

@@ -20,6 +20,9 @@ function CookingMode() {
const [showInlineIngredients, setShowInlineIngredients] = useState(true);
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
const [isFullscreen, setIsFullscreen] = useState(false);
const [ingredientsCollapsed, setIngredientsCollapsed] = useState(false);
const [collapsedSteps, setCollapsedSteps] = useState<Set<number>>(new Set());
const [autoCollapseOnComplete, setAutoCollapseOnComplete] = useState(false);
useEffect(() => {
if (id) {
@@ -71,12 +74,34 @@ function CookingMode() {
const newCompleted = new Set(completedSteps);
if (newCompleted.has(stepNumber)) {
newCompleted.delete(stepNumber);
// When unchecking, expand the step
if (autoCollapseOnComplete) {
const newCollapsed = new Set(collapsedSteps);
newCollapsed.delete(stepNumber);
setCollapsedSteps(newCollapsed);
}
} else {
newCompleted.add(stepNumber);
// When checking, auto-collapse if enabled
if (autoCollapseOnComplete) {
const newCollapsed = new Set(collapsedSteps);
newCollapsed.add(stepNumber);
setCollapsedSteps(newCollapsed);
}
}
setCompletedSteps(newCompleted);
};
const toggleStepCollapse = (stepNumber: number) => {
const newCollapsed = new Set(collapsedSteps);
if (newCollapsed.has(stepNumber)) {
newCollapsed.delete(stepNumber);
} else {
newCollapsed.add(stepNumber);
}
setCollapsedSteps(newCollapsed);
};
const exitCookingMode = () => {
if (document.fullscreenElement) {
document.exitFullscreen();
@@ -219,6 +244,15 @@ function CookingMode() {
Show ingredients with steps
</label>
<label className="toggle-auto-collapse">
<input
type="checkbox"
checked={autoCollapseOnComplete}
onChange={(e) => setAutoCollapseOnComplete(e.target.checked)}
/>
Auto collapse completed steps
</label>
<button onClick={() => navigate(`/recipes/${id}/edit`)} className="manage-btn">
Edit Recipe
</button>
@@ -243,32 +277,43 @@ function CookingMode() {
{/* Ingredients Section */}
<div className="cooking-mode-ingredients-section">
<h2>Ingredients</h2>
<h2
onClick={() => setIngredientsCollapsed(!ingredientsCollapsed)}
className="collapsible-header"
style={{ cursor: 'pointer' }}
>
<span className="collapse-icon">{ingredientsCollapsed ? '▶' : '▼'}</span>
Ingredients
</h2>
{recipe.sections && recipe.sections.length > 0 ? (
{!ingredientsCollapsed && (
<>
{recipe.sections.map(section => (
<div key={section.id} className="ingredient-section">
<h3>{section.name}</h3>
{section.timing && <p className="section-timing">{section.timing}</p>}
<ul>
{section.ingredients?.map((ingredient) => (
<li key={ingredient.id}>
{getScaledIngredientText(ingredient)}
</li>
))}
</ul>
</div>
))}
{recipe.sections && recipe.sections.length > 0 ? (
<>
{recipe.sections.map(section => (
<div key={section.id} className="ingredient-section">
<h3>{section.name}</h3>
{section.timing && <p className="section-timing">{section.timing}</p>}
<ul>
{section.ingredients?.map((ingredient) => (
<li key={ingredient.id}>
{getScaledIngredientText(ingredient)}
</li>
))}
</ul>
</div>
))}
</>
) : (
<ul>
{allIngredients.map((ingredient) => (
<li key={ingredient.id}>
{getScaledIngredientText(ingredient)}
</li>
))}
</ul>
)}
</>
) : (
<ul>
{allIngredients.map((ingredient) => (
<li key={ingredient.id}>
{getScaledIngredientText(ingredient)}
</li>
))}
</ul>
)}
</div>
@@ -292,25 +337,101 @@ function CookingMode() {
{section.timing && <span className="section-timing">{section.timing}</span>}
</div>
{sectionInstructions.map(({ instruction, matchedIngredients }) => (
<div
key={instruction.id}
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''}`}
>
<div className="step-header">
<label className="step-checkbox">
<input
type="checkbox"
checked={completedSteps.has(instruction.step)}
onChange={() => toggleStep(instruction.step)}
/>
<span className="step-number">Step {instruction.step}</span>
</label>
{instruction.timing && (
<span className="instruction-timing">{instruction.timing}</span>
{sectionInstructions.map(({ instruction, matchedIngredients }) => {
const isCollapsed = collapsedSteps.has(instruction.step);
return (
<div
key={instruction.id}
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''} ${isCollapsed ? 'collapsed' : ''}`}
>
<div className="step-header">
<label className="step-checkbox">
<input
type="checkbox"
checked={completedSteps.has(instruction.step)}
onChange={() => toggleStep(instruction.step)}
/>
<span
className="step-number"
onClick={() => toggleStepCollapse(instruction.step)}
style={{ cursor: 'pointer' }}
>
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
Step {instruction.step}
</span>
</label>
{instruction.timing && (
<span className="instruction-timing">{instruction.timing}</span>
)}
</div>
{!isCollapsed && (
<>
<div className="step-text">{instruction.text}</div>
{instruction.imageUrl && (
<img
src={instruction.imageUrl}
alt={`Step ${instruction.step}`}
className="step-image"
/>
)}
{showInlineIngredients && matchedIngredients.length > 0 && (
<div className="inline-ingredients">
<strong>Ingredients needed:</strong>
<ul>
{matchedIngredients.map((ingredient) => (
<li key={ingredient.id} className="ingredient-item">
<span className="ingredient-text">
{getScaledIngredientText(ingredient)}
</span>
</li>
))}
</ul>
</div>
)}
</>
)}
</div>
);
})}
</div>
);
})}
</>
) : (
<>
{instructionsWithIngredients.map(({ instruction, matchedIngredients }) => {
const isCollapsed = collapsedSteps.has(instruction.step);
return (
<div
key={instruction.id}
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''} ${isCollapsed ? 'collapsed' : ''}`}
>
<div className="step-header">
<label className="step-checkbox">
<input
type="checkbox"
checked={completedSteps.has(instruction.step)}
onChange={() => toggleStep(instruction.step)}
/>
<span
className="step-number"
onClick={() => toggleStepCollapse(instruction.step)}
style={{ cursor: 'pointer' }}
>
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
Step {instruction.step}
</span>
</label>
{instruction.timing && (
<span className="instruction-timing">{instruction.timing}</span>
)}
</div>
{!isCollapsed && (
<>
<div className="step-text">{instruction.text}</div>
{instruction.imageUrl && (
@@ -335,60 +456,12 @@ function CookingMode() {
</ul>
</div>
)}
</div>
))}
</>
)}
</div>
);
})}
</>
) : (
<>
{instructionsWithIngredients.map(({ instruction, matchedIngredients }) => (
<div
key={instruction.id}
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''}`}
>
<div className="step-header">
<label className="step-checkbox">
<input
type="checkbox"
checked={completedSteps.has(instruction.step)}
onChange={() => toggleStep(instruction.step)}
/>
<span className="step-number">Step {instruction.step}</span>
</label>
{instruction.timing && (
<span className="instruction-timing">{instruction.timing}</span>
)}
</div>
<div className="step-text">{instruction.text}</div>
{instruction.imageUrl && (
<img
src={instruction.imageUrl}
alt={`Step ${instruction.step}`}
className="step-image"
/>
)}
{showInlineIngredients && matchedIngredients.length > 0 && (
<div className="inline-ingredients">
<strong>Ingredients needed:</strong>
<ul>
{matchedIngredients.map((ingredient) => (
<li key={ingredient.id} className="ingredient-item">
<span className="ingredient-text">
{getScaledIngredientText(ingredient)}
</span>
</li>
))}
</ul>
</div>
)}
</div>
))}
</>
)}
</div>

View File

@@ -423,3 +423,52 @@
border: 1px solid #ccc;
}
}
/* Collapsible Elements */
.collapsible-header {
display: flex;
align-items: center;
gap: 0.5rem;
user-select: none;
}
.collapse-icon {
display: inline-block;
font-size: 0.9em;
margin-right: 0.25rem;
transition: transform 0.2s ease;
}
.step-number {
display: flex;
align-items: center;
gap: 0.25rem;
}
.instruction-step.collapsed {
opacity: 0.6;
}
.instruction-step.collapsed .step-header {
margin-bottom: 0;
}
/* Toggle auto-collapse checkbox */
.toggle-auto-collapse {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
background-color: white;
padding: 0.75rem 1rem;
border-radius: 8px;
border: 2px solid #e0e0e0;
font-size: 1rem;
}
.toggle-auto-collapse input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
cursor: pointer;
}