├── .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 |
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 |
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 | {
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)} |
59 | );
60 | })
61 | }
62 |
63 | );
64 | })
65 | }
66 |
67 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------