├── .gitignore ├── .tool-versions ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── postcss.config.js ├── public ├── banner-root-ui-ux-book.png ├── favicon.ico ├── index.html └── robots.txt ├── src ├── components │ ├── categories.tsx │ ├── category.tsx │ ├── footer.tsx │ ├── infoTable.tsx │ ├── searchBar.tsx │ ├── subcategory.tsx │ ├── tagline.tsx │ └── toast.tsx ├── crawler.js ├── css │ └── index.css ├── images │ ├── cheatsheet.png │ └── logo.svg ├── index.tsx ├── modules │ ├── cheatsheet.json │ ├── index.tsx │ └── models │ │ ├── category.tsx │ │ └── subcategory.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupTests.ts ├── store.tsx ├── utils │ ├── copyTextToClipboard.ts │ ├── googleAnalytics.tsx │ └── modules.d.ts └── views │ ├── app.tsx │ ├── home.tsx │ └── no_match.tsx ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /public/cheatsheet 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 20.10.0 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "cheatsheet" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tailwindcomponents 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Ibis logo 3 | 4 | ## Tailwindcomponents Cheatsheet] 5 | This repo content a cheatsheet page from [tailwindcomponents.com](https://tailwindcomponents.com/cheatsheet/), it's a react base app build with typescript. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ## Project setup 20 | ``` 21 | yarn install 22 | ``` 23 | 24 | ### Compiles and hot-reloads for development 25 | ``` 26 | yarn start 27 | ``` 28 | 29 | ### Compiles and minifies for production 30 | ``` 31 | yarn build 32 | ``` 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwind-cheatsheet", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/compat-data": "^7.16.4", 7 | "@babel/preset-env": "^7.16.7", 8 | "@tailwindcss/forms": "^0.4.0", 9 | "@testing-library/jest-dom": "^5.16.1", 10 | "@testing-library/react": "^9.5.0", 11 | "@testing-library/user-event": "^10.0.0", 12 | "@types/classnames": "^2.3.1", 13 | "@types/jest": "^25.1.3", 14 | "@types/node": "^13.7.7", 15 | "@types/react": "^16.9.23", 16 | "@types/react-dom": "^16.9.0", 17 | "@types/react-redux": "^7.1.21", 18 | "@types/react-router-dom": "^5.3.2", 19 | "autoprefixer": "^10.4.1", 20 | "classnames": "^2.3.1", 21 | "connected-react-router": "^6.9.2", 22 | "postcss": "^8.4.5", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2", 25 | "react-ga": "^3.3.1", 26 | "react-masonry-css": "^1.0.16", 27 | "react-redux": "^7.2.6", 28 | "react-router": "^5.1.2", 29 | "react-router-dom": "^5.1.2", 30 | "redux": "^4.1.2", 31 | "tailwindcss": "^3.1.8", 32 | "typescript": "~3.8.3", 33 | "use-bus": "1.1.1" 34 | }, 35 | "devDependencies": { 36 | "react-scripts": "^5.0.0" 37 | }, 38 | "scripts": { 39 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 40 | "build": "PUBLIC_URL=\"/cheatsheet/\" SKIP_PREFLIGHT_CHECK=true react-scripts build", 41 | "postbuild": "rm -rf ../public/cheatsheet && mv build ../public/cheatsheet", 42 | "test": "react-scripts test" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/banner-root-ui-ux-book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindcomponents/cheatsheet/5e024b3e7fd8f8385c9fd4a83726cf06dc4b6254/public/banner-root-ui-ux-book.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindcomponents/cheatsheet/5e024b3e7fd8f8385c9fd4a83726cf06dc4b6254/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 13 | 14 | Tailwind CSS Cheat Sheet 15 | 16 | 17 | 18 | 19 | 29 | 30 | 31 | 32 | 33 | 37 | 38 |

39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/components/categories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Masonry from 'react-masonry-css'; 3 | import Category from '../modules/models/category'; 4 | import CategoryComponent from './category'; 5 | 6 | const Categories = ({ cheatsheet } : { cheatsheet: Category[] }) => { 7 | return ( 8 | 18 | { 19 | cheatsheet.map((category: Category) => ) 20 | } 21 | 22 | ); 23 | } 24 | 25 | export default Categories; 26 | -------------------------------------------------------------------------------- /src/components/category.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import CategoryType from "../modules/models/category"; 3 | import Subcategory from "../modules/models/subcategory"; 4 | import SubcategoryComponent from "./subcategory"; 5 | 6 | const ChevronDown = () => { 7 | return ( 8 | 16 | 21 | 22 | ); 23 | }; 24 | 25 | const ArrowDown = ({ 26 | action, 27 | value, 28 | 'aria-label': ariaLabel, 29 | }: { 30 | action: () => void; 31 | value: boolean; 32 | 'aria-label'?: string; 33 | }) => { 34 | return ( 35 | 42 | ); 43 | }; 44 | 45 | const Category = ({ category }: { category: CategoryType }) => { 46 | const [isOpen, setIsOpen] = useState(true); 47 | const toggle = () => { 48 | setIsOpen((p) => !p); 49 | }; 50 | return ( 51 |
56 |
61 |

{category.title}

62 | 63 |
64 | { 65 | category.content.map((subcategory: Subcategory, index: Number) => { 66 | return ( 67 | subcategory.table.length > 0 && ( 68 | 73 | ) 74 | ); 75 | })} 76 |
77 | ); 78 | }; 79 | 80 | export default Category; 81 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => ( 4 | 13 | ); 14 | 15 | export default Footer; 16 | -------------------------------------------------------------------------------- /src/components/infoTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import classNames from "classnames"; 3 | import { copyTextToClipboard } from '../utils/copyTextToClipboard'; 4 | import { Toast } from "./toast"; 5 | 6 | const InfoTable = ({ table } : { table : string[][] }) => { 7 | const [showToast, setShowToast] = useState(false); 8 | const [toastText, setToastText] = useState(""); 9 | 10 | const parseText = (text: string): any => { 11 | if(text.includes("rgb(") || text === "transparent") 12 | { 13 | return
14 | } 15 | 16 | if(text === "current color") 17 | { 18 | return
19 | } 20 | 21 | return text.split('\n').map((subtext,index) =>

{subtext}

); 22 | }; 23 | 24 | function handleClick(td: string) { 25 | setToastText(td) 26 | setShowToast(true) 27 | setTimeout(() => { 28 | setShowToast(false) 29 | }, 5000) 30 | } 31 | 32 | return ( 33 | <> 34 | {showToast && } 35 | 36 | 37 | { 38 | table.map((tr: string[], index: Number) => { 39 | return ( 40 | 43 | { 44 | tr.map((td: string, index: Number) => { 45 | return ( 46 | 59 | ); 60 | }) 61 | } 62 | 63 | ); 64 | }) 65 | } 66 | 67 |
{ 48 | await copyTextToClipboard(td).then(() => { 49 | handleClick(td) 50 | } 51 | ); 52 | }} 53 | key={'td-' + index} 54 | className={classNames('cursor-copy font-mono text-xs hover:underline p-2', { 55 | 'text-purple-700 dark:text-purple-300 whitespace-nowrap': index === 0, 56 | 'text-blue-700 dark:text-blue-300': index === 1, 57 | 'text-gray-500 dark:text-gray-300 text-xs': index === 2, 58 | })}>{parseText(td)}
68 | 69 | ); 70 | } 71 | 72 | export default InfoTable; 73 | -------------------------------------------------------------------------------- /src/components/searchBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | import { ReactComponent as Logo } from '../images/logo.svg'; 3 | import { dispatch } from 'use-bus'; 4 | import { useLocation } from 'react-router-dom'; 5 | 6 | let searchTimeout: number | null = null 7 | function clearSearch() { 8 | if (searchTimeout !== null) { 9 | clearTimeout(searchTimeout) 10 | } 11 | } 12 | 13 | const useSearchParams = () => { 14 | return new URLSearchParams(useLocation().search); 15 | } 16 | 17 | const SearchBar = ({ searchFilter }: { searchFilter : ( text:string ) => void }) => { 18 | const query = useSearchParams().get('q') ?? undefined; 19 | 20 | useEffect(() => { 21 | searchFilter(query || ''); 22 | // eslint-disable-next-line react-hooks/exhaustive-deps 23 | }, []); 24 | 25 | const tailwindVersion = "3.0.24"; 26 | const searchInputRef = useRef(null); 27 | 28 | const handleFocus = (e: KeyboardEvent) => { 29 | if (e.key === "k" && (e.ctrlKey || e.metaKey)) { 30 | //preventDefault if ctrl + k is already binded to any browser function 31 | e.preventDefault(); 32 | searchInputRef?.current?.focus(); 33 | } 34 | 35 | if (e.key === "Escape") { 36 | searchInputRef?.current?.blur(); 37 | } 38 | }; 39 | 40 | useEffect(() => { 41 | document.addEventListener("keydown", handleFocus); 42 | return () => { 43 | document.removeEventListener("keydown", handleFocus); 44 | }; 45 | }, []); 46 | 47 | // The search is currently very expensive, as it redraws many elements on 48 | // the screen. This little wrapper adds an artificial delay so that the 49 | // app doesn't block user input. 50 | const search = (event: any) => { 51 | const text: string = event.target.value.toLowerCase(); 52 | if (text.length < 5) { 53 | clearSearch() 54 | searchTimeout = window.setTimeout(() => searchFilter(text), 300) 55 | } else { 56 | clearSearch() 57 | searchFilter(text) 58 | } 59 | } 60 | 61 | const clearInput = () => { 62 | const inputElement = searchInputRef?.current 63 | if (inputElement) { 64 | inputElement.value = '' 65 | clearSearch() 66 | searchFilter('') 67 | } 68 | } 69 | 70 | let shouldRenderClearBtn = false 71 | const length = searchInputRef?.current?.value?.length 72 | if (length !== undefined && length > 0) { 73 | shouldRenderClearBtn = true 74 | } 75 | 76 | return ( 77 |
78 |
79 |
80 |
81 | 82 |

Cheatsheet {tailwindVersion}

83 |
84 | 85 |
86 | 95 | {shouldRenderClearBtn ? ( 96 | 101 | ) : ( 102 | 103 | Ctrl k 104 | 105 | )} 106 |
107 | 108 |
109 | 110 | 111 | Back  to components 112 | 113 | 114 | Go to GitHub repository 115 | 116 | 117 | Source Code On 118 | 119 |
120 |
121 |
122 |
123 | ); 124 | }; 125 | 126 | export default SearchBar; 127 | -------------------------------------------------------------------------------- /src/components/subcategory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, MouseEvent } from 'react'; 2 | import classNames from "classnames"; 3 | import InfoTable from './infoTable'; 4 | import useBus from 'use-bus'; 5 | 6 | import SubcategoryType from "../modules/models/subcategory"; 7 | 8 | const Subcategory = ({ subcategory,shouldHide }: { subcategory : SubcategoryType, shouldHide?:boolean }) => { 9 | const [isVisible, setIsVisible] = useState(false); 10 | 11 | const toggleCollapse = (): void => { 12 | setIsVisible(!isVisible); 13 | }; 14 | 15 | const onClickLink = (event: MouseEvent): void => { 16 | event.stopPropagation(); 17 | }; 18 | 19 | useBus( 20 | 'ui/expand', 21 | () => setIsVisible(true) 22 | ); 23 | 24 | useBus( 25 | 'ui/collapse', 26 | () => setIsVisible(false) 27 | ); 28 | 29 | return ( 30 |
31 |
35 |

{subcategory.title}

36 | 43 | Docs 44 | 45 |
46 |
49 |

{subcategory.description}

50 | 51 |
52 |
53 | ); 54 | } 55 | 56 | export default Subcategory; 57 | -------------------------------------------------------------------------------- /src/components/tagline.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Tagline = () => ( 4 |
5 |

6 | Tailwind CSS Cheat Sheet 7 |

8 | 9 |

10 | Find quickly all the class names and CSS properties with this interactive cheat sheet. The only Tailwind CheatSheet you will ever need! 11 |

12 |
13 | 14 | Roots of UI/UX Design - Learn to Develop Intuitive Web Experiences. 15 | 16 |
17 | 18 |

19 | “Never memorize something that you can look up.” - Albert Einstein 20 |

21 |
22 | ); 23 | 24 | export default Tagline; 25 | -------------------------------------------------------------------------------- /src/components/toast.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Toast = ({ text }: { text: string }) => { 4 | 5 | return ( 6 |
10 |

"{text}" copied into your clipboard

11 |
12 | ); 13 | }; -------------------------------------------------------------------------------- /src/crawler.js: -------------------------------------------------------------------------------- 1 | // https://nerdcave.com/tailwind-cheat-sheet 2 | // Activar las opciones de ocultar "search highlight" y "version labels" 3 | 4 | let categories = []; 5 | 6 | document.querySelector("#app > div.min-w-0.flex.w-full.flex-1.\\/.pb-16.pl-4.pt-16.\\/.lg\\:pt-1.lg\\:pl-52").querySelectorAll("section > div > div").forEach(el => { 7 | let category = {}; 8 | category.title = el.querySelector("header h2").innerText; 9 | category.content = []; 10 | 11 | el.querySelectorAll("ul > li").forEach(subCategory => { 12 | let subCat = {}; 13 | 14 | console.log(subCategory.querySelector("div:first-child a").href) 15 | subCat.title = subCategory.querySelector("div:first-child > span").innerHTML; 16 | subCat.docs = subCategory.querySelector("div:first-child a").href; 17 | subCat.description = subCategory.querySelector("div:last-child div:first-child > p").innerHTML; 18 | subCat.table = []; 19 | 20 | subCategory.querySelectorAll("div:last-child div:last-child > table tr").forEach(tr => { 21 | let trTable = []; 22 | let count = 0; 23 | 24 | tr.querySelectorAll("td span").forEach(td => { 25 | let tableCont = null; 26 | 27 | if (count === 0 && subCat.title.includes("color")) { 28 | tableCont = td.style.background; 29 | } else { 30 | tableCont = td.innerHTML; 31 | } 32 | 33 | if (tableCont !== null) { 34 | trTable.push(tableCont); 35 | } 36 | 37 | count++; 38 | }); 39 | 40 | subCat.table.push(trTable); 41 | }); 42 | 43 | category.content.push(subCat); 44 | }); 45 | 46 | categories.push(category); 47 | }); 48 | 49 | console.log(JSON.stringify(categories)); 50 | -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap"); 2 | 3 | @import "tailwindcss/base"; 4 | @import "tailwindcss/components"; 5 | @import "tailwindcss/utilities"; -------------------------------------------------------------------------------- /src/images/cheatsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindcomponents/cheatsheet/5e024b3e7fd8f8385c9fd4a83726cf06dc4b6254/src/images/cheatsheet.png -------------------------------------------------------------------------------- /src/images/logo.svg: -------------------------------------------------------------------------------- 1 |
2 | 3 |

tailwindcomponents

4 |
-------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'connected-react-router'; 5 | import store, { history } from './store'; 6 | import App from './views/app'; 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | import './css/index.css'; 10 | 11 | const target = document.querySelector('#root'); 12 | 13 | render( 14 | 15 | 16 | 17 | 18 | , 19 | target 20 | ); 21 | 22 | serviceWorker.unregister(); -------------------------------------------------------------------------------- /src/modules/index.tsx: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { connectRouter } from 'connected-react-router'; 3 | import { History } from 'history'; 4 | 5 | export default (history: History) => combineReducers({ 6 | router: connectRouter(history), 7 | }); 8 | -------------------------------------------------------------------------------- /src/modules/models/category.tsx: -------------------------------------------------------------------------------- 1 | import Subcategory from "./subcategory"; 2 | 3 | export default interface Category { 4 | title: string; 5 | content: Subcategory[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/models/subcategory.tsx: -------------------------------------------------------------------------------- 1 | export default interface Subcategory { 2 | title: string; 3 | docs: string; 4 | description: string; 5 | table: string[][]; 6 | } 7 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready.then(registration => { 142 | registration.unregister(); 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/store.tsx: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { routerMiddleware } from 'connected-react-router'; 3 | import * as History from 'history'; 4 | import rootReducer from './modules'; 5 | 6 | export const history = History.createBrowserHistory({ 7 | basename: process.env.PUBLIC_URL 8 | }); 9 | 10 | const initialState = {}; 11 | const enhancers = []; 12 | const middleware = [routerMiddleware(history)]; 13 | 14 | if (process.env.NODE_ENV === 'development') { 15 | const devToolsExtension = (window as any).__REDUX_DEVTOOLS_EXTENSION__; 16 | 17 | if (typeof devToolsExtension === 'function') { 18 | enhancers.push(devToolsExtension()); 19 | } 20 | } 21 | 22 | const composedEnhancers = compose( 23 | applyMiddleware(...middleware), 24 | ...enhancers 25 | ); 26 | 27 | export default createStore( 28 | rootReducer(history), 29 | initialState, 30 | composedEnhancers 31 | ); 32 | -------------------------------------------------------------------------------- /src/utils/copyTextToClipboard.ts: -------------------------------------------------------------------------------- 1 | export async function copyTextToClipboard(text: string) { 2 | if ("clipboard" in navigator) { 3 | return await navigator.clipboard.writeText(text); 4 | } else { 5 | return document.execCommand("copy", true, text); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/googleAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | 3 | ReactGA.initialize('UA-110182129-1'); 4 | let timeoutTrackSearch: any; 5 | 6 | export const trackView = (viewName: string) => { 7 | ReactGA.pageview(viewName); 8 | } 9 | 10 | export const trackSearch = (searchText: string) => { 11 | clearTimeout(timeoutTrackSearch); 12 | 13 | timeoutTrackSearch = setTimeout(() => { 14 | ReactGA.event({ 15 | category: "Search input", 16 | action: "Search in cheatsheet", 17 | label: searchText 18 | }); 19 | }, 500); 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/modules.d.ts: -------------------------------------------------------------------------------- 1 | // Si algún paquete no soporta ts lo añadimos aquí para evitar problemas al compilar 2 | // Para aplicar este cambio detenemos react y lo iniciamos de nuevo 3 | declare module 'use-bus'; 4 | -------------------------------------------------------------------------------- /src/views/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Switch, Route } from "react-router-dom"; 3 | import Home from './home'; 4 | import NoMatch from './no_match'; 5 | import ReactGA from 'react-ga'; 6 | 7 | class App extends Component { 8 | setGA = () => { 9 | ReactGA.initialize('UA-110182129-1'); 10 | ReactGA.pageview('Init page view'); 11 | }; 12 | 13 | componentDidMount() { 14 | this.setGA(); 15 | }; 16 | 17 | render() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/views/home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import Category from '../modules/models/category'; 4 | import Subcategory from '../modules/models/subcategory'; 5 | 6 | import { trackView, trackSearch } from '../utils/googleAnalytics'; 7 | 8 | import SearchBar from '../components/searchBar'; 9 | import Categories from '../components/categories'; 10 | import Footer from '../components/footer'; 11 | import Tagline from '../components/tagline'; 12 | 13 | const Home = () => { 14 | const json: Category[] = require('../modules/cheatsheet.json'); 15 | const [cheatsheet, setCheatsheet] = useState(json); 16 | 17 | useEffect(() => { 18 | trackView('/cheatsheet'); 19 | }, []); 20 | 21 | const search = (text: string) => { 22 | let newCheatsheet: Category[] = json.map((category: Category) => { 23 | if (window.history?.pushState) { 24 | const { origin, pathname } = window.location; 25 | const newUrl = `${origin}${pathname}${text ? `?q=${text}` : ''}`; 26 | window.history.pushState({ path: newUrl }, '', newUrl); 27 | } 28 | if (category.title.toLowerCase().includes(text)) { 29 | return category; 30 | } else { 31 | return { 32 | title: category.title, 33 | content: category.content.map((subcategory: Subcategory) => { 34 | // Si el texto de búsqueda existe en el título o la descripción se muestra toda la tabla, si no pues se filtra en ella 35 | // If the search text exists in the title or description, the entire table is displayed, if not, then it is filtered on it 36 | if ( 37 | subcategory.title.toLowerCase().includes(text) || 38 | subcategory.description.toLowerCase().includes(text) 39 | ) { 40 | return subcategory; 41 | } else { 42 | return { 43 | title: subcategory.title, 44 | docs: subcategory.docs, 45 | description: subcategory.description, 46 | table: subcategory.table.filter((tr) => { 47 | //no forEach needed as we need only one match to show entire row 48 | for (let td = 0; td < tr.length; td++) { 49 | if (tr[td].includes(text)) { 50 | return true; 51 | } 52 | } 53 | return false; 54 | }), 55 | }; 56 | } 57 | }) 58 | } 59 | }; 60 | }); 61 | 62 | setCheatsheet(newCheatsheet); 63 | trackSearch(text); 64 | }; 65 | 66 | return ( 67 |
68 | 69 | 70 | 71 |
72 |
73 | ); 74 | } 75 | 76 | export default Home; 77 | -------------------------------------------------------------------------------- /src/views/no_match.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from "react-router-dom"; 3 | 4 | const NoMatch = () => ( 5 |
6 |

Error 404

7 | Go to home 8 |
9 | ); 10 | 11 | export default NoMatch; 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx,svg}"], 3 | darkMode: "class", 4 | theme: { 5 | extend: { 6 | colors: { 7 | primary: "#0ED3CF", 8 | }, 9 | fontFamily: { 10 | roboto: ["Roboto", "sans-serif"], 11 | }, 12 | }, 13 | }, 14 | plugins: [require("@tailwindcss/forms")], 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------