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 [showInlineIngredients, setShowInlineIngredients] = useState(true);
|
||||||
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
|
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [ingredientsCollapsed, setIngredientsCollapsed] = useState(false);
|
||||||
|
const [collapsedSteps, setCollapsedSteps] = useState<Set<number>>(new Set());
|
||||||
|
const [autoCollapseOnComplete, setAutoCollapseOnComplete] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -71,12 +74,34 @@ function CookingMode() {
|
|||||||
const newCompleted = new Set(completedSteps);
|
const newCompleted = new Set(completedSteps);
|
||||||
if (newCompleted.has(stepNumber)) {
|
if (newCompleted.has(stepNumber)) {
|
||||||
newCompleted.delete(stepNumber);
|
newCompleted.delete(stepNumber);
|
||||||
|
// When unchecking, expand the step
|
||||||
|
if (autoCollapseOnComplete) {
|
||||||
|
const newCollapsed = new Set(collapsedSteps);
|
||||||
|
newCollapsed.delete(stepNumber);
|
||||||
|
setCollapsedSteps(newCollapsed);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
newCompleted.add(stepNumber);
|
newCompleted.add(stepNumber);
|
||||||
|
// When checking, auto-collapse if enabled
|
||||||
|
if (autoCollapseOnComplete) {
|
||||||
|
const newCollapsed = new Set(collapsedSteps);
|
||||||
|
newCollapsed.add(stepNumber);
|
||||||
|
setCollapsedSteps(newCollapsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setCompletedSteps(newCompleted);
|
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 = () => {
|
const exitCookingMode = () => {
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
@@ -219,6 +244,15 @@ function CookingMode() {
|
|||||||
Show ingredients with steps
|
Show ingredients with steps
|
||||||
</label>
|
</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">
|
<button onClick={() => navigate(`/recipes/${id}/edit`)} className="manage-btn">
|
||||||
✏️ Edit Recipe
|
✏️ Edit Recipe
|
||||||
</button>
|
</button>
|
||||||
@@ -243,8 +277,17 @@ function CookingMode() {
|
|||||||
|
|
||||||
{/* Ingredients Section */}
|
{/* Ingredients Section */}
|
||||||
<div className="cooking-mode-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>
|
||||||
|
|
||||||
|
{!ingredientsCollapsed && (
|
||||||
|
<>
|
||||||
{recipe.sections && recipe.sections.length > 0 ? (
|
{recipe.sections && recipe.sections.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{recipe.sections.map(section => (
|
{recipe.sections.map(section => (
|
||||||
@@ -270,6 +313,8 @@ function CookingMode() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Instructions Section */}
|
{/* Instructions Section */}
|
||||||
@@ -292,10 +337,12 @@ function CookingMode() {
|
|||||||
{section.timing && <span className="section-timing">{section.timing}</span>}
|
{section.timing && <span className="section-timing">{section.timing}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sectionInstructions.map(({ instruction, matchedIngredients }) => (
|
{sectionInstructions.map(({ instruction, matchedIngredients }) => {
|
||||||
|
const isCollapsed = collapsedSteps.has(instruction.step);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={instruction.id}
|
key={instruction.id}
|
||||||
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''}`}
|
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''} ${isCollapsed ? 'collapsed' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="step-header">
|
<div className="step-header">
|
||||||
<label className="step-checkbox">
|
<label className="step-checkbox">
|
||||||
@@ -304,13 +351,22 @@ function CookingMode() {
|
|||||||
checked={completedSteps.has(instruction.step)}
|
checked={completedSteps.has(instruction.step)}
|
||||||
onChange={() => toggleStep(instruction.step)}
|
onChange={() => toggleStep(instruction.step)}
|
||||||
/>
|
/>
|
||||||
<span className="step-number">Step {instruction.step}</span>
|
<span
|
||||||
|
className="step-number"
|
||||||
|
onClick={() => toggleStepCollapse(instruction.step)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
|
||||||
|
Step {instruction.step}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{instruction.timing && (
|
{instruction.timing && (
|
||||||
<span className="instruction-timing">{instruction.timing}</span>
|
<span className="instruction-timing">{instruction.timing}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<>
|
||||||
<div className="step-text">{instruction.text}</div>
|
<div className="step-text">{instruction.text}</div>
|
||||||
|
|
||||||
{instruction.imageUrl && (
|
{instruction.imageUrl && (
|
||||||
@@ -335,18 +391,23 @@ function CookingMode() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{instructionsWithIngredients.map(({ instruction, matchedIngredients }) => (
|
{instructionsWithIngredients.map(({ instruction, matchedIngredients }) => {
|
||||||
|
const isCollapsed = collapsedSteps.has(instruction.step);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={instruction.id}
|
key={instruction.id}
|
||||||
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''}`}
|
className={`instruction-step ${completedSteps.has(instruction.step) ? 'completed' : ''} ${isCollapsed ? 'collapsed' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="step-header">
|
<div className="step-header">
|
||||||
<label className="step-checkbox">
|
<label className="step-checkbox">
|
||||||
@@ -355,13 +416,22 @@ function CookingMode() {
|
|||||||
checked={completedSteps.has(instruction.step)}
|
checked={completedSteps.has(instruction.step)}
|
||||||
onChange={() => toggleStep(instruction.step)}
|
onChange={() => toggleStep(instruction.step)}
|
||||||
/>
|
/>
|
||||||
<span className="step-number">Step {instruction.step}</span>
|
<span
|
||||||
|
className="step-number"
|
||||||
|
onClick={() => toggleStepCollapse(instruction.step)}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<span className="collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
|
||||||
|
Step {instruction.step}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{instruction.timing && (
|
{instruction.timing && (
|
||||||
<span className="instruction-timing">{instruction.timing}</span>
|
<span className="instruction-timing">{instruction.timing}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<>
|
||||||
<div className="step-text">{instruction.text}</div>
|
<div className="step-text">{instruction.text}</div>
|
||||||
|
|
||||||
{instruction.imageUrl && (
|
{instruction.imageUrl && (
|
||||||
@@ -386,8 +456,11 @@ function CookingMode() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -423,3 +423,52 @@
|
|||||||
border: 1px solid #ccc;
|
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