Star rate in reviews, login menu & other fixes

Added star rate to reviews, coding login menu, coding review form & other fixes
This commit is contained in:
RailTH
2024-04-09 15:39:29 +11:00
parent 6db8b00965
commit 8a5d717386
12 changed files with 454 additions and 147 deletions

View File

@@ -77,62 +77,12 @@ $accent-color: #EB5E28;
border: 2px solid $main-color;
border-radius: 12px;
.rating-div__stars-container {
position: relative;
width: 150px;
height: 30px;
background-image: url(assets/icons/rating__star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
.rating-div__star-radio {
appearance: none;
position: absolute;
top: 0;
left: 0;
height: 30px;
}
.rating-div__star-radio:checked, .rating-div__star-radio:hover {
background-image: url(assets/icons/rating__filled-star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
}
.rating-div__star-radio:hover ~ .rating-div__star-radio {
background-image: url(assets/icons/rating__star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
}
.rating-div__star-radio:nth-of-type(1) {
z-index: 5;
width: 30px;
}
.rating-div__star-radio:nth-of-type(2) {
z-index: 4;
width: 60px;
}
.rating-div__star-radio:nth-of-type(3) {
z-index: 3;
width: 90px;
}
.rating-div__star-radio:nth-of-type(4) {
z-index: 2;
width: 120px;
}
.rating-div__star-radio:nth-of-type(5) {
z-index: 1;
width: 150px;
}
}
.rating-div__rate-value {
color: white;
font-size: 24px;
font-weight: 400;
line-height: 29px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
}
@@ -196,7 +146,7 @@ $accent-color: #EB5E28;
font-size: 20px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
@@ -215,7 +165,7 @@ $accent-color: #EB5E28;
font-size: 24px;
font-weight: 600;
line-height: 29px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
}
@@ -232,16 +182,18 @@ $accent-color: #EB5E28;
gap: 20px;
margin-top: 48px;
width: 310px;
.rate-block__rating {
display: flex;
align-items: center;
gap: 20px;
.rate-block__rate-number {
color: white;
font-size: 36px;
font-weight: 600;
line-height: 44px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
@@ -291,7 +243,7 @@ $accent-color: #EB5E28;
font-size: 20px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
.progressbars-group__progressbar-container {
@@ -324,10 +276,107 @@ $accent-color: #EB5E28;
}
}
.product-page__review-form {
display: flex;
flex-direction: column;
margin-top: 48px;
gap: 20px;
.review-form__heading {
color: white;
font-size: 26px;
font-weight: 500;
letter-spacing: 0px;
text-align: left;
}
.review-form__stars-container {
position: relative;
width: 150px;
height: 30px;
background-image: url(assets/icons/rating__star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
.review-form__star-radio {
appearance: none;
position: absolute;
top: 0;
left: 0;
height: 30px;
}
.review-form__star-radio:checked, .review-form__star-radio:hover {
background-image: url(assets/icons/rating__filled-star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
}
.review-form__star-radio:hover ~ .review-form__star-radio {
background-image: url(assets/icons/rating__star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
}
.review-form__star-radio:nth-of-type(1) {
z-index: 5;
width: 30px;
}
.review-form__star-radio:nth-of-type(2) {
z-index: 4;
width: 60px;
}
.review-form__star-radio:nth-of-type(3) {
z-index: 3;
width: 90px;
}
.review-form__star-radio:nth-of-type(4) {
z-index: 2;
width: 120px;
}
.review-form__star-radio:nth-of-type(5) {
z-index: 1;
width: 150px;
}
}
.review-form__textarea {
width: 100%;
border: 2px solid $main-color;
border-radius: 15px;
color: white;
padding: 10px;
font-size: 24px;
}
.review-form__image-attach {
width: 34px;
img {
width: 100%;
}
}
.review-form__image-input {
display: none;
}
.review-form__send-button {
padding: 20px;
border-radius: 15px;
background-color: $accent-color;
font-size: 20px;
color: white;
font-weight: 500;
letter-spacing: 0px;
}
}
.product-page__reviews-container {
display: flex;
flex-direction: column;
margin-top: 50px;
margin-bottom: 30px;
gap: 50px;
}
}
@@ -337,7 +386,7 @@ $accent-color: #EB5E28;
font-size: 32px;
font-weight: 500;
line-height: 39px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
}

View File

@@ -1,7 +1,4 @@
<svg width="48.000000" height="48.000000" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip3_51">
<rect id="location_on_FILL0_wght400_GRAD0_opsz48 1" width="48.000000" height="48.000000" fill="white" fill-opacity="0"/>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,7 +1,4 @@
<svg width="48.000000" height="48.000000" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip6_70">
<rect id="info_FILL0_wght400_GRAD0_opsz48 1" width="48.000000" height="48.000000" fill="white" fill-opacity="0"/>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,7 +1,4 @@
<svg width="48.000000" height="48.000000" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip3_40">
<rect id="person_FILL0_wght400_GRAD0_opsz48 1" width="48.000000" height="48.000000" fill="white" fill-opacity="0"/>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-330q0-17 11.5-28.5T400-720q17 0 28.5 11.5T440-680v330q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-350q0-17 11.5-28.5T680-720q17 0 28.5 11.5T720-680v350Z" fill="#CCC5B9"/></svg>

After

Width:  |  Height:  |  Size: 515 B

View File

@@ -3,6 +3,7 @@ import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import Logotype from "../assets/img/amongasik.png";
import CatalogMenu from "./CatalogMenu";
import LoginMenu from "./LoginMenu";
interface HeaderProps {
togglePopupMap: () => void;
@@ -12,17 +13,32 @@ interface HeaderCatalogMenuState {
isCatalogMenuVisible: boolean;
}
interface HeaderLoginMenuState {
isLoginMenuVisible: boolean;
}
export default function Header({ togglePopupMap }: HeaderProps): JSX.Element {
const [state, setState] = useState<HeaderCatalogMenuState>({
const [stateCatalog, setStateCatalog] = useState<HeaderCatalogMenuState>({
isCatalogMenuVisible: false,
});
const [stateLogin, setStateLogin] = useState<HeaderLoginMenuState>({
isLoginMenuVisible: false,
});
const toggleCatalogMenu = () => {
setState((prevState) => ({
setStateCatalog((prevState) => ({
...prevState,
isCatalogMenuVisible: !prevState.isCatalogMenuVisible,
}));
};
const toggleLoginMenu = () => {
setStateLogin((prevState) => ({
...prevState,
isLoginMenuVisible: !prevState.isLoginMenuVisible,
}));
};
return(
<header className="header">
@@ -60,18 +76,17 @@ export default function Header({ togglePopupMap }: HeaderProps): JSX.Element {
{/* Код для svg */}
</div>
</form>
<Link
to="/profile"
<motion.button
className="header__profile-a"
whileTap={{scale: 0.9}}
transition={{duration: 0.2, type: "spring"}}
onClick={toggleLoginMenu}
>
{/* Код для svg */}
<motion.svg width="48.000000" height="48.000000" viewBox="0 0 48 48" fill="none"
whileTap={{scale: 0.9}}
transition={{duration: 0.2, type: "spring"}}
>
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip3_40">
<rect id="person_FILL0_wght400_GRAD0_opsz48 1" width="48.000000" height="48.000000" fill="white" fillOpacity="0"/>
@@ -83,7 +98,7 @@ export default function Header({ togglePopupMap }: HeaderProps): JSX.Element {
</g>
</motion.svg>
{/* Код для svg */}
</Link>
</motion.button>
<motion.button
className="header__popupmap-button"
whileTap={{scale: 0.9}}
@@ -92,9 +107,6 @@ export default function Header({ togglePopupMap }: HeaderProps): JSX.Element {
>
{/* Код для svg */}
<svg width="48.000000" height="48.000000" viewBox="0 0 48 48" fill="none">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip3_51">
<rect id="location_on_FILL0_wght400_GRAD0_opsz48 1" width="48.000000" height="48.000000" fill="white" fillOpacity="0"/>
@@ -115,9 +127,6 @@ export default function Header({ togglePopupMap }: HeaderProps): JSX.Element {
>
{/* Код для svg */}
<svg width="48.000000" height="48.000000" viewBox="0 0 48 48" fill="none">
<desc>
Created with Pixso.
</desc>
<defs>
<clipPath id="clip6_70">
<rect id="info_FILL0_wght400_GRAD0_opsz48 1" width="48.000000" height="48.000000" fill="white" fillOpacity="0"/>
@@ -131,7 +140,8 @@ export default function Header({ togglePopupMap }: HeaderProps): JSX.Element {
{/* Код для svg */}
</motion.a>
</nav>
{state.isCatalogMenuVisible && <CatalogMenu toggleCatalogMenu={toggleCatalogMenu}/>}
{stateCatalog.isCatalogMenuVisible && <CatalogMenu toggleCatalogMenu={toggleCatalogMenu}/>}
{stateLogin.isLoginMenuVisible && <LoginMenu toggleLoginMenu={toggleLoginMenu}/>}
</header>
)
}

View File

@@ -1,8 +1,42 @@
import React from "react";
import { motion } from "framer-motion";
export default function LoginMenu() {
interface LoginMenuProps {
toggleLoginMenu: () => void;
}
export default function LoginMenu({ toggleLoginMenu }: LoginMenuProps): JSX.Element {
return(
<>
<div className="background-blackout" onClick={toggleLoginMenu}></div>
<form className="popup-login">
<div className="popup-login__top-container">
<div className="top-container__headings-text">
<h5 className="popup-menu__heading">
SusMarket <span>ID</span>
</h5>
<p className="top-container__text">
Войдите с SusMarket ID
</p>
</div>
</div>
<div className="popup-login__inputs-container">
<input type="text" name="userName" id="userName" className="popup-login__name-input" placeholder="Логин"/>
<input type="password" name="userPassword" id="userPassword" className="popup-login__password-input" placeholder="Пароль"/>
</div>
<div className="popup-login__bottom-container">
<p className="popup-menu__prompt-url">
У вас нет аккаунта? <u>Зарегестрироваться</u>
</p>
<motion.button
className="popup-login__login-button"
whileTap={{scale: 0.98}}
transition={{duration: 0.2, type: "spring"}}
>
Войти
</motion.button>
</div>
</form>
</>
)
}

View File

@@ -13,9 +13,44 @@ export default function Review() {
Святослав Васильев
</h4>
</div>
<div className="review-container__review-info">=
<div className="review-container__review-info">
<div className="review-info__star-rate">
<input
type="radio"
className="star-rate__star-radio"
name="review-rating"
value={1}
aria-label="Плохо"
/>
<input
type="radio"
className="star-rate__star-radio"
name="review-rating"
value={2}
aria-label="Удовлетворительно"
/>
<input
type="radio"
className="star-rate__star-radio"
name="review-rating"
value={3}
aria-label="Нормально"
/>
<input
type="radio"
className="star-rate__star-radio"
name="review-rating"
value={4}
aria-label="Хорошо"
/>
<input
type="radio"
className="star-rate__star-radio"
name="review-rating"
value={5}
aria-label="Отлично"
checked
/>
</div>
<time className="review-info__review-date" dateTime="2019-09-09">
09.09.2019

View File

@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import '../ProductStyle.scss';
import ImageAttachIcon from "../assets/icons/review-form__add-image-icon.svg";
import { motion } from 'framer-motion';
interface ReviewState {
text: string;
rating: number;
image: File | null;
}
export default function ReviewForm() {
const [review, setReview] = useState<ReviewState>({ text: '', rating: 1, image: null });
function handleTextChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
setReview({ ...review, text: event.target.value });
}
function handleRatingChange(event: React.ChangeEvent<HTMLInputElement>) {
setReview({ ...review, rating: Number(event.target.value) });
}
function handleImageChange(event: React.ChangeEvent<HTMLInputElement>) {
if (event.target.files) {
setReview({ ...review, image: event.target.files[0] });
}
}
function handleSubmit(event: React.FormEvent) {
event.preventDefault();
console.log(review);
}
return(
<form className='product-page__review-form' onSubmit={handleSubmit}>
<h5 className='review-form__heading'>
Оставить отзыв
</h5>
<div className="review-form__stars-container">
{[...Array(5)].map((_, index) => (
<input
key={index}
type="radio"
className="review-form__star-radio"
name="rating"
value={index + 1}
aria-label={`Рейтинг ${index + 1}`}
checked={review.rating === index + 1}
onChange={handleRatingChange}
/>
))}
</div>
<textarea
className='review-form__textarea'
cols={30}
rows={5}
placeholder='Комментарий'
value={review.text}
onChange={handleTextChange}
/>
<label htmlFor="review-image" className='review-form__image-attach'>
<img src={ImageAttachIcon} alt="Прикрепить изображение"/>
</label>
<input
className='review-form__image-input'
type="file"
name="review image"
id="review-image"
accept='.png, .jpg, .jpeg'
onChange={handleImageChange}
/>
<motion.button
className='review-form__send-button'
type='submit'
whileTap={{scale: 0.98}}
transition={{duration: 0.2, type: "spring"}}
>
Отправить отзыв
</motion.button>
</form>
)
}

View File

@@ -340,7 +340,7 @@ body {
font-size: 24px;
font-weight: 600;
line-height: 29px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
}
@@ -352,7 +352,53 @@ body {
width: 100%;
.review-info__star-rate {
display: flex;
position: relative;
width: 150px;
height: 30px;
background-image: url(assets/icons/rating__star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
.star-rate__star-radio {
appearance: none;
position: absolute;
top: 0;
left: 0;
height: 30px;
}
.star-rate__star-radio:checked, .rating-div__star-radio:hover {
background-image: url(assets/icons/rating__filled-star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
}
.star-rate__star-radio:hover ~ .rating-div__star-radio {
background-image: url(assets/icons/rating__star-icon.svg);
background-size: 30px auto;
background-repeat: repeat-x;
}
.star-rate__star-radio:nth-of-type(1) {
z-index: 5;
width: 30px;
}
.star-rate__star-radio:nth-of-type(2) {
z-index: 4;
width: 60px;
}
.star-rate__star-radio:nth-of-type(3) {
z-index: 3;
width: 90px;
}
.star-rate__star-radio:nth-of-type(4) {
z-index: 2;
width: 120px;
}
.star-rate__star-radio:nth-of-type(5) {
z-index: 1;
width: 150px;
}
}
.review-info__review-date {
@@ -360,7 +406,7 @@ body {
font-size: 24px;
font-weight: 500;
line-height: 29px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
}
@@ -371,7 +417,7 @@ body {
font-size: 24px;
font-weight: 400;
line-height: 29px;
letter-spacing: 0%;
letter-spacing: 0px;
text-align: left;
}
@@ -379,4 +425,119 @@ body {
max-width: 300px;
border-radius: 10px;
}
}
.popup-login {
width: 450px;
display: flex;
position: fixed;
left: 50%;
top: 108px;
transform:translateX(-50%);
flex-direction: column;
gap: 40px;
padding: 30px;
box-sizing: border-box;
border-radius: 20px;
box-shadow: 4px 4px 10px 0px rgba(0, 0, 0, 0.25);
background: $background-color;
.popup-login__top-container {
width: 100%;
display: flex;
flex-direction: column;
.popup-login__close-button {
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50px;
background: rgb(53, 52, 49);
}
.top-container__headings-text {
display: flex;
flex-direction: column;
gap: 20px;
.popup-menu__heading {
color: white;
font-size: 24px;
font-weight: 600;
line-height: 29px;
letter-spacing: 0px;
text-align: left;
span {
color: $accent-color;
}
}
.top-container__text {
color: $main-color;
font-size: 16px;
font-weight: 600;
line-height: 20px;
letter-spacing: 0px;
text-align: left;
}
}
}
.popup-login__inputs-container {
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
.popup-login__name-input, .popup-login__password-input {
display: flex;
width: 100%;
height: 70px;
align-items: center;
padding: 0px 20px;
box-sizing: border-box;
border: 2px solid $main-color;
border-radius: 15px;
color: white;
font-size: 24px;
font-weight: 600;
line-height: 29px;
letter-spacing: 0px;
text-align: left;
}
}
.popup-login__bottom-container {
display: flex;
flex-direction: column;
gap: 20px;
.popup-menu__prompt-url {
color: $main-color;
font-size: 16px;
font-weight: 600;
line-height: 20px;
letter-spacing: 0px;
text-align: left;
}
.popup-login__login-button {
width: 100%;
height: 70px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 15px;
background: rgb(235, 94, 40);
color: white;
font-size: 24px;
font-weight: 600;
line-height: 29px;
letter-spacing: 0%;
text-align: left;
}
}
}

View File

@@ -1,20 +1,15 @@
import React, { useState } from 'react';
import React from 'react';
import Review from '../components/Review';
import '../ProductStyle.scss';
import ProductImage from "../assets/img/product-image-1.webp";
import ShareIcon from "../assets/icons/share-icon.svg";
import ReviewForm from '../components/ReviewForm';
interface StarRatingProps {
value: number;
}
export default function ProductPage({ value }: StarRatingProps) {
const [selectedRating, setSelectedRating] = useState(1);
const handleRatingChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedRating(Number(event.target.value));
};
return(
<section className="product-page">
<section className="product-page__main-section">
@@ -32,54 +27,6 @@ export default function ProductPage({ value }: StarRatingProps) {
</span>
<div className="product-page__container-div">
<div className="product-page__rating-share-div">
<div className="product-page__rating-div">
<div className="rating-div__stars-container">
<input
type="radio"
className="rating-div__star-radio"
name="rating" value={1}
aria-label="Плохо"
checked={selectedRating === 1}
onChange={handleRatingChange}
/>
<input
type="radio"
className="rating-div__star-radio"
name="rating" value={2}
aria-label="Удовлетворительно"
checked={selectedRating === 2}
onChange={handleRatingChange}
/>
<input
type="radio"
className="rating-div__star-radio"
name="rating" value={3}
aria-label="Нормально" checked={selectedRating === 3}
onChange={handleRatingChange}
/>
<input
type="radio"
className="rating-div__star-radio"
name="rating"
value={4}
aria-label="Хорошо"
checked={selectedRating === 4}
onChange={handleRatingChange}
/>
<input
type="radio"
className="rating-div__star-radio"
name="rating"
value={5}
aria-label="Отлично"
checked={selectedRating === 5}
onChange={handleRatingChange}
/>
</div>
<span className="rating-div__rate-value">
{selectedRating}
</span>
</div>
<button className="product-page__share-button">
<img src={ShareIcon as unknown as string} alt="" />
</button>
@@ -182,12 +129,9 @@ export default function ProductPage({ value }: StarRatingProps) {
</div>
</div>
</div>
<ReviewForm />
<div className='product-page__reviews-container'>
<Review />
<Review />
<Review />
<Review />
<Review />
</div>
</section>
</section>

View File

@@ -1,5 +1,5 @@
declare module "*.svg" {
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
const content: any;
export default content;
}