├── .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 |
12 | );
13 |
14 | const Home = () => {
15 | return (
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 | {figs.map((fig) => (
27 |
28 |

34 |
{fig.name}
35 |
{fig.description}
36 |
37 |
38 | ))}
39 |
40 |
41 |
42 |
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 |
49 |
50 |
51 |
52 |
53 |
Debounced Select Example
54 |
63 |
64 | Current Selected Option: {selectedOption}
65 |
66 |
67 |
68 |
69 |
Non-Fig Debounced Search Example
70 |
{
72 | setSearchTerm(event.target.value);
73 | }}
74 | minLength={3}
75 | debounceTimeout={100}
76 | >
77 |
78 | {/* <- input element wrapped with Debounce */}
79 |
80 |
81 |
82 | Current Search Term: {searchTerm}
83 |
84 |
85 |
86 |
87 |
88 |
Fig Throttled Search Example
89 |
90 | {
93 | setFigSearchTermThrottle(event.target.value);
94 | }}
95 | />{' '}
96 |
97 |
98 | Current Search Term: {figSearchTermThrottle}
99 |
100 |
101 |
102 |
103 |
104 |
Non-Fig Throttled Search Example
105 |
{
107 | setSearchTermThrottle(event.target.value);
108 | }}
109 | minLength={0}
110 | throttleTimeout={500}
111 | >
112 |
113 | {/* <- input element wrapped with Throttle */}
114 |
115 |
116 |
117 | Current Search Term: {searchTermThrottle}
118 |
119 |
120 | >
121 | );
122 | };
123 |
124 | export default ExampleComponent;
125 |
--------------------------------------------------------------------------------
/package/nonMemoFeoFig.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import LazyLoad from './utils/lazyload';
3 | import Debounce from './utils/debounce';
4 | import Throttle from './utils/throttle';
5 | import validateConfigs from './types/validateConfig';
6 | const Fig = ({ children, config, placeholder }) => {
7 | useEffect(() => {
8 | // tests to see if user inputs for config are valid, throws error if not
9 | validateConfigs(config);
10 | }, [config]);
11 | // might get rid of these since it doesn't type guard well
12 | const isLazyLoadEnabled = config && config.lazyload;
13 | const isDebounceEnabled = config && config.debounce;
14 | const isThrottleEnabled = config && config.throttle;
15 | // recursively iterates through elements to find desired type to wrap
16 | // worried about how this will affect performance especially with deeply nested component trees. maybe memoization or a hook to trigger selectively
17 | const elementIsolator = (node) => {
18 | var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
19 | // check if the node is a Fig component, if so then ignore Fig
20 | if (React.isValidElement(node) && node.type === Fig) {
21 | return node;
22 | }
23 | // might need to add a check here for other custom non-native wrappers
24 | // preserves non-element nodes like strings
25 | if (!React.isValidElement(node))
26 | return node;
27 | // if node is an image, wrap it with LazyLoad
28 | if (isLazyLoadEnabled) {
29 | if (node.type === 'img') {
30 | return (React.createElement(LazyLoad, { key: crypto.randomUUID(), threshold: ((_a = config.lazyload) === null || _a === void 0 ? void 0 : _a.threshold) || 0, placeholder: placeholder, once: ((_b = config.lazyload) === null || _b === void 0 ? void 0 : _b.once) !== false, offset: ((_c = config.lazyload) === null || _c === void 0 ? void 0 : _c.offset) || '0px' }, node));
31 | }
32 | }
33 | // still need to filter by config.target
34 | if (isDebounceEnabled) {
35 | if (Array.isArray((_d = config.debounce) === null || _d === void 0 ? void 0 : _d.target)) {
36 | }
37 | else if (node.type === 'input') {
38 | return (React.createElement(React.Fragment, null,
39 | React.createElement(Debounce, { onChange: node.props.onChange, minLength: ((_e = config.debounce) === null || _e === void 0 ? void 0 : _e.minLength) || 0,
40 | // there is a bug when delay is set to 100, idk why yet so adding 1 ms if user sets it to 100
41 | debounceTimeout: ((_f = config.debounce) === null || _f === void 0 ? void 0 : _f.delay) === undefined ||
42 | ((_g = config.debounce) === null || _g === void 0 ? void 0 : _g.delay) === 100
43 | ? 101
44 | : (_h = config.debounce) === null || _h === void 0 ? void 0 : _h.delay }, node)));
45 | // default if array is not provided
46 | // add debounceing/throttling depending on which is enabled and return
47 | } // maybe account for other handlers besides button
48 | }
49 | // still need to filter by config.target
50 | if (isThrottleEnabled) {
51 | if (Array.isArray((_j = config.throttle) === null || _j === void 0 ? void 0 : _j.target)) {
52 | }
53 | else if (node.type === 'input') {
54 | return (React.createElement(React.Fragment, null,
55 | React.createElement(Throttle, { onChange: node.props.onChange, minLength: ((_k = config.throttle) === null || _k === void 0 ? void 0 : _k.minLength) || 0, throttleTimeout: ((_l = config.throttle) === null || _l === void 0 ? void 0 : _l.delay) === undefined
56 | ? 100
57 | : (_m = config.throttle) === null || _m === void 0 ? void 0 : _m.delay }, node)));
58 | // default if array is not provided
59 | // add debounceing/throttling depending on which is enabled and return
60 | } // maybe account for other handlers besides button
61 | }
62 | // can filter for more node types and apply other wrappers below:
63 | // if node has children, recursively transform them to fit react props children array format
64 | if (node.props && node.props.children) {
65 | const children = React.Children.toArray(node.props.children).map(elementIsolator);
66 | return React.cloneElement(node, Object.assign(Object.assign({}, node.props), { children: children }));
67 | }
68 | return node;
69 | };
70 | // calls recursive function to apply FEO wrappers to each child
71 | const wrapper = (child, index) => {
72 | if (!React.isValidElement(child)) {
73 | return child;
74 | }
75 | // calls recursive function, add more checks here if necessary
76 | if (isLazyLoadEnabled || isDebounceEnabled || isThrottleEnabled) {
77 | return elementIsolator(child) || child;
78 | }
79 | };
80 | return (React.createElement(React.Fragment, null, React.Children.map(children, (child, index) => {
81 | return wrapper(child, index);
82 | })));
83 | };
84 | export default Fig;
85 |
--------------------------------------------------------------------------------
/package/types/validateConfig.js:
--------------------------------------------------------------------------------
1 | const validateConfigs = (config) => {
2 | // checks if config file exist
3 | if (!config)
4 | throw new Error('No configuration file was provided!');
5 | // validates object file
6 | if (typeof config !== 'object')
7 | throw new Error('Configuration file must be an object!');
8 | // checks for additional unexpected properties
9 | const allowedProps = ['lazyload', 'debounce', 'throttle', 'pauseAnimation'];
10 | const actualProps = Object.keys(config);
11 | const invalidProps = actualProps.filter((prop) => !allowedProps.includes(prop));
12 | if (invalidProps.length > 0) {
13 | throw new Error(`Invalid properties found in the Configuration file: ${invalidProps.join(', ')}`);
14 | }
15 | // validates "lazyload"
16 | if (config.lazyload) {
17 | const { threshold, once, offset } = config.lazyload;
18 | // checks for additional unexpected properties
19 | const allowedProps = ['threshold', 'once', 'offset'];
20 | const actualProps = Object.keys(config.lazyload);
21 | const invalidProps = actualProps.filter((prop) => !allowedProps.includes(prop));
22 | if (invalidProps.length > 0) {
23 | throw new Error(`Invalid properties found in the lazyload configuration: ${invalidProps.join(', ')}`);
24 | }
25 | // validates "threshold"
26 | if (threshold !== undefined &&
27 | (typeof threshold !== 'number' || threshold < 0 || threshold > 1))
28 | throw new Error('Lazyload: threshold must be a valid number between 0 and 1');
29 | // validates "once"
30 | if (once !== undefined && typeof once !== 'boolean')
31 | throw new Error('Lazyload: Once must be a boolean');
32 | // validates "offset"
33 | const offsetPattern = /^-?\d+px$/;
34 | if (offset !== undefined &&
35 | (typeof offset !== 'string' || !offsetPattern.test(offset)))
36 | throw new Error('Lazyload: offset must be a string containing a number followed by px. (Ex: "100px")');
37 | }
38 | // prevents debounce and throttle simultaneously
39 | if (config.debounce && config.throttle) {
40 | console.error('Both debounce and throttle are currently enabled in one of the configurations but are mutually exclusive. Please remove one of them');
41 | }
42 | // validates "debounce"
43 | if (config.debounce) {
44 | const { delay, minLength } = config.debounce; // add target later
45 | // checks for additional unexpected properties
46 | const allowedProps = ['delay', 'minLength'];
47 | const actualProps = Object.keys(config.debounce);
48 | const invalidProps = actualProps.filter((prop) => !allowedProps.includes(prop));
49 | if (invalidProps.length > 0) {
50 | throw new Error(`Invalid properties found in the debounce configuration: ${invalidProps.join(', ')}`);
51 | }
52 | // validates "delay"
53 | if (delay !== undefined && (typeof delay !== 'number' || delay < 0))
54 | throw new Error('Debounce: delay must be a valid, positive number');
55 | // validates "minLength"
56 | if (minLength !== undefined &&
57 | (typeof minLength !== 'number' || minLength < 0))
58 | throw new Error('Debounce: minLength must be a valid, positive number');
59 | }
60 | // validates "throttle"
61 | if (config.throttle) {
62 | const { delay, minLength } = config.throttle; // add target later
63 | // checks for additional unexpected properties
64 | const allowedProps = ['delay', 'minLength'];
65 | const actualProps = Object.keys(config.throttle);
66 | const invalidProps = actualProps.filter((prop) => !allowedProps.includes(prop));
67 | if (invalidProps.length > 0) {
68 | throw new Error(`Invalid properties found in the throttle configuration: ${invalidProps.join(', ')}`);
69 | }
70 | // validates "delay"
71 | if (delay !== undefined && (typeof delay !== 'number' || delay < 0))
72 | throw new Error('Debounce: delay must be a valid, positive number');
73 | // validates "minLength"
74 | if (minLength !== undefined &&
75 | (typeof minLength !== 'number' || minLength < 0))
76 | throw new Error('Debounce: minLength must be a valid, positive number');
77 | }
78 | // validates "pauseAnimation"
79 | if (config.pauseAnimation) {
80 | const { threshold, offset, classes } = config.pauseAnimation;
81 | // checks for additional unexpected properties
82 | const allowedProps = ['threshold', 'offset', 'classes'];
83 | const actualProps = Object.keys(config.pauseAnimation);
84 | const invalidProps = actualProps.filter((prop) => !allowedProps.includes(prop));
85 | if (invalidProps.length > 0) {
86 | throw new Error(`Invalid properties found in the lazyload configuration: ${invalidProps.join(', ')}`);
87 | }
88 | // validates "threshold"
89 | if (threshold !== undefined &&
90 | (typeof threshold !== 'number' || threshold < 0 || threshold > 1))
91 | throw new Error('PauseAnimations: threshold must be a valid number between 0 and 1');
92 | // validates "offset"
93 | const offsetPattern = /^-?\d+px$/;
94 | if (offset !== undefined &&
95 | (typeof offset !== 'string' || !offsetPattern.test(offset)))
96 | throw new Error('PauseAnimations: offset must be a string containing a number followed by px. (Ex: "100px")');
97 | // validates "classes"
98 | if (classes !== undefined && !Array.isArray(classes)) {
99 | throw new Error('PauseAnimations: classes must be an array');
100 | }
101 | if (classes !== undefined) {
102 | for (let el of classes) {
103 | if (typeof el !== 'string') {
104 | throw new Error('PauseAnimations: classes array can only contain strings');
105 | }
106 | }
107 | }
108 | }
109 | };
110 | export default validateConfigs;
111 |
--------------------------------------------------------------------------------
/dev/types/validateConfig.ts:
--------------------------------------------------------------------------------
1 | import {Config} from './types';
2 |
3 | const validateConfigs = (config: Config) => {
4 | // checks if config file exist
5 | if (!config) throw new Error('No configuration file was provided!');
6 |
7 | // validates object file
8 | if (typeof config !== 'object')
9 | throw new Error('Configuration file must be an object!');
10 |
11 | // checks for additional unexpected properties
12 | const allowedProps = ['lazyload', 'debounce', 'throttle', 'pauseAnimation'];
13 | const actualProps = Object.keys(config);
14 | const invalidProps = actualProps.filter(
15 | (prop) => !allowedProps.includes(prop)
16 | );
17 | if (invalidProps.length > 0) {
18 | throw new Error(
19 | `Invalid properties found in the Configuration file: ${invalidProps.join(
20 | ', '
21 | )}`
22 | );
23 | }
24 |
25 | // validates "lazyload"
26 | if (config.lazyload) {
27 | const {threshold, once, offset} = config.lazyload;
28 |
29 | // checks for additional unexpected properties
30 | const allowedProps = ['threshold', 'once', 'offset'];
31 | const actualProps = Object.keys(config.lazyload);
32 | const invalidProps = actualProps.filter(
33 | (prop) => !allowedProps.includes(prop)
34 | );
35 | if (invalidProps.length > 0) {
36 | throw new Error(
37 | `Invalid properties found in the lazyload configuration: ${invalidProps.join(
38 | ', '
39 | )}`
40 | );
41 | }
42 |
43 | // validates "threshold"
44 | if (
45 | threshold !== undefined &&
46 | (typeof threshold !== 'number' || threshold < 0 || threshold > 1)
47 | )
48 | throw new Error(
49 | 'Lazyload: threshold must be a valid number between 0 and 1'
50 | );
51 |
52 | // validates "once"
53 | if (once !== undefined && typeof once !== 'boolean')
54 | throw new Error('Lazyload: Once must be a boolean');
55 |
56 | // validates "offset"
57 | const offsetPattern = /^-?\d+px$/;
58 | if (
59 | offset !== undefined &&
60 | (typeof offset !== 'string' || !offsetPattern.test(offset))
61 | )
62 | throw new Error(
63 | 'Lazyload: offset must be a string containing a number followed by px. (Ex: "100px")'
64 | );
65 | }
66 |
67 | // prevents debounce and throttle simultaneously
68 | if (config.debounce && config.throttle) {
69 | console.error(
70 | 'Both debounce and throttle are currently enabled in one of the configurations but are mutually exclusive. Please remove one of them'
71 | );
72 | }
73 |
74 | // validates "debounce"
75 | if (config.debounce) {
76 | const {delay, minLength} = config.debounce; // add target later
77 |
78 | // checks for additional unexpected properties
79 | const allowedProps = ['delay', 'minLength'];
80 | const actualProps = Object.keys(config.debounce);
81 | const invalidProps = actualProps.filter(
82 | (prop) => !allowedProps.includes(prop)
83 | );
84 | if (invalidProps.length > 0) {
85 | throw new Error(
86 | `Invalid properties found in the debounce configuration: ${invalidProps.join(
87 | ', '
88 | )}`
89 | );
90 | }
91 |
92 | // validates "delay"
93 | if (delay !== undefined && (typeof delay !== 'number' || delay < 0))
94 | throw new Error('Debounce: delay must be a valid, positive number');
95 | // validates "minLength"
96 | if (
97 | minLength !== undefined &&
98 | (typeof minLength !== 'number' || minLength < 0)
99 | )
100 | throw new Error('Debounce: minLength must be a valid, positive number');
101 | }
102 |
103 | // validates "throttle"
104 | if (config.throttle) {
105 | const {delay, minLength} = config.throttle; // add target later
106 |
107 | // checks for additional unexpected properties
108 | const allowedProps = ['delay', 'minLength'];
109 | const actualProps = Object.keys(config.throttle);
110 | const invalidProps = actualProps.filter(
111 | (prop) => !allowedProps.includes(prop)
112 | );
113 | if (invalidProps.length > 0) {
114 | throw new Error(
115 | `Invalid properties found in the throttle configuration: ${invalidProps.join(
116 | ', '
117 | )}`
118 | );
119 | }
120 |
121 | // validates "delay"
122 | if (delay !== undefined && (typeof delay !== 'number' || delay < 0))
123 | throw new Error('Debounce: delay must be a valid, positive number');
124 | // validates "minLength"
125 | if (
126 | minLength !== undefined &&
127 | (typeof minLength !== 'number' || minLength < 0)
128 | )
129 | throw new Error('Debounce: minLength must be a valid, positive number');
130 | }
131 |
132 | // validates "pauseAnimation"
133 | if (config.pauseAnimation) {
134 | const {threshold, offset, classes} = config.pauseAnimation;
135 |
136 | // checks for additional unexpected properties
137 | const allowedProps = ['threshold', 'offset', 'classes'];
138 | const actualProps = Object.keys(config.pauseAnimation);
139 | const invalidProps = actualProps.filter(
140 | (prop) => !allowedProps.includes(prop)
141 | );
142 | if (invalidProps.length > 0) {
143 | throw new Error(
144 | `Invalid properties found in the lazyload configuration: ${invalidProps.join(
145 | ', '
146 | )}`
147 | );
148 | }
149 |
150 | // validates "threshold"
151 | if (
152 | threshold !== undefined &&
153 | (typeof threshold !== 'number' || threshold < 0 || threshold > 1)
154 | )
155 | throw new Error(
156 | 'PauseAnimations: threshold must be a valid number between 0 and 1'
157 | );
158 |
159 | // validates "offset"
160 | const offsetPattern = /^-?\d+px$/;
161 | if (
162 | offset !== undefined &&
163 | (typeof offset !== 'string' || !offsetPattern.test(offset))
164 | )
165 | throw new Error(
166 | 'PauseAnimations: offset must be a string containing a number followed by px. (Ex: "100px")'
167 | );
168 |
169 | // validates "classes"
170 | if (classes !== undefined && !Array.isArray(classes)) {
171 | throw new Error('PauseAnimations: classes must be an array');
172 | }
173 |
174 | if (classes !== undefined) {
175 | for (let el of classes) {
176 | if (typeof el !== 'string') {
177 | throw new Error(
178 | 'PauseAnimations: classes array can only contain strings'
179 | );
180 | }
181 | }
182 | }
183 | }
184 | };
185 |
186 | export default validateConfigs;
187 |
--------------------------------------------------------------------------------
/package/feofig.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from 'react';
2 | import LazyLoad from './utils/lazyload';
3 | import Debounce from './utils/debounce';
4 | import Throttle from './utils/throttle';
5 | import validateConfigs from './types/validateConfig';
6 | import PauseAnimation from './utils/pauseAnimation';
7 | const Fig = ({ children, config, placeholder }) => {
8 | const [transformedChildren, setTransformedChildren] = useState(null);
9 | const [finishedTransforming, setFinishedTransforming] = useState(false);
10 | useEffect(() => {
11 | // tests to see if user inputs for config are valid, throws error if not
12 | if (config.validate === undefined || config.validate === true)
13 | validateConfigs(config);
14 | }, [config]);
15 | const isLazyLoadEnabled = config && config.lazyload;
16 | const isDebounceEnabled = config && config.debounce;
17 | const isThrottleEnabled = config && config.throttle;
18 | const isPauseAnimationEnabled = config && config.pauseAnimation;
19 | // Memoize the elementIsolator function to prevent unnecessary recalculations
20 | const memoizedElementIsolator = useMemo(() => {
21 | // Recursively iterates through elements to find desired type to wrap
22 | // May affect performance when recursing on every rerender for deeply nested code.
23 | const elementIsolator = (node) => {
24 | var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
25 | // check if the node is a Fig component, if so then ignore Fig
26 | if (React.isValidElement(node) && node.type === Fig) {
27 | return node;
28 | }
29 | // preserves non-element nodes like strings
30 | if (!React.isValidElement(node))
31 | return node;
32 | if (isPauseAnimationEnabled) {
33 | // in the Config, developer will designate which css classes to disable by adding css class names to the "classes" property on 'pauseAnimation'
34 | // conditional is checking if any of the designated classes are applied to the node
35 | if ((_a = config.pauseAnimation) === null || _a === void 0 ? void 0 : _a.classes.includes(node.props.className)) {
36 | return (React.createElement(React.Fragment, null,
37 | React.createElement(PauseAnimation, { threshold: (_b = config.pauseAnimation) === null || _b === void 0 ? void 0 : _b.threshold, offset: (_c = config.pauseAnimation) === null || _c === void 0 ? void 0 : _c.offset }, node)));
38 | }
39 | }
40 | // if node is an image, wrap it with LazyLoad
41 | if (isLazyLoadEnabled) {
42 | if (node.type === 'img') {
43 | return (React.createElement(LazyLoad, { key: (Math.random() * Math.random() * 99999).toString(), threshold: ((_d = config.lazyload) === null || _d === void 0 ? void 0 : _d.threshold) || 0, placeholder: placeholder, once: ((_e = config.lazyload) === null || _e === void 0 ? void 0 : _e.once) !== false, offset: ((_f = config.lazyload) === null || _f === void 0 ? void 0 : _f.offset) || '0px' }, node));
44 | }
45 | }
46 | // still need to filter by config.target
47 | if (isDebounceEnabled) {
48 | if (Array.isArray((_g = config.debounce) === null || _g === void 0 ? void 0 : _g.target)) {
49 | }
50 | else if (node.type === 'input' ||
51 | node.type === 'textarea' ||
52 | node.type === 'select') {
53 | return (React.createElement(React.Fragment, null,
54 | React.createElement(Debounce, { onChange: node.props.onChange, minLength: ((_h = config.debounce) === null || _h === void 0 ? void 0 : _h.minLength) || 0,
55 | // 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 in debounce.tsx
56 | debounceTimeout: ((_j = config.debounce) === null || _j === void 0 ? void 0 : _j.delay) === undefined ||
57 | ((_k = config.debounce) === null || _k === void 0 ? void 0 : _k.delay) === 100
58 | ? 101
59 | : (_l = config.debounce) === null || _l === void 0 ? void 0 : _l.delay }, node)));
60 | }
61 | else if (node.type === 'form') {
62 | return (React.createElement("form", Object.assign({}, node.props), React.Children.map(node.props.children, (child) => memoizedElementIsolator(child) || child)));
63 | }
64 | }
65 | if (isThrottleEnabled) {
66 | if (Array.isArray((_m = config.throttle) === null || _m === void 0 ? void 0 : _m.target)) {
67 | }
68 | else if (node.type === 'input') {
69 | return (React.createElement(React.Fragment, null,
70 | React.createElement(Throttle, { onChange: node.props.onChange, minLength: ((_o = config.throttle) === null || _o === void 0 ? void 0 : _o.minLength) || 0, throttleTimeout: ((_p = config.throttle) === null || _p === void 0 ? void 0 : _p.delay) === undefined
71 | ? 100
72 | : (_q = config.throttle) === null || _q === void 0 ? void 0 : _q.delay }, node)));
73 | }
74 | }
75 | // if node has children, recursively transform them to fit react props children array format
76 | if (node.props && node.props.children) {
77 | const children = React.Children.toArray(node.props.children).map(elementIsolator);
78 | return React.cloneElement(node, Object.assign(Object.assign({}, node.props), { children: children }));
79 | }
80 | return node;
81 | };
82 | return elementIsolator;
83 | }, [config]); // Add dependencies that affect the transformation
84 | // calls recursive function to apply FEO wrappers to each child
85 | const wrapper = (child, index) => {
86 | if (!React.isValidElement(child)) {
87 | return child;
88 | }
89 | // calls recursive function, add more checks here if necessary
90 | if (isLazyLoadEnabled ||
91 | isDebounceEnabled ||
92 | isThrottleEnabled ||
93 | isPauseAnimationEnabled) {
94 | return memoizedElementIsolator(child) || child;
95 | }
96 | };
97 | // Memoize the transformed children
98 | const memoizedChildren = useMemo(() => {
99 | return React.Children.map(children, wrapper);
100 | }, [children, memoizedElementIsolator]);
101 | return React.createElement(React.Fragment, null, memoizedChildren);
102 | };
103 | export default Fig;
104 |
--------------------------------------------------------------------------------
/dev/feofig.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useState } from 'react';
2 | import LazyLoad from './utils/lazyload';
3 | import Debounce from './utils/debounce';
4 | import Throttle from './utils/throttle';
5 | import validateConfigs from './types/validateConfig';
6 | import PauseAnimation from './utils/pauseAnimation';
7 | import { FigProps } from './types/types';
8 |
9 | const Fig = ({ children, config, placeholder }: FigProps) => {
10 | const [transformedChildren, setTransformedChildren] =
11 | useState(null);
12 | const [finishedTransforming, setFinishedTransforming] =
13 | useState(false);
14 |
15 | useEffect(() => {
16 | // tests to see if user inputs for config are valid, throws error if not
17 |
18 | if (config.validate === undefined || config.validate === true)
19 | validateConfigs(config);
20 | }, [config]);
21 |
22 | const isLazyLoadEnabled = config && config.lazyload;
23 | const isDebounceEnabled = config && config.debounce;
24 | const isThrottleEnabled = config && config.throttle;
25 | const isPauseAnimationEnabled = config && config.pauseAnimation;
26 |
27 | // Memoize the elementIsolator function to prevent unnecessary recalculations
28 | const memoizedElementIsolator = useMemo(() => {
29 | // Recursively iterates through elements to find desired type to wrap
30 | // May affect performance when recursing on every rerender for deeply nested code.
31 | const elementIsolator = (node: React.ReactNode): React.ReactNode => {
32 | // check if the node is a Fig component, if so then ignore Fig
33 | if (React.isValidElement(node) && node.type === Fig) {
34 | return node;
35 | }
36 |
37 | // preserves non-element nodes like strings
38 | if (!React.isValidElement(node)) return node;
39 |
40 | if (isPauseAnimationEnabled) {
41 | // in the Config, developer will designate which css classes to disable by adding css class names to the "classes" property on 'pauseAnimation'
42 | // conditional is checking if any of the designated classes are applied to the node
43 | if (
44 | config.pauseAnimation?.classes.includes(
45 | (node as React.ReactElement).props.className
46 | )
47 | ) {
48 | return (
49 | <>
50 |
54 | {node}
55 |
56 | >
57 | );
58 | }
59 | }
60 |
61 | // if node is an image, wrap it with LazyLoad
62 | if (isLazyLoadEnabled) {
63 | if (node.type === 'img') {
64 | return (
65 |
72 | {node}
73 |
74 | );
75 | }
76 | }
77 |
78 | // still need to filter by config.target
79 | if (isDebounceEnabled) {
80 | if (Array.isArray(config.debounce?.target)) {
81 | } else if (
82 | (node as React.ReactElement).type === 'input' ||
83 | (node as React.ReactElement).type === 'textarea' ||
84 | (node as React.ReactElement).type === 'select'
85 | ) {
86 | return (
87 | <>
88 |
99 | {node}
100 |
101 | >
102 | );
103 | } else if ((node as React.ReactElement).type === 'form') {
104 | return (
105 |
112 | );
113 | }
114 | }
115 |
116 | if (isThrottleEnabled) {
117 | if (Array.isArray(config.throttle?.target)) {
118 | } else if ((node as React.ReactElement).type === 'input') {
119 | return (
120 | <>
121 |
130 | {node}
131 |
132 | >
133 | );
134 | }
135 | }
136 |
137 | // if node has children, recursively transform them to fit react props children array format
138 | if (node.props && (node as React.ReactElement).props.children) {
139 | const children = React.Children.toArray(
140 | (node as React.ReactElement).props.children
141 | ).map(elementIsolator);
142 |
143 | return React.cloneElement(node as React.ReactElement, {
144 | ...node.props,
145 | children: children,
146 | });
147 | }
148 |
149 | return node;
150 | };
151 | return elementIsolator;
152 | }, [config]); // Add dependencies that affect the transformation
153 |
154 | // calls recursive function to apply FEO wrappers to each child
155 | const wrapper = (child: React.ReactElement, index: number) => {
156 | if (!React.isValidElement(child)) {
157 | return child;
158 | }
159 |
160 | // calls recursive function, add more checks here if necessary
161 | if (
162 | isLazyLoadEnabled ||
163 | isDebounceEnabled ||
164 | isThrottleEnabled ||
165 | isPauseAnimationEnabled
166 | ) {
167 | return memoizedElementIsolator(child) || child;
168 | }
169 | };
170 |
171 | // Memoize the transformed children
172 | const memoizedChildren = useMemo(() => {
173 | return React.Children.map(children, wrapper);
174 | }, [children, memoizedElementIsolator]);
175 |
176 | return <>{memoizedChildren}>;
177 | };
178 |
179 | export default Fig;
180 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # FEOFig
4 | **_✨ Front End Optimizer & Configurer ✨_**
5 |
6 | Turn complex front-end optimization into a simple task while maintaining readability using our figs!
7 |
8 | ## About
9 | FEOFig is a one-stop-shop library for various front-end optimization techniques that developers can configure and apply to elements using "figs", which are reusable drop-in wrappers containing your customized settings. After configuring settings for your desired technique(s) in a separate "config" file, the figs are ready to wrap around single, multiple, or even nested elements.
10 |
11 |
12 | ## Features
13 | ➡️ Lazy Loading
14 | ➡️ Debouncing
15 | ➡️ Throttling
16 | ➡️ Pause Offscreen CSS Animations
17 |
18 | 🚀 TypeScript support
19 |
20 | ## Getting Started
21 | ### Installation
22 | ```
23 | npm install feofig
24 | ```
25 |
26 | ## Usage
27 | ### Fig Wrapper
28 | Import the Fig wrapper from the FEOFig library and wrap it around your code. The Fig wrapper will apply optimizations based on a "config" object passed into the "config" prop.
29 |
30 | ```javascript
31 | import Fig from 'feofig';
32 | import { configExample } from './config.js';
33 |
34 | const App = () => {
35 |
36 |
37 |
38 |
39 | }
40 |
41 | export default App;
42 | ```
43 |
44 | > NOTE: The Fig wrapper will apply optimizations to nested components but **WILL SKIP OVER** custom components.
45 |
46 |
47 |
48 | ### Creating your configurations
49 | In the following example, a separate Javascript file is created containing the configs. Individual configs are exported and then imported into the files they will be used in.
50 |
51 | ```javascript
52 | export const configExample = {
53 | lazyload: {
54 | threshold: 0.5,
55 | once: false,
56 | },
57 | throttle: {
58 | delay: 3000,
59 | },
60 | };
61 |
62 | export const config2 = {
63 | lazyload: {
64 | threshold: 0.25,
65 | once: true,
66 | },
67 | pauseAnimation: {
68 | threshold: 0.5,
69 | offset: '100px',
70 | classes: ['animate'],
71 | }
72 | };
73 |
74 | export const debounceConfig = {
75 | debounce: {
76 | delay: 1000,
77 | minlength: 5,
78 | }
79 | };
80 | ```
81 |
82 |
83 | ## Configuration Documentation
84 | Configurations are objects consisting of optional keys `lazyload`, `debounce`, `throttle`, and `pauseAnimation` whose values are objects containing the options for that specific optimization.
85 |
86 |
87 |
88 | ### lazyload
89 | Images will only be fetched when the image element enters the browser viewport. Only works for `
` elements.
90 | ```javascript
91 | lazyload: {
92 | threshold: number || 0,
93 | once: boolean || true,
94 | offset: string || '0px',
95 | }
96 | ```
97 | **_`threshold`_**: A value between 0 and 1 indicating what percentage of the placeholder image should be visible before fetching the source image. Will have no effect if the image element has no placeholder image.
98 |
99 | **_`once`_**: A boolean indicating whether each image should be lazy loaded only once. If set to false, images will continue to be lazy loaded after entering and leaving the browser viewport.
100 |
101 | **_`offset`_**: A string indicating the margin of the browser viewport before elements are considered visible. Example: For `offset: "10px 0px 30px 0px"` (top, right, bottom, left), an image will load when it is 10px above or 30px below the browser viewport. If only one value is provided, it will apply to all sides. Percentages may be used as well.
102 |
103 |
104 |
105 | ### debounce
106 | Applies debouncing to `onChange` function attached to following elements:
107 | - ``
108 | - `