Add angular material.

This commit is contained in:
Jesse Brault 2026-01-11 19:44:59 -06:00
parent b66f290d84
commit 2bb206b8bf
23 changed files with 300 additions and 102 deletions

View File

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

View File

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

38
package-lock.json generated
View File

@ -8,10 +8,12 @@
"name": "meals-made-easy-app",
"version": "0.0.0",
"dependencies": {
"@angular/cdk": "^21.0.6",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/forms": "^21.0.0",
"@angular/material": "^21.0.6",
"@angular/platform-browser": "^21.0.0",
"@angular/router": "^21.0.0",
"@fortawesome/angular-fontawesome": "^4.0.0",
@ -424,6 +426,22 @@
}
}
},
"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": {
"version": "21.0.2",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.0.2.tgz",
@ -555,6 +573,7 @@
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.3.tgz",
"integrity": "sha512-W60auwyDmsglIlHAbP/eol0LyzQ6FCz8LHghNx2B4RjIpuIMyjBLBZfC0JHU0gyiKB/JfX8W4FdphvyT7I4sIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@ -569,6 +588,23 @@
"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": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.3.tgz",
@ -7157,7 +7193,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
@ -7211,7 +7246,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,14 +3,12 @@
<section>
<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()">
<input id="file" type="file" (change)="onFileChange($event)" />
<div class="action-buttons-container">
<button type="button" (click)="onClear()">Clear</button>
<button type="submit" [disabled]="!recipeUploadForm.valid || inferenceInProgress()">
<button matButton="outlined" type="button" (click)="onClear()">Clear</button>
<button matButton="filled" type="submit" [disabled]="!recipeUploadForm.valid || inferenceInProgress()">
AI Auto-Complete
</button>
<app-spinner [enabled]="inferenceInProgress()"></app-spinner>
@ -22,21 +20,20 @@
<h2>Recipe Form</h2>
<div id="recipe-form-and-source">
<form [formGroup]="recipeForm" (ngSubmit)="onRecipeSubmit()">
<input
id="recipe-title"
type="text"
formControlName="title"
placeholder="Recipe Title"
/>
<textarea
id="recipe-text"
formControlName="recipeText"
(input)="onRecipeTextChange($event)"
placeholder="Recipe text"
></textarea>
<button type="submit" [disabled]="!recipeForm.valid || inferenceInProgress()">
Add Recipe
</button>
<mat-form-field>
<mat-label>Recipe Title</mat-label>
<input matInput id="recipe-title" type="text" formControlName="title" />
</mat-form-field>
<mat-form-field>
<mat-label>Recipe Text</mat-label>
<textarea
matInput
id="recipe-text"
formControlName="recipeText"
(input)="onRecipeTextChange($event)"
></textarea>
</mat-form-field>
<button matButton="filled" type="submit" [disabled]="!recipeForm.valid || inferenceInProgress()">Add Recipe</button>
</form>
<div>
@if (sourceRecipeImage()) {

View File

@ -1,17 +1,13 @@
import { Component, inject, signal } from '@angular/core';
import {
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { SseClient } from 'ngx-sse-client';
import { Spinner } from '../../shared/components/spinner/spinner';
import { MatButton } from '@angular/material/button';
import { MatFormField, MatInput, MatLabel } from '@angular/material/input';
@Component({
selector: 'app-recipe-upload-page',
imports: [ReactiveFormsModule, Spinner],
imports: [ReactiveFormsModule, Spinner, MatButton, MatFormField, MatInput, MatLabel],
templateUrl: './recipe-upload-page.html',
styleUrl: './recipe-upload-page.css',
})
@ -83,9 +79,7 @@ export class RecipeUploadPage {
});
// must do this so we auto-resize the textarea
document
.getElementById('recipe-text')
?.dispatchEvent(new Event('input', { bubbles: true }));
document.getElementById('recipe-text')?.dispatchEvent(new Event('input', { bubbles: true }));
}
},
complete: () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,9 +31,7 @@
<li class="comment">
<div class="comment-username-time">
<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 class="comment-text" [innerHTML]="recipeComment.text"></div>
</li>

View File

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

View File

@ -6,11 +6,7 @@ import { QueryParams } from '../models/Query.model';
providedIn: 'root',
})
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();
if (queryParams?.page !== undefined) {
urlSearchParams.set('page', queryParams.page.toString());

View File

@ -19,16 +19,12 @@ export class RecipeService {
public getRecipes(): Promise<Recipe[]> {
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> {
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 {
@ -50,11 +46,7 @@ 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(
this.http.get<RecipeComments>(
this.endpointService.getUrl('recipes', [username, slug, 'comments'], queryParams),
@ -62,18 +54,11 @@ 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(
this.http.post<RecipeComment>(
`http://localhost:8080/recipes/${username}/${slug}/comments`,
{
text: commentText,
},
),
this.http.post<RecipeComment>(`http://localhost:8080/recipes/${username}/${slug}/comments`, {
text: commentText,
}),
);
await this.queryClient.invalidateQueries({
queryKey: ['recipeComments', username, slug],

View File

@ -14,6 +14,7 @@
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"
/>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
</head>
<body>
<app-root></app-root>

43
src/material-theme.scss Normal file
View File

@ -0,0 +1,43 @@
// 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 {
--primary-font-family: 'Inter';
--primary-font-family: "Inter";
--primary-white: #ffffff;
--primary-red: #91351d;
--primary-yellow: #ffb61d;
@ -34,17 +34,17 @@ a:hover {
color: hsl(from var(--primary-red) h s l / 0.9);
}
button {
background-color: var(--off-white-2);
border: none;
border-radius: 5px;
padding: 10px;
}
/*button {*/
/* background-color: var(--off-white-2);*/
/* border: none;*/
/* border-radius: 5px;*/
/* padding: 10px;*/
/*}*/
button:hover:not(:disabled) {
background-color: var(--off-white-1);
cursor: pointer;
}
/*button:hover:not(:disabled) {*/
/* background-color: var(--off-white-1);*/
/* cursor: pointer;*/
/*}*/
fa-icon {
color: var(--primary-red);

137
src/theme-colors.scss Normal file
View File

@ -0,0 +1,137 @@
// 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);