2
									
								
								.github/workflows/css-linting.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -20,4 +20,4 @@ jobs: | ||||
|       - name: Install dependencies | ||||
|         run: sudo npm install | ||||
|       - name: Run tests | ||||
|         run: sudo npm run lintCSS | ||||
|         run: sudo npm run lint-css | ||||
							
								
								
									
										2
									
								
								.github/workflows/html-linting.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -20,4 +20,4 @@ jobs: | ||||
|       - name: Install dependencies | ||||
|         run: sudo npm install | ||||
|       - name: Run tests | ||||
|         run: sudo npm run lintHTML | ||||
|         run: sudo npm run lint-html | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/js-linting.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -22,4 +22,4 @@ jobs: | ||||
|       - name: Install dependencies | ||||
|         run: sudo npm install | ||||
|       - name: Run tests | ||||
|         run: sudo npm run lint | ||||
|         run: sudo npm run lint-js | ||||
							
								
								
									
										2
									
								
								.github/workflows/js-unittest.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -23,5 +23,7 @@ jobs: | ||||
|         uses: actions/checkout@v3 | ||||
|       - name: Install dependencies | ||||
|         run: sudo npm install | ||||
|       - name: Start local http server | ||||
|         run: sudo npm run http-server &  | ||||
|       - name: Run tests | ||||
|         run: sudo npm test | ||||
| @@ -1,3 +1,4 @@ | ||||
| { | ||||
|   "attr-value-not-empty": false | ||||
|   "attr-value-not-empty": false, | ||||
|   "space-tab-mixed-disabled": "tab" | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,7 @@ | ||||
| { | ||||
| 	"extends": "stylelint-config-standard" | ||||
| 	"extends": "stylelint-config-standard", | ||||
| 	"ignore": ["inside-parens", "param", "value"], | ||||
| 	"rules":{ | ||||
| 		"indentation": "tab" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|     "liveServer.settings.port": 5501 | ||||
| } | ||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -4,16 +4,20 @@ | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "test": "mocha --recursive --require mock-local-storage './{,!(node_modules)/**}/*.test.js'", | ||||
|     "lint": "eslint '**/*.js'", | ||||
|     "fix-style": "eslint --fix **/*.js", | ||||
|     "lintHTML": "htmlhint '**/*.html'", | ||||
|     "lintCSS": "stylelint '**/*.css'" | ||||
|     "lint-js": "eslint **/*.js", | ||||
|     "fix-js": "eslint --fix **/*.js", | ||||
|     "lint-html": "htmlhint **/*.html", | ||||
|     "lint-css": "stylelint **/*.css", | ||||
|     "fix-css": "stylelint --fix **/*.css", | ||||
|     "http-server": "http-server source" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "eslint": "^8.27.0", | ||||
|     "htmlhint": "1.1.4", | ||||
|     "http-server": "", | ||||
|     "mocha": "10", | ||||
|     "mock-local-storage": "^1.1.23", | ||||
|     "puppeteer": "^18.2.1", | ||||
|     "stylelint": "14.14.1", | ||||
|     "stylelint-config-standard": "^29.0.0" | ||||
|   } | ||||
|   | ||||
| @@ -1,77 +1,93 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|  | ||||
| <head> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|   <title>Food Journal</title> | ||||
| 	<head> | ||||
| 		<meta charset="UTF-8" /> | ||||
| 		<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
| 		<title>Food Journal</title> | ||||
|  | ||||
|   <!-- Recipe Card Custom Element --> | ||||
|   <script src="assets/scripts/ReviewCard.js" type="module"></script> | ||||
| 		<!--Add Favicon--> | ||||
| 		<link rel="icon" type="image/x-icon" href="./assets/images/favicon.ico"> | ||||
|  | ||||
| 		<!-- Recipe Card Custom Element --> | ||||
| 		<script src="assets/scripts/ReviewCard.js" type="module"></script> | ||||
|  | ||||
|  | ||||
|   <!-- Main Stylesheets & Scripts --> | ||||
|   <!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> | ||||
|   <!-- <link rel="stylesheet" href="/static/reset.css" /> --> | ||||
|   <link rel="stylesheet" href="./static/CreatePage.css" /> | ||||
|   <script src="assets/scripts/main.js" type="module"></script> | ||||
| 		<!-- Main Stylesheets & Scripts --> | ||||
| 		<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> | ||||
| 		<!-- <link rel="stylesheet" href="/static/reset.css" /> --> | ||||
| 		<link rel="stylesheet" href="./static/CreatePage.css" /> | ||||
| 		<link rel="stylesheet" href="./static/Form.css" /> | ||||
| 		<script src="./assets/scripts/CreatePage.js" type="module"></script> | ||||
| 	</head> | ||||
|  | ||||
| </head> | ||||
| 	<header> | ||||
| 		<div class="top-bar"> | ||||
| 			<img src ="./assets/images/Logo.png" alt="logo" /> | ||||
| 			<h1> Food Journal </h1> | ||||
| 			<img src ="./assets/images/Logo.png" alt="logo" /> | ||||
| 		</div> | ||||
| 	</header> | ||||
| 	 | ||||
| <body> | ||||
|   <form id="new-food-entry"> | ||||
|     <fieldset> | ||||
|       <legend>Pic:</legend> | ||||
|       <label for="mealImage"> | ||||
|         Source: | ||||
|         <input type="text" id="mealImg" name="mealImg"> | ||||
|       </label> | ||||
|       <label for="image-alt"> | ||||
|         Alt Text: | ||||
|         <input type="text" id="imgAlt" name="imgAlt"> | ||||
|       </label> | ||||
|     </fieldset> | ||||
|     <fieldset> | ||||
| 	<body> | ||||
| 		<div class="journal-form"> | ||||
| 			<h1>New Entry</h1> | ||||
|  | ||||
|       <legend> Meal: </legend> | ||||
|       <label for="Meal: ">Meal: | ||||
|         <input type="text" id="mealName" name="mealName" required> | ||||
|       </label> | ||||
|       <label for="comments">Comments: | ||||
|         <br> | ||||
|         <textarea name="comments" id="comments"></textarea> | ||||
|       </label> | ||||
|     </fieldset> | ||||
| 			<form id="new-food-entry"> | ||||
| 				 | ||||
|     <fieldset class="rating"> | ||||
|       <legend> Rating: </legend> | ||||
|       <input type="radio" id="s5" name="rating" value="5"/> <label for="s5"> 5 stars </label> | ||||
|       <input type="radio" id="s4" name="rating" value="4"/> <label for="s4"> 4 stars </label> | ||||
|       <input type="radio" id="s3" name="rating" value="3"/> <label for="s3"> 3 stars </label> | ||||
|       <input type="radio" id="s2" name="rating" value="2"/> <label for="s2"> 2 stars </label> | ||||
|       <input type="radio" id="s1" name="rating" value="1"/> <label for="s1"> 1 star </label> | ||||
|     </fieldset> | ||||
| 				<fieldset> | ||||
| 					<legend>PICTURE:</legend> | ||||
| 					<select id="select" name="select"> | ||||
| 						<option value="file">File Upload</option> | ||||
| 						<option value="url">From an URL</option> | ||||
| 					</select> | ||||
| 					<label for="mealImage" id="source"> | ||||
| 						<input type="file" accept="image/*" id="mealImg" name="mealImg"> | ||||
| 					</label> | ||||
| 				</fieldset> | ||||
| 				 | ||||
|     <fieldset> | ||||
|       <legend>Other Info:</legend> | ||||
|       <label for="restaurant"> | ||||
|         Restaurant: | ||||
|         <input type="text" id="restaurant" name="restaurant" required> | ||||
|       </label> | ||||
|       <label for="tag-form"> | ||||
|         Tags: | ||||
|         <input type="text" id="tag-form" name="tag-form"> | ||||
|         <div class='tag-container' id="tag-container-form"> | ||||
| 				<fieldset> | ||||
| 					<legend>MEAL NAME:</legend> | ||||
| 					<label for="Name: "> <input type="text" id="mealName" name="mealName" required> </label> | ||||
| 				</fieldset> | ||||
|  | ||||
|         </div> | ||||
|         <button type="button" id="tagAdd">Add Tag</button> | ||||
|       </label> | ||||
| 				<fieldset> | ||||
| 					<legend>RESTAURANT NAME:</legend> | ||||
| 					<label for="Name:"> <input type="text" id="restaurant" name="restaurant" required> </label> | ||||
| 				</fieldset> | ||||
| 			 | ||||
|     </fieldset> | ||||
|     <button type="submit" value="Submit">Add Review</button> | ||||
|     <button type="button" class="danger">Clear Review Journal</button> | ||||
|   </form> | ||||
| </body> | ||||
| 				<fieldset> | ||||
| 					<legend>RATING:</legend> | ||||
| 					<div style="display: flex; justify-content: flex-start; align-items: center;"> | ||||
| 						<div class="rating"> | ||||
| 							<input type="radio" id="s5" name="rating" value="5"/> <label for="s5" id="s5-select"> 5 stars </label> | ||||
| 							<input type="radio" id="s4" name="rating" value="4"/> <label for="s4" id="s4-select"> 4 stars </label> | ||||
| 							<input type="radio" id="s3" name="rating" value="3"/> <label for="s3" id="s3-select"> 3 stars </label> | ||||
| 							<input type="radio" id="s2" name="rating" value="2"/> <label for="s2" id="s2-select"> 2 stars </label> | ||||
| 							<input type="radio" id="s1" name="rating" value="1"/> <label for="s1" id="s1-select"> 1 star </label> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</fieldset> | ||||
|  | ||||
| 				<fieldset> | ||||
| 					<legend>COMMENTS:</legend> | ||||
| 					<textarea name="comments" id="comments" rows="5" style="resize: none; width: 100%;"></textarea> | ||||
| 				</fieldset> | ||||
|  | ||||
| 				<fieldset> | ||||
| 					<legend>TAGS: (ex. cuisine, distance, cost, etc)</legend> | ||||
| 					<input type="text" id="tag-form" name="tag-form"> | ||||
|  | ||||
| 					<div class='tag-container' id="tag-container-form"> | ||||
| 					</div> | ||||
| 					<button type="button" id="tag-add-btn"> + </button> | ||||
| 				</fieldset> | ||||
|  | ||||
| 			<button type="submit" id="save-btn" value="Submit">Save</button> | ||||
| 			<input type="button" value="Cancel" id="home-btn" onclick="window.location.assign('./index.html')"> | ||||
| 			</form> | ||||
| 		</div>  | ||||
| 	</body> | ||||
|  | ||||
| </html> | ||||
| @@ -1,77 +1,122 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|   <title>Food Journal</title> | ||||
| 	<head> | ||||
| 		<meta charset="UTF-8" /> | ||||
| 		<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
| 		<title>Food Journal</title> | ||||
|  | ||||
|   <!-- Recipe Card Custom Element --> | ||||
|   <script src="assets/scripts/ReviewCard.js" type="module"></script> | ||||
| 		<!--Add Favicon--> | ||||
| 		<link rel="icon" type="image/x-icon" href="./assets/images/icons/favicon.ico"> | ||||
|  | ||||
| 		<!-- Recipe Card Custom Element --> | ||||
| 		<script src="assets/scripts/ReviewCard.js" type="module"></script> | ||||
|  | ||||
|   <!-- Main Stylesheets & Scripts -->  | ||||
|   <!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> | ||||
|   <!-- <link rel="stylesheet" href="/static/reset.css" /> --> | ||||
|   <link rel="stylesheet" href="./static/ReviewCard.css" /> | ||||
|   <script src="assets/scripts/ReviewDetails.js" type="module"></script> | ||||
| 		<!-- Main Stylesheets & Scripts -->  | ||||
| 		<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> | ||||
| 		<!-- <link rel="stylesheet" href="/static/reset.css" /> --> | ||||
| 		<link rel="stylesheet" href="./static/ReviewDetails.css" /> | ||||
| 		<link rel="stylesheet" href="./static/Form.css" /> | ||||
| 		<script src="assets/scripts/ReviewDetails.js" type="module"></script> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<header> | ||||
| 			<div class="top-bar"> | ||||
| 				<img src ="./assets/images/Logo.png" alt="logo" /> | ||||
| 				<h1> Food Journal </h1> | ||||
| 				<img src ="./assets/images/Logo.png" alt="logo" /> | ||||
| 			</div> | ||||
| 		</header> | ||||
|  | ||||
| </head> | ||||
| <body> | ||||
|   <main> | ||||
|     <button type="button" id="update">Update</button> | ||||
|     <button type="button" id="delete" class="danger">Delete</button> | ||||
|   </main> | ||||
|     <!----> <form id="update-food-entry" class="hidden"> | ||||
|       <fieldset> | ||||
|         <legend>Pic:</legend> | ||||
|         <label for="mealImage"> | ||||
|           Source: | ||||
|           <input type="text" id="mealImg" name="mealImg"> | ||||
|         </label> | ||||
|         <label for="image-alt"> | ||||
|           Alt Text: | ||||
|           <input type="text" id="imgAlt" name="imgAlt"> | ||||
|         </label> | ||||
|       </fieldset> | ||||
|       <fieldset> | ||||
| 		<main> | ||||
| 			<div class="journal-form" id="review-details"> | ||||
| 				<form> | ||||
| 					<fieldset class = "meal-name"> | ||||
| 						<h1 id="d-mealName" style="font-family: Century Gothic;"></h1> | ||||
| 						<h1 id="d-restaurant" style="font-family: Century Gothic; font-size: 30px;"></h1> | ||||
| 					</fieldset>  | ||||
|  | ||||
|         <legend> Meal: </legend> | ||||
|         <label for="Meal: ">Meal: | ||||
|           <input type="text" id="mealName" name="mealName" required> | ||||
|         </label> | ||||
|         <label for="comments">Comments:  | ||||
|           <br> | ||||
|           <textarea name="comments" id="comments"></textarea> | ||||
|         </label> | ||||
|       </fieldset> | ||||
| 					<fieldset class = "meal-pics"> | ||||
| 						<!-- image source -->  | ||||
| 						<img width=40% height=40% id="d-mealImg" style="margin-left: auto; margin-right: auto; display: block;"/> | ||||
| 					</fieldset> | ||||
| 		 | ||||
|       <fieldset class="rating"> | ||||
|         <legend> Rating: </legend> | ||||
|         <input type="radio" id="s5" name="rating" value="5"/> <label for="s5"> 5 stars </label> | ||||
|         <input type="radio" id="s4" name="rating" value="4"/> <label for="s4"> 4 stars </label> | ||||
|         <input type="radio" id="s3" name="rating" value="3"/> <label for="s3"> 3 stars </label> | ||||
|         <input type="radio" id="s2" name="rating" value="2"/> <label for="s2"> 2 stars </label> | ||||
|         <input type="radio" id="s1" name="rating" value="1"/> <label for="s1"> 1 star </label> | ||||
|       </fieldset> | ||||
| 					<fieldset class = "stars-and-comments" style="text-align: center;"> | ||||
| 						<img width=30% height=30% id="d-rating" style="margin-left: auto; margin-right: auto; display: block;"/> | ||||
| 						<p id = "d-comments"></p> | ||||
| 					</fieldset> | ||||
|  | ||||
|       <fieldset> | ||||
|         <legend>Other Info:</legend> | ||||
|         <label for="restaurant"> | ||||
|           Restaurant: | ||||
|           <input type="text" id="restaurant" name="restaurant" required> | ||||
|         </label> | ||||
|         <label for="tag-form"> | ||||
|           Tags: | ||||
|           <input type="text" id="tag-form" name="tag-form"> | ||||
|           <div class='tag-container' id="tag-container-form"> | ||||
| 					<fieldset class = "meal-tags"> | ||||
| 						<div class = "tag-container" id="d-tags" style="justify-content: center;"></div> | ||||
| 					</fieldset> | ||||
|  | ||||
|           </div> | ||||
|           <button type="button" id="tagAdd">Add Tag</button> | ||||
|         </label> | ||||
| 				</form> | ||||
| 			</div> | ||||
| 		 | ||||
|       </fieldset> | ||||
|       <button type="submit" value="Submit">Add Review</button> | ||||
|       </form> | ||||
| </body> | ||||
| 			<!---Navigation Buttons--> | ||||
| 			<div style="display: flex; justify-content: center;"> | ||||
| 				<img src="./assets/images/home_button_for_interface.png" style="margin: 20px 10px 20px 10px;" id="home-btn" onclick="window.location.assign('./index.html')" height="50" width="50"/> | ||||
| 				<img src ="./assets/images/edit_button_for_interface.png" style="margin: 20px 10px 20px 10px;" id="update-btn" height="50" width="50"/> | ||||
| 				<img src ="./assets/images/delete_icon_for_interface.png" style="margin: 20px 10px 20px 10px;" id="delete-btn" class="danger" height="50" width="50"/> | ||||
| 			</div> | ||||
| 		</main> | ||||
|  | ||||
| 		<div class="journal-form hidden" id="update-form"> | ||||
| 			<h1>Update Entry</h1> | ||||
|  | ||||
| 			<form id="new-food-entry"> | ||||
| 				 | ||||
| 				<fieldset> | ||||
| 					<legend>PICTURE:</legend> | ||||
| 					<select id="select" name="select"> | ||||
| 						<option value="file">File Upload</option> | ||||
| 						<option value="url">From an URL</option> | ||||
| 					</select> | ||||
| 					<label for="mealImage" id="source"> | ||||
| 						<input type="file" accept="image/*" id="mealImg" name="mealImg"> | ||||
| 					</label> | ||||
| 				</fieldset> | ||||
| 				 | ||||
| 				<fieldset> | ||||
| 					<legend>MEAL NAME:</legend> | ||||
| 					<label for="Name: "> <input type="text" id="mealName" name="mealName" required> </label> | ||||
| 				</fieldset> | ||||
|  | ||||
| 				<fieldset> | ||||
| 					<legend>RESTAURANT NAME:</legend> | ||||
| 					<label for="Name:"> <input type="text" id="restaurant" name="restaurant" required> </label> | ||||
| 				</fieldset> | ||||
| 			 | ||||
| 				<fieldset> | ||||
| 					<legend>RATING:</legend> | ||||
| 					<div style="display: flex; justify-content: flex-start; align-items: center;"> | ||||
| 						<div class="rating"> | ||||
| 							<input type="radio" id="s5" name="rating" value="5"/> <label for="s5" id="s5-select"> 5 stars </label> | ||||
| 							<input type="radio" id="s4" name="rating" value="4"/> <label for="s4" id="s4-select"> 4 stars </label> | ||||
| 							<input type="radio" id="s3" name="rating" value="3"/> <label for="s3" id="s3-select"> 3 stars </label> | ||||
| 							<input type="radio" id="s2" name="rating" value="2"/> <label for="s2" id="s2-select"> 2 stars </label> | ||||
| 							<input type="radio" id="s1" name="rating" value="1"/> <label for="s1" id="s1-select"> 1 star </label> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</fieldset> | ||||
|  | ||||
| 				<fieldset> | ||||
| 					<legend>COMMENTS:</legend> | ||||
| 					<textarea name="comments" id="comments" rows="5" style="resize: none; width: 100%;"></textarea> | ||||
| 				</fieldset> | ||||
|  | ||||
| 				<fieldset> | ||||
| 					<legend>TAGS: (ex. cuisine, distance, cost, etc)</legend> | ||||
| 					<input type="text" id="tag-form" name="tag-form"> | ||||
|  | ||||
| 					<div class='tag-container' id="tag-container-form"> | ||||
| 					</div> | ||||
| 					<button type="button" id="tag-add-btn"> + </button> | ||||
| 				</fieldset> | ||||
|  | ||||
| 			<button type="submit" id="save-btn" value="Submit">Save</button> | ||||
| 			<input type="button" value="Cancel" id="home-btn" onclick="window.location.assign('./index.html')"> | ||||
| 			</form> | ||||
| 		</div> | ||||
| 	</body> | ||||
| </html> | ||||
|   | ||||
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								source/assets/images/Grouppink.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								source/assets/images/Logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 311 KiB | 
							
								
								
									
										
											BIN
										
									
								
								source/assets/images/delete_icon_for_interface.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 40 KiB | 
							
								
								
									
										
											BIN
										
									
								
								source/assets/images/edit_button_for_interface.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 9.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								source/assets/images/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								source/assets/images/home_button_for_interface.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.4 KiB | 
| Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB | 
| Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 253 KiB | 
							
								
								
									
										110
									
								
								source/assets/scripts/CreatePage.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,110 @@ | ||||
| import { newReviewToStorage } from "./localStorage.js"; | ||||
|  | ||||
| window.addEventListener("DOMContentLoaded", init); | ||||
|  | ||||
| function init() { | ||||
| 	// get next id | ||||
|  | ||||
| 	// creates the key | ||||
| 	initFormHandler(); | ||||
|      | ||||
| } | ||||
|  | ||||
| function initFormHandler() { | ||||
|  | ||||
| 	//accessing form components | ||||
| 	let tagContainer = document.getElementById("tag-container-form"); | ||||
| 	let form = document.querySelector("form"); | ||||
|  | ||||
| 	/* | ||||
| 	* change the input source of the image between local file and URL  | ||||
| 	* depending on user's selection | ||||
| 	*/ | ||||
| 	let select = document.getElementById("select"); | ||||
| 	select.addEventListener("change", function() { | ||||
| 		const input = document.getElementById("source"); | ||||
| 	 | ||||
| 		if (select.value == "file") { | ||||
| 			input.innerHTML = ` | ||||
| 			Source: | ||||
| 			<input type="file" accept="image/*" id="mealImg" name="mealImg"> | ||||
| 			`; | ||||
| 		} | ||||
| 		//TODO: change to photo taking for sprint 3 | ||||
| 		else { | ||||
| 			input.innerHTML = ` | ||||
| 			Source: | ||||
| 			<input type="text" id="mealImg" name="mealImg"> | ||||
| 			`; | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	//addressing sourcing image from local file | ||||
| 	let imgDataURL = ""; | ||||
| 	document.getElementById("mealImg").addEventListener("change", function() { | ||||
| 		const reader = new FileReader(); | ||||
| 		 | ||||
| 		//store image data URL after successful image load | ||||
| 		reader.addEventListener("load", ()=>{ | ||||
| 			imgDataURL = reader.result; | ||||
| 		}, false); | ||||
| 		 | ||||
| 		//convert image file into data URL for local storage | ||||
| 		reader.readAsDataURL(document.getElementById("mealImg").files[0]); | ||||
| 	}); | ||||
| 		 | ||||
| 	form.addEventListener("submit", function(e){ | ||||
| 	/* | ||||
|     *  User submits the form for their review. | ||||
|     *  We create reviewCard and put in storage | ||||
|     */ | ||||
| 		e.preventDefault(); | ||||
| 		let formData = new FormData(form); | ||||
| 		let reviewObject = {}; | ||||
| 		for (let [key, value] of formData) { | ||||
| 			console.log(`${key}`); | ||||
| 			console.log(`${value}`); | ||||
| 			if (`${key}` !== "tag-form") { | ||||
| 				reviewObject[`${key}`] = `${value}`; | ||||
| 			} | ||||
| 			if (`${key}` === "mealImg" && select.value == "file") { | ||||
| 				reviewObject["mealImg"] = imgDataURL; | ||||
| 			} | ||||
| 		} | ||||
| 		if(reviewObject["rating"] != null){ | ||||
| 			reviewObject["tags"] = []; | ||||
|  | ||||
| 			let tags = document.querySelectorAll(".tag"); | ||||
| 			for(let i = 0; i < tags.length; i ++) { | ||||
| 				reviewObject["tags"].push(tags[i].innerHTML); | ||||
| 				tagContainer.removeChild(tags[i]); | ||||
| 			} | ||||
|  | ||||
| 			let nextReviewId = newReviewToStorage(reviewObject); | ||||
| 			sessionStorage.setItem("currID", JSON.stringify(nextReviewId)); | ||||
|  | ||||
| 			window.location.assign("./ReviewDetails.html"); | ||||
| 		} else{ | ||||
| 			window.alert("NO! FILL IN STARS"); | ||||
| 		} | ||||
|          | ||||
| 	}); | ||||
|  | ||||
| 	let tagAddBtn = document.getElementById("tag-add-btn"); | ||||
| 	tagAddBtn.addEventListener("click", ()=> { | ||||
| 		let tagField = document.getElementById("tag-form"); | ||||
| 		if (tagField.value.length > 0) { | ||||
| 			let tagLabel = document.createElement("label"); | ||||
| 			tagLabel.innerHTML = tagField.value; | ||||
| 			tagLabel.setAttribute("class","tag"); | ||||
| 			tagLabel.addEventListener("click",()=> { | ||||
| 				tagContainer.removeChild(tagLabel); | ||||
| 			}); | ||||
|        | ||||
| 			tagContainer.append(tagLabel); | ||||
| 			tagField.value = ""; | ||||
|  | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| } | ||||
| @@ -6,91 +6,108 @@ class ReviewCard extends HTMLElement { | ||||
| 	constructor() { | ||||
| 		super();  | ||||
|  | ||||
|  | ||||
| 		let shadowEl = this.attachShadow({mode:"open"}); | ||||
|  | ||||
| 		let articleEl = document.createElement("article"); | ||||
|  | ||||
| 		let styleEl = document.createElement("style"); | ||||
| 		styleEl.textContent = ` | ||||
|       * { | ||||
|         font-family: sans-serif; | ||||
|         margin: 0; | ||||
|         padding: 0; | ||||
|       } | ||||
| 		* { | ||||
| 			font-family: Century Gothic; | ||||
| 			margin: 0; | ||||
| 			padding: 0; | ||||
| 			overflow-wrap: anywhere; | ||||
| 		} | ||||
| 		 | ||||
|       a { | ||||
|         text-decoration: none; | ||||
|       } | ||||
| 		a { | ||||
| 			text-decoration: none; | ||||
| 		} | ||||
| 		 | ||||
|       a:hover { | ||||
|         text-decoration: underline; | ||||
|       } | ||||
| 		a:hover { | ||||
| 			text-decoration: underline; | ||||
| 		} | ||||
| 		 | ||||
|       article { | ||||
|         align-items: center; | ||||
|         border: 1px solid rgb(223, 225, 229); | ||||
|         border-radius: 8px; | ||||
|         display: grid; | ||||
|         grid-template-rows: 118px 56px 14px 18px 15px 36px; | ||||
|         height: auto; | ||||
|         row-gap: 5px; | ||||
|         padding: 0 16px 16px 16px; | ||||
|         width: 178px; | ||||
|       } | ||||
| 		article { | ||||
| 			align-items: center; | ||||
| 			border: 2px solid rgb(31, 41, 32); | ||||
| 			border-radius: 8px; | ||||
| 			display: grid; | ||||
| 			grid-template-rows: 118px 56px 14px 18px 15px 36px; | ||||
| 			height: auto; | ||||
| 			row-gap: 5px; | ||||
| 			padding: 0 16px 16px 16px; | ||||
| 			width: 178px; | ||||
| 			margin: 8px 8px 8px 8px; | ||||
| 		} | ||||
| 		 | ||||
|       div.rating { | ||||
|         align-items: center; | ||||
|         column-gap: 5px; | ||||
|         display: flex; | ||||
|       } | ||||
| 		div.rating { | ||||
| 			align-items: center; | ||||
| 			column-gap: 5px; | ||||
| 			display: flex; | ||||
| 		} | ||||
| 		 | ||||
|       div.rating>img { | ||||
|         height: auto; | ||||
|         display: inline-block; | ||||
|         object-fit: scale-down; | ||||
|         width: 78px; | ||||
|       } | ||||
| 		div.rating>img { | ||||
| 			height: auto; | ||||
| 			display: inline-block; | ||||
| 			object-fit: scale-down; | ||||
| 			width: 78px; | ||||
| 		} | ||||
| 		 | ||||
|       article>img { | ||||
|         border-top-left-radius: 8px; | ||||
|         border-top-right-radius: 8px; | ||||
|         height: 118px; | ||||
|         object-fit: cover; | ||||
|         margin-left: -16px; | ||||
|         width: calc(100% + 32px); | ||||
|       } | ||||
| 		article>img { | ||||
| 			border-top-left-radius: 6px; | ||||
| 			border-top-right-radius: 6px; | ||||
| 			height: 119px; | ||||
| 			object-fit: cover; | ||||
| 			margin-left: -16px; | ||||
| 			width: calc(100% + 32px); | ||||
| 		} | ||||
| 		 | ||||
|       label.restaurant-name { | ||||
|         color: black !important; | ||||
|       } | ||||
| 		label.restaurant-name { | ||||
| 			color: black !important; | ||||
| 		} | ||||
| 		 | ||||
|       label.meal-name { | ||||
|         display: -webkit-box; | ||||
|         font-size: 16px; | ||||
|         height: 36px; | ||||
|         line-height: 18px; | ||||
|         overflow: hidden; | ||||
|         -webkit-line-clamp: 2; | ||||
|         -webkit-box-orient: vertical; | ||||
|       } | ||||
| 		label.meal-name { | ||||
| 			display: -webkit-box; | ||||
| 			font-size: 16px; | ||||
| 			height: 36px; | ||||
| 			line-height: 18px; | ||||
| 			overflow: hidden; | ||||
| 			-webkit-line-clamp: 2; | ||||
| 			-webkit-box-orient: vertical; | ||||
| 		} | ||||
| 		 | ||||
|       label:not(.meal-name), | ||||
|       span, | ||||
|       time { | ||||
|         color: #70757A; | ||||
|         font-size: 12px; | ||||
|       } | ||||
|     `; | ||||
| 		label:not(.meal-name), | ||||
| 		span, | ||||
| 		time { | ||||
| 			color: #70757A; | ||||
| 			font-size: 12px; | ||||
| 		} | ||||
|  | ||||
| 		.tag-container { | ||||
| 			margin-top: 20px; | ||||
| 			display: flex; | ||||
| 			flex-flow: row wrap; | ||||
| 		} | ||||
| 		 | ||||
| 		.a-tag { | ||||
| 			background-color:#94da97; | ||||
| 			border-radius: 7px; | ||||
| 			color: #94da97; | ||||
| 			padding-right: 7px; | ||||
| 			padding-left: 7px; | ||||
| 			margin: 3px; | ||||
| 			font-weight: bold; | ||||
| 		} | ||||
|     	`; | ||||
| 		articleEl.append(styleEl); | ||||
| 		shadowEl.append(articleEl); | ||||
| 		this.shadowEl = shadowEl; | ||||
| 		//attach event listener to each recipe-card | ||||
| 		this.addEventListener("click", (event) => { | ||||
| 			console.log(event.target); | ||||
| 			console.log(event.target.data); | ||||
| 			console.log(event.target.reviewId); | ||||
| 			//Option 1: sending current data to second html page using localStorage (could also just store index) | ||||
| 			sessionStorage.setItem("current", JSON.stringify(event.target.data)); | ||||
| 			sessionStorage.setItem("currID", JSON.stringify(event.target.data.reviewID)); | ||||
| 			window.location.assign("./ReviewDetails.html"); | ||||
| 			/* | ||||
|       //Option 2: sending current data to second html page using string query w/ url (currently not storing value) | ||||
| @@ -117,7 +134,6 @@ class ReviewCard extends HTMLElement { | ||||
|    *                        following format: | ||||
|    *                        { | ||||
|    *                          "mealImg": "string", | ||||
|    *                          "imgAlt": "string", | ||||
|    *                          "mealName": "string", | ||||
|    *                          "comments": "string", | ||||
|    *                          "rating": number, | ||||
| @@ -133,12 +149,17 @@ class ReviewCard extends HTMLElement { | ||||
| 		let articleEl = this.shadowEl.querySelector("article"); | ||||
|      | ||||
| 		// setting the article elements for the review card | ||||
| 		this.reviewID = data["reviewID"]; | ||||
|  | ||||
| 		//image setup | ||||
| 		let mealImg = document.createElement("img"); | ||||
| 		mealImg.setAttribute("id", "a-mealImg"); | ||||
| 		mealImg.setAttribute("alt","Meal Photo Corrupted"); | ||||
| 		mealImg.setAttribute("src",data["mealImg"]); | ||||
| 		mealImg.setAttribute("alt",data["imgAlt"]); | ||||
| 		mealImg.addEventListener("error", function(e) { | ||||
| 			mealImg.setAttribute("src", "./assets/images/plate_with_cutlery.png"); | ||||
| 			e.onerror = null; | ||||
| 		}); | ||||
|  | ||||
| 		//meal name setup | ||||
| 		let mealLabel = document.createElement("label"); | ||||
| @@ -147,26 +168,6 @@ class ReviewCard extends HTMLElement { | ||||
| 		mealLabel.innerHTML = data["mealName"]; | ||||
|  | ||||
| 		//restaurant name setup | ||||
| 		/* | ||||
|     //review page link | ||||
|     //giving it functionality to save the review card's info to session storage for loading the review page | ||||
|     let reviewLink = document.createElement('a'); | ||||
|     reviewLink.setAttribute('href','./review.html') | ||||
|     reviewLink.innerHTML = 'review page' | ||||
|     reviewLink.addEventListener('click', () => { | ||||
|       sessionStorage.clear(); | ||||
|       let currReview = { | ||||
|         "imgSrc": data['imgSrc'], | ||||
|         "imgAlt": data['imgAlt'], | ||||
|         "mealName": data['mealName'], | ||||
|         "restaurant": data['restaurant'], | ||||
|         "comments": data['comments'], | ||||
|         "rating": data['rating'], | ||||
|         "tags": data['tags']                 | ||||
|       } | ||||
|       sessionStorage.setItem('currReview', JSON.stringify(currReview)); | ||||
|     }); | ||||
| */ | ||||
| 		let restaurantLabel = document.createElement("label"); | ||||
| 		restaurantLabel.setAttribute("id", "a-restaurant"); | ||||
| 		restaurantLabel.setAttribute("class","restaurant-name"); | ||||
| @@ -183,7 +184,7 @@ class ReviewCard extends HTMLElement { | ||||
| 		ratingDiv.setAttribute("class", "rating"); | ||||
| 		let starsImg = document.createElement("img"); | ||||
| 		starsImg.setAttribute("id", "a-rating"); | ||||
| 		starsImg.setAttribute("src", "./assets/images/icons/"+data["rating"]+"-star.svg"); | ||||
| 		starsImg.setAttribute("src", "./assets/images/"+data["rating"]+"-star.svg"); | ||||
| 		starsImg.setAttribute("alt", data["rating"] +" stars"); | ||||
| 		starsImg.setAttribute("num", data["rating"]); | ||||
| 		ratingDiv.append(starsImg); | ||||
| @@ -196,15 +197,16 @@ class ReviewCard extends HTMLElement { | ||||
| 		if(data["tags"]){ | ||||
| 			for (let i = 0; i < data["tags"].length; i++) { | ||||
| 				let newTag = document.createElement("label"); | ||||
| 				newTag.setAttribute("class","tag"); | ||||
| 				newTag.innerHTML = data["tags"][i] + "   "; | ||||
| 				newTag.setAttribute("class","a-tag"); | ||||
| 				newTag.innerHTML = data["tags"][i]; | ||||
| 				tagContainer.append(newTag); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		//adding final ID to data! | ||||
|  | ||||
| 		articleEl.append(mealImg); | ||||
| 		articleEl.append(mealLabel); | ||||
| 		//articleEl.append(reviewLink) | ||||
| 		articleEl.append(restaurantLabel); | ||||
| 		articleEl.append(ratingDiv); | ||||
| 		articleEl.append(tagContainer); | ||||
| @@ -224,7 +226,6 @@ class ReviewCard extends HTMLElement { | ||||
|    *                        following format: | ||||
|    *                        { | ||||
|    *                          "mealImg": "string", | ||||
|    *                          "imgAlt": "string", | ||||
|    *                          "mealName": "string", | ||||
|    *                          "comments": "string", | ||||
|    *                          "rating": number, | ||||
| @@ -237,11 +238,11 @@ class ReviewCard extends HTMLElement { | ||||
| 		let dataContainer = {}; | ||||
|      | ||||
| 		// getting the article elements for the review card | ||||
| 		dataContainer["reviewID"] = this.reviewID; | ||||
|  | ||||
| 		//get image | ||||
| 		let mealImg = this.shadowEl.getElementById("a-mealImg"); | ||||
| 		dataContainer["mealImg"] = mealImg.getAttribute("src"); | ||||
| 		dataContainer["imgAlt"] = mealImg.getAttribute("alt"); | ||||
|  | ||||
| 		//get meal name | ||||
| 		let mealLabel = this.shadowEl.getElementById("a-mealName"); | ||||
|   | ||||
| @@ -1,124 +1,194 @@ | ||||
| //reviewDetails.js | ||||
| import {getReviewsFromStorage, saveReviewsToStorage} from "./localStorage.js"; | ||||
| import {deleteReviewFromStorage, getReviewFromStorage, updateReviewToStorage} from "./localStorage.js"; | ||||
|  | ||||
| // Run the init() function when the page has loaded | ||||
| window.addEventListener("DOMContentLoaded", init); | ||||
|  | ||||
| function init(){ | ||||
| 	setupInfo(); | ||||
| 	setupDelete(); | ||||
| 	setupUpdate(); | ||||
| } | ||||
|  | ||||
| function setupInfo(){ | ||||
| 	let currID = JSON.parse(sessionStorage.getItem("currID")); | ||||
| 	let currReview = getReviewFromStorage(currID); | ||||
| 	 | ||||
| 	//meal image | ||||
| 	let mealImg = document.getElementById("d-mealImg"); | ||||
| 	mealImg.setAttribute("src",currReview["mealImg"]); | ||||
| 	mealImg.addEventListener("error", function(e) { | ||||
| 		mealImg.setAttribute("src", "./assets/images/plate_with_cutlery.png"); | ||||
| 		e.onerror = null; | ||||
| 	}); | ||||
|  | ||||
| 	//meal name | ||||
| 	let mealLabel = document.getElementById("d-mealName"); | ||||
| 	mealLabel.innerHTML = currReview["mealName"]; | ||||
|  | ||||
| 	//restaurant name | ||||
| 	let restaurantLabel = document.getElementById("d-restaurant"); | ||||
| 	restaurantLabel.innerHTML = currReview["restaurant"]; | ||||
| 	 | ||||
| 	//comments | ||||
| 	let comments = document.getElementById("d-comments"); | ||||
| 	comments.innerText = currReview["comments"]; | ||||
|  | ||||
| 	//rating | ||||
| 	let starsImg = document.getElementById("d-rating"); | ||||
| 	starsImg.setAttribute("src", "./assets/images/"+currReview["rating"]+"-star.svg"); | ||||
| 	starsImg.setAttribute("alt", currReview["rating"] +" stars"); | ||||
| 	 | ||||
| 	//tags | ||||
| 	let tagContainer = document.getElementById("d-tags"); | ||||
| 	if(currReview["tags"]){ | ||||
| 		for (let i = 0; i < currReview["tags"].length; i++) { | ||||
| 			let newTag = document.createElement("label"); | ||||
| 			newTag.setAttribute("class","d-tag"); | ||||
| 			newTag.innerHTML = currReview["tags"][i]; | ||||
| 			tagContainer.append(newTag); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function setupDelete(){ | ||||
| 	let deleteBtn = document.getElementById("delete"); | ||||
| 	let reviews = getReviewsFromStorage(); | ||||
| 	let current = JSON.parse(sessionStorage.getItem("current")); | ||||
| 	let deleteBtn = document.getElementById("delete-btn"); | ||||
| 	let currID = JSON.parse(sessionStorage.getItem("currID")); | ||||
| 	deleteBtn.addEventListener("click", function(){ | ||||
| 		if(window.confirm("Are you sure you want to delete this entry?")){ | ||||
| 			//delete function | ||||
| 			if(current){ | ||||
| 				console.log(current); | ||||
| 				for(let i = 0; i < reviews.length; i++){ | ||||
| 					console.log(reviews[i]); | ||||
| 					if(reviews[i]["mealName"] == current["mealName"] && reviews[i]["restaurant"] == current["restaurant"]){ | ||||
| 						console.log("match found"); | ||||
| 						reviews.splice(i,1); | ||||
| 						saveReviewsToStorage(reviews); | ||||
| 						sessionStorage.removeItem("current"); | ||||
| 						window.location.assign("./index.html"); | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			deleteReviewFromStorage(currID); | ||||
| 			sessionStorage.removeItem("currID"); | ||||
| 			window.location.assign("./index.html"); | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function setupUpdate(){ | ||||
| 	let updateBtn = document.getElementById("update"); | ||||
| 	let reviews = getReviewsFromStorage(); | ||||
| 	let current = JSON.parse(sessionStorage.getItem("current")); | ||||
| 	let form = document.getElementById("update-food-entry"); | ||||
| 	let updateBtn = document.getElementById("update-btn"); | ||||
| 	let currID = JSON.parse(sessionStorage.getItem("currID")); | ||||
| 	let currReview = getReviewFromStorage(currID); | ||||
| 	let form = document.getElementById("new-food-entry"); | ||||
| 	let updateDiv = document.getElementById("update-form"); | ||||
| 	updateBtn.addEventListener("click", function(){ | ||||
| 		//update function | ||||
| 		if(current){ | ||||
| 			console.log(current); | ||||
| 			form.style.display = "block"; | ||||
| 			let tagContainer = document.getElementById("tag-container-form"); | ||||
| 			console.log(document.querySelectorAll("#update-food-entry input")); | ||||
|  | ||||
| 			//Set value of each input element to current's values | ||||
| 			document.getElementById("mealImg").defaultValue = current["mealImg"]; | ||||
| 			document.getElementById("imgAlt").defaultValue = current["imgAlt"]; | ||||
| 			document.getElementById("mealName").defaultValue = current["mealName"]; | ||||
| 			document.getElementById("comments").textContent = current["comments"]; | ||||
| 			document.getElementById("rating-" + `${current["rating"]}`).checked = true; | ||||
| 			document.getElementById("restaurant").defaultValue = current["restaurant"]; | ||||
| 		updateDiv.classList.remove("hidden"); | ||||
|  | ||||
| 			if(current["tags"]){ | ||||
| 				for (let i = 0; i < current["tags"].length; i++) { | ||||
| 					let newTag = document.createElement("label"); | ||||
| 					newTag.setAttribute("class","tag"); | ||||
| 					newTag.innerHTML = current["tags"][i] + "   "; | ||||
| 					newTag.addEventListener("click",()=> { | ||||
| 						tagContainer.removeChild(newTag); | ||||
| 					}); | ||||
| 					tagContainer.append(newTag); | ||||
| 		let tagContainer = document.getElementById("tag-container-form"); | ||||
|  | ||||
| 		//Set value of each input element to current's values | ||||
| 		document.getElementById("mealImg").defaultValue = currReview["mealImg"]; | ||||
| 		document.getElementById("mealName").defaultValue = currReview["mealName"]; | ||||
| 		document.getElementById("comments").textContent = currReview["comments"]; | ||||
| 		document.getElementById("s" + `${currReview["rating"]}`).checked = true; | ||||
| 		document.getElementById("restaurant").defaultValue = currReview["restaurant"]; | ||||
|  | ||||
| 		if(currReview["tags"]){ | ||||
| 			while (tagContainer.firstChild) { | ||||
| 				tagContainer.removeChild(tagContainer.firstChild); | ||||
| 			} | ||||
| 			for (let i = 0; i < currReview["tags"].length; i++) { | ||||
| 				let newTag = document.createElement("label"); | ||||
| 				newTag.setAttribute("class","tag"); | ||||
| 				newTag.innerHTML = currReview["tags"][i]; | ||||
| 				newTag.addEventListener("click",()=> { | ||||
| 					tagContainer.removeChild(newTag); | ||||
| 				}); | ||||
| 				tagContainer.append(newTag); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		/* | ||||
| 		* change the input source of the image between local file and URL  | ||||
| 		* depending on user's selection | ||||
| 		*/ | ||||
| 		let select = document.getElementById("select"); | ||||
| 		select.addEventListener("change", function() { | ||||
| 			const input = document.getElementById("source"); | ||||
| 		 | ||||
| 			if (select.value == "file") { | ||||
| 				input.innerHTML = ` | ||||
| 				Source: | ||||
| 				<input type="file" accept="image/*" id="mealImg" name="mealImg"> | ||||
| 				`; | ||||
| 			} | ||||
| 			//TODO: change to photo taking for sprint 3 | ||||
| 			else { | ||||
| 				input.innerHTML = ` | ||||
| 				Source: | ||||
| 				<input type="text" id="mealImg" name="mealImg"> | ||||
| 				`; | ||||
| 			} | ||||
| 		}); | ||||
|  | ||||
| 		//addressing sourcing image from local file | ||||
| 		let imgDataURL = ""; | ||||
| 		document.getElementById("mealImg").addEventListener("change", function() { | ||||
| 			console.log("reading used"); | ||||
| 			const reader = new FileReader(); | ||||
| 			 | ||||
| 			//store image data URL after successful image load | ||||
| 			reader.addEventListener("load", ()=>{ | ||||
| 				imgDataURL = reader.result; | ||||
| 			}, false); | ||||
| 			 | ||||
| 			//convert image file into data URL for local storage | ||||
| 			reader.readAsDataURL(document.getElementById("mealImg").files[0]); | ||||
| 		}); | ||||
|  | ||||
| 		 | ||||
| 		//Take formdata values as newData when submit | ||||
| 		form.addEventListener("submit", function(){ | ||||
| 			/* | ||||
| 			*  User submits the form for their review. | ||||
| 			*  We create reviewCard and put in storage | ||||
| 			*/ | ||||
| 			let formData = new FormData(form); | ||||
| 			let newData = {}; | ||||
| 			for (let [key, value] of formData) { | ||||
| 				console.log(`${key}`); | ||||
| 				console.log(`${value}`); | ||||
| 				if (`${key}` !== "tag-form") { | ||||
| 					newData[`${key}`] = `${value}`; | ||||
| 				} | ||||
| 				//Account for the case where image is not updated | ||||
| 				if (`${key}` === "mealImg" && document.getElementById("mealImg").value === "") { | ||||
| 					newData["mealImg"] = currReview["mealImg"]; | ||||
| 				} | ||||
| 				else if (`${key}` === "mealImg" && select.value == "file") { | ||||
| 					newData["mealImg"] = imgDataURL; | ||||
| 				} | ||||
| 			} | ||||
| 			//Take formdata values as newData when submit | ||||
| 			form.addEventListener("submit", function(){ | ||||
| 				/* | ||||
|                 *  User submits the form for their review. | ||||
|                 *  We create reviewCard and put in storage | ||||
|                 */ | ||||
| 				let formData = new FormData(form); | ||||
| 				let newData = {}; | ||||
| 				for (let [key, value] of formData) { | ||||
| 					console.log(`${key}`); | ||||
| 					console.log(`${value}`); | ||||
| 					if (`${key}` !== "tag-form") { | ||||
| 						newData[`${key}`] = `${value}`; | ||||
| 					} | ||||
| 				} | ||||
| 				newData["tags"] = []; | ||||
| 			newData["tags"] = []; | ||||
| 		 | ||||
| 				let tags = document.querySelectorAll(".tag"); | ||||
| 				for(let i = 0; i < tags.length; i ++) { | ||||
| 					newData["tags"].push(tags[i].innerHTML); | ||||
| 					tagContainer.removeChild(tags[i]); | ||||
| 				} | ||||
| 			let tags = document.querySelectorAll(".tag"); | ||||
| 			for(let i = 0; i < tags.length; i ++) { | ||||
| 				newData["tags"].push(tags[i].innerHTML); | ||||
| 				tagContainer.removeChild(tags[i]); | ||||
| 			} | ||||
|  | ||||
| 				for(let i = 0; i < reviews.length; i++){ | ||||
| 					console.log(reviews[i]); | ||||
| 					if(reviews[i]["mealName"] == current["mealName"] && reviews[i]["restaurant"] == current["restaurant"]){ | ||||
| 						console.log("match found"); | ||||
| 						reviews.splice(i,1,newData); | ||||
| 						saveReviewsToStorage(reviews); | ||||
| 						sessionStorage.setItem("current", JSON.stringify(newData)); | ||||
| 						break; | ||||
| 					} | ||||
| 				} | ||||
| 			newData["reviewID"] = currID; | ||||
|  | ||||
| 				form.style.display = "none"; | ||||
| 			updateReviewToStorage(currID, newData); | ||||
|  | ||||
| 			}); | ||||
| 			updateDiv.classList.add("hidden"); | ||||
|  | ||||
| 			let tagAddBtn = document.getElementById("tagAdd"); | ||||
| 			tagAddBtn.addEventListener("click", ()=> { | ||||
| 				let tagField = document.getElementById("tag-form"); | ||||
| 				if (tagField.value.length > 0) { | ||||
| 					let tagLabel = document.createElement("label"); | ||||
| 					tagLabel.innerHTML = tagField.value; | ||||
| 					tagLabel.setAttribute("class","tag"); | ||||
| 					tagLabel.addEventListener("click",()=> { | ||||
| 						tagContainer.removeChild(tagLabel); | ||||
| 					}); | ||||
| 		}); | ||||
|  | ||||
| 					tagContainer.append(tagLabel); | ||||
| 					tagField.value = ""; | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 		let tagAddBtn = document.getElementById("tag-add-btn"); | ||||
| 		tagAddBtn.addEventListener("click", ()=> { | ||||
| 			let tagField = document.getElementById("tag-form"); | ||||
| 			if (tagField.value.length > 0) { | ||||
| 				let tagLabel = document.createElement("label"); | ||||
| 				tagLabel.innerHTML = tagField.value; | ||||
| 				tagLabel.setAttribute("class","tag"); | ||||
| 				tagLabel.addEventListener("click",()=> { | ||||
| 					tagContainer.removeChild(tagLabel); | ||||
| 				}); | ||||
| 			 | ||||
| 				tagContainer.append(tagLabel); | ||||
| 				tagField.value = ""; | ||||
| 			} | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| // Run the init() function when the page has loaded | ||||
| window.addEventListener("DOMContentLoaded", init); | ||||
|  | ||||
| function init() { | ||||
| 	let result = sessionStorage.getItem("currReview"); | ||||
|  | ||||
| 	let main = document.querySelector("main"); | ||||
|      | ||||
| 	main.innerHTML = result; | ||||
| 	let p = document.createElement("p"); | ||||
| 	p.innerHTML = JSON.parse(result)["comments"]; | ||||
| 	main.append(p); | ||||
| } | ||||
							
								
								
									
										73
									
								
								source/assets/scripts/appTestHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,73 @@ | ||||
| import {strict as assert} from "node:assert"; | ||||
|  | ||||
| /** | ||||
|  * Fills out a create or update review form | ||||
|  * @param {Object} page the page object which contains the create or update form | ||||
|  * @param {Object} review review data to input into the form  | ||||
|  */ | ||||
| export async function setReviewForm(page, review) { | ||||
| 	 | ||||
| 	// Set text fields | ||||
| 	await page.$eval("#mealName", (el, value) => el.value = value, review.mealName); | ||||
| 	await page.$eval("#comments", (el, value) => el.value = value, review.comments); | ||||
| 	await page.$eval("#restaurant", (el, value) => el.value = value, review.restaurant); | ||||
|  | ||||
| 	// Get all tag elements and click them to delete them | ||||
| 	let tag_items = await page.$$(".tag"); | ||||
| 	if(tag_items !== null){ | ||||
| 		for(let i = 0; i < tag_items.length; i++){ | ||||
| 			await tag_items[i].click(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Get the button needed to add new tags | ||||
| 	let tag_btn = await page.$("#tag-add-btn"); | ||||
| 	for(let i = 0; i < review.tags.length; i++){ | ||||
| 		await page.$eval("#tag-form", (el, value) => el.value = value, review.tags[i]); | ||||
| 		await tag_btn.click(); | ||||
| 	} | ||||
|  | ||||
| 	// Select a new rating | ||||
| 	let rating_select = await page.$(`#s${review.rating}-select`); | ||||
| 	await rating_select.click({delay: 100}); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Tests a page or shadowDOM for correct element text or src values | ||||
|  * @param {Object} root page or shodowDOM to test | ||||
|  * @param {string} prefix prefix character for element IDs | ||||
|  * @param {Object} expected values for each element | ||||
|  */ | ||||
| export async function checkCorrectness(root, prefix, expected){ | ||||
| 	// Get the review image and check src | ||||
| 	let img = await root.$(`#${prefix}-mealImg`); | ||||
| 	let imgSrc = await img.getProperty("src"); | ||||
| 	// Check src | ||||
| 	assert.strictEqual(await imgSrc.jsonValue(), expected.imgSrc); | ||||
|  | ||||
| 	// Get the title, comment, and restaurant | ||||
| 	let title = await root.$(`#${prefix}-mealName`); | ||||
| 	let title_text = await title.getProperty("innerText"); | ||||
| 	let comment = await root.$(`#${prefix}-comments`); | ||||
| 	let comment_text = await comment.getProperty("innerText"); | ||||
| 	let restaurant = await root.$(`#${prefix}-restaurant`); | ||||
| 	let restaurant_text = await restaurant.getProperty("innerText"); | ||||
|  | ||||
| 	// Check title, comment, and restaurant | ||||
| 	assert.strictEqual(await title_text.jsonValue(), expected.mealName); | ||||
| 	assert.strictEqual(await comment_text.jsonValue(), expected.comments); | ||||
| 	assert.strictEqual(await restaurant_text.jsonValue(), expected.restaurant); | ||||
|  | ||||
| 	// Check tags | ||||
| 	let tags = await root.$$(`.${prefix}-tag`); | ||||
| 	assert.strictEqual(await tags.length, expected.tags.length); | ||||
| 	for(let i = 0; i < expected.tags.length; i++){ | ||||
| 		let tag_text = await tags[i].getProperty("innerText"); | ||||
| 		assert.strictEqual(await tag_text.jsonValue(), expected.tags[i]); | ||||
| 	} | ||||
|  | ||||
| 	// Check stars | ||||
| 	let stars = await root.$(`#${prefix}-rating`); | ||||
| 	let stars_src = await stars.getProperty("src"); | ||||
| 	assert.strictEqual(await stars_src.jsonValue(), expected.rating); | ||||
| } | ||||
| @@ -1,19 +1,78 @@ | ||||
| /** | ||||
|  * @returns {Array<Object>} An array of reviews found in localStorage | ||||
|  * Creates a new review to storage and performs related meta tasks | ||||
|  * @param {Object} review to store | ||||
|  * @return {number} ID of the newly added review | ||||
|  */ | ||||
| export function getReviewsFromStorage() { | ||||
| 	let result = JSON.parse(localStorage.getItem("reviews")); | ||||
| 	if (result) { | ||||
| 		return result; | ||||
| 	} | ||||
| 	return new Array(0); | ||||
| export function newReviewToStorage(review){ | ||||
| 	//grabbing the nextID, and putting our review object in storage associated with the ID | ||||
| 	let nextReviewId = JSON.parse(localStorage.getItem("nextID")); | ||||
| 	review["reviewID"] = nextReviewId; | ||||
|  | ||||
| 	// set the review entry to the review object | ||||
| 	localStorage.setItem(`review${nextReviewId}`, JSON.stringify(review)); | ||||
| 	 | ||||
| 	//updating our activeIDS list | ||||
| 	let tempIdArr = JSON.parse(localStorage.getItem("activeIDS")); | ||||
| 	tempIdArr.push(nextReviewId); | ||||
| 	localStorage.setItem("activeIDS", JSON.stringify(tempIdArr)); | ||||
| 	 | ||||
| 	//increment nextID for next review creation | ||||
| 	localStorage.setItem("nextID", JSON.stringify(nextReviewId + 1)); | ||||
|  | ||||
| 	return nextReviewId; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Takes in an array of reviews, converts it to a string, and then | ||||
|  * saves that string to 'reviews' in localStorage | ||||
|  * @param {Array<Object>} reviews An array of reviews | ||||
|  * Gets a single review by ID from storage | ||||
|  * @param {string} ID of the review to get | ||||
|  * @returns {Object} review object corresponding to param ID | ||||
|  */ | ||||
| export function saveReviewsToStorage(reviews) { | ||||
| 	localStorage.setItem("reviews", JSON.stringify(reviews)); | ||||
| export function getReviewFromStorage(ID){ | ||||
| 	return JSON.parse(localStorage.getItem(`review${ID}`)); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Updates a single review by ID to storage | ||||
|  * @param {string} ID of review to update | ||||
|  * @param {Object} review to store | ||||
|  */ | ||||
| export function updateReviewToStorage(ID, review){ | ||||
| 	// set the review entry with ID to the review object | ||||
| 	localStorage.setItem(`review${ID}`, JSON.stringify(review)); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Deletes a review by ID from storage | ||||
|  * @param {string} ID of the review to delete | ||||
|  */ | ||||
| export function deleteReviewFromStorage(ID){ | ||||
| 	let activeIDS = JSON.parse(localStorage.getItem("activeIDS")); | ||||
|  | ||||
| 	for (let i in activeIDS) { | ||||
| 		if (activeIDS[i] == ID) { | ||||
| 			activeIDS.splice(i,1); | ||||
| 			localStorage.setItem("activeIDS", JSON.stringify(activeIDS)); | ||||
| 			localStorage.removeItem(`review${ID}`); | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	console.error(`could not find review${ID} in localStorage`); | ||||
| } | ||||
|  | ||||
| // legacy function | ||||
| export function getAllReviewsFromStorage() { | ||||
| 	if (!(localStorage.getItem("activeIDS"))) { | ||||
| 		// we wanna init the active ID array and start the nextID count | ||||
| 		localStorage.setItem("activeIDS", JSON.stringify([])); | ||||
| 		localStorage.setItem("nextID",  JSON.stringify(0)); | ||||
| 	} | ||||
| 	//iterate thru activeIDS | ||||
| 	let activeIDS = JSON.parse(localStorage.getItem("activeIDS")); | ||||
| 	let reviews = []; | ||||
| 	for (let i = 0; i < activeIDS.length; i++) { | ||||
| 		let currReview = JSON.parse(localStorage.getItem(`review${activeIDS[i]}`)); | ||||
| 		reviews.push(currReview); | ||||
| 	} | ||||
| 	return reviews; | ||||
| } | ||||
| @@ -1,48 +1,103 @@ | ||||
| import {strict as assert} from "node:assert"; | ||||
| import {describe, it, beforeEach} from "mocha"; | ||||
| import {saveReviewsToStorage, getReviewsFromStorage} from "./localStorage.js"; | ||||
|  | ||||
| beforeEach(() => { | ||||
| 	localStorage.clear(); | ||||
| }); | ||||
| import {describe, it, before, after} from "mocha"; | ||||
| import {newReviewToStorage, getReviewFromStorage, updateReviewToStorage, deleteReviewFromStorage, getAllReviewsFromStorage} from "./localStorage.js"; | ||||
|  | ||||
| describe("test app localStorage interaction", () => { | ||||
| 	it("get after init", () => { | ||||
| 		assert.deepEqual(getReviewsFromStorage(), []); | ||||
| 	 | ||||
| 	before(() => { | ||||
| 		localStorage.clear(); | ||||
| 	}); | ||||
| 	it("store one then get", () => { | ||||
| 		let reviews = [{ | ||||
|  | ||||
| 	it("test localStorage state after init", () => { | ||||
| 		assert.deepEqual(getAllReviewsFromStorage(), []); | ||||
| 		assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), []); | ||||
| 		assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 0); | ||||
| 	}); | ||||
|  | ||||
| 	it("test localStorage state after adding one review", () => { | ||||
| 		let review = { | ||||
| 			"imgSrc": "sample src", | ||||
| 			"imgAlt": "sample alt", | ||||
| 			"mealName": "sample name", | ||||
| 			"restaurant": "sample restaurant", | ||||
| 			"rating": 5, | ||||
| 			"tags": ["tag 1", "tag  2", "tag 3"] | ||||
| 		}]; | ||||
| 		}; | ||||
|  | ||||
| 		saveReviewsToStorage(reviews); | ||||
| 		assert.deepEqual(getReviewsFromStorage(), reviews); | ||||
| 		newReviewToStorage(review); | ||||
|  | ||||
| 		review.reviewID = 0; | ||||
|  | ||||
| 		assert.deepEqual(getAllReviewsFromStorage(), [review]); | ||||
| 		assert.deepEqual(getReviewFromStorage(0), review); | ||||
| 		assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), [0]); | ||||
| 		assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 1); | ||||
| 	}); | ||||
| 	it("repeated store one more and get", () => { | ||||
| 		let reviews = []; | ||||
|  | ||||
| 		assert.deepEqual(getReviewsFromStorage(), reviews); | ||||
| 	it("test localStorage state during adding 999 reviews", () => { | ||||
| 		let reviews = getAllReviewsFromStorage(); | ||||
| 		let ids = [0]; | ||||
|  | ||||
| 		for(let i = 1; i < 1000; i++){ | ||||
| 			ids.push(i); | ||||
| 			let new_review = { | ||||
| 				"imgSrc": `sample src ${i}`, | ||||
| 				"mealName": `sample name ${i}`, | ||||
| 				"restaurant": `sample restaurant ${i}`, | ||||
| 				"rating": i, | ||||
| 				"tags": [`tag ${3*i}`, `tag ${3*i + 1}`, `tag ${3*i + 2}`] | ||||
| 			}; | ||||
|  | ||||
| 			newReviewToStorage(new_review); | ||||
|  | ||||
| 			new_review.reviewID = i; | ||||
| 			reviews.push(new_review); | ||||
|  | ||||
| 			assert.deepEqual(getAllReviewsFromStorage(), reviews); | ||||
| 			assert.deepEqual(getReviewFromStorage(i), new_review); | ||||
| 			assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), ids); | ||||
| 			assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), (i+1)); | ||||
| 		} | ||||
| 	}).timeout(5000); | ||||
|  | ||||
| 	it("test localStorage state during updating 1000 reviews", () => { | ||||
| 		let reviews = getAllReviewsFromStorage(); | ||||
| 		let ids = JSON.parse(localStorage.getItem("activeIDS")); | ||||
|  | ||||
| 		for(let i = 0; i < 1000; i++){ | ||||
| 			reviews = getReviewsFromStorage(); | ||||
| 			let new_review = { | ||||
| 				"imgSrc": `updated sample src ${i}`, | ||||
| 				"mealName": `updated sample name ${i}`, | ||||
| 				"restaurant": `updated sample restaurant ${i}`, | ||||
| 				"rating": i*2+i, | ||||
| 				"tags": [`tag ${3*i}`, `tag ${3*i + 1}`, `tag ${3*i + 2}`] | ||||
| 			}; | ||||
| 			new_review.reviewID = i; | ||||
|  | ||||
| 			reviews.push( | ||||
| 				{ | ||||
| 					"imgSrc": `sample src ${i}`, | ||||
| 					"imgAlt": `sample alt ${i}`, | ||||
| 					"mealName": `sample name ${i}`, | ||||
| 					"restaurant": `sample restaurant ${i}`, | ||||
| 					"rating": i, | ||||
| 					"tags": [`tag ${3*i}`, `tag ${3*i + 1}`, `tag ${3*i + 2}`] | ||||
| 				} | ||||
| 			); | ||||
| 			saveReviewsToStorage(reviews); | ||||
| 			assert.deepEqual(getReviewsFromStorage(), reviews); | ||||
| 			reviews[i] = new_review; | ||||
|  | ||||
| 			updateReviewToStorage(i, new_review); | ||||
|  | ||||
| 			assert.deepEqual(getAllReviewsFromStorage(), reviews); | ||||
| 			assert.deepEqual(getReviewFromStorage(i), new_review); | ||||
| 			assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), ids); | ||||
| 			assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 1000); | ||||
| 		} | ||||
| 	}).timeout(10000); | ||||
| 	}).timeout(5000); | ||||
|  | ||||
| 	it("test localStorage state during deleting 1000 reviews", () => { | ||||
| 		let reviews = getAllReviewsFromStorage(); | ||||
| 		let ids = JSON.parse(localStorage.getItem("activeIDS")); | ||||
|  | ||||
| 		for(let i = 999; i >= 0; i--){ | ||||
| 			deleteReviewFromStorage(i); | ||||
| 			ids.pop(); | ||||
| 			reviews.pop(); | ||||
|  | ||||
| 			assert.deepEqual(getAllReviewsFromStorage(), reviews); | ||||
| 			assert.deepEqual(JSON.parse(localStorage.getItem("activeIDS")), ids); | ||||
| 			assert.strictEqual(JSON.parse(localStorage.getItem("nextID")), 1000); | ||||
| 		} | ||||
| 	}).timeout(5000); | ||||
|  | ||||
| 	after(() => {}); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										232
									
								
								source/assets/scripts/main.e2e.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,232 @@ | ||||
| import {strict as assert} from "node:assert"; | ||||
| import {describe, it, before, after} from "mocha"; | ||||
| import puppeteer from "puppeteer-core"; | ||||
| import {exit} from "node:process"; | ||||
| import {setReviewForm, checkCorrectness} from "./appTestHelpers.js"; | ||||
|  | ||||
| describe("test App end to end", async () => { | ||||
|  | ||||
| 	let browser; | ||||
| 	let page; | ||||
|  | ||||
| 	before(async () => { | ||||
| 		let root; | ||||
| 		try { | ||||
| 			root =  process.getuid() == 0; | ||||
| 		} | ||||
| 		catch (error) { | ||||
| 			root = false; | ||||
| 		} | ||||
|  | ||||
| 		//browser = await puppeteer.launch({headless: false, slowMo: 250, args: root ? ['--no-sandbox'] : undefined}); | ||||
| 		browser = await puppeteer.launch({args: root ? ["--no-sandbox"] : undefined}); | ||||
| 		page = await browser.newPage(); | ||||
| 		try{ | ||||
| 			await page.goto("http://localhost:8080", {timeout: 2000}); | ||||
| 			await console.log(`✔ connected to localhost webserver as ${root ? "root" : "user"}`); | ||||
| 		} | ||||
| 		catch (error) { | ||||
| 			await console.log("❌ failed to connect to localhost webserver on port 8080"); | ||||
| 			await exit(1); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	describe("test simple properties", async () => { | ||||
| 		it("page should have correct title", async () => { | ||||
| 			assert.strictEqual(await page.title(), "Food Journal"); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	describe("test CRUD on simple inputs and default image", () => { | ||||
|  | ||||
| 		describe("test create 1 new review", async () => { | ||||
| 			it("create 1 new review", async () => { | ||||
| 				// Click the button to create a new review | ||||
| 				let create_btn = await page.$("#create-btn"); | ||||
| 				await create_btn.click(); | ||||
| 				await page.waitForNavigation(); | ||||
|  | ||||
| 				// create a new review | ||||
| 				let review = { | ||||
| 					mealName: "sample name", | ||||
| 					comments: "sample comment", | ||||
| 					restaurant: "sample restaurant", | ||||
| 					tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], | ||||
| 					rating: 1 | ||||
| 				}; | ||||
| 				await setReviewForm(page, review); | ||||
|  | ||||
| 				// Click the save button to save updates | ||||
| 				let save_btn = await page.$("#save-btn"); | ||||
| 				await save_btn.click(); | ||||
| 				await page.waitForNavigation(); | ||||
| 			}); | ||||
|  | ||||
| 			it("check details page", async () => { | ||||
| 				// check the details page for correctness | ||||
| 				let expected = { | ||||
| 					imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", | ||||
| 					mealName: "sample name", | ||||
| 					comments: "sample comment", | ||||
| 					restaurant: "sample restaurant", | ||||
| 					tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], | ||||
| 					rating: "http://localhost:8080/assets/images/1-star.svg" | ||||
| 				}; | ||||
| 				await checkCorrectness(page, "d", expected); | ||||
| 			}); | ||||
| 		 | ||||
| 			it("check home page", async () => { | ||||
| 				// Click the button to return to the home page | ||||
| 				let home_btn = await page.$("#home-btn"); | ||||
| 				home_btn.click(); | ||||
| 				await page.waitForNavigation(); | ||||
|  | ||||
| 				// Get the review card again and get its shadowRoot | ||||
| 				let review_card = await page.$("review-card"); | ||||
| 				let shadowRoot = await review_card.getProperty("shadowRoot"); | ||||
|  | ||||
| 				let expected = { | ||||
| 					imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", | ||||
| 					mealName: "sample name", | ||||
| 					comments: "sample comment", | ||||
| 					restaurant: "sample restaurant", | ||||
| 					tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], | ||||
| 					rating: "http://localhost:8080/assets/images/1-star.svg" | ||||
| 				}; | ||||
| 				await checkCorrectness(shadowRoot, "a", expected); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		describe("test read 1 review after refresh", async () => { | ||||
| 			it("refresh page", async () => { | ||||
| 				// Reload the page | ||||
| 				await page.reload({ waitUntil: ["networkidle0", "domcontentloaded"] }); | ||||
| 			}); | ||||
|  | ||||
| 			it("check details page", async () => { | ||||
| 				// click review card | ||||
| 				let review_card = await page.$("review-card"); | ||||
| 				await review_card.click(); | ||||
| 				await page.waitForNavigation(); | ||||
|  | ||||
| 				// check the details page for correctness | ||||
| 				let expected = { | ||||
| 					imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", | ||||
| 					mealName: "sample name", | ||||
| 					comments: "sample comment", | ||||
| 					restaurant: "sample restaurant", | ||||
| 					tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], | ||||
| 					rating: "http://localhost:8080/assets/images/1-star.svg" | ||||
| 				}; | ||||
| 				await checkCorrectness(page, "d", expected); | ||||
| 			}); | ||||
|  | ||||
| 			it("check home page", async () => { | ||||
| 				// Click the button to return to the home page | ||||
| 				let home_btn = await page.$("#home-btn"); | ||||
| 				home_btn.click(); | ||||
| 				await page.waitForNavigation(); | ||||
|  | ||||
| 				// Get the review card again and get its shadowRoot | ||||
| 				let review_card = await page.$("review-card"); | ||||
| 				let shadowRoot = await review_card.getProperty("shadowRoot"); | ||||
|  | ||||
| 				// check the details page for correctness | ||||
| 				let expected = { | ||||
| 					imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", | ||||
| 					mealName: "sample name", | ||||
| 					comments: "sample comment", | ||||
| 					restaurant: "sample restaurant", | ||||
| 					tags: ["tag 0", "tag 1", "tag 2", "tag 3", "tag 4"], | ||||
| 					rating: "http://localhost:8080/assets/images/1-star.svg" | ||||
| 				}; | ||||
| 				await checkCorrectness(shadowRoot, "a", expected); | ||||
| 			}); | ||||
| 		}); | ||||
|  | ||||
| 		describe("test update 1 review", async () => { | ||||
|  | ||||
| 			it("update 1 review", async () => { | ||||
|  | ||||
| 				// Get the only review card and click it | ||||
| 				let review_card = await page.$("review-card"); | ||||
| 				await review_card.click(); | ||||
| 				await page.waitForNavigation(); | ||||
|  | ||||
| 				// Click the button to show update form | ||||
| 				let update_btn = await page.$("#update-btn"); | ||||
| 				await update_btn.click(); | ||||
|  | ||||
| 				// create a new review | ||||
| 				let review = { | ||||
| 					mealName: "updated name", | ||||
| 					comments: "updated comment", | ||||
| 					restaurant: "updated restaurant", | ||||
| 					tags: ["tag -0", "tag -1", "tag -2", "tag -3", "tag -4", "tag -5"], | ||||
| 					rating: 5 | ||||
| 				}; | ||||
| 				await setReviewForm(page, review); | ||||
|  | ||||
| 				// Click the save button to save updates | ||||
| 				let save_btn = await page.$("#save-btn"); | ||||
| 				await save_btn.click(); | ||||
| 				await page.waitForNavigation(); | ||||
| 			}).timeout(10000); | ||||
|  | ||||
| 			it("check details page", async () => { | ||||
| 				// check the details page for correctness | ||||
| 				let expected = { | ||||
| 					imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", | ||||
| 					mealName: "updated name", | ||||
| 					comments: "updated comment", | ||||
| 					restaurant: "updated restaurant", | ||||
| 					tags: ["tag -0", "tag -1", "tag -2", "tag -3", "tag -4", "tag -5"], | ||||
| 					rating: "http://localhost:8080/assets/images/5-star.svg" | ||||
| 				}; | ||||
| 				await checkCorrectness(page, "d", expected); | ||||
| 			}); | ||||
|  | ||||
| 			it("check home page", async () => { | ||||
| 				// Click the button to return to the home page | ||||
| 				let home_btn = await page.$("#home-btn"); | ||||
| 				home_btn.click(); | ||||
| 				await page.waitForNavigation(); | ||||
|  | ||||
| 				// Get the review card again and get its shadowRoot | ||||
| 				let review_card = await page.$("review-card"); | ||||
| 				let shadowRoot = await review_card.getProperty("shadowRoot"); | ||||
|  | ||||
| 				// check the details page for correctness | ||||
| 				let expected = { | ||||
| 					imgSrc: "http://localhost:8080/assets/images/plate_with_cutlery.png", | ||||
| 					mealName: "updated name", | ||||
| 					comments: "updated comment", | ||||
| 					restaurant: "updated restaurant", | ||||
| 					tags: ["tag -0", "tag -1", "tag -2", "tag -3", "tag -4", "tag -5"], | ||||
| 					rating: "http://localhost:8080/assets/images/5-star.svg" | ||||
| 				}; | ||||
| 				await checkCorrectness(shadowRoot, "a", expected); | ||||
| 			}); | ||||
|  | ||||
| 		}); | ||||
|  | ||||
| 		describe("test delete 1 review", async () => { | ||||
| 			it("delete 1 review", async () => { | ||||
| 				// Get the only review card and click it | ||||
| 				let review_card = await page.$("review-card"); | ||||
| 				await review_card.click(); | ||||
| 				await page.waitForNavigation(); | ||||
|  | ||||
| 				page.on("dialog", async dialog => { | ||||
| 					console.log(dialog.message()); | ||||
| 					await dialog.accept(); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	after(async () => { | ||||
| 		await page.close(); | ||||
| 		await browser.close(); | ||||
| 	}); | ||||
| }); | ||||
| @@ -1,28 +1,29 @@ | ||||
| // main.js | ||||
| import {getReviewsFromStorage, saveReviewsToStorage} from "./localStorage.js"; | ||||
| import {getAllReviewsFromStorage} from "./localStorage.js"; | ||||
|  | ||||
| // Run the init() function when the page has loaded | ||||
| window.addEventListener("DOMContentLoaded", init); | ||||
|  | ||||
| function init() { | ||||
| 	// Get the reviews from localStorage | ||||
| 	let reviews = getReviewsFromStorage(); | ||||
| 	let reviews = getAllReviewsFromStorage(); | ||||
| 	// Add each reviews to the <main> element | ||||
| 	addReviewsToDocument(reviews); | ||||
| 	// Add the event listeners to the form elements | ||||
| 	initFormHandler(); | ||||
| } | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * @param {Array<Object>} reviews An array of reviews | ||||
|  */ | ||||
| function addReviewsToDocument(reviews) { | ||||
| 	let mainEl = document.querySelector("main"); | ||||
| 	let reviewBox = document.getElementById("review-container"); | ||||
| 	reviews.forEach(review => { | ||||
| 		let newReview = document.createElement("review-card"); | ||||
| 		newReview.data = review; | ||||
| 		//TODO: want to append it to whatever the box is in layout  | ||||
| 		mainEl.append(newReview); | ||||
| 		reviewBox.append(newReview); | ||||
| 	}); | ||||
|  | ||||
| } | ||||
| @@ -33,87 +34,9 @@ function addReviewsToDocument(reviews) { | ||||
|  */ | ||||
| function initFormHandler() { | ||||
|  | ||||
| 	/* | ||||
| 	//btn to create form (could be its own function?) | ||||
| 	let createBtn = document.getElementById("create"); | ||||
| 	let createBtn = document.getElementById("create-btn"); | ||||
| 	createBtn.addEventListener("click", function(){ | ||||
| 		window.location.assign("./CreatePage.html"); | ||||
| 	});*/ | ||||
|  | ||||
| 	//accessing form components | ||||
| 	let tagContainer = document.getElementById("tag-container-form"); | ||||
| 	let form = document.querySelector("form"); | ||||
|    | ||||
| 	form.addEventListener("submit", function(){ | ||||
| 	/* | ||||
|     *  User submits the form for their review. | ||||
|     *  We create reviewCard and put in storage | ||||
|     */ | ||||
| 		let formData = new FormData(form); | ||||
| 		let reviewObject = {}; | ||||
| 		for (let [key, value] of formData) { | ||||
| 			console.log(`${key}`); | ||||
| 			console.log(`${value}`); | ||||
| 			if (`${key}` !== "tag-form") { | ||||
| 				reviewObject[`${key}`] = `${value}`; | ||||
| 			} | ||||
| 		} | ||||
| 		reviewObject["tags"] = []; | ||||
|  | ||||
| 		let tags = document.querySelectorAll(".tag"); | ||||
| 		for(let i = 0; i < tags.length; i ++) { | ||||
| 			reviewObject["tags"].push(tags[i].innerHTML); | ||||
| 			tagContainer.removeChild(tags[i]); | ||||
| 		} | ||||
|      | ||||
|  | ||||
| 		let newReview = document.createElement("review-card"); | ||||
| 		newReview.data = reviewObject; | ||||
|  | ||||
| 		//TODO: want to append it to whatever the box is in layout  | ||||
| 		let mainEl = document.querySelector("main"); | ||||
| 		mainEl.append(newReview); | ||||
|  | ||||
| 		let storedReviews = getReviewsFromStorage(); | ||||
| 		storedReviews.push(reviewObject); | ||||
| 		saveReviewsToStorage(storedReviews); | ||||
| 		document.getElementById("new-food-entry").reset(); | ||||
| 	}); | ||||
|  | ||||
| 	// DEV-MODE: for testing purposes  | ||||
| 	let clearBtn = document.querySelector(".danger"); | ||||
| 	clearBtn.addEventListener("click", function() { | ||||
| 		localStorage.clear(); | ||||
| 		let mainEl = document.querySelector("main"); | ||||
| 		while (mainEl.firstChild) { | ||||
| 			mainEl.removeChild(mainEl.firstChild); | ||||
| 		} | ||||
| 		let deleteTags = document.querySelectorAll(".tag"); | ||||
| 		for(let i = 0; i < deleteTags.length; i ++) { | ||||
| 			tagContainer.removeChild(deleteTags[i]); | ||||
| 		} | ||||
|      | ||||
| 		//clears reviews AS WELL as resets form | ||||
| 		document.getElementById("new-food-entry").reset(); | ||||
|       | ||||
|       | ||||
| 	}); | ||||
|  | ||||
| 	let tagAddBtn = document.getElementById("tagAdd"); | ||||
| 	tagAddBtn.addEventListener("click", ()=> { | ||||
| 		let tagField = document.getElementById("tag-form"); | ||||
| 		if (tagField.value.length > 0) { | ||||
| 			let tagLabel = document.createElement("label"); | ||||
| 			tagLabel.innerHTML = tagField.value; | ||||
| 			tagLabel.setAttribute("class","tag"); | ||||
| 			tagLabel.addEventListener("click",()=> { | ||||
| 				tagContainer.removeChild(tagLabel); | ||||
| 			}); | ||||
|        | ||||
| 			tagContainer.append(tagLabel); | ||||
| 			tagField.value = ""; | ||||
|  | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,79 +1,57 @@ | ||||
|  | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|   <meta charset="UTF-8" /> | ||||
|   <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|   <title>Food Journal</title> | ||||
| 	<head> | ||||
| 		<meta charset="UTF-8" /> | ||||
| 		<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
| 		<title>Food Journal</title> | ||||
|  | ||||
|   <!-- Recipe Card Custom Element --> | ||||
|   <script src="assets/scripts/ReviewCard.js" type="module"></script> | ||||
| 		<!--Add Favicon--> | ||||
| 		<link rel="icon" type="image/x-icon" href="./assets/images/favicon.ico"> | ||||
|  | ||||
| 		<!-- Recipe Card Custom Element --> | ||||
| 		<script src="assets/scripts/ReviewCard.js" type="module"></script> | ||||
|  | ||||
|   <!-- Main Stylesheets & Scripts -->  | ||||
|   <!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> | ||||
|   <!-- <link rel="stylesheet" href="/static/reset.css" /> --> | ||||
|   <link rel="stylesheet" href="./static/ReviewCard.css" /> | ||||
|   <script src="assets/scripts/main.js" type="module"></script> | ||||
| 		<!-- Main Stylesheets & Scripts -->  | ||||
| 		<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> | ||||
| 		<!-- <link rel="stylesheet" href="/static/reset.css" /> --> | ||||
| 		<link rel="stylesheet" href="./static/homepage.css" /> | ||||
| 		<script src="assets/scripts/main.js" type="module"></script> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<header> | ||||
| 			<div class="top-bar"> | ||||
| 				<img src ="./assets/images/Logo.png" alt="logo" /> | ||||
| 				<h1> Food Journal </h1> | ||||
| 				<img src ="./assets/images/Logo.png" alt="logo" /> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 		<main> | ||||
| 			<div class="body-container"> | ||||
| 				<div style="width: 20%;"></div> | ||||
| 				<div style="width: 60%;"> | ||||
| 					<div class="search-bar"> | ||||
| 						<form id="form"> | ||||
| 							<input type="search" id="searching" name="searchBar" placeholder="Search journal..."> | ||||
| 							<button class="click" type="search"> Search </button> | ||||
| 							<div class="Filter-box"> | ||||
| 								 | ||||
| </head> | ||||
| <body> | ||||
|   <main> | ||||
|     <!-- Add Food Entries Here --> | ||||
|   </main> | ||||
|   <!--<button type="button" id="create">CREATE</button>--> | ||||
|   <!----> <form id="new-food-entry"> | ||||
|     <fieldset> | ||||
|       <legend>Pic:</legend> | ||||
|       <label for="mealImage"> | ||||
|         Source: | ||||
|         <input type="text" id="mealImg" name="mealImg"> | ||||
|       </label> | ||||
|       <label for="image-alt"> | ||||
|         Alt Text: | ||||
|         <input type="text" id="imgAlt" name="imgAlt"> | ||||
|       </label> | ||||
|     </fieldset> | ||||
|     <fieldset> | ||||
| 							</div> | ||||
| 						</form> | ||||
| 					</div> | ||||
| 					<div class="center-display">		 | ||||
| 						<img src ="./assets/images/Grouppink.png" alt="CREATE" style="opacity: 100%;" id="create-btn" onclick="window.location.assign('./CreatePage.html')"/> | ||||
| 						<h2 id="recent-reviews-text"> Recent Reviews </h2> | ||||
| 						<img src ="./assets/images/Grouppink.png" style="opacity:0;"/> | ||||
| 						 | ||||
|       <legend> Meal: </legend> | ||||
|       <label for="Meal: ">Meal: | ||||
|         <input type="text" id="mealName" name="mealName" required> | ||||
|       </label> | ||||
|       <label for="comments">Comments:  | ||||
|         <br> | ||||
|         <textarea name="comments" id="comments"></textarea> | ||||
|       </label> | ||||
|     </fieldset> | ||||
| 					</div> | ||||
|  | ||||
|     <fieldset class="rating"> | ||||
|       <legend> Rating: </legend> | ||||
|       <input type="radio" id="s5" name="rating" value="5"/> <label for="s5"> 5 stars </label> | ||||
|       <input type="radio" id="s4" name="rating" value="4"/> <label for="s4"> 4 stars </label> | ||||
|       <input type="radio" id="s3" name="rating" value="3"/> <label for="s3"> 3 stars </label> | ||||
|       <input type="radio" id="s2" name="rating" value="2"/> <label for="s2"> 2 stars </label> | ||||
|       <input type="radio" id="s1" name="rating" value="1"/> <label for="s1"> 1 star </label> | ||||
|     </fieldset> | ||||
|  | ||||
|     <fieldset> | ||||
|       <legend>Other Info:</legend> | ||||
|       <label for="restaurant"> | ||||
|         Restaurant: | ||||
|         <input type="text" id="restaurant" name="restaurant" required> | ||||
|       </label> | ||||
|       <label for="tag-form"> | ||||
|         Tags: | ||||
|         <input type="text" id="tag-form" name="tag-form"> | ||||
|         <div class='tag-container' id="tag-container-form"> | ||||
|  | ||||
|         </div> | ||||
|         <button type="button" id="tagAdd">Add Tag</button> | ||||
|       </label> | ||||
|  | ||||
|     </fieldset> | ||||
|     <button type="submit" value="Submit">Add Review</button> | ||||
|     <button type="button" class="danger">Clear Review Journal</button> | ||||
|   </form> <!----> | ||||
| </body> | ||||
| 					<div class="review-container" id="review-container"></div> | ||||
| 				</div> | ||||
| 				<div style="width: 20%;"> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</main> | ||||
| 	</body> | ||||
| </html> | ||||
|   | ||||
| @@ -1,16 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Food Journal</title> | ||||
|      | ||||
|     <script src="assets/scripts/reviewpage.js" type="module"></script> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Current Review:</h1> | ||||
|     <main> | ||||
|     </main> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										
											BIN
										
									
								
								source/static/CoveredByYourGrace-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -1,83 +1,30 @@ | ||||
| /* CreatePage.css */ | ||||
|  | ||||
| * { | ||||
|   font-family: sans-serif; | ||||
| @font-face { | ||||
| 	font-family: testFont; | ||||
| 	src: url("CoveredByYourGrace-Regular.ttf"); | ||||
| } | ||||
|  | ||||
| body { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| 	background-color: #f8f3f1; | ||||
| } | ||||
|  | ||||
| fieldset { | ||||
|   border: 2px solid rgb(214 214 214); | ||||
|   box-sizing: border-box; | ||||
|   display: block; | ||||
|   width: max-content; | ||||
| .top-bar { | ||||
| 	display: flex; | ||||
| 	justify-content: center; | ||||
| } | ||||
|  | ||||
| form button { | ||||
|   display: block; | ||||
|   margin-top: 5px; | ||||
| .top-bar > img { | ||||
| 	position: relative; | ||||
| 	align-self: center; | ||||
| 	padding-left: 2.5%; | ||||
| 	padding-right: 2.5%; | ||||
| } | ||||
|  | ||||
| label[for="ingredients"] p { | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| label[for="numRatings"] { | ||||
|   margin: 10px 0 0; | ||||
| } | ||||
|  | ||||
| label[for^="rating"] { | ||||
|   padding-right: 10px; | ||||
| } | ||||
|  | ||||
| label:not([for^="rating"]) { | ||||
|   display: block; | ||||
|   margin-bottom: 5px; | ||||
| } | ||||
|  | ||||
| main { | ||||
|   column-gap: 10px; | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   height: auto; | ||||
|   max-width: 660px; | ||||
|   row-gap: 10px; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .tag-container { | ||||
|   display: flex; | ||||
|   flex-flow: row wrap; | ||||
| } | ||||
|  | ||||
| .tag { | ||||
|   background-color: grey; | ||||
|   border-radius: 7px; | ||||
|   color: white; | ||||
|   padding-right: 7px; | ||||
|   padding-left: 7px; | ||||
|   margin: 3px; | ||||
| } | ||||
|  | ||||
| .tag::before { | ||||
|   display: inline-block; | ||||
|   content: "x"; | ||||
|   height: 15px; | ||||
|   width: 15px; | ||||
|   margin-right: 4px; | ||||
|   text-align: center; | ||||
|   color: white; | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| .tag:hover::before { | ||||
|   color: red; | ||||
| } | ||||
|  | ||||
| .danger { | ||||
|   background-color: rgb(254 171 171); | ||||
|   border-color: red; | ||||
| .top-bar > h1 { | ||||
| 	position: relative; | ||||
| 	text-align: center; | ||||
| 	color: #516754; | ||||
| 	font-size: 6rem; | ||||
| 	font-family: testFont, sans-serif; | ||||
| } | ||||
|   | ||||
							
								
								
									
										153
									
								
								source/static/Form.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,153 @@ | ||||
| @font-face { | ||||
| 	font-family: testFont; | ||||
| 	src: url("CoveredByYourGrace-Regular.ttf"); | ||||
| } | ||||
|  | ||||
| .journal-form h1 { | ||||
| 	font-family: testFont, sans-serif; | ||||
| 	text-align: center; | ||||
| } | ||||
|  | ||||
| .journal-form { | ||||
| 	font-size: 120%; | ||||
| 	font-family: "Century Gothic", sans-serif; | ||||
| 	width: 50%; | ||||
| 	margin: auto; | ||||
| 	color: #516754; | ||||
| 	border: 2px solid rgb(31 41 32); | ||||
| 	border-radius: 8px; | ||||
| 	background-color: #f7dfd5; | ||||
| } | ||||
|  | ||||
| fieldset { | ||||
| 	border: none; | ||||
| } | ||||
|  | ||||
| input[type="text"] { | ||||
| 	width: 100%; | ||||
| 	box-sizing: border-box; | ||||
| 	background-color: #f7dfd5; | ||||
| 	border: none; | ||||
| 	border-bottom: 1px solid rgb(0 0 0); | ||||
| } | ||||
|  | ||||
| input[type="text"]:focus { | ||||
| 	outline: none; | ||||
| 	border-bottom: 1px solid rgb(0 0 0); | ||||
| } | ||||
|  | ||||
| .rating { | ||||
| 	display: flex; | ||||
| 	flex-flow: nowrap row-reverse; | ||||
| } | ||||
|  | ||||
| .hidden, | ||||
| .rating:not(:checked) > input { /* Hide radio circles while star rating */ | ||||
| 	display: none; | ||||
| } | ||||
|  | ||||
| /* Unchecked stars */ | ||||
| .rating:not(:checked) > label { | ||||
| 	/* Make stars line up sideways and not vertically */ | ||||
| 	float: right; | ||||
|  | ||||
| 	/* Hide label text */ | ||||
| 	width: 1em; | ||||
| 	overflow: hidden; | ||||
| 	white-space: nowrap; | ||||
|  | ||||
| 	/* Star default color and size */ | ||||
| 	font-size: 200%; | ||||
| 	line-height: 1.2; | ||||
| 	color: #b3b3cc; | ||||
| } | ||||
|  | ||||
| .rating > label:active { | ||||
| 	position: relative; | ||||
| } | ||||
|  | ||||
| .rating:not(:checked) > label::before { | ||||
| 	content: "★"; | ||||
| } | ||||
|  | ||||
| /* Checked star color */ | ||||
| .rating > input:checked ~ label { | ||||
| 	color: #ffbf00; | ||||
| } | ||||
|  | ||||
| .rating:not(:checked) > label:hover, | ||||
| .rating:not(:checked) > label:hover ~ label { | ||||
| 	color: orangered; | ||||
| } | ||||
|  | ||||
| .rating > input:checked + label:hover, | ||||
| .rating > input:checked ~ label:hover, | ||||
| .rating > input:checked + label:hover ~ label, | ||||
| .rating > input:checked ~ label:hover ~ label, | ||||
| .rating > label:hover ~ input:checked ~ label { | ||||
| 	color: orangered; | ||||
| } | ||||
|  | ||||
| .tag-container { | ||||
| 	display: flex; | ||||
| 	flex-flow: wrap row; | ||||
| } | ||||
|  | ||||
| .tag { | ||||
| 	background-color: grey; | ||||
| 	border-radius: 7px; | ||||
| 	color: white; | ||||
| 	padding-right: 7px; | ||||
| 	padding-left: 7px; | ||||
| 	margin: 3px; | ||||
| } | ||||
|  | ||||
| .tag::before { | ||||
| 	display: inline-block; | ||||
| 	content: "x"; | ||||
| 	height: 15px; | ||||
| 	width: 15px; | ||||
| 	margin-right: 4px; | ||||
| 	text-align: center; | ||||
| 	color: white; | ||||
| 	cursor: pointer; | ||||
| } | ||||
|  | ||||
| .tag:hover::before { | ||||
| 	color: red; | ||||
| } | ||||
|  | ||||
| #tag-add-btn { | ||||
| 	background-color: #94da97; /* Green */ | ||||
| 	border: round; | ||||
| 	color: rgb(206 83 179); | ||||
| 	text-align: center; | ||||
| 	text-decoration: none; | ||||
| 	display: inline-block; | ||||
| 	font-size: 20px; | ||||
| 	cursor: pointer; | ||||
| 	border-radius: 10%; | ||||
| 	margin-top: 5px; | ||||
| } | ||||
|  | ||||
| #tag-add-btn:hover { | ||||
| 	background-color: rgb(206 83 179); /* Green */ | ||||
| 	border: round; | ||||
| 	color: #94da97; | ||||
| 	text-align: center; | ||||
| 	text-decoration: none; | ||||
| 	display: inline-block; | ||||
| 	font-size: 20px; | ||||
| 	cursor: pointer; | ||||
| 	border-radius: 10%; | ||||
| 	margin-top: 5px; | ||||
| } | ||||
|  | ||||
| .hidden { | ||||
| 	display: none; | ||||
| } | ||||
|  | ||||
| .tag-container * { | ||||
| 	max-width: 100%; | ||||
| 	overflow-wrap: anywhere; | ||||
| } | ||||
							
								
								
									
										93
									
								
								source/static/OFL.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,93 @@ | ||||
| Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com) | ||||
|  | ||||
| This Font Software is licensed under the SIL Open Font License, Version 1.1. | ||||
| This license is copied below, and is also available with a FAQ at: | ||||
| http://scripts.sil.org/OFL | ||||
|  | ||||
|  | ||||
| ----------------------------------------------------------- | ||||
| SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | ||||
| ----------------------------------------------------------- | ||||
|  | ||||
| PREAMBLE | ||||
| The goals of the Open Font License (OFL) are to stimulate worldwide | ||||
| development of collaborative font projects, to support the font creation | ||||
| efforts of academic and linguistic communities, and to provide a free and | ||||
| open framework in which fonts may be shared and improved in partnership | ||||
| with others. | ||||
|  | ||||
| The OFL allows the licensed fonts to be used, studied, modified and | ||||
| redistributed freely as long as they are not sold by themselves. The | ||||
| fonts, including any derivative works, can be bundled, embedded,  | ||||
| redistributed and/or sold with any software provided that any reserved | ||||
| names are not used by derivative works. The fonts and derivatives, | ||||
| however, cannot be released under any other type of license. The | ||||
| requirement for fonts to remain under this license does not apply | ||||
| to any document created using the fonts or their derivatives. | ||||
|  | ||||
| DEFINITIONS | ||||
| "Font Software" refers to the set of files released by the Copyright | ||||
| Holder(s) under this license and clearly marked as such. This may | ||||
| include source files, build scripts and documentation. | ||||
|  | ||||
| "Reserved Font Name" refers to any names specified as such after the | ||||
| copyright statement(s). | ||||
|  | ||||
| "Original Version" refers to the collection of Font Software components as | ||||
| distributed by the Copyright Holder(s). | ||||
|  | ||||
| "Modified Version" refers to any derivative made by adding to, deleting, | ||||
| or substituting -- in part or in whole -- any of the components of the | ||||
| Original Version, by changing formats or by porting the Font Software to a | ||||
| new environment. | ||||
|  | ||||
| "Author" refers to any designer, engineer, programmer, technical | ||||
| writer or other person who contributed to the Font Software. | ||||
|  | ||||
| PERMISSION & CONDITIONS | ||||
| Permission is hereby granted, free of charge, to any person obtaining | ||||
| a copy of the Font Software, to use, study, copy, merge, embed, modify, | ||||
| redistribute, and sell modified and unmodified copies of the Font | ||||
| Software, subject to the following conditions: | ||||
|  | ||||
| 1) Neither the Font Software nor any of its individual components, | ||||
| in Original or Modified Versions, may be sold by itself. | ||||
|  | ||||
| 2) Original or Modified Versions of the Font Software may be bundled, | ||||
| redistributed and/or sold with any software, provided that each copy | ||||
| contains the above copyright notice and this license. These can be | ||||
| included either as stand-alone text files, human-readable headers or | ||||
| in the appropriate machine-readable metadata fields within text or | ||||
| binary files as long as those fields can be easily viewed by the user. | ||||
|  | ||||
| 3) No Modified Version of the Font Software may use the Reserved Font | ||||
| Name(s) unless explicit written permission is granted by the corresponding | ||||
| Copyright Holder. This restriction only applies to the primary font name as | ||||
| presented to the users. | ||||
|  | ||||
| 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | ||||
| Software shall not be used to promote, endorse or advertise any | ||||
| Modified Version, except to acknowledge the contribution(s) of the | ||||
| Copyright Holder(s) and the Author(s) or with their explicit written | ||||
| permission. | ||||
|  | ||||
| 5) The Font Software, modified or unmodified, in part or in whole, | ||||
| must be distributed entirely under this license, and must not be | ||||
| distributed under any other license. The requirement for fonts to | ||||
| remain under this license does not apply to any document created | ||||
| using the Font Software. | ||||
|  | ||||
| TERMINATION | ||||
| This license becomes null and void if any of the above conditions are | ||||
| not met. | ||||
|  | ||||
| DISCLAIMER | ||||
| THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | ||||
| OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | ||||
| COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||||
| INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | ||||
| DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||||
| FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | ||||
| OTHER DEALINGS IN THE FONT SOFTWARE. | ||||
| @@ -1,83 +0,0 @@ | ||||
| /* main.css */ | ||||
|  | ||||
| * { | ||||
|   font-family: sans-serif; | ||||
| } | ||||
|  | ||||
| body { | ||||
|   height: 100%; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| fieldset { | ||||
|   border: 2px solid rgb(214 214 214); | ||||
|   box-sizing: border-box; | ||||
|   display: block; | ||||
|   width: max-content; | ||||
| } | ||||
|  | ||||
| form button { | ||||
|   display: block; | ||||
|   margin-top: 5px; | ||||
| } | ||||
|  | ||||
| label[for="ingredients"] p { | ||||
|   margin: 0; | ||||
| } | ||||
|  | ||||
| label[for="numRatings"] { | ||||
|   margin: 10px 0 0; | ||||
| } | ||||
|  | ||||
| label[for^="rating"] { | ||||
|   padding-right: 10px; | ||||
| } | ||||
|  | ||||
| label:not([for^="rating"]) { | ||||
|   display: block; | ||||
|   margin-bottom: 5px; | ||||
| } | ||||
|  | ||||
| main { | ||||
|   column-gap: 10px; | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   height: auto; | ||||
|   max-width: 660px; | ||||
|   row-gap: 10px; | ||||
|   width: 100%; | ||||
| } | ||||
|  | ||||
| .tag-container { | ||||
|   display: flex; | ||||
|   flex-flow: row wrap; | ||||
| } | ||||
|  | ||||
| .tag { | ||||
|   background-color: grey; | ||||
|   border-radius: 7px; | ||||
|   color: white; | ||||
|   padding-right: 7px; | ||||
|   padding-left: 7px; | ||||
|   margin: 3px; | ||||
| } | ||||
|  | ||||
| .tag::before { | ||||
|   display: inline-block; | ||||
|   content: "x"; | ||||
|   height: 15px; | ||||
|   width: 15px; | ||||
|   margin-right: 4px; | ||||
|   text-align: center; | ||||
|   color: white; | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| .tag:hover::before { | ||||
|   color: red; | ||||
| } | ||||
|  | ||||
| .danger { | ||||
|   background-color: rgb(254 171 171); | ||||
|   border-color: red; | ||||
| } | ||||
							
								
								
									
										45
									
								
								source/static/ReviewDetails.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,45 @@ | ||||
| /* ReviewDetails.css */ | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: testFont; | ||||
| 	src: url("CoveredByYourGrace-Regular.ttf"); | ||||
| } | ||||
|  | ||||
| body { | ||||
| 	background-color: #f8f3f1; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
| 	text-align: center; | ||||
| } | ||||
|  | ||||
| .top-bar { | ||||
| 	display: flex; | ||||
| 	justify-content: center; | ||||
| } | ||||
|  | ||||
| .top-bar > img { | ||||
| 	position: relative; | ||||
| 	align-self: center; | ||||
| 	padding-left: 2.5%; | ||||
| 	padding-right: 2.5%; | ||||
| } | ||||
|  | ||||
| .top-bar > h1 { | ||||
| 	position: relative; | ||||
| 	text-align: center; | ||||
|  | ||||
| 	/* color: #e4c3d2; */ | ||||
| 	color: #516754; | ||||
| 	font-size: 6rem; | ||||
| 	font-family: testFont, sans-serif; | ||||
| } | ||||
|  | ||||
| .d-tag { | ||||
| 	background-color: grey; | ||||
| 	border-radius: 7px; | ||||
| 	color: white; | ||||
| 	padding-right: 7px; | ||||
| 	padding-left: 7px; | ||||
| 	margin: 10px; | ||||
| } | ||||
							
								
								
									
										93
									
								
								source/static/homepage.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,93 @@ | ||||
| /* homepage.css */ | ||||
|  | ||||
| @font-face { | ||||
| 	font-family: testFont; | ||||
| 	src: url("CoveredByYourGrace-Regular.ttf"); | ||||
| } | ||||
|  | ||||
| /* Color */ | ||||
| body { | ||||
| 	/* background-color: #97a5bd */ | ||||
|  | ||||
| 	/* background-color: #E3E3EC; */ | ||||
| 	background-color: #f8f3f1; | ||||
| } | ||||
|  | ||||
| .top-bar { | ||||
| 	display: flex; | ||||
| 	justify-content: center; | ||||
| } | ||||
|  | ||||
| .top-bar > img { | ||||
| 	position: relative; | ||||
| 	align-self: center; | ||||
| 	padding-left: 2.5%; | ||||
| 	padding-right: 2.5%; | ||||
| } | ||||
|  | ||||
| .top-bar > h1 { | ||||
| 	position: relative; | ||||
| 	text-align: center; | ||||
|  | ||||
| 	/* color: #e4c3d2; */ | ||||
|  | ||||
| 	/* color: rgb(145, 124, 175); */ | ||||
| 	color: #516754; | ||||
| 	font-size: 6rem; | ||||
| 	font-family: testFont, sans-serif; | ||||
| } | ||||
|  | ||||
| .body-container { | ||||
| 	display: flex; | ||||
| 	max-height: 100%; | ||||
| } | ||||
|  | ||||
| .center-display { | ||||
| 	display: flex; | ||||
| 	flex-direction: row; | ||||
| 	justify-content: center; | ||||
| 	align-items: center; | ||||
| } | ||||
|  | ||||
| .search-bar { | ||||
| 	display: flex; | ||||
| 	justify-content: center; | ||||
| } | ||||
|  | ||||
| .search-bar > form { | ||||
| 	float: right; | ||||
| 	padding: 6px 10px; | ||||
| 	margin-top: 8px; | ||||
| 	margin-right: 16px; | ||||
| 	background: rgb(239 183 183); | ||||
| 	font-size: 17px; | ||||
| 	border: none; | ||||
| 	border-radius: 12px; | ||||
| 	cursor: pointer; | ||||
| } | ||||
|  | ||||
| #recent-reviews-text { | ||||
| 	text-align: center; | ||||
| 	font-size: 4rem; | ||||
| 	color: #516754; | ||||
| 	font-family: testFont, sans-serif; | ||||
| } | ||||
|  | ||||
| img#create-btn { | ||||
| 	position: relative; | ||||
| 	align-self: center; | ||||
| 	padding-left: 2.5%; | ||||
| 	padding-right: 2.5%; | ||||
| } | ||||
|  | ||||
| .review-container { | ||||
| 	display: flex; | ||||
| 	position: relative; | ||||
| 	flex-wrap: wrap; | ||||
| 	justify-content: center; | ||||
| } | ||||
|  | ||||
| .review-container > div { | ||||
| 	background-color: #f1f1f1; | ||||
| 	text-align: center; | ||||
| } | ||||
| @@ -1,158 +0,0 @@ | ||||
| /* This is a generic CSS file that sets preliminary rules for content that should be the same across pages */ | ||||
|  | ||||
| html, | ||||
| body, | ||||
| div, | ||||
| span, | ||||
| object, | ||||
| iframe, | ||||
| h1, | ||||
| h2, | ||||
| h3, | ||||
| h4, | ||||
| h5, | ||||
| h6, | ||||
| p, | ||||
| blockquote, | ||||
| pre, | ||||
| abbr, | ||||
| address, | ||||
| cite, | ||||
| code, | ||||
| del, | ||||
| dfn, | ||||
| em, | ||||
| img, | ||||
| ins, | ||||
| kbd, | ||||
| q, | ||||
| samp, | ||||
| small, | ||||
| strong, | ||||
| sub, | ||||
| sup, | ||||
| var, | ||||
| b, | ||||
| i, | ||||
| dl, | ||||
| dt, | ||||
| dd, | ||||
| ol, | ||||
| ul, | ||||
| li, | ||||
| fieldset, | ||||
| form, | ||||
| label, | ||||
| legend, | ||||
| table, | ||||
| caption, | ||||
| tbody, | ||||
| tfoot, | ||||
| thead, | ||||
| tr, | ||||
| th, | ||||
| td, | ||||
| article, | ||||
| aside, | ||||
| canvas, | ||||
| details, | ||||
| figcaption, | ||||
| figure, | ||||
| footer, | ||||
| header, | ||||
| hgroup, | ||||
| menu, | ||||
| nav, | ||||
| section, | ||||
| summary, | ||||
| time, | ||||
| mark, | ||||
| audio, | ||||
| video { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   border: 0; | ||||
|   outline: 0; | ||||
|   font-size: 100%; | ||||
|   vertical-align: baseline; | ||||
|   background: transparent; | ||||
| } | ||||
|  | ||||
| body { | ||||
|   line-height: 1; | ||||
| } | ||||
|  | ||||
| article, | ||||
| aside, | ||||
| details, | ||||
| figcaption, | ||||
| figure, | ||||
| footer, | ||||
| header, | ||||
| hgroup, | ||||
| menu, | ||||
| nav, | ||||
| section { | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| nav ul { | ||||
|   list-style: none; | ||||
| } | ||||
|  | ||||
| blockquote, | ||||
| q { | ||||
|   quotes: none; | ||||
| } | ||||
|  | ||||
| blockquote::before, | ||||
| blockquote::after, | ||||
| q::before, | ||||
| q::after { | ||||
|   content: ""; | ||||
|   content: none; | ||||
| } | ||||
|  | ||||
| a { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   font-size: 100%; | ||||
|   vertical-align: baseline; | ||||
|   background: transparent; | ||||
| } | ||||
|  | ||||
| table { | ||||
|   border-collapse: collapse; | ||||
|   border-spacing: 0; | ||||
| } | ||||
|  | ||||
| input, | ||||
| select { | ||||
|   vertical-align: middle; | ||||
| } | ||||
|  | ||||
| img, | ||||
| fieldset, | ||||
| object { | ||||
|   border: none; | ||||
| } | ||||
|  | ||||
| *, | ||||
| *::after, | ||||
| *::before { | ||||
|   box-sizing: border-box; | ||||
| } | ||||
|  | ||||
| button, | ||||
| label { | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| html, | ||||
| body { | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| form { | ||||
|   border: solid; | ||||
| } | ||||
							
								
								
									
										19
									
								
								specs/adrs/111622-e2etesting-puppeteer.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| # Use puppeteer for JS unit testing framework | ||||
|  | ||||
| - Status: accept | ||||
| - Deciders: Arthur Lu, Marc Reta | ||||
| - Date: 11 / 16 / 22 | ||||
|  | ||||
| ## Decision Drivers | ||||
|  | ||||
| - Need end to end testing framework which runs headlessly and quickly | ||||
| - Framework should integrate well with Mocha, the existing unit testing framework | ||||
| - Framework should be easy to implement end to end tests with | ||||
|  | ||||
| ## Considered Options | ||||
| - puppeteer | ||||
| - selenium-webdriver | ||||
|  | ||||
| ## Decision Outcome | ||||
|  | ||||
| Chosen Option: Puppeteer for its ease of use with mocha.  | ||||