├── .gitignore ├── images └── logo.png ├── js ├── script.js ├── fake-results.json ├── shop.js ├── fake-products.json └── ask.js ├── .prettierrc.json ├── README.md ├── css ├── style.css ├── footer.css ├── main.css ├── results.css ├── button.css ├── products.css ├── global.css ├── search.css ├── ask.css └── header.css ├── .vscode └── settings.json ├── index.html ├── about └── index.html ├── shop └── index.html └── ask └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/orangespaceman/tfs-project-ai-frontend/main/images/logo.png -------------------------------------------------------------------------------- /js/script.js: -------------------------------------------------------------------------------- 1 | import ask from "./ask.js"; 2 | import shop from "./shop.js"; 3 | 4 | ask.init(); 5 | shop.init(); 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Project Frontend 2 | 3 | This is an example frontend for the AI project. 4 | 5 | You will want to fork this repository so that you can integrate the API and make modifications to the design and functionality. 6 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @import url("global.css"); 2 | @import url("header.css"); 3 | @import url("main.css"); 4 | @import url("button.css"); 5 | @import url("ask.css"); 6 | @import url("results.css"); 7 | @import url("search.css"); 8 | @import url("products.css"); 9 | @import url("footer.css"); 10 | -------------------------------------------------------------------------------- /css/footer.css: -------------------------------------------------------------------------------- 1 | /* Footer */ 2 | .footer { 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | margin-left: auto; 7 | margin-right: auto; 8 | max-width: var(--width-max); 9 | padding: var(--spacing-l); 10 | } 11 | .footer__link { 12 | text-decoration: none; 13 | } 14 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | .main { 2 | padding: var(--spacing-l); 3 | max-width: var(--width-max); 4 | margin-left: auto; 5 | margin-right: auto; 6 | margin-top: calc(-1 * var(--spacing-xl)); 7 | background: var(--colour-background-white); 8 | border-radius: var(--border-radius-l); 9 | box-shadow: var(--box-shadow); 10 | } 11 | -------------------------------------------------------------------------------- /js/fake-results.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Jupiter", 4 | "description": "A big planet" 5 | }, 6 | { 7 | "title": "Saturn", 8 | "description": "A planet with rings" 9 | }, 10 | { 11 | "title": "The Moon", 12 | "description": "Not a planet" 13 | }, 14 | { 15 | "title": "Aliens", 16 | "description": "The truth is out there..." 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[json]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[html]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[css]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[jsonc]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[javascriptreact]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | } 21 | } -------------------------------------------------------------------------------- /css/results.css: -------------------------------------------------------------------------------- 1 | /* Results */ 2 | .results { 3 | display: none; 4 | text-align: left; 5 | padding: var(--spacing-l); 6 | border-top: 2px solid var(--colour-background-light); 7 | } 8 | 9 | .results.is-shown { 10 | display: block; 11 | } 12 | 13 | .results__list { 14 | display: flex; 15 | flex-direction: column; 16 | align-items: stretch; 17 | gap: var(--spacing-l); 18 | } 19 | 20 | .results__item { 21 | background: var(--colour-background-white); 22 | color: var(--colour-text-dark); 23 | padding: var(--spacing-l); 24 | border: 1px solid var(--colour-background-light); 25 | border-radius: var(--border-radius-l); 26 | } 27 | 28 | .results__show-more { 29 | margin-top: var(--spacing-l); 30 | padding: var(--spacing-l); 31 | text-align: center; 32 | } 33 | -------------------------------------------------------------------------------- /css/button.css: -------------------------------------------------------------------------------- 1 | .buttons { 2 | display: flex; 3 | gap: var(--spacing-l); 4 | align-items: center; 5 | } 6 | 7 | .button { 8 | padding: var(--spacing-s) var(--spacing-m); 9 | border-radius: var(--border-radius-s); 10 | cursor: pointer; 11 | transition: all 0.25s; 12 | } 13 | 14 | .button:disabled { 15 | opacity: 0.3; 16 | pointer-events: none; 17 | } 18 | 19 | .button--primary { 20 | border: 1px solid var(--colour-primary); 21 | background: var(--colour-primary); 22 | color: var(--colour-text-light); 23 | } 24 | 25 | .button--primary:hover { 26 | background: var(--colour-background-white); 27 | color: var(--colour-primary); 28 | } 29 | 30 | .button--secondary { 31 | background: transparent; 32 | border: 1px solid var(--colour-background-light); 33 | color: var(--colour-primary); 34 | } 35 | 36 | .button--secondary:hover { 37 | border-color: var(--colour-primary); 38 | } 39 | -------------------------------------------------------------------------------- /css/products.css: -------------------------------------------------------------------------------- 1 | .products { 2 | display: none; 3 | text-align: left; 4 | padding: var(--spacing-l); 5 | border-top: 2px solid var(--colour-background-light); 6 | } 7 | 8 | .products.is-shown { 9 | display: block; 10 | } 11 | 12 | .products__list { 13 | display: flex; 14 | flex-wrap: wrap; 15 | flex-direction: row; 16 | align-items: stretch; 17 | gap: var(--spacing-l); 18 | } 19 | 20 | .products__item { 21 | background: var(--colour-background-white); 22 | color: var(--colour-text-dark); 23 | padding: var(--spacing-l); 24 | border: 1px solid var(--colour-background-light); 25 | border-radius: var(--border-radius-l); 26 | } 27 | 28 | @media (min-width: 800px) { 29 | .products__item { 30 | width: 31%; 31 | } 32 | } 33 | 34 | .products__item-image { 35 | width: 100%; 36 | } 37 | 38 | .products__show-more { 39 | margin-top: var(--spacing-l); 40 | padding: var(--spacing-l); 41 | text-align: center; 42 | } 43 | -------------------------------------------------------------------------------- /css/global.css: -------------------------------------------------------------------------------- 1 | /* Variables */ 2 | :root { 3 | --colour-primary: #6200ea; 4 | --colour-secondary: #ff0080; 5 | --colour-background-white: white; 6 | --colour-background-light: #f0f0f0; 7 | --colour-background-dark: black; 8 | --colour-text-light: white; 9 | --colour-text-dark: black; 10 | 11 | --width-max: 800px; 12 | 13 | --spacing-xs: 8px; 14 | --spacing-s: 10px; 15 | --spacing-m: 15px; 16 | --spacing-l: 20px; 17 | --spacing-xl: 40px; 18 | --spacing-xxl: 80px; 19 | 20 | --font-size-heading: 48px; 21 | --font-size-subheading: 24px; 22 | --font-size-body: 16px; 23 | --font-size-small: 12px; 24 | --font-weight-bold: bold; 25 | --font-family-body: Arial, sans-serif; 26 | 27 | --border-radius-s: 5px; 28 | --border-radius-m: 10px; 29 | --border-radius-l: 20px; 30 | 31 | --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 32 | } 33 | 34 | *, 35 | *:before, 36 | *:after { 37 | box-sizing: border-box; 38 | } 39 | 40 | /* General Styles */ 41 | body { 42 | font-family: var(--font-family-body); 43 | margin: 0; 44 | padding: 0; 45 | text-align: center; 46 | } 47 | -------------------------------------------------------------------------------- /css/search.css: -------------------------------------------------------------------------------- 1 | .search { 2 | padding: var(--spacing-xl) var(--spacing-l); 3 | text-align: left; 4 | } 5 | 6 | .search__input-wrapper { 7 | display: flex; 8 | flex-direction: column; 9 | gap: var(--spacing-l); 10 | } 11 | 12 | @media (min-width: 800px) { 13 | .search__input-wrapper { 14 | flex-direction: row; 15 | } 16 | } 17 | 18 | .search__input { 19 | flex: 1; 20 | border: 1px solid var(--colour-background-light); 21 | border-radius: var(--border-radius-s); 22 | background: var(--colour-background-light); 23 | color: var(--colour-text-dark); 24 | padding: var(--spacing-l); 25 | font-size: var(--font-size-body); 26 | font-family: var(--font-family-body); 27 | line-height: 1.5; 28 | } 29 | 30 | .search__submit { 31 | padding-left: var(--spacing-xl); 32 | padding-right: var(--spacing-xl); 33 | } 34 | 35 | .search__footer { 36 | margin-top: var(--spacing-l); 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | } 41 | 42 | .search__result-count { 43 | font-size: var(--font-size-small); 44 | font-style: italic; 45 | } 46 | 47 | .search__loading { 48 | display: none; 49 | } 50 | 51 | .search__loading.is-loading { 52 | display: block; 53 | } 54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Home | Ask the Brains 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 22 |
23 |
24 |

Ask the brains

25 |

...

26 |
27 |
28 |
29 |

...

30 |
31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | About | Ask the Brains 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 22 |
23 |
24 |

About the brains

25 |

...

26 |
27 |
28 |
29 |

...

30 |
31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /css/ask.css: -------------------------------------------------------------------------------- 1 | /* Ask Section */ 2 | .ask { 3 | padding: var(--spacing-xl) var(--spacing-l); 4 | text-align: left; 5 | } 6 | 7 | .ask__label { 8 | display: block; 9 | font-size: var(--font-size-subheading); 10 | margin-bottom: var(--spacing-l); 11 | } 12 | 13 | .ask__input-wrapper { 14 | position: relative; 15 | } 16 | 17 | .ask__input { 18 | width: 100%; 19 | border: 1px solid var(--colour-background-light); 20 | border-radius: var(--border-radius-s); 21 | background: var(--colour-background-light); 22 | color: var(--colour-text-dark); 23 | padding: var(--spacing-l); 24 | font-size: var(--font-size-body); 25 | font-family: var(--font-family-body); 26 | line-height: 1.5; 27 | } 28 | 29 | .ask__button-example { 30 | position: absolute; 31 | z-index: 2; 32 | right: var(--spacing-l); 33 | top: var(--spacing-l); 34 | background: none; 35 | border: none; 36 | cursor: pointer; 37 | opacity: 1; 38 | transition: opacity 0.25s; 39 | } 40 | 41 | .ask__button-example.is-hidden { 42 | opacity: 0; 43 | pointer-events: none; 44 | } 45 | 46 | .ask__footer { 47 | margin-top: var(--spacing-l); 48 | } 49 | 50 | @media (min-width: 800px) { 51 | .ask__footer { 52 | display: flex; 53 | justify-content: space-between; 54 | align-items: center; 55 | } 56 | } 57 | 58 | .ask__char-count { 59 | font-size: var(--font-size-small); 60 | font-style: italic; 61 | } 62 | 63 | .ask__char-count.has-error { 64 | color: var(--colour-secondary); 65 | } 66 | 67 | .ask__loading { 68 | display: none; 69 | } 70 | 71 | .ask__loading.is-loading { 72 | display: block; 73 | } 74 | -------------------------------------------------------------------------------- /css/header.css: -------------------------------------------------------------------------------- 1 | /* Header */ 2 | .header { 3 | background: linear-gradient( 4 | to bottom left, 5 | var(--colour-primary), 6 | var(--colour-secondary) 7 | ); 8 | color: var(--colour-text-light); 9 | padding-top: var(--spacing-xl); 10 | padding-bottom: var(--spacing-xxl); 11 | } 12 | 13 | .header__content { 14 | max-width: var(--width-max); 15 | display: flex; 16 | flex-direction: column; 17 | margin: 0 auto; 18 | justify-content: space-between; 19 | align-items: center; 20 | padding: var(--spacing-l); 21 | } 22 | 23 | @media (min-width: 800px) { 24 | .header__content { 25 | flex-direction: row; 26 | } 27 | } 28 | 29 | /* header nav */ 30 | 31 | .header__nav { 32 | border-bottom: 1px solid var(--colour-text-light); 33 | padding-bottom: 5px; 34 | display: flex; 35 | gap: var(--spacing-l); 36 | margin-top: var(--spacing-l); 37 | } 38 | 39 | @media (min-width: 800px) { 40 | .header__nav { 41 | margin-top: 0; 42 | } 43 | } 44 | 45 | .header__link { 46 | color: var(--colour-text-light); 47 | text-decoration: none; 48 | position: relative; 49 | } 50 | 51 | .header__link::after { 52 | content: ""; 53 | display: block; 54 | position: absolute; 55 | bottom: -5px; 56 | left: 0; 57 | right: 0; 58 | border-bottom: 2px solid var(--colour-text-light); 59 | opacity: 0; 60 | transition: all 0.25s; 61 | } 62 | 63 | .header__link:hover::after, 64 | .header__link--active::after { 65 | opacity: 1; 66 | } 67 | 68 | /* header titles */ 69 | 70 | .header__titles { 71 | padding-left: var(--spacing-l); 72 | padding-right: var(--spacing-l); 73 | } 74 | 75 | .header__title { 76 | font-size: var(--font-size-heading); 77 | } 78 | 79 | .header__subtitle { 80 | font-size: var(--font-size-subheading); 81 | max-width: 700px; 82 | margin-left: auto; 83 | margin-right: auto; 84 | } 85 | -------------------------------------------------------------------------------- /shop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Buy the brains | Ask the Brains 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 22 |
23 |
24 |

Buy the brains

25 |

26 | We need to shift some of this stuff so we can continue to operate, 27 | please help us 28 |

29 |
30 |
31 |
32 | 49 |
50 |

Products

51 |
52 |
53 | 54 |
55 |
56 |
57 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /ask/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ask | Ask the Brains 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 22 |
23 |
24 |

Ask the brains

25 |

26 | Post an open-ended question here and see some suggestions from our 27 | specially trained AI... 28 |

29 |
30 |
31 |
32 |
33 | 34 |
35 | 40 | 41 |
42 | 55 |

Loading...

56 |
57 |
58 |

Results

59 |
60 |
61 | 62 |
63 |
64 |
65 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /js/shop.js: -------------------------------------------------------------------------------- 1 | class Shop { 2 | constructor() { 3 | this.searchContainer = document.querySelector(".search"); 4 | if (this.searchContainer) { 5 | this.searchInput = this.searchContainer.querySelector(".search__input"); 6 | this.searchButton = this.searchContainer.querySelector(".search__submit"); 7 | this.searchResultCount = this.searchContainer.querySelector( 8 | ".search__result-count" 9 | ); 10 | this.loading = this.searchContainer.querySelector(".search__loading"); 11 | 12 | this.productsContainer = document.querySelector(".products"); 13 | this.productsList = 14 | this.productsContainer.querySelector(".products__list"); 15 | } 16 | } 17 | 18 | init() { 19 | if (!this.searchContainer) return; 20 | this.searchInput.addEventListener("input", (e) => this.checkInput(e)); 21 | this.searchButton.addEventListener("click", (e) => this.search(e)); 22 | this.checkInput(); 23 | this.search(); 24 | } 25 | 26 | checkInput() { 27 | this.searchButton.disabled = this.searchInput.value.length === 0; 28 | } 29 | 30 | async search(e) { 31 | if (e) e.preventDefault(); 32 | 33 | this.loading.classList.add("is-loading"); 34 | this.productsContainer.classList.remove("is-shown"); 35 | this.searchResultCount.textContent = ""; 36 | 37 | while (this.productsList.firstChild) { 38 | this.productsList.removeChild(this.productsList.lastChild); 39 | } 40 | 41 | const url = "../js/fake-products.json"; 42 | try { 43 | const response = await fetch(url); 44 | if (!response.ok) { 45 | throw new Error(`Response status: ${response.status}`); 46 | } 47 | 48 | await setTimeout(async () => { 49 | const json = await response.json(); 50 | this.processProducts(json); 51 | this.loading.classList.remove("is-loading"); 52 | }, 1000); 53 | } catch (error) { 54 | console.error(error.message); 55 | this.loading.classList.remove("is-loading"); 56 | } 57 | } 58 | 59 | processProducts(data) { 60 | const searchTerm = this.searchInput.value.toLowerCase(); 61 | const filteredProducts = data.filter( 62 | (product) => 63 | product.title.toLowerCase().includes(searchTerm) || 64 | product.description.toLowerCase().includes(searchTerm) 65 | ); 66 | 67 | this.searchResultCount.textContent = `${filteredProducts.length} products found`; 68 | 69 | if (filteredProducts.length > 0) { 70 | this.productsContainer.classList.add("is-shown"); 71 | } else { 72 | this.productsContainer.classList.remove("is-shown"); 73 | } 74 | 75 | filteredProducts.forEach((product) => { 76 | const productsItem = document.createElement("div"); 77 | productsItem.classList.add("products__item"); 78 | this.productsList.appendChild(productsItem); 79 | 80 | const productsItemImage = document.createElement("img"); 81 | productsItemImage.classList.add("products__item-image"); 82 | productsItemImage.src = product.img; 83 | productsItem.appendChild(productsItemImage); 84 | 85 | const productsItemTitle = document.createElement("h3"); 86 | productsItemTitle.classList.add("products__item-title"); 87 | productsItemTitle.textContent = product.title; 88 | productsItem.appendChild(productsItemTitle); 89 | 90 | const productsItemDescription = document.createElement("p"); 91 | productsItemDescription.classList.add("products__item-description"); 92 | productsItemDescription.textContent = product.description; 93 | productsItem.appendChild(productsItemDescription); 94 | 95 | const productsItemStars = document.createElement("p"); 96 | productsItemStars.classList.add("products__item-stars"); 97 | productsItemStars.textContent = "⭐".repeat(product.stars); 98 | productsItem.appendChild(productsItemStars); 99 | 100 | const productsItemPrice = document.createElement("p"); 101 | productsItemPrice.classList.add("products__item-price"); 102 | productsItemPrice.textContent = product.price; 103 | productsItem.appendChild(productsItemPrice); 104 | }); 105 | } 106 | } 107 | 108 | // Expose an instance of the 'Shop' class 109 | export default new Shop(); 110 | -------------------------------------------------------------------------------- /js/fake-products.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "img": "https://placehold.co/600x400/0000FF/FFFFFF?text=Galactic+Telescope", 4 | "title": "Galactic Telescope", 5 | "description": "A high-powered telescope for stargazing and exploring deep space.", 6 | "stars": 5, 7 | "price": "£199.99" 8 | }, 9 | { 10 | "img": "https://placehold.co/600x400/FF0000/FFFFFF?text=Martian+Rover+Toy", 11 | "title": "Martian Rover Toy", 12 | "description": "A detailed model of the latest Mars exploration rover.", 13 | "stars": 4, 14 | "price": "£29.99" 15 | }, 16 | { 17 | "img": "https://placehold.co/600x400/00FF00/000000?text=Lunar+Globe", 18 | "title": "Lunar Globe", 19 | "description": "A glow-in-the-dark moon globe with detailed craters and landmarks.", 20 | "stars": 5, 21 | "price": "£14.99" 22 | }, 23 | { 24 | "img": "https://placehold.co/600x400/FFFF00/000000?text=NASA+Backpack", 25 | "title": "NASA Backpack", 26 | "description": "A stylish and durable backpack with an official NASA logo.", 27 | "stars": 4, 28 | "price": "£34.99" 29 | }, 30 | { 31 | "img": "https://placehold.co/600x400/000000/FFFFFF?text=Solar+System+Model", 32 | "title": "Solar System Model", 33 | "description": "An educational 3D model of our solar system with rotating planets.", 34 | "stars": 5, 35 | "price": "£24.99" 36 | }, 37 | { 38 | "img": "https://placehold.co/600x400/800080/FFFFFF?text=Astronaut+Helmet", 39 | "title": "Astronaut Helmet", 40 | "description": "A replica astronaut helmet with working LED lights and a retractable visor.", 41 | "stars": 4, 42 | "price": "£49.99" 43 | }, 44 | { 45 | "img": "https://placehold.co/600x400/FF4500/FFFFFF?text=Rocket+Lamp", 46 | "title": "Rocket Lamp", 47 | "description": "A decorative lamp in the shape of a launching rocket.", 48 | "stars": 4, 49 | "price": "£19.99" 50 | }, 51 | { 52 | "img": "https://placehold.co/600x400/FFFFFF/0000FF?text=Black+Hole+Poster", 53 | "title": "Black Hole Poster", 54 | "description": "A stunning high-resolution print of a black hole.", 55 | "stars": 3, 56 | "price": "£9.99" 57 | }, 58 | { 59 | "img": "https://placehold.co/600x400/00CED1/000000?text=Zero+Gravity+Pen", 60 | "title": "Zero Gravity Pen", 61 | "description": "A pen that writes in zero gravity, underwater, and upside down.", 62 | "stars": 5, 63 | "price": "£12.99" 64 | }, 65 | { 66 | "img": "https://placehold.co/600x400/32CD32/FFFFFF?text=Moon+Base+LEGO+Set", 67 | "title": "Moon Base LEGO Set", 68 | "description": "A fun and detailed LEGO set for building your own moon base.", 69 | "stars": 5, 70 | "price": "£49.99" 71 | }, 72 | { 73 | "img": "https://placehold.co/600x400/FFA500/000000?text=Comet+Puzzle", 74 | "title": "Comet Puzzle", 75 | "description": "A challenging 1000-piece puzzle of a stunning comet streaking across space.", 76 | "stars": 4, 77 | "price": "£15.99" 78 | }, 79 | { 80 | "img": "https://placehold.co/600x400/00008B/FFFFFF?text=Saturn+Ring+Replica", 81 | "title": "Saturn Ring Replica", 82 | "description": "A scientifically accurate scale model of Saturn and its rings.", 83 | "stars": 4, 84 | "price": "£39.99" 85 | }, 86 | { 87 | "img": "https://placehold.co/600x400/DA70D6/000000?text=Aurora+Borealis+Lamp", 88 | "title": "Aurora Borealis Lamp", 89 | "description": "A colour-changing lamp simulating the Northern Lights.", 90 | "stars": 5, 91 | "price": "£27.99" 92 | }, 93 | { 94 | "img": "https://placehold.co/600x400/B22222/FFFFFF?text=Space+Trivia+Game", 95 | "title": "Space Trivia Game", 96 | "description": "A fun and educational game testing your knowledge of space.", 97 | "stars": 3, 98 | "price": "£18.99" 99 | }, 100 | { 101 | "img": "https://placehold.co/600x400/2E8B57/FFFFFF?text=Galaxy+Projector", 102 | "title": "Galaxy Projector", 103 | "description": "A projector that fills your room with moving stars and nebulae.", 104 | "stars": 5, 105 | "price": "£44.99" 106 | }, 107 | { 108 | "img": "https://placehold.co/600x400/8B0000/FFFFFF?text=ISS+Model+Kit", 109 | "title": "ISS Model Kit", 110 | "description": "A build-your-own model of the International Space Station.", 111 | "stars": 4, 112 | "price": "£59.99" 113 | } 114 | ] 115 | -------------------------------------------------------------------------------- /js/ask.js: -------------------------------------------------------------------------------- 1 | class Ask { 2 | maxLength = 160; 3 | 4 | constructor() { 5 | this.askContainer = document.querySelector(".ask"); 6 | if (this.askContainer) { 7 | this.askInput = this.askContainer.querySelector(".ask__input"); 8 | this.exampleButton = this.askContainer.querySelector( 9 | ".ask__button-example" 10 | ); 11 | this.askButton = this.askContainer.querySelector(".ask__button-ask"); 12 | this.resetButton = this.askContainer.querySelector(".ask__button-reset"); 13 | this.charCounter = this.askContainer.querySelector(".ask__char-count"); 14 | this.loading = this.askContainer.querySelector(".ask__loading"); 15 | 16 | this.resultsContainer = document.querySelector(".results"); 17 | this.resultsList = this.resultsContainer.querySelector(".results__list"); 18 | } 19 | } 20 | 21 | init() { 22 | if (!this.askContainer) return; 23 | this.askInput.addEventListener("input", (e) => this.checkInput(e)); 24 | this.exampleButton.addEventListener("click", (e) => this.setExample(e)); 25 | this.askButton.addEventListener("click", (e) => this.askClicked(e)); 26 | this.resetButton.addEventListener("click", (e) => this.resetClicked(e)); 27 | this.checkInput(); 28 | } 29 | 30 | checkInput() { 31 | // check submission validity 32 | const charsRemaining = this.maxLength - this.askInput.value.length; 33 | if (charsRemaining < 0) { 34 | this.askButton.disabled = true; 35 | this.charCounter.classList.add("has-error"); 36 | } else { 37 | this.askButton.disabled = false; 38 | this.charCounter.classList.remove("has-error"); 39 | } 40 | this.charCounter.textContent = `${charsRemaining} characters remaining`; 41 | 42 | // check whether to display example button 43 | if (this.askInput.value.length === 0) { 44 | this.askButton.disabled = true; 45 | this.exampleButton.classList.remove("is-hidden"); 46 | } else { 47 | this.exampleButton.classList.add("is-hidden"); 48 | } 49 | } 50 | 51 | setExample(event) { 52 | event.preventDefault(); 53 | console.log("setting example"); 54 | this.askInput.value = 55 | "Tell me about some of the best things I could see with a telescope from Brighton (assuming it ever stops raining)"; 56 | this.checkInput(); 57 | } 58 | 59 | resetClicked(event) { 60 | event.preventDefault(); 61 | this.askInput.value = ""; 62 | this.checkInput(); 63 | } 64 | 65 | async askClicked(event) { 66 | event.preventDefault(); 67 | this.loading.classList.add("is-loading"); 68 | 69 | const url = "../js/fake-results.json"; 70 | try { 71 | const response = await fetch(url); 72 | if (!response.ok) { 73 | throw new Error(`Response status: ${response.status}`); 74 | } 75 | 76 | // fake a one second wait, use the two lines below for an instant response 77 | // const json = await response.json(); 78 | // this.processResults(json); 79 | 80 | await setTimeout(async () => { 81 | const json = await response.json(); 82 | this.processResults(json); 83 | this.loading.classList.remove("is-loading"); 84 | }, 1000); 85 | } catch (error) { 86 | console.error(error.message); 87 | this.loading.classList.remove("is-loading"); 88 | } 89 | } 90 | 91 | processResults(data) { 92 | if (data.length > 0) { 93 | this.resultsContainer.classList.add("is-shown"); 94 | } else { 95 | this.resultsContainer.classList.remove("is-shown"); 96 | } 97 | 98 | data.forEach((result) => { 99 | const resultsItem = document.createElement("div"); 100 | resultsItem.classList.add("results__item"); 101 | this.resultsList.appendChild(resultsItem); 102 | 103 | const resultsItemTitle = document.createElement("h3"); 104 | resultsItemTitle.classList.add("results__item-title"); 105 | resultsItemTitle.textContent = result.title; 106 | resultsItem.appendChild(resultsItemTitle); 107 | 108 | const resultsItemDescription = document.createElement("p"); 109 | resultsItemDescription.classList.add("results__item-description"); 110 | resultsItemDescription.textContent = result.description; 111 | resultsItem.appendChild(resultsItemDescription); 112 | }); 113 | } 114 | } 115 | 116 | // Expose an instance of the 'Ask' class 117 | export default new Ask(); 118 | --------------------------------------------------------------------------------