+
-
)
}
-
export default Usage
diff --git a/showcase/src/patterns/10.js b/showcase/src/patterns/10.js
index a2ffb36..40bba52 100644
--- a/showcase/src/patterns/10.js
+++ b/showcase/src/patterns/10.js
@@ -1,427 +1,372 @@
import React, {
- useReducer,
- useState,
- useEffect,
- useCallback,
- useLayoutEffect,
- forwardRef,
- useRef
- } from 'react'
-
- import mojs from 'mo-js'
- import { generateRandomNumber } from '../utils/generateRandomNumber'
- import styles from './index.css'
- import userStyles from './usage.css'
-
- /** ====================================
- * 🔰Hook
- Hook for Holding Previous Vals
- ==================================== **/
- function usePrevious (value) {
- const ref = useRef()
- useEffect(() => {
- ref.current = value
- })
- return ref.current === undefined ? null : ref.current
- }
-
- /** ====================================
- * 🔰Hook
- Hook for Animation
- ==================================== **/
-
- const useClapAnimation = ({
- duration: tlDuration,
- bounceEl,
- fadeEl,
- burstEl
- }) => {
- const [animationTimeline, setAnimationTimeline] = useState(
- new mojs.Timeline()
- )
-
- useLayoutEffect(
- () => {
- if (!bounceEl || !fadeEl || !burstEl) {
- return
- }
-
- const triangleBurst = new mojs.Burst({
- parent: burstEl,
- radius: { 50: 95 },
- count: 5,
- angle: 30,
- children: {
- shape: 'polygon',
- radius: { 6: 0 },
- scale: 1,
- stroke: 'rgba(211,84,0 ,0.5)',
- strokeWidth: 2,
- angle: 210,
- delay: 30,
- speed: 0.2,
- easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
- duration: tlDuration
- }
- })
-
- const circleBurst = new mojs.Burst({
- parent: burstEl,
- radius: { 50: 75 },
- angle: 25,
- duration: tlDuration,
- children: {
- shape: 'circle',
- fill: 'rgba(149,165,166 ,0.5)',
- delay: 30,
- speed: 0.2,
- radius: { 3: 0 },
- easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
- }
- })
-
- const countAnimation = new mojs.Html({
- el: bounceEl,
- isShowStart: false,
- isShowEnd: true,
- y: { 0: -30 },
- opacity: { 0: 1 },
- duration: tlDuration
- }).then({
- opacity: { 1: 0 },
- y: -80,
- delay: tlDuration / 2
- })
-
- const countTotalAnimation = new mojs.Html({
- el: fadeEl,
- isShowStart: false,
- isShowEnd: true,
- opacity: { 0: 1 },
- delay: (3 * tlDuration) / 2,
- duration: tlDuration,
- y: { 0: -3 }
- })
-
- const scaleButton = new mojs.Html({
- el: burstEl,
- duration: tlDuration,
- scale: { 1.3: 1 },
- easing: mojs.easing.out
- })
-
- if (typeof burstEl === 'string') {
- const id = burstEl.slice(1, burstEl.length)
- const el = document.getElementById(id)
- el.style.transform = 'scale(1, 1)'
- } else {
- burstEl.style.transform = 'scale(1, 1)'
- }
-
- const updatedAnimationTimeline = animationTimeline.add([
- countAnimation,
- countTotalAnimation,
- scaleButton,
- circleBurst,
- triangleBurst
- ])
-
- setAnimationTimeline(updatedAnimationTimeline)
- },
- [tlDuration, animationTimeline, bounceEl, fadeEl, burstEl]
- )
-
- return animationTimeline
- }
-
- /** ====================================
- * 🔰Hook
- Hook for Clap State
- ==================================== **/
- const MAX_CLAP = 50
- const INIT_STATE = {
- count: 0,
- countTotal: generateRandomNumber(500, 10000),
- isClicked: false
- }
-
- const callFnsInSequence = (...fns) => (...args) =>
- fns.forEach(fn => fn && fn(...args))
-
- const clapReducer = (state, { type, payload }) => {
- const { count, countTotal } = state
-
- switch (type) {
- case useClapState.types.clap:
- return {
- count: count + 1,
- countTotal: countTotal + 1,
- isClicked: true
- }
- case useClapState.types.reset:
- return payload
- default:
- return state
+ useState,
+ useLayoutEffect,
+ useCallback,
+ useRef,
+ useEffect,
+ useReducer
+} from 'react'
+import mojs from 'mo-js'
+import styles from './index.css'
+import userStyles from './usage.css'
+
+const INITIAL_STATE = {
+ count: 0,
+ countTotal: 267,
+ isClicked: false
+}
+
+/**
+ * Custom Hook for animation
+ */
+const useClapAnimation = ({ clapEl, countEl, clapTotalEl }) => {
+ const [animationTimeline, setAnimationTimeline] = useState(
+ () => new mojs.Timeline()
+ )
+
+ useLayoutEffect(() => {
+ if (!clapEl || !countEl || !clapTotalEl) {
+ return
}
- }
-
- const useClapState = ({
- initialState = INIT_STATE,
- reducer = clapReducer
- } = {}) => {
- const initialStateRef = useRef(initialState)
- const [clapState, dispatch] = useReducer(reducer, initialStateRef.current)
- const { count } = clapState
-
- const handleClapClick = () => dispatch({ type: 'clap' })
-
- const resetRef = useRef(0)
- // reset only if there's a change. It's possible to check changes to other state values e.g. countTotal & isClicked
- const reset = useCallback(() => {
- dispatch({ type: 'reset', payload: initialStateRef.current })
- ++resetRef.current
- }, [])
-
- const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
- onClick: callFnsInSequence(handleClapClick, onClick),
- 'aria-pressed': clapState.isClicked,
- ...otherProps
+
+ const tlDuration = 300
+ const scaleButton = new mojs.Html({
+ el: clapEl,
+ duration: tlDuration,
+ scale: { 1.3: 1 },
+ easing: mojs.easing.ease.out
})
-
- const getCounterProps = ({ ...otherProps }) => ({
- count,
- 'aria-valuemax': MAX_CLAP,
- 'aria-valuemin': 0,
- 'aria-valuenow': count,
- ...otherProps
+
+ const triangleBurst = new mojs.Burst({
+ parent: clapEl,
+ radius: { 50: 95 },
+ count: 5,
+ angle: 30,
+ children: {
+ shape: 'polygon',
+ radius: { 6: 0 },
+ stroke: 'rgba(211,54,0,0.5)',
+ strokeWidth: 2,
+ angle: 210,
+ delay: 30,
+ speed: 0.2,
+ easing: mojs.easing.bezier(0.1, 1, 0.3, 1),
+ duration: tlDuration
+ }
})
-
- return {
- clapState,
- getTogglerProps,
- getCounterProps,
- reset,
- resetDep: resetRef.current
+
+ const circleBurst = new mojs.Burst({
+ parent: clapEl,
+ radius: { 50: 75 },
+ angle: 25,
+ duration: tlDuration,
+ children: {
+ shape: 'circle',
+ fill: 'rgba(149,165,166,0.5)',
+ delay: 30,
+ speed: 0.2,
+ radius: { 3: 0 },
+ easing: mojs.easing.bezier(0.1, 1, 0.3, 1)
+ }
+ })
+
+ const countAnimation = new mojs.Html({
+ el: countEl,
+ opacity: { 0: 1 },
+ y: { 0: -30 },
+ duration: tlDuration
+ }).then({
+ opacity: { 1: 0 },
+ y: -80,
+ delay: tlDuration / 2
+ })
+
+ const countTotalAnimation = new mojs.Html({
+ el: clapTotalEl,
+ opacity: { 0: 1 },
+ delay: (3 * tlDuration) / 2,
+ duration: tlDuration,
+ y: { 0: -3 }
+ })
+
+ if (typeof clapEl === 'string') {
+ const clap = document.getElementById('clap')
+ clap.style.transform = 'scale(1,1)'
+ } else {
+ clapEl.style.transform = 'scale(1,1)'
}
- }
-
- useClapState.reducer = clapReducer
- useClapState.types = {
- clap: 'clap',
- reset: 'reset'
- }
-
- /** ====================================
- * 🔰Hook
- useEffectAfterMount
- ==================================== **/
-
- function useEffectAfterMount (cb, deps) {
- const componentJustMounted = useRef(true)
- useEffect(() => {
- if (!componentJustMounted.current) {
- return cb()
+
+ const newAnimationTimeline = animationTimeline.add([
+ scaleButton,
+ countTotalAnimation,
+ countAnimation,
+ triangleBurst,
+ circleBurst
+ ])
+ setAnimationTimeline(newAnimationTimeline)
+ }, [clapEl, countEl, clapTotalEl])
+
+ return animationTimeline
+}
+
+/**
+ * useDOMRef Hook
+ */
+const useDOMRef = () => {
+ const [DOMRef, setRefState] = useState({})
+
+ const setRef = useCallback(node => {
+ setRefState(prevRefState => ({
+ ...prevRefState,
+ [node.dataset.refkey]: node
+ }))
+ }, [])
+
+ return [DOMRef, setRef]
+}
+/**
+ *
+ * custom hook for getting preivous prop/state
+ */
+const usePrevious = value => {
+ const ref = useRef()
+ useEffect(() => {
+ ref.current = value
+ })
+ return ref.current
+}
+
+// const handleClick = (evt) => { ... }
+//
+const callFnsInSequence = (...fns) => (...args) => {
+ fns.forEach(fn => fn && fn(...args))
+}
+
+/**
+ * custom hook for useClapState
+ */
+const MAXIMUM_USER_CLAP = 50
+const internalReducer = ({ count, countTotal }, { type, payload }) => {
+ switch (type) {
+ case 'clap':
+ return {
+ isClicked: true,
+ count: Math.min(count + 1, MAXIMUM_USER_CLAP),
+ countTotal: count < MAXIMUM_USER_CLAP ? countTotal + 1 : countTotal } - componentJustMounted.current = false - // eslint-disable-next-line react-hooks/exhaustive-deps - }, deps) + case 'reset': + return payload + default: + break } - - /** ==================================== - * 🔰Hook - useDOMRef - ==================================== **/ - const useDOMRef = () => {
- const [DOMRef, setDOMRef] = useState({})
- const setRef = useCallback(node => {
- if (node !== null) {
- setDOMRef(prevDOMRefs => ({
- ...prevDOMRefs,
- [node.dataset.refkey]: node
- }))
- }
- }, [])
-
- return [DOMRef, setRef]
+}
+const useClapState = (
+ initialState = INITIAL_STATE,
+ reducer = internalReducer
+) => {
+ const userInitialState = useRef(initialState)
+
+ const [clapState, dispatch] = useReducer(reducer, initialState)
+ const { count, countTotal } = clapState
+
+ const updateClapState = () => dispatch({ type: 'clap' })
+
+ // glorified counter
+ const resetRef = useRef(0)
+ const prevCount = usePrevious(count)
+ const reset = useCallback(() => {
+ // ⚠️ The video lesson had this wrapped in an if statement which I've removed ...
+ // owing to the bug opened by Matija here https://www.udemy.com/instructor/communication/qa/9651560/detail/
+
+ dispatch({ type: 'reset', payload: userInitialState.current })
+ resetRef.current++
+ }, [prevCount, count, dispatch])
+
+ const getTogglerProps = ({ onClick, ...otherProps } = {}) => ({
+ onClick: callFnsInSequence(updateClapState, onClick),
+ 'aria-pressed': clapState.isClicked,
+ ...otherProps
+ })
+
+ const getCounterProps = ({ ...otherProps }) => ({
+ count,
+ 'aria-valuemax': MAXIMUM_USER_CLAP,
+ 'aria-valuemin': 0,
+ 'aria-valuenow': count,
+ ...otherProps
+ })
+
+ return {
+ clapState,
+ updateClapState,
+ getTogglerProps,
+ getCounterProps,
+ reset,
+ resetDep: resetRef.current
}
-
- /** ====================================
- * 🔰SubComponents
- Smaller Component used by
- ==================================== **/
-
- const ClapContainer = forwardRef(
- (
- { children, handleClick, className, style: userStyles = {}, ...restProps },
- ref
- ) => {
- const classNames = [styles.clap, className].join(' ').trim()
-
- return (
-
- {children}
-
- )
+}
+
+useClapState.reducer = internalReducer
+useClapState.types = {
+ clap: 'clap',
+ reset: 'reset'
+}
+
+/**
+ * custom useEffectAfterMount hook
+ */
+const useEffectAfterMount = (cb, deps) => {
+ const componentJustMounted = useRef(true)
+ useEffect(() => {
+ if (!componentJustMounted.current) {
+ return cb()
}
+ componentJustMounted.current = false
+ }, deps)
+}
+
+/**
+ * subcomponents
+ */
+
+const ClapContainer = ({ children, setRef, handleClick, ...restProps }) => {
+ return (
+
+ {children}
+
)
-
- const ClapIcon = ({ className = '', style: userStyles = {}, isClicked }) => {
- const classNames = [styles.icon, isClicked ? styles.checked : '', className]
- .join(' ')
- .trim()
-
- return (
-
-
-
- )
- }
-
- const ClapCount = forwardRef(
- ({ count, className = '', style: userStyles = {}, ...restProps }, ref) => {
- const classNames = [styles.count, className].join(' ').trim()
-
- return (
-
- +{count}
-
- )
- }
+}
+const ClapIcon = ({ isClicked }) => {
+ return (
+
+
+
)
-
- const CountTotal = forwardRef(
- (
- { countTotal, className = '', style: userStyles = {}, ...restProps },
- ref
- ) => {
- const classNames = [styles.total, className].join(' ').trim()
-
- return (
-
- {countTotal}
-
- )
- }
+}
+const ClapCount = ({ count, setRef, ...restProps }) => {
+ return (
+
+ + {count}
+
)
-
- /** ====================================
- * 🔰USAGE
- Below's how a potential user
- may consume the component API
- ==================================== **/
-
- const initialState = { count: 10, countTotal: 22, isClicked: false }
- const Usage = () => {
- const [timesClapped, setTimeClapped] = useState(0)
- const clappedTooMuch = timesClapped>= 7
-
- const reducer = (state, action) => {
- if (action.type === useClapState.types.clap && clappedTooMuch) {
- return state
- }
- return useClapState.reducer(state, action)
- }
-
- const {
- clapState,
- getTogglerProps,
- getCounterProps,
- reset,
- resetDep
- } = useClapState({ initialState, reducer })
- const { count, countTotal, isClicked } = clapState
-
- const [
- { clapContainerRef, clapCountRef, countTotalRef },
- setRef
- ] = useDOMRef()
-
- const animationTimeline = useClapAnimation({
- duration: 300,
- bounceEl: clapCountRef,
- fadeEl: countTotalRef,
- burstEl: clapContainerRef
- })
-
- const onClick = () => {
- setTimeClapped(t => t + 1)
- !clappedTooMuch && animationTimeline.replay()
+}
+
+const CountTotal = ({ countTotal, setRef, ...restProps }) => {
+ return (
+
+ {countTotal}
+
+ )
+}
+
+/**
+ * Usage
+ */
+const userInitialState = {
+ count: 0,
+ countTotal: 1000,
+ isClicked: false
+}
+
+const Usage = () => {
+ const [timesClapped, setTimeClapped] = useState(0)
+ const isClappedTooMuch = timesClapped>= 7 // true/false
+ const reducer = (state, action) => {
+ if (action.type === useClapState.types.clap && isClappedTooMuch) {
+ return state
}
-
- // Side effect after reset has occured.
- const [uploadingReset, setUpload] = useState(false)
- useEffectAfterMount(
- () => {
- setTimeClapped(0)
- setUpload(true)
-
- const id = setTimeout(() => {
- setUpload(false)
- console.log('RESET COMPLETE!!!')
- }, 3000)
-
- return () => clearTimeout(id)
- },
- [resetDep]
- )
-
- return (
-
-
-
-
-
-
-
-
- reset
-
-
- {JSON.stringify({ timesClapped, count, countTotal })}
-
-
- {uploadingReset ? `uploading reset ${resetDep}...` : ''}
-
-
+ return useClapState.reducer(state, action)
+ }
+
+ const {
+ clapState,
+ getTogglerProps,
+ getCounterProps,
+ reset,
+ resetDep
+ } = useClapState(userInitialState, reducer)
+
+ const { count, countTotal, isClicked } = clapState
+
+ const [{ clapRef, clapCountRef, clapTotalRef }, setRef] = useDOMRef()
+
+ const animationTimeline = useClapAnimation({
+ clapEl: clapRef,
+ countEl: clapCountRef,
+ clapTotalEl: clapTotalRef
+ })
+
+ useEffectAfterMount(() => {
+ animationTimeline.replay()
+ }, [count])
+
+ const [uploadingReset, setUpload] = useState(false)
+ useEffectAfterMount(() => {
+ setUpload(true)
+ setTimeClapped(0)
+
+ const id = setTimeout(() => {
+ setUpload(false)
+ }, 3000)
+
+ return () => clearTimeout(id)
+ }, [resetDep])
+
+ const handleClick = () => {
+ setTimeClapped(t => t + 1)
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ reset
+
+
+ {JSON.stringify({ timesClapped, count, countTotal })}
+
+
+ {uploadingReset ? `uploading reset ${resetDep} ...` : ''}
+
- {clappedTooMuch ? `You clapped too much. Don't be so generous!` : ''}
+ {isClappedTooMuch
+ ? `You have clapped too much. Don't be so generous!`
+ : ''}
-
- )
- }
-
- export default Usage
\ No newline at end of file
+
+
+ )
+}
+export default Usage
diff --git a/showcase/src/patterns/index.css b/showcase/src/patterns/index.css
index baa6240..8427f45 100644
--- a/showcase/src/patterns/index.css
+++ b/showcase/src/patterns/index.css
@@ -10,7 +10,7 @@
user-select: none;
}
.clap:after {
- content: "";
+ content: '';
position: absolute;
top: 0;
left: 0;
@@ -82,8 +82,6 @@
/* Clap Info */
.info {
-position: absolute;
-left: -20px;
-right: -20px;
-bottom: -50px;
-}
\ No newline at end of file
+ position: relative;
+ top: 47px;
+}
diff --git a/showcase/src/patterns/usage.css b/showcase/src/patterns/usage.css
index a345649..1005afc 100644
--- a/showcase/src/patterns/usage.css
+++ b/showcase/src/patterns/usage.css
@@ -1,56 +1,60 @@
/** total **/
.total {
- color: #896EAF;
+ color: white;
+ background: #896eaf;
}
- /* count */
+/* count */
.count {
- background: #896EAF;
+ background: #896eaf;
}
- /* clap */
+/* clap */
.clap {
- border: 1px solid #896EAF;
+ border: 1px solid #896eaf;
}
.clap:hover {
- border: 1px solid #896EAF;
+ border: 1px solid #896eaf;
+}
+/* icon */
+.icon {
}
.cupContainer {
- width: 100px;
- min-height: 100px;
- text-align: center;
+ width: 100px;
+ min-height: 100px;
+ text-align: center;
}
.cupBody {
- display: flex;
+ display: flex;
}
-.cupStream> svg{
- height: 40px;
+.cupStream> svg {
+ height: 40px;
}
.cupBody> svg {
- min-height: 50px;
+ min-height: 50px;
}
/* 1. cup handle 2. cup bowl*/
.cupBody> svg:nth-child(1) {
- width: 20%;
+ width: 20%;
}
.cupBody> svg:nth-child(2) {
- width: 80%;
- position: relative;
- right: 10px;
+ width: 80%;
+ position: relative;
+ right: 10px;
}
.resetBtn {
- border-bottom: 1px solid #bdc3c7;
- color: '#27ae60';
- margin-top: 30px;
- padding: 10px 20px;
- border-radius: 20px;
- outline: 0;
- display: block;
- width: 100%;
+ border-bottom: 1px solid #bdc3c7;
+ color: '#27ae60';
+ margin-top: 30px;
+ padding: 10px 20px;
+ border-radius: 20px;
+ outline: 0;
+ display: block;
+ width: 100%;
}
.resetMsg {
- color: black;
-}
\ No newline at end of file
+ color: black;
+}
diff --git a/showcase/webpack.config.base.js b/showcase/webpack.config.base.js
index 085c1a0..f375cdb 100644
--- a/showcase/webpack.config.base.js
+++ b/showcase/webpack.config.base.js
@@ -5,7 +5,8 @@ module.exports = {
entry: './src/index.js',
output: {
filename: 'app.bundle.js',
- path: path.join(__dirname, 'dist')
+ path: path.join(__dirname, 'dist'),
+ publicPath: '/'
},
module: {
rules: [
diff --git a/showcase/webpack.config.dev.js b/showcase/webpack.config.dev.js
index cc41701..c8f056e 100644
--- a/showcase/webpack.config.dev.js
+++ b/showcase/webpack.config.dev.js
@@ -5,11 +5,12 @@ module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
port: 4646,
- open: 'Google Chrome',
+ open: true,
overlay: {
warnings: true,
errors: true
},
+ historyApiFallback: true,
hot: true
},
devtool: 'source-map'