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
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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user