(feat): feather icons ratings

This commit is contained in:
Maksym Sadovnychyy 2022-05-22 01:07:10 +02:00
parent 7e4da741ec
commit 28d115af0e
14 changed files with 346 additions and 132 deletions

View File

@ -1,7 +1,8 @@
// React
import React, { FC, useEffect } from 'react'
import { Route, Routes, useLocation } from 'react-router'
//Redux
// Redux
import { useSelector, useDispatch } from 'react-redux'
import { actionCreators as settingsActionCreators } from './store/reducers/Settings'
@ -33,8 +34,6 @@ const NestedRoutes = (routes: IRoute[], tag: string | undefined = undefined) =>
})
}
const App: FC = () => {
const { pathname } = useLocation()
const dispatch = useDispatch()

View File

@ -6,6 +6,7 @@ interface IFeatherIcon {
size?: string | number,
className?: string,
fill?: string,
stroke?: string,
otherProps?: any
}
@ -17,7 +18,7 @@ interface IFeatherIcon {
*/
const FeatherIcon : FC<IFeatherIcon> = (props : IFeatherIcon) => {
const { icon, size = 24, className = '', fill = 'none', otherProps } = props
const { icon, size = 24, className = '', fill = 'none', stroke = 'black', otherProps } = props
if (!icon) {
return null
@ -29,7 +30,7 @@ const FeatherIcon : FC<IFeatherIcon> = (props : IFeatherIcon) => {
height={size}
viewBox="0 0 24 24"
fill={fill}
stroke="currentColor"
stroke={stroke}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"

View File

@ -0,0 +1,57 @@
import React, { FC } from 'react'
import { FeatherIcon } from '../FeatherIcons'
import { ICreateIconProps, ICreateIconResponse, IFeatherRating } from './interfaces'
const FeatherRating: FC<IFeatherRating> = ({
value,
icons = {
complete: { icon: "star", color: '#ffbf00' },
half: { icon: "star", color: '#ffdf80' },
empty: { icon: "star", color: '#ffffff' }
},
max = 5,
}) => {
const createIcon = ({ value, icons, max }: ICreateIconProps): ICreateIconResponse[] => {
let i = +value
const iconsToPrint: ICreateIconResponse[] = []
const { complete, half, empty } = icons
for (i; i >= 1; i -= 1) {
iconsToPrint.push({
icon: complete.icon,
fill: complete.color,
stroke: complete.color
})
}
if (i >= 0.5 && i <= 1) {
iconsToPrint.push({
icon: half.icon,
fill: half.color,
stroke: half.color
})
}
while (iconsToPrint.length < max) {
iconsToPrint.push({
icon: empty.icon,
fill: empty.color,
stroke: empty.color
})
}
return iconsToPrint
}
return <div>
{createIcon({ value, icons, max }).map((icon, i) =>
<FeatherIcon key={i} {...icon} />
)}
</div>
}
export {
FeatherRating
}

View File

@ -0,0 +1,36 @@
export interface IIcon {
icon: string,
color: string
}
export interface IAllowIconsType {
complete: IIcon
half: IIcon
empty: IIcon
}
export interface IFeatherRating {
value: number
icons?: IAllowIconsType
max?: number
}
export interface ICreateIconProps {
value: number
icons: IAllowIconsType
max: number
}
export interface ICreateIconResponse {
icon: string
fill: string
stroke: string
}
export interface ICustomStylesProps {
current: string
icons: IAllowIconsType
colors: string[]
type: string
}

View File

@ -1,4 +1,4 @@
const getKeyValue = <T extends object, U extends keyof T>(key: U) => (obj: T) => obj[key];
const getKeyValue = <T extends object, U extends keyof T>(key: U) => (obj: T) => obj[key]
export {
getKeyValue

View File

@ -0,0 +1,46 @@
/**
* The code was extracted from:
* https://github.com/davidchambers/Base64.js
*/
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
class InvalidCharacterError extends Error {
message: string
constructor(message: string) {
super(message)
// Set the prototype explicitly.
Object.setPrototypeOf(this, InvalidCharacterError.prototype)
this.message = message
this.name = 'InvalidCharacterError'
}
}
const polyfill = (input: any) => {
var str = String(input).replace(/=+$/, '')
if (str.length % 4 == 1) {
throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.")
}
for (
// initialize result and counters
var bc = 0, bs = 0, buffer, idx = 0, output = '';
// get next character
buffer = str.charAt(idx++);
// character found in table? initialize bit storage and add its ascii value;
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer)
}
return output
}
export default typeof window !== 'undefined' && window.atob && window.atob.bind(window) || polyfill

View File

@ -0,0 +1,34 @@
import atob from './atob'
const b64DecodeUnicode = (str: string) => {
return decodeURIComponent(atob(str).replace(/(.)/g, function (m, p) {
var code = p.charCodeAt(0).toString(16).toUpperCase()
if (code.length < 2) {
code = '0' + code
}
return '%' + code
}))
}
export default (str: string) => {
var output = str.replace(/-/g, '+').replace(/_/g, '/')
switch (output.length % 4) {
case 0:
break
case 2:
output += '=='
break
case 3:
output += '='
break
default:
throw 'Illegal base64url string!'
}
try {
return b64DecodeUnicode(output)
} catch (err) {
return atob(output)
}
}

View File

@ -0,0 +1,42 @@
'use strict'
import base64_url_decode from './base64_url_decode'
class InvalidTokenError extends Error {
message: string
constructor(message: string) {
super(message)
// Set the prototype explicitly.
Object.setPrototypeOf(this, InvalidTokenError.prototype)
this.message = message
this.name = 'InvalidTokenError'
}
}
interface IOptions {
header: boolean | undefined
}
const JwtDecode = function (token: string, options: IOptions) {
if (typeof token !== 'string') {
throw new InvalidTokenError('Invalid token specified')
}
options = options || {}
var pos = options.header === true ? 0 : 1
try {
return JSON.parse(base64_url_decode(token.split('.')[pos]))
} catch (e) {
if (e instanceof Error) {
throw new InvalidTokenError('Invalid token specified: ' + e.message)
}
}
}
JwtDecode.InvalidTokenError = InvalidTokenError
export default JwtDecode

View File

@ -20,4 +20,9 @@ export interface IMenuItem extends ISubMenuItem {
items?: ISubMenuItem []
}
export interface IImage {
src: string,
alt: string
}

View File

@ -1,6 +1,7 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { Card, CardBody, CardHeader, CardImg, Col, Container, Row } from 'reactstrap'
import { Categories, Empty, Search } from '../SideWidgets'
const BlogCatalog = () => {
const items = [
@ -46,7 +47,7 @@ const BlogCatalog = () => {
</Card>
<Row>
{items.map((item, index) => <Col key={index} lg={6}>
{items.map((item, index) => <Col key={index} className="lg-6">
<Card className="mb-4">
<Link to={`${item.slug}`}>
@ -79,44 +80,10 @@ const BlogCatalog = () => {
</Row>
</Col>
<Col className="lg-4">
<Card className="mb-4">
<CardHeader>Search</CardHeader>
<CardBody>
<div className="input-group">
<input className="form-control" type="text" placeholder="Enter search term..." aria-label="Enter search term..." aria-describedby="button-search" />
<button className="btn btn-primary" id="button-search" type="button">Go!</button>
</div>
</CardBody>
</Card>
<Card className="mb-4">
<CardHeader>Categories</CardHeader>
<CardBody>
<Row>
<Col className="sm-6">
<ul className="list-unstyled mb-0">
<li><a href="#!">Web Design</a></li>
<li><a href="#!">HTML</a></li>
<li><a href="#!">Freebies</a></li>
</ul>
</Col>
<Col sm={6}>
<ul className="list-unstyled mb-0">
<li><a href="#!">JavaScript</a></li>
<li><a href="#!">CSS</a></li>
<li><a href="#!">Tutorials</a></li>
</ul>
</Col>
</Row>
</CardBody>
</Card>
<Card className="mb-4">
<CardHeader>Side Widget</CardHeader>
<CardBody>You can put anything you want inside of these side widgets. They are easy to use, and feature the Bootstrap 5 card component!</CardBody>
</Card>
<Col lg="4">
<Search />
<Categories />
<Empty/>
</Col>
</Row>
</Container>

View File

@ -1,5 +1,6 @@
import React from 'react'
import { Card, CardBody, CardHeader, Col, Container, Row } from 'reactstrap'
import { Categories, Empty, Search } from '../SideWidgets'
const BlogItem = () => {
@ -8,9 +9,9 @@ const BlogItem = () => {
comments: []
}
return <Container fluid className="mt-5">
return <Container fluid mt="5">
<Row>
<Col className="lg-8">
<Col lg="8">
<article>
<header className="mb-4">
@ -41,9 +42,8 @@ const BlogItem = () => {
<section className="mb-5">
<div className="card bg-light">
<div className="card-body">
<form className="mb-4">
{/*<textarea class="form-control" rows="3" placeholder="Join the discussion and leave a comment!"></textarea>*/}
<form className="mb-4">
<textarea className="form-control" rows={3} placeholder="Join the discussion and leave a comment!"></textarea>
</form>
<div className="d-flex mb-4">
@ -84,45 +84,10 @@ const BlogItem = () => {
<Col className="lg-4">
<Card className="mb-4">
<CardHeader>Search</CardHeader>
<CardBody>
<div className="input-group">
<input className="form-control" type="text" placeholder="Enter search term..." aria-label="Enter search term..." aria-describedby="button-search" />
<button className="btn btn-primary" id="button-search" type="button">Go!</button>
</div>
</CardBody>
</Card>
<Card className="mb-4">
<CardHeader>Categories</CardHeader>
<CardBody>
<Row>
<Col className="sm-6">
<ul className="list-unstyled mb-0">
<li><a href="#!">Web Design</a></li>
<li><a href="#!">HTML</a></li>
<li><a href="#!">Freebies</a></li>
</ul>
</Col>
<Col className="sm-6">
<ul className="list-unstyled mb-0">
<li><a href="#!">JavaScript</a></li>
<li><a href="#!">CSS</a></li>
<li><a href="#!">Tutorials</a></li>
</ul>
</Col>
</Row>
</CardBody>
</Card>
<Card className="mb-4">
<CardHeader>Side Widget</CardHeader>
<CardBody>
You can put anything you want inside of these side widgets. They are easy to use, and feature the Bootstrap 5 card component!
</CardBody>
</Card>
<Col lg="4">
<Search />
<Categories />
<Empty/>
</Col>
</Row>
</Container>

View File

@ -1,5 +1,53 @@
import React from 'react'
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
const SideWidgets = () => {
return null
const Search = () => {
return <Card className="mb-4">
<CardHeader>Search</CardHeader>
<CardBody>
<div className="input-group">
<input className="form-control" type="text" placeholder="Enter search term..." aria-label="Enter search term..." aria-describedby="button-search" />
<button className="btn btn-primary" id="button-search" type="button">Go!</button>
</div>
</CardBody>
</Card>
}
const Categories = () => {
return <Card className="mb-4">
<CardHeader>Categories</CardHeader>
<CardBody>
<Row>
<Col sm="6">
<ul className="list-unstyled mb-0">
<li><a href="#!">Web Design</a></li>
<li><a href="#!">HTML</a></li>
<li><a href="#!">Freebies</a></li>
</ul>
</Col>
<Col sm="6">
<ul className="list-unstyled mb-0">
<li><a href="#!">JavaScript</a></li>
<li><a href="#!">CSS</a></li>
<li><a href="#!">Tutorials</a></li>
</ul>
</Col>
</Row>
</CardBody>
</Card>
}
const Empty = () => {
return <Card className="mb-4">
<CardHeader>Side Widget</CardHeader>
<CardBody>
You can put anything you want inside of these side widgets. They are easy to use, and feature the Bootstrap 5 card component!
</CardBody>
</Card>
}
export {
Search,
Categories,
Empty
}

View File

@ -2,32 +2,10 @@ import React, { FC } from 'react'
import { Link } from 'react-router-dom'
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
import { FeatherIcon } from '../../components/FeatherIcons'
import { IImage } from '../../interfaces'
import style from './scss/style.module.scss'
interface IImage {
src: string,
alt: string
}
interface IBlogAuthor {
name: string,
image: IImage
}
interface IBlogItem {
image: IImage,
badge: string,
title: string,
text: string,
author: IBlogAuthor,
date: string,
readTime: string
}
interface ITitleSection {
title: string,
text: string
@ -120,6 +98,21 @@ const TestimonialsSection: FC<ITestimonialsSection> = (props) => {
</section>
}
interface IBlogAuthor {
name: string,
image: IImage
}
interface IBlogItem {
image: IImage,
badge: string,
title: string,
text: string,
author: IBlogAuthor,
date: string,
readTime: string
}
interface IFromOurBlogSection {
title: string,
text: string,
@ -205,7 +198,7 @@ const Home = () => {
<ul>
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
<li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>
<li><a href='https://getbootstrap.com/'>Bootstrap</a> + <a href='https://reactstrap.github.io/?path=/story/home-installation--page'>Reactstrap</a> + <a href="https://feathericons.com/">Feather icons</a> for layout and styling</li>
<li><a href='https://getbootstrap.com/'>Bootstrap</a>, <a href='https://reactstrap.github.io/?path=/story/home-installation--page'>Reactstrap</a> and <a href="https://feathericons.com/">Feather icons</a> for layout and styling</li>
</ul>
`
}

View File

@ -2,32 +2,55 @@ import * as React from 'react'
import { Link } from 'react-router-dom'
import { Card, CardBody, CardFooter, CardImg, Col, Container, Row } from 'reactstrap'
import { FeatherRating } from '../../../components/FeatherRating'
const ShopCatalog = () => {
const items = [
{
id: "1"
id: "1",
rating: 5,
price: "$20.00"
},
{
id: "2"
id: "2",
rating: 3.5,
price: "$20.00",
newPrice: "$10.00"
},
{
id: "3"
id: "3",
rating: 2,
price: "$20.00",
newPrice: "$10.00"
},
{
id: "4"
id: "4",
rating: 4,
price: "$20.00"
},
{
id: "5"
id: "5",
rating: 4.5,
price: "$20.00",
newPrice: "$10.00"
},
{
id: "6"
id: "6",
rating: 5,
price: "$20.00",
newPrice: "$10.00"
},
{
id: "7"
id: "7",
rating: 2,
price: "$20.00"
},
{
id: "8"
id: "8",
rating: 3,
price: "$20.00",
newPrice: "$10.00"
}
]
@ -55,16 +78,14 @@ const ShopCatalog = () => {
<div className="text-center">
<h5 className="fw-bolder">Fancy Product</h5>
<div className="d-flex justify-content-center small text-warning mb-2">
<div className="bi-star-fill"></div>
<div className="bi-star-fill"></div>
<div className="bi-star-fill"></div>
<div className="bi-star-fill"></div>
<div className="bi-star-fill"></div>
</div>
<FeatherRating {...{
value: item.rating
}} />
{item.newPrice
? <><span className="text-muted text-decoration-line-through">{item.price}</span> {item.newPrice}</>
: item.price}
<span className="text-muted text-decoration-line-through">$20.00</span>
$10.00
</div>
</CardBody>
<CardFooter className="p-4 pt-0 border-top-0 bg-transparent">