├── .gitignore ├── package ├── types │ ├── types.js │ ├── validateConfig.d.ts │ ├── types.d.ts │ └── validateConfig.js ├── utils │ ├── pauseAnimation.module.css │ ├── lazyload.d.ts │ ├── pauseAnimation.d.ts │ ├── debounce.d.ts │ ├── throttle.d.ts │ ├── fontdisable.d.ts │ ├── pauseAnimation.js │ ├── fontdisable.js │ ├── throttle.js │ ├── lazyload.js │ └── debounce.js ├── feofig.d.ts ├── nonMemoFeoFig.d.ts ├── package.json ├── license.MD ├── tests │ └── benchmark.test.js ├── nonMemoFeoFig.js ├── feofig.js └── README.md ├── declarations.d.ts ├── .DS_Store ├── .babelrc ├── dev ├── utils │ ├── pauseAnimation.module.css │ ├── pauseAnimation.tsx │ ├── throttle.tsx │ ├── lazyload.tsx │ └── debounce.tsx ├── types │ ├── types.ts │ └── validateConfig.ts └── feofig.tsx ├── client ├── public │ ├── placeholder.webp │ └── index.html ├── dist │ ├── c8d3654a12744139c48e32d9b55d3e68.webp │ ├── index.html │ └── 9163fb6b9bc44f903c38.scss └── src │ ├── App.jsx │ ├── index.js │ ├── AnimateComponent.jsx │ ├── config.js │ ├── Home.js │ ├── FigList.js │ ├── style.scss │ └── ExampleComponent.jsx ├── tsconfig.json ├── license.MD ├── package.json ├── webpack.config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /package/types/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/feofig/HEAD/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /dev/utils/pauseAnimation.module.css: -------------------------------------------------------------------------------- 1 | .pause { 2 | animation-play-state: paused; 3 | } 4 | -------------------------------------------------------------------------------- /package/utils/pauseAnimation.module.css: -------------------------------------------------------------------------------- 1 | .pause { 2 | animation-play-state: paused 3 | } -------------------------------------------------------------------------------- /client/public/placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/feofig/HEAD/client/public/placeholder.webp -------------------------------------------------------------------------------- /client/dist/c8d3654a12744139c48e32d9b55d3e68.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/feofig/HEAD/client/dist/c8d3654a12744139c48e32d9b55d3e68.webp -------------------------------------------------------------------------------- /package/types/validateConfig.d.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './types'; 2 | declare const validateConfigs: (config: Config) => void; 3 | export default validateConfigs; 4 | -------------------------------------------------------------------------------- /package/feofig.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FigProps } from './types/types'; 3 | declare const Fig: ({ children, config, placeholder }: FigProps) => React.JSX.Element; 4 | export default Fig; 5 | -------------------------------------------------------------------------------- /package/nonMemoFeoFig.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FigProps } from './types/types'; 3 | declare const Fig: ({ children, config, placeholder }: FigProps) => React.JSX.Element; 4 | export default Fig; 5 | -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from './Home.js'; 3 | import './style.scss'; 4 | 5 | const App = () => { 6 | return ( 7 |
8 | 9 |
10 | 11 | ); 12 | }; 13 | 14 | export default App; -------------------------------------------------------------------------------- /package/utils/lazyload.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LazyLoadProps } from '../types/types'; 3 | declare const LazyLoad: ({ children, threshold, placeholder, once, offset, }: LazyLoadProps) => React.ReactElement>; 4 | export default LazyLoad; 5 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import App from './App.jsx'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /package/utils/pauseAnimation.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PauseAnimationProps } from '../types/types'; 3 | declare const PauseAnimation: ({ children, threshold, offset, }: PauseAnimationProps) => React.ReactElement>; 4 | export default PauseAnimation; 5 | -------------------------------------------------------------------------------- /package/utils/debounce.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DebounceProps } from '../types/types'; 3 | declare const Debounce: ({ onChange, value: propValue, minLength, debounceTimeout, children, inputRef, ...props }: DebounceProps) => React.ReactElement>; 4 | export default Debounce; 5 | -------------------------------------------------------------------------------- /package/utils/throttle.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ThrottleProps } from '../types/types'; 3 | declare const Throttle: ({ onChange, value: propValue, minLength, throttleTimeout, children, inputRef, ...props }: ThrottleProps) => React.ReactElement>; 4 | export default Throttle; 5 | -------------------------------------------------------------------------------- /package/utils/fontdisable.d.ts: -------------------------------------------------------------------------------- 1 | declare const fontdisableConfig: { 2 | fontdisable: { 3 | "2g": { 4 | disable: boolean; 5 | exlude: null; 6 | }; 7 | "3g": { 8 | disable: boolean; 9 | exclude: string; 10 | }; 11 | "4g": { 12 | disable: boolean; 13 | }; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | FeoFig 9 | 10 | 11 | 12 |
FeoFig
13 | 14 | 15 | -------------------------------------------------------------------------------- /client/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | FeoFig 9 | 10 | 11 | 12 |
FeoFig
13 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/AnimateComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {pauseAnimationConfig} from './config'; 3 | import Fig from '../../dev/feofig'; 4 | 5 | const AnimateComponent = () => { 6 | return ( 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 | ); 19 | }; 20 | 21 | export default AnimateComponent; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./package", 4 | "rootDir": "./dev", 5 | "strict": true, 6 | "module": "ESNext", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleResolution": "node", 10 | "target": "es6", 11 | "jsx": "react", 12 | "esModuleInterop": true, 13 | "noImplicitAny": true, 14 | "noUncheckedIndexedAccess": true, 15 | "declaration": true, 16 | // "sourceMap": true, // for debugging, disable for production 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx", "dev/utils/lazyload.tsx"], 19 | "exclude": ["node_modules", "dist", "./package"] 20 | } 21 | -------------------------------------------------------------------------------- /client/src/config.js: -------------------------------------------------------------------------------- 1 | export const lazyLoadConfig = { 2 | lazyload: { 3 | threshold: 0.5, 4 | once: true, 5 | }, 6 | }; 7 | 8 | export const debounceConfig = { 9 | debounce: { 10 | delay: 1000, 11 | minLength: 3, 12 | }, 13 | } 14 | 15 | export const debounceConfig2 = { 16 | debounce: { 17 | delay: 3000, 18 | minLength: 3, 19 | }, 20 | } 21 | 22 | export const throttleConfig = { 23 | throttle: { 24 | delay: 1000, 25 | minLength: 3, 26 | }, 27 | } 28 | 29 | export const pauseAnimationConfig = { 30 | pauseAnimation: { 31 | threshold: 0.5, 32 | offset: "100px", 33 | classes: ['animate'] 34 | } 35 | } -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feofig", 3 | "version": "1.0.1", 4 | "description": "Front-End Optimizer & Configurer", 5 | "main": "feofig.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/oslabs-beta/feofig.git" 9 | }, 10 | "author": "Andy Lam, Cristina Lee, Keidy Wuang, Zack Rauzi", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/oslabs-beta/feofig/issues" 14 | }, 15 | "homepage": "https://github.com/oslabs-beta/feofig#readme", 16 | "dependencies": { 17 | "yargs": "^17.7.2", 18 | "puppeteer": "^21.6.0" 19 | }, 20 | "bin": { 21 | "feofig": "tests/benchmark.test.js" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /license.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 FEOFig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package/license.MD: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 FEOFig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package/utils/pauseAnimation.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, cloneElement } from 'react'; 2 | import styles from './pauseAnimation.module.css'; 3 | const PauseAnimation = ({ children, threshold = 0.5, offset = '0px', }) => { 4 | const elementRef = useRef(null); 5 | useEffect(() => { 6 | const options = { 7 | root: null, 8 | rootMargin: offset, 9 | threshold: threshold, 10 | }; 11 | const handleIntersection = (entries, observer) => { 12 | entries.forEach((entry) => { 13 | const element = entry.target; 14 | if (entry.isIntersecting) { 15 | element.classList.remove(styles.pause); 16 | } 17 | else { 18 | element.classList.add(styles.pause); 19 | } 20 | }); 21 | }; 22 | const observer = new IntersectionObserver(handleIntersection, options); 23 | if (elementRef.current) { 24 | observer.observe(elementRef.current); 25 | } 26 | return () => { 27 | if (elementRef.current) { 28 | observer.unobserve(elementRef.current); 29 | } 30 | }; 31 | }, []); 32 | return cloneElement(children, { ref: elementRef }); 33 | }; 34 | export default PauseAnimation; 35 | -------------------------------------------------------------------------------- /client/dist/9163fb6b9bc44f903c38.scss: -------------------------------------------------------------------------------- 1 | 2 | import API from "!../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js"; 3 | import domAPI from "!../../node_modules/style-loader/dist/runtime/styleDomAPI.js"; 4 | import insertFn from "!../../node_modules/style-loader/dist/runtime/insertBySelector.js"; 5 | import setAttributes from "!../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js"; 6 | import insertStyleElement from "!../../node_modules/style-loader/dist/runtime/insertStyleElement.js"; 7 | import styleTagTransformFn from "!../../node_modules/style-loader/dist/runtime/styleTagTransform.js"; 8 | import content, * as namedExport from "!!../../node_modules/css-loader/dist/cjs.js!../../node_modules/sass-loader/dist/cjs.js!./style.scss"; 9 | 10 | 11 | 12 | var options = {}; 13 | 14 | options.styleTagTransform = styleTagTransformFn; 15 | options.setAttributes = setAttributes; 16 | 17 | options.insert = insertFn.bind(null, "head"); 18 | 19 | options.domAPI = domAPI; 20 | options.insertStyleElement = insertStyleElement; 21 | 22 | var update = API(content, options); 23 | 24 | 25 | 26 | export * from "!!../../node_modules/css-loader/dist/cjs.js!../../node_modules/sass-loader/dist/cjs.js!./style.scss"; 27 | export default content && content.locals ? content.locals : undefined; 28 | -------------------------------------------------------------------------------- /package/utils/fontdisable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | //cool articles: 3 | // https://www.zachleat.com/web/the-compromise/ 4 | // https://innovation.ebayinc.com/tech/engineering/ebays-font-loading-strategy/ 5 | // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display 6 | // https://www.freecodecamp.org/news/web-fonts-in-2018-f191a48367e8/ 7 | // https://stackoverflow.com/questions/42342133/showing-on-console-slow-network-is-detected-fallback-font-will-be-used-while-l 8 | // APPROACH: 9 | // HIGH LEVEL: 10 | // check if user internet speed is slow 11 | // if user speed is slow, disable custom fonts 12 | // TECHNICAL: 13 | // use font-display? https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display 14 | // use css insert-rule / delete-rule? https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/insertRule 15 | //Important terms: 16 | // FOIT: Flash of invisible text 17 | // FOUT: Flash of unstyled text 18 | //it turns out font-display basically does everything we would want, I don't think there is a way 19 | //example configuration: 20 | const fontdisableConfig = { 21 | fontdisable: { 22 | "2g": { 23 | disable: true, 24 | exlude: null, 25 | }, 26 | "3g": { 27 | disable: true, 28 | exclude: "comic sans", 29 | }, 30 | "4g": { 31 | disable: false 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /dev/utils/pauseAnimation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, cloneElement } from 'react'; 2 | import { PauseAnimationProps } from '../types/types'; 3 | import styles from './pauseAnimation.module.css'; 4 | 5 | const PauseAnimation = ({ 6 | children, 7 | threshold = 0.5, 8 | offset = '0px', 9 | }: PauseAnimationProps) => { 10 | const elementRef = useRef(null); 11 | 12 | useEffect(() => { 13 | const options = { 14 | root: null, 15 | rootMargin: offset, 16 | threshold: threshold, 17 | }; 18 | 19 | const handleIntersection = ( 20 | entries: IntersectionObserverEntry[], 21 | observer: IntersectionObserver 22 | ) => { 23 | entries.forEach((entry) => { 24 | const element = entry.target as HTMLElement; 25 | if (entry.isIntersecting) { 26 | element.classList.remove(styles.pause); 27 | } else { 28 | element.classList.add(styles.pause); 29 | } 30 | }); 31 | }; 32 | 33 | const observer = new IntersectionObserver(handleIntersection, options); 34 | if (elementRef.current) { 35 | observer.observe(elementRef.current); 36 | } 37 | return () => { 38 | if (elementRef.current) { 39 | observer.unobserve(elementRef.current); 40 | } 41 | }; 42 | }, []); 43 | 44 | return cloneElement(children as React.ReactElement, { ref: elementRef }); 45 | }; 46 | 47 | export default PauseAnimation; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feofig", 3 | "version": "1.0.1", 4 | "description": "Front-End Optimizer & Configurer", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack serve --open", 9 | "dev": "webpack-dev-server", 10 | "build": "webpack", 11 | "benchmark": "node ./tests/benchmark.test.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/oslabs-beta/feofig.git" 16 | }, 17 | "author": "Andy Lam, Cristina Lee, Keidy Wuang, Zack Rauzi", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/oslabs-beta/feofig/issues" 21 | }, 22 | "homepage": "https://github.com/oslabs-beta/feofig#readme", 23 | "dependencies": { 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "yargs": "^17.7.2" 27 | }, 28 | "devDependencies": { 29 | "@babel/core": "^7.23.2", 30 | "@babel/preset-env": "^7.23.2", 31 | "@babel/preset-react": "^7.22.15", 32 | "@types/react": "^18.2.41", 33 | "babel-core": "^6.26.3", 34 | "babel-loader": "^9.1.3", 35 | "babel-preset-es2015": "^6.24.1", 36 | "babel-preset-react": "^6.24.1", 37 | "css-loader": "^6.8.1", 38 | "file-loader": "^6.2.0", 39 | "html-loader": "^4.2.0", 40 | "html-webpack-plugin": "^5.5.3", 41 | "puppeteer": "^21.6.0", 42 | "sass": "^1.69.5", 43 | "sass-loader": "^13.3.2", 44 | "style-loader": "^3.3.3", 45 | "ts-loader": "^9.5.1", 46 | "typescript": "^5.3.2", 47 | "webpack": "^5.89.0", 48 | "webpack-cli": "^5.1.4", 49 | "webpack-dev-server": "^4.15.1" 50 | }, 51 | "bin": "./tests/benchmark.test.js" 52 | } 53 | -------------------------------------------------------------------------------- /client/src/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Fig from '../../dev/feofig'; 3 | import { lazyLoadConfig, debounceConfig } from './config'; 4 | import image from '../public/placeholder.webp'; 5 | import figs from './FigList.js'; 6 | import './style.scss'; 7 | import ExampleComponent from './ExampleComponent.jsx'; 8 | import AnimateComponent from './AnimateComponent.jsx'; 9 | 10 | const placeholder = ( 11 | figs coming soon 12 | ); 13 | 14 | const Home = () => { 15 | return ( 16 | 17 |
18 |
19 |

The Fig Boutique

20 |

Welcome to your one-stop shop for exquisite fig varieties!

21 |
22 | 23 | 24 | 25 |
26 | {figs.map((fig) => ( 27 |
28 | {fig.name} 34 |

{fig.name}

35 |

{fig.description}

36 | 37 |
38 | ))} 39 |
40 |
41 | 42 |
43 |

© 2023 FeoFig. All rights reserved.

44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default Home; 51 | -------------------------------------------------------------------------------- /dev/types/types.ts: -------------------------------------------------------------------------------- 1 | export type Config = { 2 | lazyload?: LazyLoadConfig; 3 | throttle?: ThrottleConfig; 4 | debounce?: DebounceConfig; 5 | pauseAnimation?: PauseAnimationConfig; 6 | validate?: Boolean; 7 | }; 8 | 9 | export type LazyLoadConfig = { 10 | threshold?: number; 11 | once?: boolean; 12 | offset?: string; 13 | }; 14 | 15 | export type ThrottleConfig = { 16 | delay?: number; 17 | target?: string[]; 18 | minLength?: number; 19 | }; 20 | 21 | export type DebounceConfig = { 22 | delay?: number; 23 | target?: string[]; 24 | minLength?: number; 25 | }; 26 | 27 | export type PauseAnimationConfig = { 28 | threshold?: number, 29 | offset?: string, 30 | classes: string[] 31 | } 32 | 33 | export type FigProps = { 34 | children: React.ReactElement; 35 | config: Config; 36 | placeholder?: React.ReactElement; 37 | }; 38 | 39 | export type LazyLoadProps = { 40 | key: string; 41 | children: React.ReactElement; 42 | threshold?: number; 43 | placeholder?: React.ReactElement | null; 44 | once?: boolean; 45 | offset?: string; 46 | }; 47 | 48 | export type DebounceProps = { 49 | onChange: (...args: any[]) => void; 50 | value?: string | null; 51 | minLength?: number; 52 | debounceTimeout?: number; 53 | children: React.ReactNode; 54 | inputRef?: React.RefObject; 55 | } 56 | 57 | export type ThrottleProps = { 58 | onChange: (...args: any[]) => void; 59 | value?: string; 60 | minLength: number; 61 | throttleTimeout: number; 62 | children: React.ReactNode; 63 | inputRef?: React.RefObject; 64 | } 65 | 66 | export type PauseAnimationProps = { 67 | children: React.ReactNode; 68 | threshold?: number, 69 | offset?: string, 70 | } -------------------------------------------------------------------------------- /package/types/types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export type Config = { 3 | lazyload?: LazyLoadConfig; 4 | throttle?: ThrottleConfig; 5 | debounce?: DebounceConfig; 6 | pauseAnimation?: PauseAnimationConfig; 7 | validate?: Boolean; 8 | }; 9 | export type LazyLoadConfig = { 10 | threshold?: number; 11 | once?: boolean; 12 | offset?: string; 13 | }; 14 | export type ThrottleConfig = { 15 | delay?: number; 16 | target?: string[]; 17 | minLength?: number; 18 | }; 19 | export type DebounceConfig = { 20 | delay?: number; 21 | target?: string[]; 22 | minLength?: number; 23 | }; 24 | export type PauseAnimationConfig = { 25 | threshold?: number; 26 | offset?: string; 27 | classes: string[]; 28 | }; 29 | export type FigProps = { 30 | children: React.ReactElement; 31 | config: Config; 32 | placeholder?: React.ReactElement; 33 | }; 34 | export type LazyLoadProps = { 35 | key: string; 36 | children: React.ReactElement; 37 | threshold?: number; 38 | placeholder?: React.ReactElement | null; 39 | once?: boolean; 40 | offset?: string; 41 | }; 42 | export type DebounceProps = { 43 | onChange: (...args: any[]) => void; 44 | value?: string | null; 45 | minLength?: number; 46 | debounceTimeout?: number; 47 | children: React.ReactNode; 48 | inputRef?: React.RefObject; 49 | }; 50 | export type ThrottleProps = { 51 | onChange: (...args: any[]) => void; 52 | value?: string; 53 | minLength: number; 54 | throttleTimeout: number; 55 | children: React.ReactNode; 56 | inputRef?: React.RefObject; 57 | }; 58 | export type PauseAnimationProps = { 59 | children: React.ReactNode; 60 | threshold?: number; 61 | offset?: string; 62 | }; 63 | -------------------------------------------------------------------------------- /dev/utils/throttle.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, cloneElement, ChangeEvent } from 'react'; 2 | import { ThrottleProps } from '../types/types'; 3 | 4 | const Throttle = ({ 5 | onChange, 6 | value: propValue, 7 | minLength = 0, 8 | throttleTimeout, 9 | children, 10 | inputRef, 11 | ...props 12 | }:ThrottleProps) => { 13 | const [value, setValue] = useState(propValue ?? ''); 14 | const timeoutRef = useRef(null); 15 | 16 | useEffect(() => { 17 | if (propValue !== undefined && propValue !== null && value !== propValue) { 18 | setValue(propValue); 19 | } 20 | }, [propValue]); 21 | 22 | const handleChange = (event: ChangeEvent) => { 23 | event.persist(); 24 | const { value: newValue } = event.target; 25 | 26 | setValue(newValue); 27 | 28 | if (newValue.length >= minLength) { 29 | if (!timeoutRef.current) { 30 | // No existing timeout, create one and notify after throttling duration 31 | timeoutRef.current = setTimeout(() => { 32 | timeoutRef.current = null; 33 | onChange(event); 34 | }, throttleTimeout); 35 | } 36 | } else { 37 | // Clear existing timeout if length is less than minLength 38 | clearTimeout(timeoutRef.current!); 39 | timeoutRef.current = null; 40 | } 41 | }; 42 | 43 | const maybeRef = inputRef ? { ref: inputRef } : {}; 44 | 45 | 46 | const returnRenderedElement = (children: React.ReactElement) => { 47 | return cloneElement(children as React.ReactElement, { 48 | ...props, 49 | onChange: handleChange, 50 | value, 51 | ...maybeRef, 52 | }); 53 | }; 54 | 55 | const newReactElement = returnRenderedElement(children as React.ReactElement); 56 | 57 | return newReactElement; 58 | }; 59 | 60 | export default Throttle; -------------------------------------------------------------------------------- /package/utils/throttle.js: -------------------------------------------------------------------------------- 1 | var __rest = (this && this.__rest) || function (s, e) { 2 | var t = {}; 3 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 4 | t[p] = s[p]; 5 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 6 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 7 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 8 | t[p[i]] = s[p[i]]; 9 | } 10 | return t; 11 | }; 12 | import { useState, useEffect, useRef, cloneElement } from 'react'; 13 | const Throttle = (_a) => { 14 | var { onChange, value: propValue, minLength = 0, throttleTimeout, children, inputRef } = _a, props = __rest(_a, ["onChange", "value", "minLength", "throttleTimeout", "children", "inputRef"]); 15 | const [value, setValue] = useState(propValue !== null && propValue !== void 0 ? propValue : ''); 16 | const timeoutRef = useRef(null); 17 | useEffect(() => { 18 | if (propValue !== undefined && propValue !== null && value !== propValue) { 19 | setValue(propValue); 20 | } 21 | }, [propValue]); 22 | const handleChange = (event) => { 23 | event.persist(); 24 | const { value: newValue } = event.target; 25 | setValue(newValue); 26 | if (newValue.length >= minLength) { 27 | if (!timeoutRef.current) { 28 | // No existing timeout, create one and notify after throttling duration 29 | timeoutRef.current = setTimeout(() => { 30 | timeoutRef.current = null; 31 | onChange(event); 32 | }, throttleTimeout); 33 | } 34 | } 35 | else { 36 | // Clear existing timeout if length is less than minLength 37 | clearTimeout(timeoutRef.current); 38 | timeoutRef.current = null; 39 | } 40 | }; 41 | const maybeRef = inputRef ? { ref: inputRef } : {}; 42 | const returnRenderedElement = (children) => { 43 | return cloneElement(children, Object.assign(Object.assign(Object.assign({}, props), { onChange: handleChange, value }), maybeRef)); 44 | }; 45 | const newReactElement = returnRenderedElement(children); 46 | return newReactElement; 47 | }; 48 | export default Throttle; 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | const entry = path.resolve(__dirname, './client/src/index.js'); 5 | 6 | const output = { 7 | path: path.resolve(__dirname, 'client','dist'), 8 | filename: 'bundle.js', 9 | }; 10 | 11 | module.exports = { 12 | mode: 'development', 13 | entry, 14 | output, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?/, 19 | // exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: [ 24 | ['@babel/preset-env', { targets: 'defaults' }], 25 | ['@babel/preset-react', { targets: 'defaults' }], 26 | ], 27 | }, 28 | }, 29 | }, 30 | { 31 | test: /\.tsx?$/, 32 | use: 'ts-loader', 33 | // exclude: /node_modules/, 34 | }, 35 | { 36 | test: /\.s[ac]ss$/i, 37 | use: [ 38 | // Creates `style` nodes from JS strings 39 | 'style-loader', 40 | // Translates CSS into CommonJS 41 | 'css-loader', 42 | // Compiles Sass to CSS 43 | 'sass-loader', 44 | ], 45 | }, 46 | { 47 | test: /\.css$/i, 48 | use: ['style-loader', 'css-loader'], 49 | }, 50 | { 51 | test: /\.html$/i, 52 | loader: 'html-loader', 53 | }, 54 | { 55 | test: /\.(png|jpg|webp|gif|svg|mp4)$/, 56 | use: [ 57 | { 58 | loader: 'file-loader', 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | plugins: [ 65 | new HtmlWebpackPlugin({ 66 | template: path.resolve(__dirname, './client/public/index.html'), 67 | }), 68 | ], 69 | devServer: { 70 | static: { 71 | directory: path.join(__dirname, 'client', 'dist'), 72 | publicPath: '/', 73 | }, 74 | host: 'localhost', 75 | port: 8080, 76 | headers: { 'Access-Control-Allow-Origin': '*' }, 77 | open: true, 78 | hot: true, 79 | compress: true, 80 | historyApiFallback: true, 81 | }, 82 | resolve: { 83 | // Enable importing JS / JSX files without specifying their extension 84 | extensions: ['.jsx', '.js', '.tsx', '.ts'], 85 | fallback: { 86 | fs: false, 87 | async_hooks: false, 88 | }, 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /dev/utils/lazyload.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef} from 'react'; 2 | import {LazyLoadProps} from '../types/types'; 3 | 4 | const LazyLoad = ({ 5 | children, 6 | threshold = 0, 7 | placeholder, 8 | once, 9 | offset = '0px', 10 | }: LazyLoadProps) => { 11 | const elementRef = useRef(null); 12 | 13 | useEffect(() => { 14 | const options = { 15 | root: null, 16 | // NEEDS CHANGE: rootmargin default to '0px 0px 0px 0px', currently only accepts one '-px'. 17 | rootMargin: offset, // user config for offset 18 | threshold: threshold, // user config for threshold 19 | }; 20 | 21 | const handleIntersection = ( 22 | entries: IntersectionObserverEntry[], 23 | observer: IntersectionObserver 24 | ) => { 25 | entries.forEach((entry) => { 26 | const image = entry.target as HTMLImageElement; 27 | if (entry.isIntersecting) { 28 | image.className = children.props.className; // replace placeholder className to img className 29 | image.src = children.props.src; // replace placeholder src to img src 30 | if (once) observer.unobserve(image); 31 | } else if (!once && !entry.isIntersecting) { 32 | // prioritizes local image placeholder if it exists 33 | if (children.props.placeholder) { 34 | image.className = children.props.placeholder.props.className; 35 | image.src = children.props.placeholder.props.src; 36 | } else { 37 | image.className = placeholder?.props.className; 38 | image.src = placeholder?.props.src; 39 | } 40 | } 41 | }); 42 | }; 43 | 44 | const observer = new IntersectionObserver(handleIntersection, options); 45 | 46 | if (elementRef.current) { 47 | observer.observe(elementRef.current); 48 | } 49 | return () => { 50 | if (elementRef.current) { 51 | observer.unobserve(elementRef.current); 52 | } 53 | }; 54 | }, []); 55 | 56 | const returnRenderedElement = ( 57 | children: React.ReactElement 58 | ): React.ReactElement => { 59 | // prioritizes local image placeholder if it exists 60 | if (children.props.placeholder) 61 | return React.cloneElement(children.props.placeholder, {ref: elementRef}); 62 | if (placeholder) return React.cloneElement(placeholder, {ref: elementRef}); 63 | else return React.cloneElement(children, {ref: elementRef, src: null}); 64 | }; 65 | 66 | const newReactElement = returnRenderedElement(children); 67 | 68 | return newReactElement; 69 | }; 70 | 71 | export default LazyLoad; 72 | -------------------------------------------------------------------------------- /client/src/FigList.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 1, 4 | name: 'Black Mission Figs', 5 | description: 'Sweet and densely flavored, perfect for baking and snacking.', 6 | imageUrl: 7 | 'https://www.melissas.com/cdn/shop/products/1-pound-image-of-black-mission-figs-fruit-28656452010028_600x600.jpg?v=1628114077', 8 | }, 9 | { 10 | id: 2, 11 | name: 'Calimyrna Figs', 12 | description: 'Large and nutty, great for jams and preserves.', 13 | imageUrl: 14 | 'https://askthefoodgeek.com/wp-content/uploads/2021/09/figs-1-1-of-1-1440x1440.jpg', 15 | }, 16 | { 17 | id: 3, 18 | name: 'Brown Turkey Figs', 19 | description: 'Mildly sweet with a robust flavor, ideal for fresh eating.', 20 | imageUrl: 21 | 'https://www.melissas.com/cdn/shop/products/1-pound-image-of-brown-turkey-figs-fruit-30093262946348_600x600.png?v=1650057434', 22 | }, 23 | { 24 | id: 4, 25 | name: 'Kadota Figs', 26 | description: 'Less sweet, with a thick skin and creamy texture.', 27 | imageUrl: 28 | 'https://www.fourwindsgrowers.com/cdn/shop/files/Kadota-fig-tree-primo-size-for-sale_1_1024x1024.jpg?v=1697648295', 29 | }, 30 | { 31 | id: 5, 32 | name: 'Sierra Figs', 33 | description: 'Lightly sweet with a nutty flavor, excellent for drying.', 34 | imageUrl: 35 | 'https://askthefoodgeek.com/wp-content/uploads/2021/09/sierra-green-figs.jpg', 36 | }, 37 | { 38 | id: 6, 39 | name: 'Tiger Figs', 40 | description: 'Distinctly striped skin with a rich and spicy flavor.', 41 | imageUrl: 'https://m.media-amazon.com/images/I/81EJDd4VNwL.jpg', 42 | }, 43 | { 44 | id: 7, 45 | name: 'Adriatic Figs', 46 | description: 'Bright green skin with a sweet and tangy flavor.', 47 | imageUrl: 48 | 'https://amaranthhome.com/cdn/shop/products/figs_smallest_530x@2x.jpg?v=1499178144', 49 | }, 50 | { 51 | id: 8, 52 | name: 'Panachée Figs', 53 | description: 'Striped appearance with a sweet, fruity flavor.', 54 | imageUrl: 55 | 'https://i.etsystatic.com/26687866/r/il/1628da/4665798420/il_340x270.4665798420_maht.jpg', 56 | }, 57 | { 58 | id: 9, 59 | name: 'Celeste Figs', 60 | description: 'Small and sweet, with a smooth, purple skin.', 61 | imageUrl: 62 | 'https://m.media-amazon.com/images/I/61MYwzAaFqL._AC_UF894,1000_QL80_.jpg', 63 | }, 64 | { 65 | id: 10, 66 | name: 'Desert King Figs', 67 | description: 'Large, sweet, and juicy, perfect for fresh eating.', 68 | imageUrl: 69 | 'https://i0.wp.com/tallcloverfarm.com/wp-content/uploads/2013/10/Ripe-and-Cut-Dessert-King-Figs-.jpg?fit=500%2C350&ssl=1', 70 | }, 71 | ]; 72 | -------------------------------------------------------------------------------- /package/utils/lazyload.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | const LazyLoad = ({ children, threshold = 0, placeholder, once, offset = '0px', }) => { 3 | const elementRef = useRef(null); 4 | useEffect(() => { 5 | const options = { 6 | root: null, 7 | // NEEDS CHANGE: rootmargin default to '0px 0px 0px 0px', currently only accepts one '-px'. 8 | rootMargin: offset, // user config for offset 9 | threshold: threshold, // user config for threshold 10 | }; 11 | const handleIntersection = (entries, observer) => { 12 | entries.forEach((entry) => { 13 | const image = entry.target; 14 | if (entry.isIntersecting) { 15 | image.className = children.props.className; // replace placeholder className to img className 16 | image.src = children.props.src; // replace placeholder src to img src 17 | if (once) 18 | observer.unobserve(image); 19 | } 20 | else if (!once && !entry.isIntersecting) { 21 | // prioritizes local image placeholder if it exists 22 | if (children.props.placeholder) { 23 | image.className = children.props.placeholder.props.className; 24 | image.src = children.props.placeholder.props.src; 25 | } 26 | else { 27 | image.className = placeholder === null || placeholder === void 0 ? void 0 : placeholder.props.className; 28 | image.src = placeholder === null || placeholder === void 0 ? void 0 : placeholder.props.src; 29 | } 30 | } 31 | }); 32 | }; 33 | const observer = new IntersectionObserver(handleIntersection, options); 34 | if (elementRef.current) { 35 | observer.observe(elementRef.current); 36 | } 37 | return () => { 38 | if (elementRef.current) { 39 | observer.unobserve(elementRef.current); 40 | } 41 | }; 42 | }, []); 43 | const returnRenderedElement = (children) => { 44 | // prioritizes local image placeholder if it exists 45 | if (children.props.placeholder) 46 | return React.cloneElement(children.props.placeholder, { ref: elementRef }); 47 | if (placeholder) 48 | return React.cloneElement(placeholder, { ref: elementRef }); 49 | else 50 | return React.cloneElement(children, { ref: elementRef, src: null }); 51 | }; 52 | const newReactElement = returnRenderedElement(children); 53 | return newReactElement; 54 | }; 55 | export default LazyLoad; 56 | -------------------------------------------------------------------------------- /dev/utils/debounce.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useEffect, 4 | useRef, 5 | cloneElement, 6 | ChangeEvent, 7 | } from 'react'; 8 | import { DebounceProps } from '../types/types'; 9 | 10 | const Debounce = ({ 11 | onChange, 12 | value: propValue, 13 | minLength = 0, 14 | // there is an unsolved bug when timeout is set to 100, so adding 1 ms if user sets it to 100. If set to 100, will throw an error related to the 'notify' ref. 15 | debounceTimeout = 101, 16 | children, 17 | inputRef, 18 | ...props 19 | }: DebounceProps) => { 20 | const [value, setValue] = useState( 21 | typeof propValue === 'undefined' || propValue === null ? '' : propValue 22 | ); 23 | const isDebouncing = useRef(false); 24 | const notify = useRef<((...args: any[]) => void) | null>(null); 25 | 26 | // This function is inspired from https://levelup.gitconnected.com/debounce-from-scratch-8616c8209b54 27 | const debounce = (func: (...args: any[]) => void, wait: number) => { 28 | let timerId: NodeJS.Timeout; 29 | return (...args: any[]) => { 30 | clearTimeout(timerId); 31 | timerId = setTimeout(() => func(...args), wait); 32 | }; 33 | }; 34 | 35 | // Create the notifier based on debounceTimeout 36 | const createNotifier = (timeout: number) => { 37 | if (timeout < 0) { 38 | notify.current = () => null; 39 | } else if (timeout === 0) { 40 | notify.current = doNotify; 41 | } else { 42 | const debouncedChangeFunc = debounce( 43 | (event: ChangeEvent) => { 44 | isDebouncing.current = false; 45 | doNotify(event); 46 | }, 47 | timeout 48 | ); 49 | 50 | notify.current = (event: ChangeEvent) => { 51 | isDebouncing.current = true; 52 | debouncedChangeFunc(event); 53 | }; 54 | } 55 | }; 56 | 57 | useEffect(() => { 58 | if (isDebouncing.current) { 59 | return; 60 | } 61 | 62 | if (propValue !== undefined && propValue !== null && value !== propValue) { 63 | setValue(propValue); 64 | } 65 | createNotifier(debounceTimeout); 66 | }, [propValue, debounceTimeout]); 67 | 68 | const doNotify = (...args: any[]) => { 69 | onChange(...args); 70 | }; 71 | 72 | const handleChange = (event: ChangeEvent) => { 73 | event.persist(); 74 | const { value: newValue } = event.target; 75 | 76 | setValue(newValue); 77 | 78 | if (newValue.length >= minLength) { 79 | notify.current?.(event); 80 | return; 81 | } 82 | 83 | if (value.length > newValue.length) { 84 | notify.current?.({ ...event, target: { ...event.target, value: '' } }); 85 | } 86 | }; 87 | 88 | const maybeRef = inputRef ? { ref: inputRef } : {}; 89 | 90 | const returnRenderedElement = (children: React.ReactElement) => { 91 | return cloneElement(children as React.ReactElement, { 92 | ...props, 93 | onChange: handleChange, 94 | value, 95 | ...maybeRef, 96 | }); 97 | }; 98 | 99 | const newReactElement = returnRenderedElement(children as React.ReactElement); 100 | 101 | return newReactElement; 102 | }; 103 | 104 | export default Debounce; 105 | -------------------------------------------------------------------------------- /client/src/style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5dc; 3 | } 4 | 5 | .app { 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: center; 10 | } 11 | 12 | .figs-container { 13 | display: flex; 14 | flex-wrap: wrap; 15 | justify-content: center; 16 | max-width: 600px; 17 | gap: 20px; 18 | padding: 20px; 19 | background-color: #f5f5dc; /* Beige background */ 20 | } 21 | 22 | .placeholder { 23 | display: flex; 24 | width: 600px; 25 | height: 600px; 26 | background-color: white; 27 | } 28 | 29 | .fig-item { 30 | width: 300px; 31 | border: 1px solid #ddd; 32 | border-radius: 8px; 33 | padding: 15px; 34 | text-align: center; 35 | background-color: white; 36 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 37 | } 38 | 39 | .fig-image { 40 | width: 100%; 41 | height: auto; 42 | border-radius: 5px; 43 | margin-bottom: 10px; 44 | } 45 | 46 | .buy-button { 47 | background-color: #4CAF50; /* Green background */ 48 | color: white; 49 | border: none; 50 | padding: 10px 20px; 51 | text-align: center; 52 | text-decoration: none; 53 | display: inline-block; 54 | font-size: 16px; 55 | margin: 4px 2px; 56 | cursor: pointer; 57 | border-radius: 4px; 58 | } 59 | 60 | .figs-page { 61 | text-align: center; 62 | } 63 | 64 | .figs-header { 65 | background-color: #4CAF50; /* Green background */ 66 | color: white; 67 | padding: 20px 0; 68 | margin-bottom: 30px; 69 | } 70 | 71 | .figs-header h1 { 72 | margin: 0; 73 | font-size: 2.5em; 74 | } 75 | 76 | .figs-header p { 77 | margin: 10px 0 0; 78 | font-size: 1.2em; 79 | } 80 | 81 | .figs-footer { 82 | background-color: #f5f5dc; /* Beige background */ 83 | color: #333; 84 | padding: 15px 0; 85 | margin-top: 30px; 86 | font-size: 1em; 87 | } 88 | 89 | 90 | /* ANIMATION */ 91 | .animate { 92 | -webkit-animation-name: squarePath; 93 | animation-name: squarePath; 94 | -webkit-animation-duration: 2s; 95 | animation-duration: 2s; 96 | -webkit-animation-iteration-count: infinite; /* Loop the animation */ 97 | animation-iteration-count: infinite; 98 | } 99 | 100 | @-webkit-keyframes squarePath { 101 | 0%, 102 | 100% { 103 | -webkit-transform: translate(-100%, -100%); 104 | transform: translate(-100%, -100%); 105 | } 106 | 25% { 107 | -webkit-transform: translate(100%, -100%); 108 | transform: translate(100%, -100%); 109 | } 110 | 50% { 111 | -webkit-transform: translate(100%, 100%); 112 | transform: translate(100%, 100%); 113 | } 114 | 75% { 115 | -webkit-transform: translate(-100%, 100%); 116 | transform: translate(-100%, 100%); 117 | } 118 | } 119 | 120 | @keyframes squarePath { 121 | 0%, 122 | 100% { 123 | -webkit-transform: translate(-100%, -100%); 124 | transform: translate(-100%, -100%); 125 | } 126 | 25% { 127 | -webkit-transform: translate(100%, -100%); 128 | transform: translate(100%, -100%); 129 | } 130 | 50% { 131 | -webkit-transform: translate(100%, 100%); 132 | transform: translate(100%, 100%); 133 | } 134 | 75% { 135 | -webkit-transform: translate(-100%, 100%); 136 | transform: translate(-100%, 100%); 137 | } 138 | } 139 | 140 | /* HTML: */ 141 | 142 | .animate { 143 | margin: 250px; 144 | background-color: pink; 145 | border-radius: 50%; 146 | height: 100px; 147 | transform: translate(-100%, -100%); 148 | width: 100px; 149 | box-shadow: 0 0 0 2px white, /* White border */ 0 0 0 6px purple, 150 | /* Purple border */ inset 0 0 0 10px pink; /* Pink inside */ 151 | } 152 | -------------------------------------------------------------------------------- /package/utils/debounce.js: -------------------------------------------------------------------------------- 1 | var __rest = (this && this.__rest) || function (s, e) { 2 | var t = {}; 3 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 4 | t[p] = s[p]; 5 | if (s != null && typeof Object.getOwnPropertySymbols === "function") 6 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { 7 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) 8 | t[p[i]] = s[p[i]]; 9 | } 10 | return t; 11 | }; 12 | import { useState, useEffect, useRef, cloneElement, } from 'react'; 13 | const Debounce = (_a) => { 14 | var { onChange, value: propValue, minLength = 0, 15 | // there is an unsolved bug when timeout is set to 100, so adding 1 ms if user sets it to 100. If set to 100, will throw an error related to the 'notify' ref. 16 | debounceTimeout = 101, children, inputRef } = _a, props = __rest(_a, ["onChange", "value", "minLength", "debounceTimeout", "children", "inputRef"]); 17 | const [value, setValue] = useState(typeof propValue === 'undefined' || propValue === null ? '' : propValue); 18 | const isDebouncing = useRef(false); 19 | const notify = useRef(null); 20 | // This function is inspired from https://levelup.gitconnected.com/debounce-from-scratch-8616c8209b54 21 | const debounce = (func, wait) => { 22 | let timerId; 23 | return (...args) => { 24 | clearTimeout(timerId); 25 | timerId = setTimeout(() => func(...args), wait); 26 | }; 27 | }; 28 | // Create the notifier based on debounceTimeout 29 | const createNotifier = (timeout) => { 30 | if (timeout < 0) { 31 | notify.current = () => null; 32 | } 33 | else if (timeout === 0) { 34 | notify.current = doNotify; 35 | } 36 | else { 37 | const debouncedChangeFunc = debounce((event) => { 38 | isDebouncing.current = false; 39 | doNotify(event); 40 | }, timeout); 41 | notify.current = (event) => { 42 | isDebouncing.current = true; 43 | debouncedChangeFunc(event); 44 | }; 45 | } 46 | }; 47 | useEffect(() => { 48 | if (isDebouncing.current) { 49 | return; 50 | } 51 | if (propValue !== undefined && propValue !== null && value !== propValue) { 52 | setValue(propValue); 53 | } 54 | createNotifier(debounceTimeout); 55 | }, [propValue, debounceTimeout]); 56 | const doNotify = (...args) => { 57 | onChange(...args); 58 | }; 59 | const handleChange = (event) => { 60 | var _a, _b; 61 | event.persist(); 62 | const { value: newValue } = event.target; 63 | setValue(newValue); 64 | if (newValue.length >= minLength) { 65 | (_a = notify.current) === null || _a === void 0 ? void 0 : _a.call(notify, event); 66 | return; 67 | } 68 | if (value.length > newValue.length) { 69 | (_b = notify.current) === null || _b === void 0 ? void 0 : _b.call(notify, Object.assign(Object.assign({}, event), { target: Object.assign(Object.assign({}, event.target), { value: '' }) })); 70 | } 71 | }; 72 | const maybeRef = inputRef ? { ref: inputRef } : {}; 73 | const returnRenderedElement = (children) => { 74 | return cloneElement(children, Object.assign(Object.assign(Object.assign({}, props), { onChange: handleChange, value }), maybeRef)); 75 | }; 76 | const newReactElement = returnRenderedElement(children); 77 | return newReactElement; 78 | }; 79 | export default Debounce; 80 | -------------------------------------------------------------------------------- /package/tests/benchmark.test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const puppeteer = require('puppeteer'); 4 | const yargs = require('yargs'); 5 | 6 | let { port } = yargs.argv; 7 | const includedNetworksArray = yargs.argv._; 8 | 9 | const APP = `http://localhost:${port || 8080}/`; 10 | 11 | const networkConditions = { 12 | dev: { 13 | offline: false, 14 | latency: -1, 15 | downloadThroughput: -1, // disables download throttling 16 | uploadThroughput: -1, // disables upload throttling 17 | connectionType: 'none', 18 | }, 19 | '4g': { 20 | offline: false, 21 | latency: 20, 22 | downloadThroughput: (10 * 1024 * 1024) / 8, // Adjusted for 4G (1.28 Mbps) 23 | uploadThroughput: (5 * 1024 * 1024) / 8, // Adjusted for 4G (1.28 Mbps) 24 | connectionType: 'cellular4g', 25 | }, 26 | '3g': { 27 | offline: false, 28 | latency: 40, 29 | downloadThroughput: (750 * 1024) / 8, // 750 Kilobits/sec = 93750 bytes/sec 30 | uploadThroughput: (250 * 1024) / 8, // 250 Kilobits/sec = 31250 bytes/sec 31 | connectionType: 'cellular3g', 32 | }, 33 | '2g': { 34 | offline: false, 35 | latency: 300, 36 | downloadThroughput: (150 * 1024) / 8, // 150 Kilobits/sec = 18750 bytes/sec 37 | uploadThroughput: (50 * 1024) / 8, // 50 Kilobits/sec = 6250 bytes/sec 38 | connectionType: 'cellular2g', 39 | }, 40 | }; 41 | 42 | async function run(networkCondition, networkString, timeout) { 43 | // Start headless chrome browser 44 | const browser = await puppeteer.launch({ 45 | headless: 'new', 46 | }); 47 | 48 | // Create a new page in the browser 49 | const page = await browser.newPage(); 50 | // Disable cache for more accurate load times 51 | await page.setCacheEnabled(false); 52 | 53 | // Create a Chrome DevTools Protocol session to talk to the page. 54 | const client = await page.target().createCDPSession(); 55 | await client.send('Network.enable'); 56 | await client.send('Performance.enable'); 57 | 58 | // Emulate network condition 59 | await client.send('Network.emulateNetworkConditions', networkCondition); 60 | 61 | const requests = []; 62 | const loadingFinished = []; 63 | 64 | client.on('Network.requestWillBeSent', (request) => { 65 | requests.push(request); 66 | }); 67 | 68 | client.on('Network.loadingFinished', (loaded) => { 69 | loadingFinished.push(loaded); 70 | }); 71 | 72 | // Navigate to url 73 | await page.goto(APP, { 74 | waitUntil: 'networkidle0', 75 | }); 76 | 77 | const performanceMetrics = await client.send('Performance.getMetrics'); 78 | const { metrics } = performanceMetrics; 79 | 80 | console.log( 81 | '===============================================================' 82 | ); 83 | console.log(`Benchmark for: ${networkString}`); 84 | console.log( 85 | `DOMContentLoaded: ${( 86 | metrics[metrics.length - 2].value - metrics[metrics.length - 1].value 87 | ).toFixed(4)} sec` 88 | ); 89 | console.log( 90 | `Finished: ${( 91 | loadingFinished[loadingFinished.length - 1].timestamp - 92 | requests[0].timestamp 93 | ).toFixed(4)} sec` 94 | ); 95 | 96 | await browser.close(); 97 | } 98 | 99 | async function benchmark() { 100 | console.log('Starting benchmark. This may take a few seconds. Please wait.'); 101 | 102 | await run(networkConditions.dev, 'current network'); 103 | 104 | if (includedNetworksArray.includes('4g')) 105 | await run(networkConditions['4g'], '4g'); 106 | if (includedNetworksArray.includes('3g')) { 107 | await run(networkConditions['3g'], '3g'); 108 | } 109 | if (includedNetworksArray.includes('2g')) { 110 | await run(networkConditions['2g'], '2g'); 111 | } 112 | 113 | console.log( 114 | '===============================================================' 115 | ); 116 | console.log('Benchmark completed!'); 117 | process.exit(); 118 | } 119 | 120 | benchmark(); 121 | -------------------------------------------------------------------------------- /client/src/ExampleComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import Debounce from '../../dev/utils/debounce.tsx'; 3 | import Home from './Home'; 4 | import Throttle from '../../dev/utils/throttle.tsx'; 5 | import Fig from '../../dev/feofig'; 6 | import {config1, config2, debounceConfig, debounceConfig2, throttleConfig} from './config'; 7 | 8 | const ExampleComponent = () => { 9 | const [searchTerm, setSearchTerm] = useState(''); 10 | const [figSearchTerm, setFigSearchTerm] = useState(''); 11 | const [figSearchTermThrottle, setFigSearchTermThrottle] = useState(''); 12 | const [searchTermThrottle, setSearchTermThrottle] = useState(''); 13 | const [selectedOption, setSelectedOption] = useState(''); 14 | 15 | return ( 16 | <> 17 | 18 |
19 |

Fig Debounced Search Example

20 |
21 | { 24 | setFigSearchTerm(event.target.value); 25 | }} 26 | />{' '} 27 | {/* <- input element wrapped with Debounce */} 28 |
29 |
30 | Current Search Term: {figSearchTerm} 31 |
32 |
33 |
34 |

Debounced Form Example

35 |
{ 36 | event.preventDefault(); 37 | }}> 38 |