├── 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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
Write a review
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 |
--------------------------------------------------------------------------------