Merge pull request #79 from cse110-fa22-group29/sprint-2

Sprint 2
This commit is contained in:
Arthur Lu 2022-11-22 16:40:27 -08:00 committed by GitHub
commit 33e619c2d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1529 additions and 873 deletions

View File

@ -20,4 +20,4 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: sudo npm install run: sudo npm install
- name: Run tests - name: Run tests
run: sudo npm run lintCSS run: sudo npm run lint-css

View File

@ -20,4 +20,4 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: sudo npm install run: sudo npm install
- name: Run tests - name: Run tests
run: sudo npm run lintHTML run: sudo npm run lint-html

View File

@ -22,4 +22,4 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: sudo npm install run: sudo npm install
- name: Run tests - name: Run tests
run: sudo npm run lint run: sudo npm run lint-js

View File

@ -23,5 +23,7 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Install dependencies - name: Install dependencies
run: sudo npm install run: sudo npm install
- name: Start local http server
run: sudo npm run http-server &
- name: Run tests - name: Run tests
run: sudo npm test run: sudo npm test

View File

@ -1,3 +1,4 @@
{ {
"attr-value-not-empty": false "attr-value-not-empty": false,
"space-tab-mixed-disabled": "tab"
} }

View File

@ -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
View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

View File

@ -4,16 +4,20 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "mocha --recursive --require mock-local-storage './{,!(node_modules)/**}/*.test.js'", "test": "mocha --recursive --require mock-local-storage './{,!(node_modules)/**}/*.test.js'",
"lint": "eslint '**/*.js'", "lint-js": "eslint **/*.js",
"fix-style": "eslint --fix **/*.js", "fix-js": "eslint --fix **/*.js",
"lintHTML": "htmlhint '**/*.html'", "lint-html": "htmlhint **/*.html",
"lintCSS": "stylelint '**/*.css'" "lint-css": "stylelint **/*.css",
"fix-css": "stylelint --fix **/*.css",
"http-server": "http-server source"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.27.0", "eslint": "^8.27.0",
"htmlhint": "1.1.4", "htmlhint": "1.1.4",
"http-server": "",
"mocha": "10", "mocha": "10",
"mock-local-storage": "^1.1.23", "mock-local-storage": "^1.1.23",
"puppeteer": "^18.2.1",
"stylelint": "14.14.1", "stylelint": "14.14.1",
"stylelint-config-standard": "^29.0.0" "stylelint-config-standard": "^29.0.0"
} }

View File

@ -1,12 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Food Journal</title> <title>Food Journal</title>
<!--Add Favicon-->
<link rel="icon" type="image/x-icon" href="./assets/images/favicon.ico">
<!-- Recipe Card Custom Element --> <!-- Recipe Card Custom Element -->
<script src="assets/scripts/ReviewCard.js" type="module"></script> <script src="assets/scripts/ReviewCard.js" type="module"></script>
@ -15,63 +18,76 @@
<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> <!-- 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/reset.css" /> -->
<link rel="stylesheet" href="./static/CreatePage.css" /> <link rel="stylesheet" href="./static/CreatePage.css" />
<script src="assets/scripts/main.js" type="module"></script> <link rel="stylesheet" href="./static/Form.css" />
<script src="./assets/scripts/CreatePage.js" type="module"></script>
</head> </head>
<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>
<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="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">
<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> </div>
<button type="button" id="tagAdd">Add Tag</button> </header>
</label>
<body>
<div class="journal-form">
<h1>New 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>
<button type="submit" value="Submit">Add Review</button>
<button type="button" class="danger">Clear Review Journal</button> <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> </form>
</body> </div>
</body>
</html> </html>

View File

@ -1,77 +1,122 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Food Journal</title> <title>Food Journal</title>
<!--Add Favicon-->
<link rel="icon" type="image/x-icon" href="./assets/images/icons/favicon.ico">
<!-- Recipe Card Custom Element --> <!-- Recipe Card Custom Element -->
<script src="assets/scripts/ReviewCard.js" type="module"></script> <script src="assets/scripts/ReviewCard.js" type="module"></script>
<!-- Main Stylesheets & Scripts --> <!-- Main Stylesheets & Scripts -->
<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> <!-- 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/reset.css" /> -->
<link rel="stylesheet" href="./static/ReviewCard.css" /> <link rel="stylesheet" href="./static/ReviewDetails.css" />
<link rel="stylesheet" href="./static/Form.css" />
<script src="assets/scripts/ReviewDetails.js" type="module"></script> <script src="assets/scripts/ReviewDetails.js" type="module"></script>
</head>
</head> <body>
<body> <header>
<main> <div class="top-bar">
<button type="button" id="update">Update</button> <img src ="./assets/images/Logo.png" alt="logo" />
<button type="button" id="delete" class="danger">Delete</button> <h1> Food Journal </h1>
</main> <img src ="./assets/images/Logo.png" alt="logo" />
<!----> <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>
<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="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> </div>
<button type="button" id="tagAdd">Add Tag</button> </header>
</label>
<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> </fieldset>
<button type="submit" value="Submit">Add Review</button>
<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 = "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 class = "meal-tags">
<div class = "tag-container" id="d-tags" style="justify-content: center;"></div>
</fieldset>
</form> </form>
</body> </div>
<!---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> </html>

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 311 KiB

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 253 KiB

View 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 = "";
}
});
}

View File

@ -6,7 +6,6 @@ class ReviewCard extends HTMLElement {
constructor() { constructor() {
super(); super();
let shadowEl = this.attachShadow({mode:"open"}); let shadowEl = this.attachShadow({mode:"open"});
let articleEl = document.createElement("article"); let articleEl = document.createElement("article");
@ -14,9 +13,10 @@ class ReviewCard extends HTMLElement {
let styleEl = document.createElement("style"); let styleEl = document.createElement("style");
styleEl.textContent = ` styleEl.textContent = `
* { * {
font-family: sans-serif; font-family: Century Gothic;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-wrap: anywhere;
} }
a { a {
@ -29,7 +29,7 @@ class ReviewCard extends HTMLElement {
article { article {
align-items: center; align-items: center;
border: 1px solid rgb(223, 225, 229); border: 2px solid rgb(31, 41, 32);
border-radius: 8px; border-radius: 8px;
display: grid; display: grid;
grid-template-rows: 118px 56px 14px 18px 15px 36px; grid-template-rows: 118px 56px 14px 18px 15px 36px;
@ -37,6 +37,7 @@ class ReviewCard extends HTMLElement {
row-gap: 5px; row-gap: 5px;
padding: 0 16px 16px 16px; padding: 0 16px 16px 16px;
width: 178px; width: 178px;
margin: 8px 8px 8px 8px;
} }
div.rating { div.rating {
@ -53,9 +54,9 @@ class ReviewCard extends HTMLElement {
} }
article>img { article>img {
border-top-left-radius: 8px; border-top-left-radius: 6px;
border-top-right-radius: 8px; border-top-right-radius: 6px;
height: 118px; height: 119px;
object-fit: cover; object-fit: cover;
margin-left: -16px; margin-left: -16px;
width: calc(100% + 32px); width: calc(100% + 32px);
@ -81,6 +82,22 @@ class ReviewCard extends HTMLElement {
color: #70757A; color: #70757A;
font-size: 12px; 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); articleEl.append(styleEl);
shadowEl.append(articleEl); shadowEl.append(articleEl);
@ -88,9 +105,9 @@ class ReviewCard extends HTMLElement {
//attach event listener to each recipe-card //attach event listener to each recipe-card
this.addEventListener("click", (event) => { this.addEventListener("click", (event) => {
console.log(event.target); 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) //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"); window.location.assign("./ReviewDetails.html");
/* /*
//Option 2: sending current data to second html page using string query w/ url (currently not storing value) //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: * following format:
* { * {
* "mealImg": "string", * "mealImg": "string",
* "imgAlt": "string",
* "mealName": "string", * "mealName": "string",
* "comments": "string", * "comments": "string",
* "rating": number, * "rating": number,
@ -133,12 +149,17 @@ class ReviewCard extends HTMLElement {
let articleEl = this.shadowEl.querySelector("article"); let articleEl = this.shadowEl.querySelector("article");
// setting the article elements for the review card // setting the article elements for the review card
this.reviewID = data["reviewID"];
//image setup //image setup
let mealImg = document.createElement("img"); let mealImg = document.createElement("img");
mealImg.setAttribute("id", "a-mealImg"); mealImg.setAttribute("id", "a-mealImg");
mealImg.setAttribute("alt","Meal Photo Corrupted");
mealImg.setAttribute("src",data["mealImg"]); 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 //meal name setup
let mealLabel = document.createElement("label"); let mealLabel = document.createElement("label");
@ -147,26 +168,6 @@ class ReviewCard extends HTMLElement {
mealLabel.innerHTML = data["mealName"]; mealLabel.innerHTML = data["mealName"];
//restaurant name setup //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"); let restaurantLabel = document.createElement("label");
restaurantLabel.setAttribute("id", "a-restaurant"); restaurantLabel.setAttribute("id", "a-restaurant");
restaurantLabel.setAttribute("class","restaurant-name"); restaurantLabel.setAttribute("class","restaurant-name");
@ -183,7 +184,7 @@ class ReviewCard extends HTMLElement {
ratingDiv.setAttribute("class", "rating"); ratingDiv.setAttribute("class", "rating");
let starsImg = document.createElement("img"); let starsImg = document.createElement("img");
starsImg.setAttribute("id", "a-rating"); 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("alt", data["rating"] +" stars");
starsImg.setAttribute("num", data["rating"]); starsImg.setAttribute("num", data["rating"]);
ratingDiv.append(starsImg); ratingDiv.append(starsImg);
@ -196,15 +197,16 @@ class ReviewCard extends HTMLElement {
if(data["tags"]){ if(data["tags"]){
for (let i = 0; i < data["tags"].length; i++) { for (let i = 0; i < data["tags"].length; i++) {
let newTag = document.createElement("label"); let newTag = document.createElement("label");
newTag.setAttribute("class","tag"); newTag.setAttribute("class","a-tag");
newTag.innerHTML = data["tags"][i] + " "; newTag.innerHTML = data["tags"][i];
tagContainer.append(newTag); tagContainer.append(newTag);
} }
} }
//adding final ID to data!
articleEl.append(mealImg); articleEl.append(mealImg);
articleEl.append(mealLabel); articleEl.append(mealLabel);
//articleEl.append(reviewLink)
articleEl.append(restaurantLabel); articleEl.append(restaurantLabel);
articleEl.append(ratingDiv); articleEl.append(ratingDiv);
articleEl.append(tagContainer); articleEl.append(tagContainer);
@ -224,7 +226,6 @@ class ReviewCard extends HTMLElement {
* following format: * following format:
* { * {
* "mealImg": "string", * "mealImg": "string",
* "imgAlt": "string",
* "mealName": "string", * "mealName": "string",
* "comments": "string", * "comments": "string",
* "rating": number, * "rating": number,
@ -237,11 +238,11 @@ class ReviewCard extends HTMLElement {
let dataContainer = {}; let dataContainer = {};
// getting the article elements for the review card // getting the article elements for the review card
dataContainer["reviewID"] = this.reviewID;
//get image //get image
let mealImg = this.shadowEl.getElementById("a-mealImg"); let mealImg = this.shadowEl.getElementById("a-mealImg");
dataContainer["mealImg"] = mealImg.getAttribute("src"); dataContainer["mealImg"] = mealImg.getAttribute("src");
dataContainer["imgAlt"] = mealImg.getAttribute("alt");
//get meal name //get meal name
let mealLabel = this.shadowEl.getElementById("a-mealName"); let mealLabel = this.shadowEl.getElementById("a-mealName");

View File

@ -1,71 +1,142 @@
//reviewDetails.js //reviewDetails.js
import {getReviewsFromStorage, saveReviewsToStorage} from "./localStorage.js"; import {deleteReviewFromStorage, getReviewFromStorage, updateReviewToStorage} from "./localStorage.js";
// Run the init() function when the page has loaded // Run the init() function when the page has loaded
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
function init(){ function init(){
setupInfo();
setupDelete(); setupDelete();
setupUpdate(); 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(){ function setupDelete(){
let deleteBtn = document.getElementById("delete"); let deleteBtn = document.getElementById("delete-btn");
let reviews = getReviewsFromStorage(); let currID = JSON.parse(sessionStorage.getItem("currID"));
let current = JSON.parse(sessionStorage.getItem("current"));
deleteBtn.addEventListener("click", function(){ deleteBtn.addEventListener("click", function(){
if(window.confirm("Are you sure you want to delete this entry?")){ if(window.confirm("Are you sure you want to delete this entry?")){
//delete function deleteReviewFromStorage(currID);
if(current){ sessionStorage.removeItem("currID");
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"); window.location.assign("./index.html");
break;
}
}
}
} }
}); });
} }
function setupUpdate(){ function setupUpdate(){
let updateBtn = document.getElementById("update"); let updateBtn = document.getElementById("update-btn");
let reviews = getReviewsFromStorage(); let currID = JSON.parse(sessionStorage.getItem("currID"));
let current = JSON.parse(sessionStorage.getItem("current")); let currReview = getReviewFromStorage(currID);
let form = document.getElementById("update-food-entry"); let form = document.getElementById("new-food-entry");
let updateDiv = document.getElementById("update-form");
updateBtn.addEventListener("click", function(){ updateBtn.addEventListener("click", function(){
//update function //update function
if(current){
console.log(current); updateDiv.classList.remove("hidden");
form.style.display = "block";
let tagContainer = document.getElementById("tag-container-form"); 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 //Set value of each input element to current's values
document.getElementById("mealImg").defaultValue = current["mealImg"]; document.getElementById("mealImg").defaultValue = currReview["mealImg"];
document.getElementById("imgAlt").defaultValue = current["imgAlt"]; document.getElementById("mealName").defaultValue = currReview["mealName"];
document.getElementById("mealName").defaultValue = current["mealName"]; document.getElementById("comments").textContent = currReview["comments"];
document.getElementById("comments").textContent = current["comments"]; document.getElementById("s" + `${currReview["rating"]}`).checked = true;
document.getElementById("rating-" + `${current["rating"]}`).checked = true; document.getElementById("restaurant").defaultValue = currReview["restaurant"];
document.getElementById("restaurant").defaultValue = current["restaurant"];
if(current["tags"]){ if(currReview["tags"]){
for (let i = 0; i < current["tags"].length; i++) { while (tagContainer.firstChild) {
tagContainer.removeChild(tagContainer.firstChild);
}
for (let i = 0; i < currReview["tags"].length; i++) {
let newTag = document.createElement("label"); let newTag = document.createElement("label");
newTag.setAttribute("class","tag"); newTag.setAttribute("class","tag");
newTag.innerHTML = current["tags"][i] + " "; newTag.innerHTML = currReview["tags"][i];
newTag.addEventListener("click",()=> { newTag.addEventListener("click",()=> {
tagContainer.removeChild(newTag); tagContainer.removeChild(newTag);
}); });
tagContainer.append(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 //Take formdata values as newData when submit
form.addEventListener("submit", function(){ form.addEventListener("submit", function(){
/* /*
@ -80,6 +151,13 @@ function setupUpdate(){
if (`${key}` !== "tag-form") { if (`${key}` !== "tag-form") {
newData[`${key}`] = `${value}`; 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;
}
} }
newData["tags"] = []; newData["tags"] = [];
@ -89,22 +167,15 @@ function setupUpdate(){
tagContainer.removeChild(tags[i]); tagContainer.removeChild(tags[i]);
} }
for(let i = 0; i < reviews.length; i++){ newData["reviewID"] = currID;
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;
}
}
form.style.display = "none"; updateReviewToStorage(currID, newData);
updateDiv.classList.add("hidden");
}); });
let tagAddBtn = document.getElementById("tagAdd"); let tagAddBtn = document.getElementById("tag-add-btn");
tagAddBtn.addEventListener("click", ()=> { tagAddBtn.addEventListener("click", ()=> {
let tagField = document.getElementById("tag-form"); let tagField = document.getElementById("tag-form");
if (tagField.value.length > 0) { if (tagField.value.length > 0) {
@ -119,6 +190,5 @@ function setupUpdate(){
tagField.value = ""; tagField.value = "";
} }
}); });
}
}); });
} }

View File

@ -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);
}

View 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);
}

View File

@ -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() { export function newReviewToStorage(review){
let result = JSON.parse(localStorage.getItem("reviews")); //grabbing the nextID, and putting our review object in storage associated with the ID
if (result) { let nextReviewId = JSON.parse(localStorage.getItem("nextID"));
return result; review["reviewID"] = nextReviewId;
}
return new Array(0); // 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 * Gets a single review by ID from storage
* saves that string to 'reviews' in localStorage * @param {string} ID of the review to get
* @param {Array<Object>} reviews An array of reviews * @returns {Object} review object corresponding to param ID
*/ */
export function saveReviewsToStorage(reviews) { export function getReviewFromStorage(ID){
localStorage.setItem("reviews", JSON.stringify(reviews)); 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;
} }

View File

@ -1,48 +1,103 @@
import {strict as assert} from "node:assert"; import {strict as assert} from "node:assert";
import {describe, it, beforeEach} from "mocha"; import {describe, it, before, after} from "mocha";
import {saveReviewsToStorage, getReviewsFromStorage} from "./localStorage.js"; import {newReviewToStorage, getReviewFromStorage, updateReviewToStorage, deleteReviewFromStorage, getAllReviewsFromStorage} from "./localStorage.js";
beforeEach(() => {
localStorage.clear();
});
describe("test app localStorage interaction", () => { 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", "imgSrc": "sample src",
"imgAlt": "sample alt",
"mealName": "sample name", "mealName": "sample name",
"restaurant": "sample restaurant", "restaurant": "sample restaurant",
"rating": 5, "rating": 5,
"tags": ["tag 1", "tag 2", "tag 3"] "tags": ["tag 1", "tag 2", "tag 3"]
}]; };
saveReviewsToStorage(reviews); newReviewToStorage(review);
assert.deepEqual(getReviewsFromStorage(), reviews);
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 = 0; i < 1000; i++){ for(let i = 1; i < 1000; i++){
reviews = getReviewsFromStorage(); ids.push(i);
let new_review = {
reviews.push(
{
"imgSrc": `sample src ${i}`, "imgSrc": `sample src ${i}`,
"imgAlt": `sample alt ${i}`,
"mealName": `sample name ${i}`, "mealName": `sample name ${i}`,
"restaurant": `sample restaurant ${i}`, "restaurant": `sample restaurant ${i}`,
"rating": i, "rating": i,
"tags": [`tag ${3*i}`, `tag ${3*i + 1}`, `tag ${3*i + 2}`] "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);
saveReviewsToStorage(reviews);
assert.deepEqual(getReviewsFromStorage(), reviews); 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++){
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[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(() => {});
}); });

View 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();
});
});

View File

@ -1,28 +1,29 @@
// main.js // main.js
import {getReviewsFromStorage, saveReviewsToStorage} from "./localStorage.js"; import {getAllReviewsFromStorage} from "./localStorage.js";
// Run the init() function when the page has loaded // Run the init() function when the page has loaded
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
function init() { function init() {
// Get the reviews from localStorage // Get the reviews from localStorage
let reviews = getReviewsFromStorage(); let reviews = getAllReviewsFromStorage();
// Add each reviews to the <main> element // Add each reviews to the <main> element
addReviewsToDocument(reviews); addReviewsToDocument(reviews);
// Add the event listeners to the form elements // Add the event listeners to the form elements
initFormHandler(); initFormHandler();
} }
/** /**
* @param {Array<Object>} reviews An array of reviews * @param {Array<Object>} reviews An array of reviews
*/ */
function addReviewsToDocument(reviews) { function addReviewsToDocument(reviews) {
let mainEl = document.querySelector("main"); let reviewBox = document.getElementById("review-container");
reviews.forEach(review => { reviews.forEach(review => {
let newReview = document.createElement("review-card"); let newReview = document.createElement("review-card");
newReview.data = review; newReview.data = review;
//TODO: want to append it to whatever the box is in layout //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() { function initFormHandler() {
/*
//btn to create form (could be its own function?) //btn to create form (could be its own function?)
let createBtn = document.getElementById("create"); let createBtn = document.getElementById("create-btn");
createBtn.addEventListener("click", function(){ createBtn.addEventListener("click", function(){
window.location.assign("./CreatePage.html"); 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 = "";
}
});
} }

View File

@ -1,79 +1,57 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Food Journal</title> <title>Food Journal</title>
<!--Add Favicon-->
<link rel="icon" type="image/x-icon" href="./assets/images/favicon.ico">
<!-- Recipe Card Custom Element --> <!-- Recipe Card Custom Element -->
<script src="assets/scripts/ReviewCard.js" type="module"></script> <script src="assets/scripts/ReviewCard.js" type="module"></script>
<!-- Main Stylesheets & Scripts --> <!-- Main Stylesheets & Scripts -->
<!-- Temporarily commented out reset.css due to furthur discussion needed on the values of the default config--> <!-- 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/reset.css" /> -->
<link rel="stylesheet" href="./static/ReviewCard.css" /> <link rel="stylesheet" href="./static/homepage.css" />
<script src="assets/scripts/main.js" type="module"></script> <script src="assets/scripts/main.js" type="module"></script>
</head>
</head> <body>
<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> <main>
<!-- Add Food Entries Here --> <div class="body-container">
</main> <div style="width: 20%;"></div>
<!--<button type="button" id="create">CREATE</button>--> <div style="width: 60%;">
<!----> <form id="new-food-entry"> <div class="search-bar">
<fieldset> <form id="form">
<legend>Pic:</legend> <input type="search" id="searching" name="searchBar" placeholder="Search journal...">
<label for="mealImage"> <button class="click" type="search"> Search </button>
Source: <div class="Filter-box">
<input type="text" id="mealImg" name="mealImg">
</label>
<label for="image-alt">
Alt Text:
<input type="text" id="imgAlt" name="imgAlt">
</label>
</fieldset>
<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="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> </div>
<button type="button" id="tagAdd">Add Tag</button> </form>
</label> </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;"/>
</fieldset> </div>
<button type="submit" value="Submit">Add Review</button>
<button type="button" class="danger">Clear Review Journal</button> <div class="review-container" id="review-container"></div>
</form> <!----> </div>
</body> <div style="width: 20%;">
</div>
</div>
</main>
</body>
</html> </html>

View File

@ -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>

Binary file not shown.

View File

@ -1,83 +1,30 @@
/* CreatePage.css */ /* CreatePage.css */
* { @font-face {
font-family: sans-serif; font-family: testFont;
src: url("CoveredByYourGrace-Regular.ttf");
} }
body { body {
height: 100%; background-color: #f8f3f1;
width: 100%;
} }
fieldset { .top-bar {
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; display: flex;
flex-wrap: wrap; justify-content: center;
height: auto;
max-width: 660px;
row-gap: 10px;
width: 100%;
} }
.tag-container { .top-bar > img {
display: flex; position: relative;
flex-flow: row wrap; align-self: center;
padding-left: 2.5%;
padding-right: 2.5%;
} }
.tag { .top-bar > h1 {
background-color: grey; position: relative;
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; text-align: center;
color: white; color: #516754;
cursor: pointer; font-size: 6rem;
} font-family: testFont, sans-serif;
.tag:hover::before {
color: red;
}
.danger {
background-color: rgb(254 171 171);
border-color: red;
} }

153
source/static/Form.css Normal file
View 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
View 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.

View File

@ -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;
}

View 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;
}

View 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;
}

View File

@ -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;
}

View 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.