(feat): feather icons ratings
This commit is contained in:
parent
7e4da741ec
commit
28d115af0e
@ -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()
|
||||
|
||||
@ -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"
|
||||
|
||||
57
clientapp/src/components/FeatherRating/index.tsx
Normal file
57
clientapp/src/components/FeatherRating/index.tsx
Normal 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
|
||||
}
|
||||
36
clientapp/src/components/FeatherRating/interfaces.ts
Normal file
36
clientapp/src/components/FeatherRating/interfaces.ts
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
46
clientapp/src/functions/jwtDecode/atob.ts
Normal file
46
clientapp/src/functions/jwtDecode/atob.ts
Normal 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
|
||||
|
||||
34
clientapp/src/functions/jwtDecode/base64_url_decode.ts
Normal file
34
clientapp/src/functions/jwtDecode/base64_url_decode.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
42
clientapp/src/functions/jwtDecode/index.ts
Normal file
42
clientapp/src/functions/jwtDecode/index.ts
Normal 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
|
||||
|
||||
@ -20,4 +20,9 @@ export interface IMenuItem extends ISubMenuItem {
|
||||
items?: ISubMenuItem []
|
||||
}
|
||||
|
||||
export interface IImage {
|
||||
src: string,
|
||||
alt: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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>
|
||||
`
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user