├── .eslintignore ├── .gitignore ├── .prettierignore ├── src ├── images │ ├── review.png │ ├── rating-empty.svg │ └── rating.svg ├── styles │ ├── base │ │ ├── breakpoints.scss │ │ ├── colours.scss │ │ ├── spacing.scss │ │ ├── function.scss │ │ ├── base.scss │ │ ├── mixin.scss │ │ └── button.scss │ └── Reviews.scss ├── components │ ├── index.ts │ ├── fail-message.tsx │ ├── success-message.tsx │ ├── pagination-button.tsx │ ├── average-rating.tsx │ ├── cards.tsx │ ├── pagination.tsx │ └── form.tsx ├── main.tsx ├── helpers │ └── helpers.tsx ├── data │ └── data.tsx └── Reviews.tsx ├── vite.config.ts ├── .prettierrc.js ├── README.md ├── index.html ├── .github └── workflows │ ├── build.yml │ └── eslint.yml ├── tsconfig.json ├── package.json ├── .eslintrc.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | node_modules/* -------------------------------------------------------------------------------- /src/images/review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrey-kudinov/reviews/HEAD/src/images/review.png -------------------------------------------------------------------------------- /src/styles/base/breakpoints.scss: -------------------------------------------------------------------------------- 1 | $breakpoints:( 2 | xs 480px 432px, 3 | sm 768px 720px, 4 | md 960px 912px, 5 | lg 1140px 1092px, 6 | xl 1920px 1872px 7 | ); 8 | -------------------------------------------------------------------------------- /src/styles/base/colours.scss: -------------------------------------------------------------------------------- 1 | $blue: #009ee0; 2 | $grey: #333333; 3 | $grey-2: #505050; 4 | $light-grey: #757575; 5 | $light-grey-2: #949494; 6 | $light-grey-3: #828282; 7 | $keyline-grey: #dadada; 8 | $red: #de1a00; 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import reactRefresh from '@vitejs/plugin-react-refresh' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [reactRefresh()] 7 | }) 8 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'none', 4 | singleQuote: true, 5 | printWidth: 90, 6 | tabWidth: 2, 7 | jsxBracketSameLine: true, 8 | endOfLine: 'auto', 9 | semi: false, 10 | jsxSingleQuote: true 11 | } 12 | -------------------------------------------------------------------------------- /src/styles/base/spacing.scss: -------------------------------------------------------------------------------- 1 | $spacing-map: ( 2 | 5xs: 4px, 3 | 4xs: 6px, 4 | 3xs: 8px, 5 | 2xs: 12px, 6 | xs: 16px, 7 | sm: 24px, 8 | md: 32px, 9 | lg: 48px, 10 | xl: 64px, 11 | 2xl: 80px, 12 | 3xl: 96px, 13 | 4xl: 104px, 14 | 5xl: 120px 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { AverageRating } from './average-rating' 2 | export { Cards } from './cards' 3 | export { FailMessage } from './fail-message' 4 | export { Form } from './form' 5 | export { Pagination } from './pagination' 6 | export { SuccessMessage } from './success-message' 7 | -------------------------------------------------------------------------------- /src/components/fail-message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const FailMessage = () => ( 4 |
5 |

Sorry, something went wrong!

6 |

7 | Try again later. Check out the DevTools Console anyway! 8 |

9 |
10 | ) 11 | -------------------------------------------------------------------------------- /src/styles/base/function.scss: -------------------------------------------------------------------------------- 1 | @function spacing($variant: default, $map: $spacing-map) { 2 | $value: map-get($map, $variant); 3 | 4 | @if type-of($value) == number { 5 | @return $value; 6 | } @else { 7 | @error 'Spacing variant `#{$variant}` not found. Available variants: #{available-names($map)}'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/success-message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const SuccessMessage = () => ( 4 |
5 |

Thank you for you review!

6 |

7 | Your review has been submitted. Check out the DevTools Console! 8 |

9 |
10 | ) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reviews 2 | 3 | Form with the Constraint validation API and a star rating system 4 | 5 | ### Commands 6 | 7 | | Command | Description | 8 | | ---------- | --------------------- | 9 | | yarn | install dependencies | 10 | | yarn dev | serve with hot reload | 11 | | yarn build | build the project | 12 | | yarn lint | run eslint | 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Reviews 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import { cardsData, productData } from './data/data' 5 | import { randomDate, randomRating } from './helpers/helpers' 6 | import Reviews from './Reviews' 7 | 8 | cardsData.forEach((card) => { 9 | card.date = String(randomDate(new Date(2012, 0, 1), new Date())) 10 | card.rating = randomRating(1, 5) 11 | }) 12 | 13 | ReactDOM.render( 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ) 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Try Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | - uses: actions/setup-node@v3 12 | name: Setup node 13 | with: 14 | node-version: 16.x.x 15 | cache: npm 16 | 17 | - run: node --version 18 | name: Check Node Version 19 | 20 | - uses: actions/checkout@v3 21 | name: Checkout code 22 | 23 | - run: yarn 24 | name: Install NPM Packages 25 | 26 | - run: yarn build 27 | name: Build Project 28 | 29 | -------------------------------------------------------------------------------- /src/styles/base/base.scss: -------------------------------------------------------------------------------- 1 | @import './colours.scss'; 2 | @import './spacing.scss'; 3 | @import './breakpoints.scss'; 4 | @import './button.scss'; 5 | @import './mixin.scss'; 6 | @import './function.scss'; 7 | 8 | body, textarea { 9 | margin: 0; 10 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 11 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | code { 17 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "types": ["vite/client"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["./src"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/base/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin breakpoint($point) { 2 | @each $breakpoint in $breakpoints { 3 | $name: nth($breakpoint, 1); 4 | $size: nth($breakpoint, 2); 5 | 6 | @if $point == $name { 7 | @media (min-width: $size) { 8 | @content; 9 | } 10 | } 11 | } 12 | } 13 | 14 | @mixin breakpoint-down($point) { 15 | @each $breakpoint in $breakpoints { 16 | $name: nth($breakpoint, 1); 17 | $size: nth($breakpoint, 2); 18 | 19 | @if $point == $name { 20 | @media (max-width: $size - 1px) { 21 | @content; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers/helpers.tsx: -------------------------------------------------------------------------------- 1 | export const randomRating = (min: number, max: number) => { 2 | return Number((Math.random() * (max - min + 1) + min).toFixed(1)) 3 | } 4 | 5 | export const randomDate = (start: Date, end: Date) => { 6 | return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())) 7 | } 8 | 9 | const consoleStyles: any = { 10 | log1: 'font: 2rem/1 Arial; color: crimson;', 11 | log2: 'font: 2rem/1 Arial; color: orangered;', 12 | log3: 'font: 2rem/1 Arial; color: olivedrab;', 13 | log4: 'font: 2rem/1 Arial; color: darkmagenta;', 14 | log5: 'font: 2rem/1 Arial; color: blue;' 15 | } 16 | 17 | export const log = (msg: string, style: string) => { 18 | console.log('%c' + msg, consoleStyles[style]) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/pagination-button.tsx: -------------------------------------------------------------------------------- 1 | import React, { KeyboardEvent } from 'react' 2 | 3 | interface IProps { 4 | // eslint-disable-next-line no-unused-vars 5 | handleChangePage: (page: number) => void 6 | // eslint-disable-next-line no-unused-vars 7 | handleKeyChangePage: (e: KeyboardEvent, page: number) => void 8 | currentPage: number 9 | shift: number 10 | } 11 | 12 | export const PaginationButton = ({ 13 | handleChangePage, 14 | handleKeyChangePage, 15 | currentPage, 16 | shift 17 | }: IProps) => ( 18 | // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions 19 |
  • handleChangePage(currentPage + shift)} 23 | onKeyDown={(e) => handleKeyChangePage(e, currentPage + shift)}> 24 | {currentPage + shift} 25 |
  • 26 | ) 27 | -------------------------------------------------------------------------------- /src/components/average-rating.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import type { IProductData } from '../data/data' 4 | 5 | export const AverageRating = ({ averageRating, numberOfReviews }: IProductData) => { 6 | /** 7 | * ratingWidth = card rating * width of one star 8 | */ 9 | const ratingWidth = { width: `${averageRating * 2}rem` } as React.CSSProperties 10 | 11 | return ( 12 |
    13 |

    Average ratings

    14 |
    15 |
    {averageRating}
    16 |
    17 |
    18 |
    19 |
    20 | {numberOfReviews} {numberOfReviews === 1 ? 'review' : 'reviews'} 21 |
    22 |
    23 |
    24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "review", 3 | "version": "1.0.0", 4 | "author": "Andrey Kudinov", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "serve": "vite preview", 9 | "lint:fix": "eslint ./src --ext .jsx,.js,.ts,.tsx --quiet --fix --ignore-path ./.gitignore", 10 | "lint:format": "prettier --loglevel warn --write \"./**/*.{js,jsx,ts,tsx,css,md,json}\" ", 11 | "lint": "yarn lint:format && yarn lint:fix ", 12 | "type-check": "tsc" 13 | }, 14 | "dependencies": { 15 | "react": "^17.0.2", 16 | "react-dom": "^17.0.2" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^17.0.14", 20 | "@types/react-dom": "^17.0.9", 21 | "@typescript-eslint/eslint-plugin": "^4.28.2", 22 | "@typescript-eslint/parser": "^4.28.2", 23 | "@vitejs/plugin-react-refresh": "^1.3.5", 24 | "eslint": "^7.30.0", 25 | "eslint-config-prettier": "^8.3.0", 26 | "eslint-plugin-import": "^2.23.4", 27 | "eslint-plugin-jsx-a11y": "^6.4.1", 28 | "eslint-plugin-prettier": "^3.4.0", 29 | "eslint-plugin-react": "^7.24.0", 30 | "eslint-plugin-simple-import-sort": "^7.0.0", 31 | "pre-commit": "^1.2.2", 32 | "prettier": "^2.3.2", 33 | "sass": "^1.43.4", 34 | "typescript": "^4.3.5", 35 | "vite": "^2.4.1" 36 | }, 37 | "pre-commit": "lint", 38 | "license": "MIT" 39 | } 40 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 2020, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | jsx: true 9 | } 10 | }, 11 | settings: { 12 | react: { 13 | version: 'detect' 14 | } 15 | }, 16 | env: { 17 | browser: true, 18 | amd: true, 19 | node: true 20 | }, 21 | extends: [ 22 | 'eslint:recommended', 23 | 'plugin:react/recommended', 24 | 'plugin:jsx-a11y/recommended', 25 | 'plugin:prettier/recommended' // Make sure this is always the last element in the array. 26 | ], 27 | plugins: ['simple-import-sort', 'prettier'], 28 | rules: { 29 | 'prettier/prettier': ['error', {}, { usePrettierrc: true }], 30 | 'react/react-in-jsx-scope': 'off', 31 | 'jsx-a11y/accessible-emoji': 'off', 32 | 'react/prop-types': 'off', 33 | '@typescript-eslint/explicit-function-return-type': 'off', 34 | 'simple-import-sort/imports': 'error', 35 | 'simple-import-sort/exports': 'error', 36 | 'jsx-a11y/anchor-is-valid': [ 37 | 'error', 38 | { 39 | components: ['Link'], 40 | specialLink: ['hrefLeft', 'hrefRight'], 41 | aspects: ['invalidHref', 'preferButton'] 42 | } 43 | ], 44 | semi: ['error', 'never'], 45 | 'jsx-quotes': ['error', 'prefer-single'], 46 | 'comma-dangle': ['error', 'never'] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/cards.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import type { ICardsData } from '../data/data' 4 | 5 | interface IProps { 6 | currentCardsOnPage: ICardsData[] 7 | } 8 | 9 | export const Cards = (props: IProps) => { 10 | const { currentCardsOnPage } = props 11 | const cards = currentCardsOnPage.map((card: ICardsData, index: number) => { 12 | /** 13 | * ratingWidth = card rating * width of one star 14 | */ 15 | const ratingWidth = { width: `${card.rating * 1}rem` } as React.CSSProperties 16 | 17 | const formatDate = (date: string) => { 18 | const parseDate = Date.parse(date) 19 | const options = { 20 | timeZone: 'Australia/Sydney' 21 | } 22 | 23 | return new Intl.DateTimeFormat('en-AU', options).format(parseDate) 24 | } 25 | 26 | return ( 27 |
    28 |

    {card.author}

    29 |
    30 |
    31 |
    32 |
    33 |
    {formatDate(card.date)}
    34 |
    35 |

    {card.title}

    36 |

    {card.body}

    37 |
    38 | ) 39 | }) 40 | 41 | return
    {cards}
    42 | } 43 | -------------------------------------------------------------------------------- /src/styles/base/button.scss: -------------------------------------------------------------------------------- 1 | button { 2 | position: relative; 3 | display: inline-flex; 4 | overflow: hidden; 5 | align-items: center; 6 | justify-content: center; 7 | box-sizing: border-box; 8 | height: 48px; 9 | padding-right: 16px; 10 | padding-left: 16px; 11 | list-style: none; 12 | cursor: pointer; 13 | user-select: none; 14 | transition: box-shadow .15s,transform .15s; 15 | text-align: left; 16 | white-space: nowrap; 17 | text-decoration: none; 18 | color: #fff; 19 | border: 0; 20 | border-radius: 6px; 21 | background-image: radial-gradient(100% 100% at 100% 0, #5adaff 0, $blue 100%); 22 | box-shadow: rgba(45, 35, 66, .4) 0 2px 4px,rgba(45, 35, 66, .3) 0 7px 13px -3px,rgba(58, 65, 111, .5) 0 -3px 0 inset; 23 | font-size: 18px; 24 | line-height: 1; 25 | font-weight: 600; 26 | letter-spacing: 0.05rem; 27 | appearance: none; 28 | touch-action: manipulation; 29 | will-change: box-shadow,transform; 30 | } 31 | 32 | button:focus-visible { 33 | box-shadow: darken($blue, 10%) 0 0 0 1.5px inset, rgba(45, 35, 66, .4) 0 2px 4px, rgba(45, 35, 66, .3) 0 7px 13px -3px, darken($blue, 10%) 0 -3px 0 inset; 34 | } 35 | 36 | button:hover { 37 | transform: translateY(-2px); 38 | box-shadow: rgba(45, 35, 66, .4) 0 4px 8px, rgba(45, 35, 66, .3) 0 7px 13px -3px, darken($blue, 10%) 0 -3px 0 inset; 39 | } 40 | 41 | button:active { 42 | transform: translateY(2px); 43 | box-shadow: darken($blue, 10%) 0 3px 7px inset; 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # ESLint is a tool for identifying and reporting on patterns 6 | # found in ECMAScript/JavaScript code. 7 | # More details at https://github.com/eslint/eslint 8 | # and https://eslint.org 9 | 10 | name: ESLint 11 | 12 | on: 13 | push: 14 | branches: [ "master" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "master" ] 18 | schedule: 19 | - cron: '50 23 * * 1' 20 | 21 | jobs: 22 | eslint: 23 | name: Run eslint scanning 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | security-events: write 28 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v3 32 | 33 | - name: Install ESLint 34 | run: | 35 | npm install eslint@8.10.0 36 | npm install @microsoft/eslint-formatter-sarif@2.1.7 37 | 38 | - name: Run ESLint 39 | run: npx eslint . 40 | --config .eslintrc.js 41 | --ext .js,.jsx,.ts,.tsx 42 | --format @microsoft/eslint-formatter-sarif 43 | --output-file eslint-results.sarif 44 | continue-on-error: true 45 | 46 | - name: Upload analysis results to GitHub 47 | uses: github/codeql-action/upload-sarif@v2 48 | with: 49 | sarif_file: eslint-results.sarif 50 | wait-for-processing: true 51 | -------------------------------------------------------------------------------- /src/components/pagination.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 2 | /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ 3 | import React from 'react' 4 | 5 | import { PaginationButton } from './pagination-button' 6 | 7 | export const Pagination = (props: any) => { 8 | const { 9 | countPages, 10 | currentPage, 11 | currentNumRef, 12 | nextCardsGroup, 13 | prevCardsGroup, 14 | handleKeyNextCardsGroup, 15 | handleKeyPrevCardsGroup, 16 | ...restProps 17 | } = props 18 | 19 | return ( 20 | 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/images/rating-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/data/data.tsx: -------------------------------------------------------------------------------- 1 | export interface IProductData { 2 | averageRating: number 3 | numberOfReviews: number 4 | } 5 | 6 | export interface ICardsData { 7 | date: string 8 | rating: number 9 | author: string 10 | title: string 11 | body: string 12 | } 13 | 14 | export const productData: IProductData = { 15 | averageRating: 4.5, 16 | numberOfReviews: 193 17 | } 18 | 19 | export const cardsData: ICardsData[] = [ 20 | { 21 | author: 'Ashley N 1', 22 | rating: 1.2, 23 | date: '3 weeks ago', 24 | title: 'Love it!!!', 25 | body: 'Love the glasses. Very durable and solid. Definitely bring out the best of me.' 26 | }, 27 | { 28 | author: 'John 1', 29 | rating: 2.5, 30 | date: '3 weeks ago', 31 | title: 'Great quality', 32 | body: 'Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato.' 33 | }, 34 | { 35 | author: 'Zoe Mitchel 1', 36 | rating: 4.3, 37 | date: '3 weeks ago', 38 | title: 'Amazing glasses', 39 | body: 'Love the glasses. Very durable and solid. Definitely bring out the best of me.' 40 | }, 41 | { 42 | author: 'Michael Ho 1', 43 | rating: 4.8, 44 | date: '3 weeks ago', 45 | title: 'Best glasses by far', 46 | body: 'Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato.' 47 | }, 48 | { 49 | author: 'Ashley N 2', 50 | rating: 1.2, 51 | date: '3 weeks ago', 52 | title: 'Love it!!!', 53 | body: 'Love the glasses. Very durable and solid. Definitely bring out the best of me.' 54 | }, 55 | { 56 | author: 'John 2', 57 | rating: 2.5, 58 | date: '3 weeks ago', 59 | title: 'Great quality', 60 | body: 'Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato.' 61 | }, 62 | { 63 | author: 'Zoe Mitchel 2', 64 | rating: 4.3, 65 | date: '3 weeks ago', 66 | title: 'Amazing glasses', 67 | body: 'Love the glasses. Very durable and solid. Definitely bring out the best of me.' 68 | }, 69 | { 70 | author: 'Michael Ho 2', 71 | rating: 4.8, 72 | date: '3 weeks ago', 73 | title: 'Best glasses by far', 74 | body: 'Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato.' 75 | }, 76 | 77 | { 78 | author: 'Ashley N 3', 79 | rating: 1.2, 80 | date: '3 weeks ago', 81 | title: 'Love it!!!', 82 | body: 'Love the glasses. Very durable and solid. Definitely bring out the best of me.' 83 | }, 84 | { 85 | author: 'John 3', 86 | rating: 2.5, 87 | date: '3 weeks ago', 88 | title: 'Great quality', 89 | body: 'Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato.' 90 | }, 91 | { 92 | author: 'Zoe Mitchel 3', 93 | rating: 4.3, 94 | date: '3 weeks ago', 95 | title: 'Amazing glasses', 96 | body: 'Love the glasses. Very durable and solid. Definitely bring out the best of me.' 97 | }, 98 | { 99 | author: 'Michael Ho 3', 100 | rating: 4.8, 101 | date: '3 weeks ago', 102 | title: 'Best glasses by far', 103 | body: 'Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato.' 104 | }, 105 | { 106 | author: 'Michael Ho 4', 107 | rating: 4.8, 108 | date: '3 weeks ago', 109 | title: 'Best glasses by far', 110 | body: 'Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato.' 111 | } 112 | ] 113 | -------------------------------------------------------------------------------- /src/images/rating.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/form.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React, { Fragment, SyntheticEvent } from 'react' 3 | 4 | import type { IData } from '../Reviews' 5 | 6 | interface IProps { 7 | data: IData 8 | handleRating: (e: SyntheticEvent) => void 9 | handleName: (e: SyntheticEvent) => void 10 | handleEmail: (e: SyntheticEvent) => void 11 | handleTitle: (e: SyntheticEvent) => void 12 | handleReview: (e: SyntheticEvent) => void 13 | handleCheck: (e: SyntheticEvent) => void 14 | handleSubmit: (e: SyntheticEvent) => void 15 | } 16 | 17 | export const Form = (props: IProps) => { 18 | const { 19 | data, 20 | handleRating, 21 | handleName, 22 | handleEmail, 23 | handleTitle, 24 | handleReview, 25 | handleCheck, 26 | handleSubmit 27 | } = props 28 | 29 | const starSvg = ( 30 | 37 | 41 | 42 | ) 43 | 44 | const starsData = [ 45 | { ariaLabel: 'Poor' }, 46 | { ariaLabel: 'Quite good' }, 47 | { ariaLabel: 'Average' }, 48 | { ariaLabel: 'Very good' }, 49 | { ariaLabel: 'Perfection' } 50 | ] 51 | 52 | const starsRender = starsData.map((star, index) => { 53 | return ( 54 | 55 | 65 | 68 | 69 | ) 70 | }) 71 | 72 | return ( 73 |
    74 |
    75 |
    76 | Rate this product 77 | 78 | * 79 | 80 |
    81 |
    {starsRender.reverse()}
    82 |
    83 | 84 |
    85 | 91 | 101 |
    102 | 103 |
    104 | 110 | 120 |
    121 | 122 |
    123 | 129 | 139 |
    140 | 141 |
    142 | 148 |