Files
brewman/overlord/src/app/recipe/recipe-detail/recipe-detail.component.html
T
tanshu e3719faddd Removed Recipe Template as it was not needed.
Moved host listeners to the component declaration.
Form submit on Return fixed.
2026-08-26 06:00:13 +00:00

422 lines
16 KiB
HTML

<h2>Recipe Editor</h2>
@if (itemResource.isLoading()) {
<app-skeleton-loader type="card" [count]="1" />
<app-skeleton-loader type="line" [count]="10" />
} @else if (itemResource.error()) {
<app-error-state [message]="'Failed to load recipe detail.'" (retryAction)="itemResource.reload()" />
} @else {
<div class="section-nav row-container wrapped">
<button mat-stroked-button (click)="go('basics')" [class.active]="selectedSection === 'basics'">Basics</button>
<button mat-stroked-button (click)="go('ingredients')" [class.active]="selectedSection === 'ingredients'">
Ingredients
</button>
<button mat-stroked-button (click)="go('steps')" [class.active]="selectedSection === 'steps'">Steps</button>
<button mat-stroked-button (click)="go('photos')" [class.active]="selectedSection === 'photos'">Photos</button>
<button mat-stroked-button (click)="go('notes')" [class.active]="selectedSection === 'notes'">Notes</button>
<button mat-raised-button color="primary" (click)="go('preview')" [class.active]="selectedSection === 'preview'">
Preview
</button>
</div>
<form class="flex-col wrapped" [formRoot]="form">
@if (selectedSection === 'basics') {
<div class="flex-col wrapped">
<h3>Basics</h3>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Date</mat-label>
<input matInput [matDatepicker]="date" [formField]="form.basics.date" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Source</mat-label>
<input type="text" matInput [formField]="form.basics.source" autocomplete="off" />
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto basis-4-5">
<mat-label>Dish / Product</mat-label>
<input
type="text"
matInput
#productElement
[matAutocomplete]="autoP"
[formField]="form.basics.product"
autocomplete="off"
/>
<mat-autocomplete
#autoP="matAutocomplete"
autoActiveFirstOption
[displayWith]="displayProduct"
(optionSelected)="productSelected($event)"
>
@for (product of products(); track product) {
<mat-option [value]="product">{{ product.name }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="flex-auto basis-1-5">
<mat-label>Yield</mat-label>
<input type="text" matInput [formField]="form.basics.recipeYield" autocomplete="off" />
</mat-form-field>
</div>
</div>
}
@if (selectedSection === 'ingredients') {
<div class="flex-col wrapped">
<h3>Ingredients</h3>
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Ingredient</mat-label>
<input
type="text"
matInput
#ingredientElement
[matAutocomplete]="autoI"
[formField]="form.addRow.ingredient"
autocomplete="off"
/>
<mat-autocomplete #autoI="matAutocomplete" autoActiveFirstOption [displayWith]="displayIngredient">
@for (product of ingredients(); track product) {
<mat-option [value]="product">{{ product.name }} ({{ product.fractionUnits }})</mat-option>
}
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="flex-auto basis-1-5">
<mat-label>Quantity</mat-label>
<input type="text" matInput [formField]="form.addRow.quantity" autocomplete="off" />
</mat-form-field>
<mat-form-field class="flex-auto basis-30">
<mat-label>Description</mat-label>
<input type="text" matInput [formField]="form.addRow.description" autocomplete="off" />
</mat-form-field>
<button mat-raised-button color="primary" type="button" (click)="addRow()" class="flex-auto basis-10">
Add
</button>
</div>
<mat-table
#table
[dataSource]="form.ingredients().value() ?? []"
matSort
(matSortChange)="sortData($event)"
aria-label="Ingredients"
>
<ng-container matColumnDef="product">
<mat-header-cell *matHeaderCellDef>Product</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.product.name }} {{ row.description }}</mat-cell>
</ng-container>
<ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef class="right">Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.quantity | number: '1.2-2' }}</mat-cell>
</ng-container>
<ng-container matColumnDef="units">
<mat-header-cell *matHeaderCellDef>Units</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.product.fractionUnits }}</mat-cell>
</ng-container>
<ng-container matColumnDef="action">
<mat-header-cell *matHeaderCellDef class="center">Action</mat-header-cell>
<mat-cell *matCellDef="let row" class="center">
<button mat-icon-button tabindex="-1" color="warn" type="button" (click)="deleteRow(row)">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
</div>
}
@if (selectedSection === 'steps') {
<div class="flex-col wrapped">
<h3>Steps</h3>
<p class="hint">Add steps and reorder them using ↑ / ↓. Keep titles short and chef-friendly.</p>
<div class="row-container">
<button mat-raised-button color="primary" type="button" (click)="addStep()">
<mat-icon>add</mat-icon>
Add Step
</button>
</div>
@for (step of form.steps; track step; let i = $index) {
<div class="step-card">
<div class="row-container space-between aligned">
<div class="row-container aligned">
<span class="step-number">Step {{ i + 1 }}</span>
</div>
<div class="row-container aligned">
<button mat-icon-button type="button" (click)="moveStepUp(i)" [disabled]="i === 0">
<mat-icon>arrow_upward</mat-icon>
</button>
<button
mat-icon-button
type="button"
(click)="moveStepDown(i)"
[disabled]="i === form.steps.length - 1"
>
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-icon-button type="button" color="warn" (click)="removeStep(i)">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div class="row-container">
<mat-form-field class="flex-auto basis-1-5">
<mat-label>Phase</mat-label>
<select matNativeControl [formField]="step.phase">
@for (p of phases; track p) {
<option [value]="p">{{ p }}</option>
}
</select>
</mat-form-field>
<mat-form-field class="flex-auto basis-3-5">
<mat-label>Title</mat-label>
<input matInput [formField]="step.title" autocomplete="off" />
</mat-form-field>
<mat-form-field class="flex-auto basis-1-5">
<mat-label>Duration (min)</mat-label>
<input matInput type="number" [formField]="step.durationMinutes" />
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Step Text</mat-label>
<textarea matInput matAutosizeMinRows="3" [formField]="step.text"></textarea>
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Notes (optional)</mat-label>
<textarea matInput matAutosizeMinRows="2" [formField]="step.notes"></textarea>
</mat-form-field>
</div>
</div>
}
</div>
}
@if (selectedSection === 'photos') {
<div class="flex-col wrapped">
<h3>Photos</h3>
<div class="row-container aligned wrapped">
<label class="file-btn">
<input type="file" accept="image/*" multiple (change)="onPhotosSelected($event)" />
<span>
<mat-icon>upload</mat-icon>
Add Photos
</span>
</label>
<span class="hint">Captions are optional. Reorder with ↑ / ↓.</span>
</div>
@if (form.photos.length === 0) {
<p class="hint">No photos yet.</p>
}
@for (p of form.photos; track p; let i = $index) {
<div class="photo-card">
<div class="row-container space-between aligned">
<div class="row-container aligned">
<img class="thumb" [src]="photoThumbSrc(formModel().photos[i])" alt="Recipe photo" />
</div>
<div class="row-container aligned">
<button mat-icon-button type="button" (click)="movePhotoUp(i)" [disabled]="i === 0">
<mat-icon>arrow_upward</mat-icon>
</button>
<button
mat-icon-button
type="button"
(click)="movePhotoDown(i)"
[disabled]="i === form.photos.length - 1"
>
<mat-icon>arrow_downward</mat-icon>
</button>
<button mat-icon-button color="warn" type="button" (click)="removePhoto(i)">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Caption</mat-label>
<input matInput [formField]="p.caption" autocomplete="off" />
</mat-form-field>
</div>
</div>
}
</div>
}
@if (selectedSection === 'notes') {
<div class="flex-col wrapped">
<h3>Notes</h3>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Garnishing</mat-label>
<textarea matInput matAutosizeMinRows="3" [formField]="form.notes.garnishing"></textarea>
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Plating</mat-label>
<textarea matInput matAutosizeMinRows="3" [formField]="form.notes.plating"></textarea>
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Additional Notes</mat-label>
<textarea matInput matAutosizeMinRows="4" [formField]="form.notes.notes"></textarea>
</mat-form-field>
</div>
</div>
}
@if (selectedSection === 'preview') {
<div class="flex-col wrapped">
<div class="row-container space-between aligned">
<h3>Preview</h3>
<div class="row-container aligned">
<button mat-stroked-button type="button" (click)="exportPdf()">
<mat-icon>picture_as_pdf</mat-icon>
Export PDF
</button>
<button mat-stroked-button type="button" (click)="print()">
<mat-icon>print</mat-icon>
Print
</button>
</div>
</div>
<div class="recipe-print" #previewElement>
<div class="recipe-print__header">
<h1 class="recipe-print__title">{{ dishName }}</h1>
<div class="recipe-print__meta">
<span><strong>Date:</strong> {{ formModel().basics.date }}</span>
<span><strong>Source:</strong> {{ formModel().basics.source }}</span>
<span>
<strong>Yield:</strong>
{{ yieldText }}
</span>
</div>
</div>
<hr class="recipe-print__hr" />
<section class="recipe-print__section">
<h2>Ingredients</h2>
<ul class="recipe-print__ingredients">
@for (ing of formModel().ingredients; track ing) {
<li>
<div>
<span class="name">{{ ing.product.name }}</span>
@if (ing.description) {
<span class="desc">— {{ ing.description }}</span>
}
</div>
<span class="qty">{{ ing.quantity | number: '1.2-2' }} {{ ing.product.fractionUnits }}</span>
</li>
}
</ul>
</section>
@if (formModel().steps.length > 0) {
<section class="recipe-print__section">
<h2>Steps</h2>
<ol class="recipe-print__steps">
@for (step of formModel().steps; track step; let i = $index) {
<li class="recipe-print__step-item">
<div class="recipe-print__step-header">
Step {{ i + 1 }} - {{ step.phase }}: {{ step.title }}
@if (step.durationMinutes) {
({{ step.durationMinutes }} min)
}
</div>
<p class="recipe-print__step-text">{{ step.text }}</p>
@if (step.notes) {
<p class="recipe-print__step-notes">Note: {{ step.notes }}</p>
}
</li>
}
</ol>
</section>
}
@if (formModel().photos.length > 0) {
<section class="recipe-print__section">
<h2>Photos</h2>
<div class="recipe-print__photos">
@for (p of formModel().photos; track p; let i = $index) {
<div class="recipe-print__photo-item">
<img
class="recipe-print__photo-img"
[src]="photoThumbSrc(formModel().photos[i])"
alt="Recipe photo"
/>
@if (p.caption) {
<div class="recipe-print__photo-caption">{{ p.caption }}</div>
}
</div>
}
</div>
</section>
}
@if (formModel().notes.garnishing || formModel().notes.plating || formModel().notes.notes) {
<section class="recipe-print__section">
<h2>Notes</h2>
@if (formModel().notes.garnishing) {
<div class="recipe-print__notes-block">
<div class="recipe-print__notes-title">Garnishing</div>
<p class="recipe-print__notes-text">{{ formModel().notes.garnishing }}</p>
</div>
}
@if (formModel().notes.plating) {
<div class="recipe-print__notes-block">
<div class="recipe-print__notes-title">Plating</div>
<p class="recipe-print__notes-text">{{ formModel().notes.plating }}</p>
</div>
}
@if (formModel().notes.notes) {
<div class="recipe-print__notes-block">
<div class="recipe-print__notes-title">Additional Notes</div>
<p class="recipe-print__notes-text">{{ formModel().notes.notes }}</p>
</div>
}
</section>
}
</div>
</div>
}
</form>
}
<div class="row-container aligned wrapped">
<button mat-raised-button color="primary" (click)="save()">
{{ id() ? 'Update' : 'Save' }}
</button>
@if (id()) {
<button mat-raised-button type="button" color="warn" (click)="confirmDelete()">Delete</button>
}
</div>