├── search.svg ├── star.svg ├── LICENSE ├── index.html ├── styles.css └── script.js /search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 WebDevSimplified 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Document 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | What others think about the product 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | Average customer rating 28 |
29 |
30 | 37 |
38 |
Reviews
39 |
40 |
41 | 42 |
43 | 44 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | *, *::before, *::after { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | font-family: Poppins; 7 | background-color: #333; 8 | font-size: 16px; 9 | } 10 | 11 | .card { 12 | background-color: #F8F8F8; 13 | padding: 84px; 14 | border-radius: 23px; 15 | } 16 | 17 | .title { 18 | font-size: 34px; 19 | font-family: Merriweather; 20 | flex-grow: 1; 21 | } 22 | 23 | .average-rating { 24 | font-size: 26px; 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | gap: 8px; 29 | margin-bottom: 8px; 30 | } 31 | 32 | .average-rating-section { 33 | font-weight: 500; 34 | font-size: 14px; 35 | padding: 17px; 36 | background-color: white; 37 | border-radius: 15px; 38 | box-shadow: 0 7px 20px 0 rgba(115, 116, 156, .08); 39 | text-align: center; 40 | min-width: 150px; 41 | } 42 | 43 | .star-icon { 44 | width: 25px; 45 | height: 25px; 46 | } 47 | 48 | .average-rating-section .star-icon { 49 | width: 46px; 50 | height: 46px; 51 | } 52 | 53 | .star-icon .icon-path { 54 | fill: #FFD66C; 55 | stroke: #EFB153; 56 | } 57 | 58 | .header { 59 | display: flex; 60 | align-items: center; 61 | gap: 80px; 62 | margin-bottom: 56px; 63 | } 64 | 65 | .search-bar { 66 | display: flex; 67 | align-items: center; 68 | margin-bottom: 32px; 69 | } 70 | 71 | .search-input-icon { 72 | position: absolute; 73 | height: 48px; 74 | padding: 16px; 75 | pointer-events: none; 76 | } 77 | 78 | .search-input { 79 | padding: 16px; 80 | border-radius: 26px; 81 | background-color: white; 82 | font-weight: 500; 83 | font-size: inherit; 84 | padding-left: calc(10px + 16px + 16px); 85 | border: none; 86 | width: 100%; 87 | } 88 | 89 | .review-rows { 90 | display: grid; 91 | grid-template-columns: auto auto 1fr auto; 92 | align-items: center; 93 | column-gap: 8px; 94 | row-gap: 18px; 95 | padding-left: 10px; 96 | padding-right: 10px; 97 | margin-top: 16px; 98 | } 99 | 100 | .review-section { 101 | margin-bottom: 56px; 102 | } 103 | 104 | .review-number { 105 | font-weight: bold; 106 | text-align: end; 107 | justify-self: flex-end; 108 | } 109 | 110 | .review-bar { 111 | --border-width: 1px; 112 | position: relative; 113 | flex-grow: 1; 114 | background-color: #EEEEEE; 115 | border: 1px solid #C9C9C9; 116 | height: 10px; 117 | border-radius: 100px; 118 | } 119 | 120 | .review-bar::after { 121 | content: ""; 122 | position: absolute; 123 | top: calc(-1 * var(--border-width)); 124 | bottom: calc(-1 * var(--border-width)); 125 | left: calc(-1 * var(--border-width)); 126 | right: calc(-1 * var(--border-width)); 127 | width: calc(2 * var(--border-width) + var(--width)); 128 | border: 1px solid #EFB153; 129 | background-color: #FFD66C; 130 | border-radius: 100px; 131 | } 132 | 133 | .review-bar.empty::after { 134 | content: none; 135 | } 136 | 137 | .review-row:last-child { 138 | margin-bottom: 0; 139 | } 140 | 141 | .review-btn { 142 | background-color: hsl(238, 49%, 19%); 143 | color: white; 144 | border-radius: 100px; 145 | padding: 16px 24px; 146 | font-size: inherit; 147 | font-family: inherit; 148 | cursor: pointer; 149 | border: none; 150 | outline-color: hsl(238, 49%, 79%); 151 | } 152 | 153 | .review-btn:hover { 154 | background-color: hsl(238, 49%, 29%); 155 | } -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | const reviewRowsContainer = document.querySelector(".review-rows") 2 | const averageReviewElem = document.querySelector("[data-average-review]") 3 | const starIcon = ` 4 | 5 | ` 6 | 7 | const REVIEWS = { 8 | 5: 120, 9 | 4: 40, 10 | 3: 20, 11 | 2: 0, 12 | 1: 0, 13 | } 14 | 15 | const totalReviews = Object.values(REVIEWS).reduce((sum, value) => { 16 | return sum + value 17 | }, 0) 18 | const averageReview = 19 | Object.entries(REVIEWS).reduce((sum, [value, quantity]) => { 20 | return sum + value * quantity 21 | }, 0) / totalReviews 22 | 23 | averageReviewElem.dataset.endValue = Math.round(averageReview * 10) / 10 24 | averageReviewElem.textContent = 0 25 | 26 | Object.entries(REVIEWS) 27 | .sort(([a], [b]) => b - a) 28 | .forEach(([value, quantity]) => { 29 | const reviewNumber = document.createElement("div") 30 | reviewNumber.textContent = value 31 | reviewNumber.classList.add("review-number") 32 | reviewRowsContainer.append(reviewNumber) 33 | const starIconWrapper = document.createElement("div") 34 | starIconWrapper.innerHTML = starIcon 35 | reviewRowsContainer.append(starIconWrapper) 36 | const reviewBar = document.createElement("div") 37 | reviewBar.dataset.endValue = (quantity / totalReviews) * 100 38 | reviewBar.classList.add("review-bar") 39 | reviewBar.classList.toggle("empty", quantity === 0) 40 | reviewRowsContainer.append(reviewBar) 41 | const reviewCount = document.createElement("div") 42 | reviewCount.dataset.endValue = quantity 43 | reviewCount.textContent = 0 44 | reviewCount.classList.add("review-count") 45 | reviewRowsContainer.append(reviewCount) 46 | }) 47 | 48 | let timeOffset 49 | const DURATION = 500 50 | function update(time) { 51 | if (timeOffset != null) { 52 | const timeElapsed = time - timeOffset 53 | const newAverage = getNewValue( 54 | averageReviewElem.dataset.endValue, 55 | timeElapsed 56 | ) 57 | averageReviewElem.textContent = Math.round(newAverage * 10) / 10 58 | const countElems = document.querySelectorAll( 59 | ".review-count[data-end-value]" 60 | ) 61 | countElems.forEach(elem => { 62 | elem.textContent = Math.round( 63 | getNewValue(elem.dataset.endValue, timeElapsed) 64 | ) 65 | }) 66 | const reviewBars = document.querySelectorAll(".review-bar[data-end-value]") 67 | reviewBars.forEach(elem => { 68 | elem.style.setProperty( 69 | "--width", 70 | `${getNewValue(elem.dataset.endValue, timeElapsed)}%` 71 | ) 72 | }) 73 | if (timeElapsed >= DURATION) return 74 | requestAnimationFrame(update) 75 | } else { 76 | timeOffset = time 77 | requestAnimationFrame(update) 78 | } 79 | } 80 | 81 | function getNewValue(endValue, timeElapsed) { 82 | return Math.min((endValue * timeElapsed) / DURATION, endValue) 83 | } 84 | 85 | requestAnimationFrame(update) 86 | --------------------------------------------------------------------------------