Compare commits

..

No commits in common. "f673db572e27d5458ebcea81b746620438411a84" and "b66f290d846889e1f37667afe5f0194adae233b6" have entirely different histories.

54 changed files with 184 additions and 1003 deletions

View File

@ -1,6 +1,5 @@
{ {
"arrowParens": "avoid", "arrowParens": "avoid",
"printWidth": 120,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"tabWidth": 4, "tabWidth": 4,

View File

@ -25,7 +25,7 @@
"input": "public" "input": "public"
} }
], ],
"styles": ["src/material-theme.scss", "src/styles.css"] "styles": ["src/styles.css"]
}, },
"configurations": { "configurations": {
"production": { "production": {

38
package-lock.json generated
View File

@ -8,12 +8,10 @@
"name": "meals-made-easy-app", "name": "meals-made-easy-app",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@angular/cdk": "^21.0.6",
"@angular/common": "^21.0.0", "@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0", "@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0", "@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0", "@angular/forms": "^21.0.0",
"@angular/material": "^21.0.6",
"@angular/platform-browser": "^21.0.0", "@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0", "@angular/router": "^21.0.0",
"@fortawesome/angular-fontawesome": "^4.0.0", "@fortawesome/angular-fontawesome": "^4.0.0",
@ -426,22 +424,6 @@
} }
} }
}, },
"node_modules/@angular/cdk": {
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.0.6.tgz",
"integrity": "sha512-5Gw8mXtKXvcvDMWEciPLRYB6Ja5vsikLAidZsdCEIF6Bc51GmoqT5Tk/Ke+ciCd5Hq9Aco/IcHxT1RC3470lZg==",
"license": "MIT",
"peer": true,
"dependencies": {
"parse5": "^8.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^21.0.0 || ^22.0.0",
"@angular/core": "^21.0.0 || ^22.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/cli": { "node_modules/@angular/cli": {
"version": "21.0.2", "version": "21.0.2",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.2.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.2.tgz",
@ -573,7 +555,6 @@
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.3.tgz", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.3.tgz",
"integrity": "sha512-W60auwyDmsglIlHAbP/eol0LyzQ6FCz8LHghNx2B4RjIpuIMyjBLBZfC0JHU0gyiKB/JfX8W4FdphvyT7I4sIw==", "integrity": "sha512-W60auwyDmsglIlHAbP/eol0LyzQ6FCz8LHghNx2B4RjIpuIMyjBLBZfC0JHU0gyiKB/JfX8W4FdphvyT7I4sIw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
}, },
@ -588,23 +569,6 @@
"rxjs": "^6.5.3 || ^7.4.0" "rxjs": "^6.5.3 || ^7.4.0"
} }
}, },
"node_modules/@angular/material": {
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-21.0.6.tgz",
"integrity": "sha512-BSbqFkVIjpXS+UGD7R1jDnuKArMCtLSKHL/1f/9mvHM4AntRFC88MQJMjS0k+pHCofN+MBMEpzBVrDOOqL+46A==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/cdk": "21.0.6",
"@angular/common": "^21.0.0 || ^22.0.0",
"@angular/core": "^21.0.0 || ^22.0.0",
"@angular/forms": "^21.0.0 || ^22.0.0",
"@angular/platform-browser": "^21.0.0 || ^22.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/platform-browser": { "node_modules/@angular/platform-browser": {
"version": "21.0.3", "version": "21.0.3",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.3.tgz", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.3.tgz",
@ -7193,6 +7157,7 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"entities": "^6.0.0" "entities": "^6.0.0"
@ -7246,6 +7211,7 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"

View File

@ -9,7 +9,8 @@
"test": "ng test" "test": "ng test"
}, },
"prettier": { "prettier": {
"printWidth": 120, "printWidth": 100,
"singleQuote": true,
"overrides": [ "overrides": [
{ {
"files": "*.html", "files": "*.html",
@ -22,12 +23,10 @@
"private": true, "private": true,
"packageManager": "npm@11.6.2", "packageManager": "npm@11.6.2",
"dependencies": { "dependencies": {
"@angular/cdk": "^21.0.6",
"@angular/common": "^21.0.0", "@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0", "@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0", "@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0", "@angular/forms": "^21.0.0",
"@angular/material": "^21.0.6",
"@angular/platform-browser": "^21.0.0", "@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0", "@angular/router": "^21.0.0",
"@fortawesome/angular-fontawesome": "^4.0.0", "@fortawesome/angular-fontawesome": "^4.0.0",

View File

@ -14,7 +14,7 @@ export const routes: Routes = [
component: RecipesSearchPage, component: RecipesSearchPage,
}, },
{ {
path: 'recipe-upload', path: 'recipes-upload',
component: RecipeUploadPage, component: RecipeUploadPage,
}, },
{ {

View File

@ -16,7 +16,7 @@ article {
align-items: center; align-items: center;
} }
#star-label { #star {
display: flex; display: flex;
align-items: center; align-items: center;
column-gap: 3px; column-gap: 3px;

View File

@ -4,12 +4,10 @@
<div> <div>
<h1>{{ recipe.title }}</h1> <h1>{{ recipe.title }}</h1>
@if (isLoggedIn()) { @if (isLoggedIn()) {
<button id="star" matButton="filled" (click)="starMutation.mutate()"> <button id="star" (click)="starMutation.mutate()">
<div id="star-label">
<fa-icon [icon]="faStar" /> <fa-icon [icon]="faStar" />
<span>Star</span> <span>Star</span>
<span id="star-count">{{ recipe.starCount }}</span> <span id="star-count">{{ recipe.starCount }}</span>
</div>
</button> </button>
} @else { } @else {
<div> <div>

View File

@ -7,11 +7,10 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { RecipeService } from '../../../shared/services/RecipeService'; import { RecipeService } from '../../../shared/services/RecipeService';
import { AuthService } from '../../../shared/services/AuthService'; import { AuthService } from '../../../shared/services/AuthService';
import { RecipeCommentsList } from '../../../shared/components/recipe-comments-list/recipe-comments-list'; import { RecipeCommentsList } from '../../../shared/components/recipe-comments-list/recipe-comments-list';
import { MatButton } from '@angular/material/button';
@Component({ @Component({
selector: 'app-recipe-page-content', selector: 'app-recipe-page-content',
imports: [FaIconComponent, RecipeCommentsList, MatButton], imports: [FaIconComponent, RecipeCommentsList],
templateUrl: './recipe-page-content.html', templateUrl: './recipe-page-content.html',
styleUrl: './recipe-page-content.css', styleUrl: './recipe-page-content.css',
}) })

View File

@ -21,15 +21,15 @@ form {
column-gap: 5px; column-gap: 5px;
} }
/*button {*/ button {
/* width: 100%;*/ width: 100%;
/*}*/ }
/*input[type="text"],*/ input[type='text'],
/*textarea {*/ textarea {
/* padding: 10px;*/ padding: 10px;
/* font-size: 16px;*/ font-size: 16px;
/*}*/ }
textarea { textarea {
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,33 +1,16 @@
<div id="recipe-upload-container"> <div id="recipe-upload-container">
<h1>Upload Recipe</h1> <h1>Upload Recipe</h1>
<app-recipe-upload-trail
[displayStep]="displayStep()"
[inProgressStep]="inProgressStep()"
[includeInfer]="includeInfer()"
(stepClick)="onStepClick($event)"
></app-recipe-upload-trail>
@if (displayStep() === RecipeUploadStep.START) {
<app-ai-or-manual
[sourceFile]="sourceFile()"
(sourceFileChange)="onSourceFileChange($event)"
(submitStep)="onAiOrManualSubmit($event)"
></app-ai-or-manual>
} @else if (displayStep() === RecipeUploadStep.INFER) {
<app-infer></app-infer>
} @else if (displayStep() === RecipeUploadStep.ENTER_DATA) {
<app-enter-recipe-data [model]="model()"></app-enter-recipe-data>
}
<!--
<section> <section>
<h2>Auto-Complete Recipe (Optional)</h2> <h2>Auto-Complete Recipe (Optional)</h2>
<p>Choose a photo of a recipe from your files, and AI will fill out the form below for you.</p> <p>
Choose a photo of a recipe from your files, and AI will fill out the form below for you.
</p>
<form id="recipe-upload-form" [formGroup]="recipeUploadForm" (ngSubmit)="onFileSubmit()"> <form id="recipe-upload-form" [formGroup]="recipeUploadForm" (ngSubmit)="onFileSubmit()">
<input id="file" type="file" (change)="onFileChange($event)" /> <input id="file" type="file" (change)="onFileChange($event)" />
<div class="action-buttons-container"> <div class="action-buttons-container">
<button matButton="outlined" type="button" (click)="onClear()">Clear</button> <button type="button" (click)="onClear()">Clear</button>
<button matButton="filled" type="submit" [disabled]="!recipeUploadForm.valid || inferenceInProgress()"> <button type="submit" [disabled]="!recipeUploadForm.valid || inferenceInProgress()">
AI Auto-Complete AI Auto-Complete
</button> </button>
<app-spinner [enabled]="inferenceInProgress()"></app-spinner> <app-spinner [enabled]="inferenceInProgress()"></app-spinner>
@ -39,20 +22,21 @@
<h2>Recipe Form</h2> <h2>Recipe Form</h2>
<div id="recipe-form-and-source"> <div id="recipe-form-and-source">
<form [formGroup]="recipeForm" (ngSubmit)="onRecipeSubmit()"> <form [formGroup]="recipeForm" (ngSubmit)="onRecipeSubmit()">
<mat-form-field> <input
<mat-label>Recipe Title</mat-label> id="recipe-title"
<input matInput id="recipe-title" type="text" formControlName="title" /> type="text"
</mat-form-field> formControlName="title"
<mat-form-field> placeholder="Recipe Title"
<mat-label>Recipe Text</mat-label> />
<textarea <textarea
matInput
id="recipe-text" id="recipe-text"
formControlName="recipeText" formControlName="recipeText"
(input)="onRecipeTextChange($event)" (input)="onRecipeTextChange($event)"
placeholder="Recipe text"
></textarea> ></textarea>
</mat-form-field> <button type="submit" [disabled]="!recipeForm.valid || inferenceInProgress()">
<button matButton="filled" type="submit" [disabled]="!recipeForm.valid || inferenceInProgress()">Add Recipe</button> Add Recipe
</button>
</form> </form>
<div> <div>
@if (sourceRecipeImage()) { @if (sourceRecipeImage()) {
@ -61,5 +45,4 @@
</div> </div>
</div> </div>
</section> </section>
-->
</div> </div>

View File

@ -1,212 +1,106 @@
import { Component, computed, inject, OnInit, signal } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import {
import { AiOrManual } from './steps/ai-or-manual/ai-or-manual'; FormBuilder,
import { AIOrManualSubmitEvent } from './steps/ai-or-manual/AIOrManualSubmitEvent'; FormControl,
import { Infer } from './steps/infer/infer'; FormGroup,
import { EnterRecipeData } from './steps/enter-recipe-data/enter-recipe-data'; ReactiveFormsModule,
import { RecipeUploadTrail } from './recipe-upload-trail/recipe-upload-trail'; Validators,
import { ActivatedRoute, Router } from '@angular/router'; } from '@angular/forms';
import { StepClickEvent } from './recipe-upload-trail/StepClickEvent'; import { SseClient } from 'ngx-sse-client';
import { RecipeUploadModel } from '../../shared/client-models/RecipeUploadModel'; import { Spinner } from '../../shared/components/spinner/spinner';
import { RecipeUploadService } from '../../shared/services/RecipeUploadService';
import { RecipeUploadStep } from '../../shared/client-models/RecipeUploadStep';
import { FileUploadEvent } from '../../shared/components/file-upload/FileUploadEvent';
import { tryMaybeInt } from '../../shared/util';
import { from, map, switchMap, tap } from 'rxjs';
@Component({ @Component({
selector: 'app-recipe-upload-page', selector: 'app-recipe-upload-page',
imports: [ReactiveFormsModule, AiOrManual, Infer, EnterRecipeData, RecipeUploadTrail], imports: [ReactiveFormsModule, Spinner],
templateUrl: './recipe-upload-page.html', templateUrl: './recipe-upload-page.html',
styleUrl: './recipe-upload-page.css', styleUrl: './recipe-upload-page.css',
}) })
export class RecipeUploadPage implements OnInit { export class RecipeUploadPage {
protected readonly model = signal<RecipeUploadModel>({ private readonly sseClient = inject(SseClient);
inProgressStep: RecipeUploadStep.START, private readonly formBuilder = inject(FormBuilder);
protected readonly sourceRecipeImage = signal<string | null>(null);
protected readonly inferenceInProgress = signal(false);
protected readonly recipeUploadForm = this.formBuilder.group({
file: this.formBuilder.control<File | null>(null, [Validators.required]),
}); });
protected readonly displayStep = signal<number>(RecipeUploadStep.START); protected readonly recipeForm = new FormGroup({
protected readonly inProgressStep = computed(() => this.model().inProgressStep); title: new FormControl('', [Validators.required]),
protected readonly includeInfer = signal(false); recipeText: new FormControl('', Validators.required),
protected readonly sourceFile = computed(() => this.model().sourceFile ?? null); });
private readonly router = inject(Router); protected onClear() {
private readonly activatedRoute = inject(ActivatedRoute); this.recipeUploadForm.reset();
private readonly recipeUploadService = inject(RecipeUploadService); this.sourceRecipeImage.set(null);
public ngOnInit(): void {
this.activatedRoute.queryParamMap
.pipe(
map((paramMap) => {
const draftIdParam: string | null = paramMap.get('draftId');
const draftId = tryMaybeInt(draftIdParam);
const stepParam: string | null = paramMap.get('step');
const step = tryMaybeInt(stepParam);
return [draftId, step];
}),
switchMap(([draftId, step]) => {
const currentModel = this.model();
if (draftId !== null && currentModel.id !== draftId) {
return this.recipeUploadService.getRecipeUploadModel(draftId).pipe(
tap((updatedModel) => {
this.model.set(updatedModel);
}),
switchMap((updatedModel) => {
if (step !== null && step <= updatedModel.inProgressStep) {
return from(this.changeDisplayStep(step));
} else {
return from(this.changeDisplayStep(updatedModel.inProgressStep));
}
}),
);
} else if (step !== null && step <= currentModel.inProgressStep) {
return from(this.changeDisplayStep(step));
} else {
return from(this.changeDisplayStep(RecipeUploadStep.START));
}
}),
)
.subscribe();
} }
private async changeDisplayStep(targetStep: number): Promise<void> { protected onFileChange(event: Event) {
this.displayStep.set(targetStep); const fileInput = event.target as HTMLInputElement;
await this.router.navigate([], { if (fileInput.files && fileInput.files.length) {
relativeTo: this.activatedRoute, const file = fileInput.files[0];
queryParams: { this.recipeUploadForm.controls.file.setValue(file);
step: targetStep, this.recipeUploadForm.controls.file.markAsTouched();
draftId: this.model().id, this.recipeUploadForm.controls.file.updateValueAndValidity();
// set source image
this.sourceRecipeImage.set(URL.createObjectURL(file));
}
}
protected onFileSubmit() {
const rawValue = this.recipeUploadForm.getRawValue();
this.inferenceInProgress.set(true);
// upload form data
const formData = new FormData();
formData.append('recipeImageFile', rawValue.file!, rawValue.file!.name);
this.sseClient
.stream(
`http://localhost:8080/inferences/recipe-extract-stream`,
{
keepAlive: false,
reconnectionDelay: 1000,
responseType: 'event',
},
{
body: formData,
},
'PUT',
)
.subscribe({
next: (event) => {
if (event.type === 'error') {
const errorEvent = event as ErrorEvent;
console.error(errorEvent.error, errorEvent.message);
} else {
const messageEvent = event as MessageEvent;
const data: { delta: string } = JSON.parse(messageEvent.data);
this.recipeForm.patchValue({
recipeText: this.recipeForm.value.recipeText + data.delta,
});
// must do this so we auto-resize the textarea
document
.getElementById('recipe-text')
?.dispatchEvent(new Event('input', { bubbles: true }));
}
},
complete: () => {
this.inferenceInProgress.set(false);
}, },
queryParamsHandling: 'merge',
}); });
} }
protected async onStepClick(event: StepClickEvent): Promise<void> { protected onRecipeSubmit() {
await this.changeDisplayStep(event.step); console.log(this.recipeForm.value);
} }
protected onSourceFileChange(event: FileUploadEvent) { protected onRecipeTextChange(event: Event) {
if (event._tag === 'file-add-event') { const textarea = event.target as HTMLTextAreaElement;
this.model.update((model) => ({ textarea.style.height = 'auto';
...model, textarea.style.height = textarea.scrollHeight + 'px';
sourceFile: event.file,
}));
} else {
this.model.update((model) => ({
...model,
sourceFile: null,
}));
} }
} }
protected async onAiOrManualSubmit(event: AIOrManualSubmitEvent): Promise<void> {
if (event.mode === 'manual') {
this.model.update((model) => ({
...model,
sourceFile: null,
inProgressStep: RecipeUploadStep.ENTER_DATA,
}));
await this.changeDisplayStep(RecipeUploadStep.ENTER_DATA);
this.includeInfer.set(false);
} else {
this.model.update((model) => ({
...model,
sourceFile: this.sourceFile(),
inProgressStep: RecipeUploadStep.INFER,
}));
await this.changeDisplayStep(RecipeUploadStep.INFER);
this.includeInfer.set(true);
this.recipeUploadService.doInference(this.model()).subscribe((updatedModel) => {
this.model.set(updatedModel);
this.changeDisplayStep(RecipeUploadStep.ENTER_DATA);
});
}
}
// private readonly sseClient = inject(SseClient);
// private readonly formBuilder = inject(FormBuilder);
//
// protected readonly sourceRecipeImage = signal<string | null>(null);
// protected readonly inferenceInProgress = signal(false);
//
// protected readonly recipeUploadForm = this.formBuilder.group({
// file: this.formBuilder.control<File | null>(null, [Validators.required]),
// });
//
// protected readonly recipeForm = new FormGroup({
// title: new FormControl('', [Validators.required]),
// recipeText: new FormControl('', Validators.required),
// });
//
// protected onClear() {
// this.recipeUploadForm.reset();
// this.sourceRecipeImage.set(null);
// }
//
// protected onFileChange(event: Event) {
// const fileInput = event.target as HTMLInputElement;
// if (fileInput.files && fileInput.files.length) {
// const file = fileInput.files[0];
// this.recipeUploadForm.controls.file.setValue(file);
// this.recipeUploadForm.controls.file.markAsTouched();
// this.recipeUploadForm.controls.file.updateValueAndValidity();
//
// // set source image
// this.sourceRecipeImage.set(URL.createObjectURL(file));
// }
// }
//
// protected onFileSubmit() {
// const rawValue = this.recipeUploadForm.getRawValue();
//
// this.inferenceInProgress.set(true);
//
// // upload form data
// const formData = new FormData();
// formData.append('recipeImageFile', rawValue.file!, rawValue.file!.name);
// this.sseClient
// .stream(
// `http://localhost:8080/inferences/recipe-extract-stream`,
// {
// keepAlive: false,
// reconnectionDelay: 1000,
// responseType: 'event',
// },
// {
// body: formData,
// },
// 'PUT',
// )
// .subscribe({
// next: (event) => {
// if (event.type === 'error') {
// const errorEvent = event as ErrorEvent;
// console.error(errorEvent.error, errorEvent.message);
// } else {
// const messageEvent = event as MessageEvent;
// const data: { delta: string } = JSON.parse(messageEvent.data);
// this.recipeForm.patchValue({
// recipeText: this.recipeForm.value.recipeText + data.delta,
// });
//
// // must do this so we auto-resize the textarea
// document.getElementById('recipe-text')?.dispatchEvent(new Event('input', { bubbles: true }));
// }
// },
// complete: () => {
// this.inferenceInProgress.set(false);
// },
// });
// }
//
// protected onRecipeSubmit() {
// console.log(this.recipeForm.value);
// }
//
// protected onRecipeTextChange(event: Event) {
// const textarea = event.target as HTMLTextAreaElement;
// textarea.style.height = 'auto';
// textarea.style.height = textarea.scrollHeight + 'px';
// }
protected readonly RecipeUploadStep = RecipeUploadStep;
}

View File

@ -1,3 +0,0 @@
export interface StepClickEvent {
step: number;
}

View File

@ -1,32 +0,0 @@
#recipe-upload-steps {
display: flex;
list-style-type: none;
margin-block: 0;
padding-inline: 0;
}
#recipe-upload-steps li {
display: inline;
}
#recipe-upload-steps li:not(:first-child)::before {
content: ">";
padding-inline: 5px;
}
.step-complete,
.step-in-progress {
cursor: pointer;
}
.step-in-progress {
opacity: 0.9;
}
.step-incomplete {
opacity: 0.6;
}
.step-displayed {
font-weight: 700;
}

View File

@ -1,18 +0,0 @@
<ul id="recipe-upload-steps">
@for (step of steps(); track step.index) {
<li
[class]="{
'step-complete': step.completed,
'step-in-progress': step.inProgress,
'step-incomplete': !step.completed,
'step-displayed': displayStep() === step.index
}"
>
@if (step.completed || step.inProgress) {
<a (click)="onStepClick(step.index)">{{ step.name }}</a>
} @else {
{{ step.name }}
}
</li>
}
</ul>

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RecipeUploadTrail } from './recipe-upload-trail';
describe('RecipeUploadTrail', () => {
let component: RecipeUploadTrail;
let fixture: ComponentFixture<RecipeUploadTrail>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RecipeUploadTrail],
}).compileComponents();
fixture = TestBed.createComponent(RecipeUploadTrail);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,53 +0,0 @@
import { Component, computed, input, output } from '@angular/core';
import { StepClickEvent } from './StepClickEvent';
import { RecipeUploadStep } from '../../../shared/client-models/RecipeUploadStep';
@Component({
selector: 'app-recipe-upload-trail',
imports: [],
templateUrl: './recipe-upload-trail.html',
styleUrl: './recipe-upload-trail.css',
})
export class RecipeUploadTrail {
public readonly displayStep = input.required<RecipeUploadStep>();
public readonly inProgressStep = input.required<RecipeUploadStep>();
public readonly includeInfer = input.required<boolean>();
public readonly stepClick = output<StepClickEvent>();
protected readonly steps = computed(() => {
const base: {
index: RecipeUploadStep;
name: string;
completed: boolean;
inProgress: boolean;
}[] = [
{
index: RecipeUploadStep.START,
name: 'Start',
completed: this.inProgressStep() > RecipeUploadStep.START,
inProgress: this.inProgressStep() === RecipeUploadStep.START,
},
{
index: RecipeUploadStep.ENTER_DATA,
name: 'Enter Recipe',
completed: this.inProgressStep() > RecipeUploadStep.ENTER_DATA,
inProgress: this.inProgressStep() === RecipeUploadStep.ENTER_DATA,
},
];
if (this.includeInfer()) {
base.push({
index: RecipeUploadStep.INFER,
name: 'Infer',
completed: this.inProgressStep() > RecipeUploadStep.INFER,
inProgress: this.inProgressStep() === RecipeUploadStep.INFER,
});
}
base.sort((a, b) => a.index - b.index);
return base;
});
protected onStepClick(stepIndex: number) {
this.stepClick.emit({ step: stepIndex });
}
}

View File

@ -1,3 +0,0 @@
export interface AIOrManualSubmitEvent {
mode: 'manual' | 'ai-assist';
}

View File

@ -1,11 +0,0 @@
<section>
<h2>Start</h2>
<p>Either upload a photo of a recipe and AI will assist you, or enter your recipe manually.</p>
<form id="ai-or-manual-form">
<app-file-upload [files]="sourceFilesArray()" (fileChange)="onFileChange($event)"></app-file-upload>
<div id="ai-or-manual-buttons">
<button matButton="outlined" type="button" (click)="onFormSubmit('manual')">Enter Manually</button>
<button matButton="filled" type="button" [disabled]="!sourceFile()" (click)="onFormSubmit('ai-assist')">Use AI Assist</button>
</div>
</form>
</section>

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AiOrManual } from './ai-or-manual';
describe('AiOrManual', () => {
let component: AiOrManual;
let fixture: ComponentFixture<AiOrManual>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AiOrManual],
}).compileComponents();
fixture = TestBed.createComponent(AiOrManual);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,35 +0,0 @@
import { Component, computed, input, output } from '@angular/core';
import { MatButton } from '@angular/material/button';
import { ReactiveFormsModule } from '@angular/forms';
import { AIOrManualSubmitEvent } from './AIOrManualSubmitEvent';
import { FileUpload } from '../../../../shared/components/file-upload/file-upload';
import { FileUploadEvent } from '../../../../shared/components/file-upload/FileUploadEvent';
@Component({
selector: 'app-ai-or-manual',
imports: [MatButton, ReactiveFormsModule, FileUpload],
templateUrl: './ai-or-manual.html',
styleUrl: './ai-or-manual.css',
})
export class AiOrManual {
public sourceFile = input.required<File | null>();
public sourceFileChange = output<FileUploadEvent>();
public submitStep = output<AIOrManualSubmitEvent>();
protected readonly sourceFilesArray = computed(() => {
const maybeSourceFile = this.sourceFile();
if (maybeSourceFile) {
return [maybeSourceFile];
} else {
return [];
}
});
protected onFileChange(event: FileUploadEvent) {
this.sourceFileChange.emit(event);
}
protected onFormSubmit(mode: 'manual' | 'ai-assist') {
this.submitStep.emit({ mode });
}
}

View File

@ -1,5 +0,0 @@
form {
display: flex;
flex-direction: column;
width: 60ch;
}

View File

@ -1,15 +0,0 @@
<h2>Enter Recipe</h2>
<form [formGroup]="recipeFormGroup">
<mat-form-field>
<mat-label>Title</mat-label>
<input matInput [formControl]="recipeFormGroup.controls.title">
</mat-form-field>
<mat-form-field>
<mat-label>Slug</mat-label>
<input matInput [formControl]="recipeFormGroup.controls.slug">
</mat-form-field>
<mat-form-field>
<mat-label>Recipe Text</mat-label>
<textarea matInput [formControl]="recipeFormGroup.controls.text"></textarea>
</mat-form-field>
</form>

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EnterRecipeData } from './enter-recipe-data';
describe('EnterRecipeData', () => {
let component: EnterRecipeData;
let fixture: ComponentFixture<EnterRecipeData>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EnterRecipeData],
}).compileComponents();
fixture = TestBed.createComponent(EnterRecipeData);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,29 +0,0 @@
import { Component, input, OnInit } from '@angular/core';
import { RecipeUploadModel } from '../../../../shared/client-models/RecipeUploadModel';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatFormField, MatInput, MatLabel } from '@angular/material/input';
@Component({
selector: 'app-enter-recipe-data',
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatInput],
templateUrl: './enter-recipe-data.html',
styleUrl: './enter-recipe-data.css',
})
export class EnterRecipeData implements OnInit {
public readonly model = input.required<RecipeUploadModel>();
public ngOnInit(): void {
const model = this.model();
this.recipeFormGroup.patchValue({
title: model.userTitle ?? model.inferredTitle ?? '',
slug: model.userSlug ?? model.inferredSlug ?? '',
text: model.userText ?? model.inferredText ?? '',
});
}
protected readonly recipeFormGroup = new FormGroup({
title: new FormControl('', Validators.required),
slug: new FormControl('', Validators.required),
text: new FormControl('', Validators.required),
});
}

View File

@ -1,2 +0,0 @@
<p>Using AI to read your recipe...</p>
<app-spinner></app-spinner>

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Infer } from './infer';
describe('Infer', () => {
let component: Infer;
let fixture: ComponentFixture<Infer>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Infer],
}).compileComponents();
fixture = TestBed.createComponent(Infer);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,10 +0,0 @@
import { Component } from '@angular/core';
import { Spinner } from '../../../../shared/components/spinner/spinner';
@Component({
selector: 'app-infer',
imports: [Spinner],
templateUrl: './infer.html',
styleUrl: './infer.css',
})
export class Infer {}

View File

@ -1,5 +0,0 @@
#search-recipes-form {
display: flex;
flex-direction: column;
width: 66%;
}

View File

@ -1,10 +1,8 @@
<h1>Search Recipes</h1> <h1>Search Recipes</h1>
<form id="search-recipes-form" [formGroup]="searchRecipesForm" (ngSubmit)="onPromptSubmit()"> <form id="search-recipes-form" [formGroup]="searchRecipesForm" (ngSubmit)="onPromptSubmit()">
<mat-form-field> <label for="prompt">Search Prompt:</label>
<mat-label>Search Prompt</mat-label> <input id="prompt" formControlName="prompt" type="text" />
<input matInput id="prompt" formControlName="prompt" type="text" /> <button type="submit" [disabled]="!searchRecipesForm.valid">Search</button>
</mat-form-field>
<button matButton="filled" type="submit" [disabled]="!searchRecipesForm.valid">Search</button>
</form> </form>
@if (givenPrompt() !== null) { @if (givenPrompt() !== null) {
@if (resultsQuery.isLoading()) { @if (resultsQuery.isLoading()) {

View File

@ -4,12 +4,10 @@ import { injectQuery } from '@tanstack/angular-query-experimental';
import { RecipeService } from '../../shared/services/RecipeService'; import { RecipeService } from '../../shared/services/RecipeService';
import { RecipeCardGrid } from '../../shared/components/recipe-card-grid/recipe-card-grid'; import { RecipeCardGrid } from '../../shared/components/recipe-card-grid/recipe-card-grid';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MatButton } from '@angular/material/button';
import { MatFormField, MatInput, MatLabel } from '@angular/material/input';
@Component({ @Component({
selector: 'app-recipes-search-page', selector: 'app-recipes-search-page',
imports: [ReactiveFormsModule, RecipeCardGrid, MatButton, MatFormField, MatInput, MatLabel], imports: [ReactiveFormsModule, RecipeCardGrid],
templateUrl: './recipes-search-page.html', templateUrl: './recipes-search-page.html',
styleUrl: './recipes-search-page.css', styleUrl: './recipes-search-page.css',
}) })

View File

@ -1,5 +0,0 @@
export interface RecipeUploadIngredientModel {
amount: string | null;
name: string;
notes: string | null;
}

View File

@ -1,19 +0,0 @@
import { RecipeUploadIngredientModel } from './RecipeUploadIngredientModel';
import { RecipeUploadStep } from './RecipeUploadStep';
export interface RecipeUploadModel {
inProgressStep: RecipeUploadStep;
id?: number | null;
sourceFile?: File | null;
inferredText?: string | null;
inferredIngredients?: RecipeUploadIngredientModel[] | null;
inferredTitle?: string | null;
inferredSlug?: string | null;
userText?: string | null;
userIngredients?: RecipeUploadIngredientModel[] | null;
userTitle?: string | null;
userSlug?: string | null;
}

View File

@ -1,5 +0,0 @@
export enum RecipeUploadStep {
START,
INFER,
ENTER_DATA,
}

View File

@ -1,11 +0,0 @@
export type FileUploadEvent = FileAddEvent | FileRemoveEvent;
export interface FileAddEvent {
_tag: 'file-add-event';
file: File;
}
export interface FileRemoveEvent {
_tag: 'file-remove-event';
fileName: string;
}

View File

@ -1,14 +0,0 @@
.file-input-container {
display: flex;
column-gap: 10px;
padding-block: 10px;
}
.file-name {
display: flex;
column-gap: 5px;
}
fa-icon {
cursor: pointer;
}

View File

@ -1,11 +0,0 @@
<input #fileInput type="file" (change)="onFileChange($event)" style="display: none" />
<div class="file-input-container">
<fa-icon [icon]="faFileUpload" size="3x" (click)="onFileUploadIconClick(fileInput)"></fa-icon>
@if (fileNames().length) {
@for (fileName of fileNames(); track $index) {
<p class="file-name"><fa-icon [icon]="faCancel" (click)="onClear(fileName)"></fa-icon>{{ fileName }}</p>
}
} @else {
<p>Click the icon to choose a file.</p>
}
</div>

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FileUpload } from './file-upload';
describe('FileUpload', () => {
let component: FileUpload;
let fixture: ComponentFixture<FileUpload>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FileUpload],
}).compileComponents();
fixture = TestBed.createComponent(FileUpload);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,42 +0,0 @@
import { Component, computed, input, output } from '@angular/core';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { faCancel, faFileUpload } from '@fortawesome/free-solid-svg-icons';
import { FileUploadEvent } from './FileUploadEvent';
@Component({
selector: 'app-file-upload',
imports: [FaIconComponent],
templateUrl: './file-upload.html',
styleUrl: './file-upload.css',
})
export class FileUpload {
public readonly files = input<File[]>([]);
public readonly fileChange = output<FileUploadEvent>();
protected fileNames = computed(() => this.files().map((file) => file.name));
protected onFileUploadIconClick(target: HTMLInputElement) {
target.click();
}
protected onClear(fileName: string): void {
this.fileChange.emit({
_tag: 'file-remove-event',
fileName,
});
}
protected onFileChange(event: Event) {
const fileInput = event.target as HTMLInputElement;
if (fileInput.files && fileInput.files.length) {
this.fileChange.emit({
_tag: 'file-add-event',
file: fileInput.files[0],
});
}
fileInput.value = '';
}
protected readonly faFileUpload = faFileUpload;
protected readonly faCancel = faCancel;
}

View File

@ -3,11 +3,11 @@
@if (isLoggedIn()) { @if (isLoggedIn()) {
<div id="header-login"> <div id="header-login">
<p>Welcome {{ username() }}!</p> <p>Welcome {{ username() }}!</p>
<button matButton="elevated" (click)="logoutClick()">Logout</button> <button (click)="logoutClick()">Logout</button>
</div> </div>
} @else { } @else {
<div id="header-login"> <div id="header-login">
<button matButton="elevated" (click)="loginClick()">Login</button> <button (click)="loginClick()">Login</button>
</div> </div>
} }
</header> </header>

View File

@ -1,10 +1,9 @@
import { Component, computed, inject } from '@angular/core'; import { Component, computed, inject } from '@angular/core';
import { AuthService } from '../../services/AuthService'; import { AuthService } from '../../services/AuthService';
import { MatButton } from '@angular/material/button';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
imports: [MatButton], imports: [],
templateUrl: './header.html', templateUrl: './header.html',
styleUrl: './header.css', styleUrl: './header.css',
}) })

View File

@ -3,6 +3,6 @@
<ul> <ul>
<li><a [routerLink]="'/'">Browse Recipes</a></li> <li><a [routerLink]="'/'">Browse Recipes</a></li>
<li><a [routerLink]="'/recipes-search'">Search Recipes</a></li> <li><a [routerLink]="'/recipes-search'">Search Recipes</a></li>
<li><a [routerLink]="'/recipe-upload'">Upload Recipe</a></li> <li><a [routerLink]="'/recipes-upload'">Upload Recipe</a></li>
</ul> </ul>
</nav> </nav>

View File

@ -31,7 +31,9 @@
<li class="comment"> <li class="comment">
<div class="comment-username-time"> <div class="comment-username-time">
<span class="comment-username">{{ recipeComment.owner.username }}</span> <span class="comment-username">{{ recipeComment.owner.username }}</span>
<span class="comment-time">{{ recipeComment.created | dateTimeFormat }}</span> <span class="comment-time">{{
recipeComment.created | dateTimeFormat
}}</span>
</div> </div>
<div class="comment-text" [innerHTML]="recipeComment.text"></div> <div class="comment-text" [innerHTML]="recipeComment.text"></div>
</li> </li>

View File

@ -7,5 +7,5 @@ import { Component, input } from '@angular/core';
styleUrl: './spinner.css', styleUrl: './spinner.css',
}) })
export class Spinner { export class Spinner {
public readonly enabled = input(true); public readonly enabled = input.required<boolean>();
} }

View File

@ -36,7 +36,9 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
} else if (error.status === 401 && error.url?.endsWith('auth/refresh')) { } else if (error.status === 401 && error.url?.endsWith('auth/refresh')) {
// our refresh token is expired // our refresh token is expired
// redirect to login page // redirect to login page
return from(router.navigate(['/'])).pipe(switchMap(() => throwError(() => error))); return from(router.navigate(['/'])).pipe(
switchMap(() => throwError(() => error)),
);
} else { } else {
return throwError(() => error); return throwError(() => error);
} }

View File

@ -6,7 +6,11 @@ import { QueryParams } from '../models/Query.model';
providedIn: 'root', providedIn: 'root',
}) })
export class EndpointService { export class EndpointService {
public getUrl(endpoint: keyof typeof Endpoints, pathParams?: string[], queryParams?: QueryParams): string { public getUrl(
endpoint: keyof typeof Endpoints,
pathParams?: string[],
queryParams?: QueryParams,
): string {
const urlSearchParams = new URLSearchParams(); const urlSearchParams = new URLSearchParams();
if (queryParams?.page !== undefined) { if (queryParams?.page !== undefined) {
urlSearchParams.set('page', queryParams.page.toString()); urlSearchParams.set('page', queryParams.page.toString());

View File

@ -19,12 +19,16 @@ export class RecipeService {
public getRecipes(): Promise<Recipe[]> { public getRecipes(): Promise<Recipe[]> {
return firstValueFrom( return firstValueFrom(
this.http.get<RecipeInfoViews>('http://localhost:8080/recipes').pipe(map((res) => res.content)), this.http
.get<RecipeInfoViews>('http://localhost:8080/recipes')
.pipe(map((res) => res.content)),
); );
} }
public getRecipeView(username: string, slug: string): Promise<RecipeView> { public getRecipeView(username: string, slug: string): Promise<RecipeView> {
return firstValueFrom(this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`)); return firstValueFrom(
this.http.get<RecipeView>(`http://localhost:8080/recipes/${username}/${slug}`),
);
} }
private getRecipeUrl(recipeView: RecipeView): string { private getRecipeUrl(recipeView: RecipeView): string {
@ -46,7 +50,11 @@ export class RecipeService {
} }
} }
public getComments(username: string, slug: string, queryParams?: QueryParams): Promise<RecipeComments> { public getComments(
username: string,
slug: string,
queryParams?: QueryParams,
): Promise<RecipeComments> {
return firstValueFrom( return firstValueFrom(
this.http.get<RecipeComments>( this.http.get<RecipeComments>(
this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams), this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams),
@ -54,11 +62,18 @@ export class RecipeService {
); );
} }
public async addComment(username: string, slug: string, commentText: string): Promise<RecipeComment> { public async addComment(
username: string,
slug: string,
commentText: string,
): Promise<RecipeComment> {
const comment = await firstValueFrom( const comment = await firstValueFrom(
this.http.post<RecipeComment>(`http://localhost:8080/recipes/${username}/${slug}/comments`, { this.http.post<RecipeComment>(
`http://localhost:8080/recipes/${username}/${slug}/comments`,
{
text: commentText, text: commentText,
}), },
),
); );
await this.queryClient.invalidateQueries({ await this.queryClient.invalidateQueries({
queryKey: ['recipeComments', username, slug], queryKey: ['recipeComments', username, slug],

View File

@ -1,31 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { delay, Observable, of } from 'rxjs';
import { RecipeUploadModel } from '../client-models/RecipeUploadModel';
import { RecipeUploadStep } from '../client-models/RecipeUploadStep';
@Injectable({
providedIn: 'root',
})
export class RecipeUploadService {
private readonly http = inject(HttpClient);
public getRecipeUploadModel(draftId: number): Observable<RecipeUploadModel> {
return of({
inProgressStep: RecipeUploadStep.ENTER_DATA,
id: 42
});
}
public doInference(model: RecipeUploadModel): Observable<RecipeUploadModel> {
return of({
inProgressStep: RecipeUploadStep.ENTER_DATA,
id: 16,
inferredTitle: 'Some recipe',
inferredSlug: 'some-recipe',
inferredText: 'Some text.',
inferredIngredients: []
}).pipe(delay(5_000));
}
}

View File

@ -1,23 +0,0 @@
export const tryInt = (s: string): number | null => {
try {
return parseInt(s);
} catch (e) {
console.error(e);
return null;
}
};
export const tryMaybeInt = (maybeString: string | null): number | null => {
if (maybeString) {
try {
return parseInt(maybeString);
} catch (e) {
console.error(e);
}
}
return null;
};
export const hasValue = <T>(value?: T | null): value is T => {
return value !== undefined && value !== null;
};

View File

@ -14,7 +14,6 @@
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@ -1,45 +0,0 @@
// Include theming for Angular Material with `mat.theme()`.
// This Sass mixin will define CSS variables that are used for styling Angular Material
// components according to the Material 3 design spec.
// Learn more about theming and how to use it for your application's
// custom components at https://material.angular.dev/guide/theming
@use "@angular/material" as mat;
@use "theme-colors" as themeColors;
html {
height: 100%;
@include mat.theme(
(
color: (
primary: themeColors.$primary-palette,
tertiary: themeColors.$tertiary-palette,
),
typography: Inter,
density: 0,
)
);
}
@include mat.form-field-overrides(
(
filled-container-color: var(--mat-sys-surface-variant),
)
);
body {
// Default the application to a light color theme. This can be changed to
// `dark` to enable the dark color theme, or to `light dark` to defer to the
// user's system settings.
color-scheme: light;
// Set a default background, font and text colors for the application using
// Angular Material's system-level CSS variables. Learn more about these
// variables at https://material.angular.dev/guide/system-variables
background-color: var(--mat-sys-surface);
color: var(--mat-sys-on-surface);
font: var(--mat-sys-body-medium);
// Reset the user agent margin.
margin: 0;
height: 100%;
}

View File

@ -1,7 +1,7 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); @import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
:root { :root {
--primary-font-family: "Inter"; --primary-font-family: 'Inter';
--primary-white: #ffffff; --primary-white: #ffffff;
--primary-red: #91351d; --primary-red: #91351d;
--primary-yellow: #ffb61d; --primary-yellow: #ffb61d;
@ -34,17 +34,17 @@ a:hover {
color: hsl(from var(--primary-red) h s l / 0.9); color: hsl(from var(--primary-red) h s l / 0.9);
} }
/*button {*/ button {
/* background-color: var(--off-white-2);*/ background-color: var(--off-white-2);
/* border: none;*/ border: none;
/* border-radius: 5px;*/ border-radius: 5px;
/* padding: 10px;*/ padding: 10px;
/*}*/ }
/*button:hover:not(:disabled) {*/ button:hover:not(:disabled) {
/* background-color: var(--off-white-1);*/ background-color: var(--off-white-1);
/* cursor: pointer;*/ cursor: pointer;
/*}*/ }
fa-icon { fa-icon {
color: var(--primary-red); color: var(--primary-red);

View File

@ -1,137 +0,0 @@
// This file was generated by running 'ng generate @angular/material:theme-color'.
// Proceed with caution if making changes to this file.
@use "sass:map";
@use "@angular/material" as mat;
// Note: Color palettes are generated from primary: #91351d, secondary: #ffb61d, neutral: #aaa, neutral variant: #252525
$_palettes: (
primary: (
0: #000000,
10: #3c0800,
20: #611200,
25: #711e07,
30: #802912,
35: #90341c,
40: #a04027,
50: #c0573c,
60: #e07053,
70: #ff8b6d,
80: #ffb4a2,
90: #ffdbd2,
95: #ffede9,
98: #fff8f6,
99: #fffbff,
100: #ffffff,
),
secondary: (
0: #000000,
10: #281900,
20: #432c00,
25: #513600,
30: #5f4100,
35: #6f4c00,
40: #7e5700,
50: #9e6e00,
60: #bf8600,
70: #e29f00,
80: #ffba36,
90: #ffdeac,
95: #ffeed9,
98: #fff8f3,
99: #fffbff,
100: #ffffff,
),
tertiary: (
0: #000000,
10: #241a00,
20: #3d2f00,
25: #4a3900,
30: #584400,
35: #665000,
40: #735b0d,
50: #8e7426,
60: #aa8e3e,
70: #c6a855,
80: #e3c36d,
90: #ffe08e,
95: #ffefce,
98: #fff8f1,
99: #fffbff,
100: #ffffff,
),
neutral: (
0: #000000,
10: #1a1c1c,
20: #2f3131,
25: #3a3c3c,
30: #464747,
35: #525253,
40: #5e5e5f,
50: #767777,
60: #909191,
70: #ababab,
80: #c7c6c6,
90: #e3e2e2,
95: #f1f0f0,
98: #faf9f9,
99: #fdfcfc,
100: #ffffff,
4: #0d0e0f,
6: #121414,
12: #1e2020,
17: #292a2a,
22: #343535,
24: #38393a,
87: #dadada,
92: #e9e8e8,
94: #eeeeed,
96: #f4f3f3,
),
neutral-variant: (
0: #000000,
10: #1b1c1c,
20: #303030,
25: #3c3b3b,
30: #474746,
35: #535252,
40: #5f5e5e,
50: #787776,
60: #929090,
70: #adabaa,
80: #c8c6c5,
90: #e4e2e1,
95: #f3f0ef,
98: #fcf9f8,
99: #fffbfb,
100: #ffffff,
),
error: (
0: #000000,
10: #410002,
20: #690005,
25: #7e0007,
30: #93000a,
35: #a80710,
40: #ba1a1a,
50: #de3730,
60: #ff5449,
70: #ff897d,
80: #ffb4ab,
90: #ffdad6,
95: #ffedea,
98: #fff8f7,
99: #fffbff,
100: #ffffff,
),
);
$_rest: (
secondary: map.get($_palettes, secondary),
neutral: map.get($_palettes, neutral),
neutral-variant: map.get($_palettes, neutral-variant),
error: map.get($_palettes, error),
);
$primary-palette: map.merge(map.get($_palettes, primary), $_rest);
$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest);