├── README.md ├── public ├── _redirects ├── favicon.ico ├── apple-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-70x70.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── android-icon-144x144.png ├── android-icon-192x192.png ├── apple-icon-precomposed.png ├── browserconfig.xml ├── manifest.json └── index.html ├── screenshot.png ├── src ├── actions │ ├── index.js │ └── app.actions.js ├── routes │ ├── index.js │ └── Home.route.js ├── config │ ├── index.js │ ├── settings.js │ └── constants.js ├── App.test.js ├── icons │ ├── star-fill.svg │ ├── dark │ │ ├── star-fill.svg │ │ ├── star-blank.svg │ │ ├── linkedin.svg │ │ ├── phone.svg │ │ ├── internet.svg │ │ ├── mail.svg │ │ └── github.svg │ ├── star-blank.svg │ ├── linkedin.svg │ ├── phone.svg │ ├── internet.svg │ ├── mail.svg │ └── github.svg ├── reducers │ ├── index.js │ ├── resume.reducer.js │ ├── app.reducer.js │ └── tools.reducer.js ├── components │ ├── Navigation │ │ ├── index.js │ │ ├── TopNavigation.js │ │ ├── SidebarHeader.js │ │ ├── SidebarCloseButton.js │ │ └── Toolbar.js │ ├── index.js │ ├── Tools │ │ ├── index.js │ │ ├── Buttons │ │ │ ├── index.js │ │ │ ├── PrintButton.js │ │ │ ├── MoreVisibilityButton.js │ │ │ ├── EditorButton.js │ │ │ ├── DownloadButton.js │ │ │ ├── ItemToggleButton.js │ │ │ ├── LocalStorageToggle.js │ │ │ ├── LoadFromFileButton.js │ │ │ └── SaveToCloudButtons.js │ │ ├── FontSelector.js │ │ ├── PaperSize.js │ │ ├── OrderChanger.js │ │ ├── MoreVisibilityModal.js │ │ ├── CodeEditor.js │ │ └── VisibilityChanger.js │ └── Resume │ │ ├── ProfessionalSummary.js │ │ ├── Education.js │ │ ├── Stars.js │ │ ├── Certifications.js │ │ ├── Experience.js │ │ ├── TechnicalSkills.js │ │ ├── Projects.js │ │ ├── index.js │ │ └── Header.js ├── index.js ├── helpers │ ├── localstorage.helper.js │ ├── app.helper.js │ ├── font.helper.js │ ├── tools.helper.js │ └── resume.helper.js ├── store.js ├── App.js ├── styles │ ├── App.css │ ├── darkmode.css │ └── Resume.css ├── resume-data.js └── tests │ └── VisibilityChanger.test.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # Online Resume Builder 2 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import app from './app.actions'; 2 | 3 | export default { app }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home.route'; 2 | 3 | export default { 4 | Home, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MainakRepositor/ResumeR/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import constants from './constants'; 2 | import settings from './settings'; 3 | 4 | export { 5 | constants, 6 | settings, 7 | }; 8 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | 3 | it('renders without crashing', () => { 4 | const div = document.createElement('div'); 5 | ReactDOM.unmountComponentAtNode(div); 6 | }); 7 | -------------------------------------------------------------------------------- /src/icons/star-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/dark/star-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import app from './app.reducer'; 3 | import resume from './resume.reducer'; 4 | import tools from './tools.reducer'; 5 | 6 | export default combineReducers({ 7 | app, 8 | resume, 9 | tools, 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import SidebarCloseButton from './SidebarCloseButton'; 2 | import ToolbarHeader from './SidebarHeader'; 3 | import TopNavigation from './TopNavigation'; 4 | 5 | export { 6 | SidebarCloseButton, 7 | ToolbarHeader, 8 | TopNavigation, 9 | }; 10 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import TopNavigation from './Navigation/TopNavigation'; 2 | import CodeEditor from './Tools/CodeEditor'; 3 | import Toolbar from './Navigation/Toolbar'; 4 | import MoreVisibilityModal from './Tools/MoreVisibilityModal'; 5 | 6 | export { 7 | TopNavigation, 8 | CodeEditor, 9 | Toolbar, 10 | MoreVisibilityModal, 11 | }; 12 | -------------------------------------------------------------------------------- /src/icons/star-blank.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/dark/star-blank.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import 'semantic-ui-css/semantic.min.css'; 5 | import 'react-toastify/dist/ReactToastify.css'; 6 | import App from './App'; 7 | import store from './store'; 8 | 9 | const container = document.getElementById('root'); 10 | const root = ReactDOM.createRoot(container); 11 | 12 | root.render( 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /src/config/settings.js: -------------------------------------------------------------------------------- 1 | const SETTINGS = { 2 | API: { 3 | URL: process.env.REACT_APP_API_URL, 4 | SAVE: (key) => `save?code=${key}`, 5 | FILE: (id, js, key) => `file?code=${key}&id=${id}&js=${js}`, 6 | SAVE_KEY: process.env.REACT_APP_SAVE_KEY, 7 | FILE_KEY: process.env.REACT_APP_FILE_KEY, 8 | }, 9 | }; 10 | 11 | export const APP = { 12 | URL: process.env.PUBLIC_URL || '/', 13 | WORKING_DIR: process.env.REACT_APP_WORKING_DIR || '', 14 | }; 15 | 16 | export default SETTINGS; 17 | -------------------------------------------------------------------------------- /src/routes/Home.route.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | CodeEditor, 4 | TopNavigation, 5 | Toolbar, 6 | MoreVisibilityModal, 7 | } from '../components'; 8 | import Resume from '../components/Resume'; 9 | 10 | function Home() { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | Home.defaultProps = { 23 | }; 24 | 25 | Home.propTypes = { 26 | }; 27 | 28 | export default Home; 29 | -------------------------------------------------------------------------------- /src/components/Tools/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | EditorButton, 3 | PrintButton, 4 | DownloadButton, 5 | LoadFromFileButton, 6 | SaveToCloudButtons, 7 | LocalStorageToggle, 8 | } from './Buttons'; 9 | import VisibilityChanger from './VisibilityChanger'; 10 | import FontSelector from './FontSelector'; 11 | import OrderChanger from './OrderChanger'; 12 | import PaperSize from './PaperSize'; 13 | 14 | export { 15 | EditorButton, 16 | FontSelector, 17 | PrintButton, 18 | VisibilityChanger, 19 | OrderChanger, 20 | DownloadButton, 21 | LoadFromFileButton, 22 | SaveToCloudButtons, 23 | LocalStorageToggle, 24 | PaperSize, 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Tools/Buttons/index.js: -------------------------------------------------------------------------------- 1 | import EditorButton from './EditorButton'; 2 | import PrintButton from './PrintButton'; 3 | import ItemToggleButton from './ItemToggleButton'; 4 | import DownloadButton from './DownloadButton'; 5 | import LoadFromFileButton from './LoadFromFileButton'; 6 | import SaveToCloudButtons from './SaveToCloudButtons'; 7 | import LocalStorageToggle from './LocalStorageToggle'; 8 | import MoreVisibilityButton from './MoreVisibilityButton'; 9 | 10 | export { 11 | EditorButton, 12 | PrintButton, 13 | ItemToggleButton, 14 | DownloadButton, 15 | LoadFromFileButton, 16 | SaveToCloudButtons, 17 | LocalStorageToggle, 18 | MoreVisibilityButton, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/Tools/Buttons/PrintButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Label, Icon, Button } from 'semantic-ui-react'; 3 | 4 | function PrintButton() { 5 | return ( 6 |
7 | 11 |
27 | ); 28 | } 29 | 30 | export default PrintButton; 31 | -------------------------------------------------------------------------------- /src/helpers/localstorage.helper.js: -------------------------------------------------------------------------------- 1 | const setItem = (key, item) => { 2 | if (window.localStorage) { 3 | localStorage.setItem(key, btoa(JSON.stringify(item))); 4 | return true; 5 | } 6 | return false; 7 | }; 8 | 9 | const getItem = (key) => { 10 | try { 11 | if (window.localStorage) { 12 | const item = localStorage.getItem(key); 13 | if (item) { 14 | return JSON.parse(atob(item)); 15 | } 16 | } 17 | } catch (error) { 18 | return undefined; 19 | } 20 | return undefined; 21 | }; 22 | 23 | const clear = (key) => { 24 | if (window.localStorage) { 25 | localStorage.clear(key); 26 | } 27 | }; 28 | 29 | const removeItem = (key) => { 30 | if (window.localStorage) { 31 | localStorage.removeItem(key); 32 | } 33 | }; 34 | 35 | export default { 36 | setItem, 37 | getItem, 38 | clear, 39 | removeItem, 40 | }; 41 | -------------------------------------------------------------------------------- /src/config/constants.js: -------------------------------------------------------------------------------- 1 | import { APP } from './settings'; 2 | 3 | const CONSTANTS = { 4 | APP: { 5 | NAME: 'JSON Resume', 6 | COMPANY: 'Andy Amaya', 7 | }, 8 | ENVIRONMENT: { 9 | TEST: 'TEST', 10 | DEVELOPMENT: 'DEVELOPMENT', 11 | PRODUCTION: 'PRODUCTION', 12 | CURRENT: process.env.REACT_APP_ENV, 13 | }, 14 | ROUTES: { 15 | HOME: { 16 | PATH: `${APP.WORKING_DIR}/`, 17 | NAME: 'Home', 18 | ENABLED: true, 19 | SHOW_IN_MENU: false, 20 | SHOW_IN_NAV: true, 21 | ICON: 'home', 22 | }, 23 | }, 24 | }; 25 | 26 | export default CONSTANTS; 27 | 28 | export const SAVE_RESUME_ERROR_TOAST_ID = 'rrterrorsaveresume'; 29 | 30 | export const SAVE_RESUME_SUCCESS_TOAST_ID = 'rrtresumesaved'; 31 | 32 | export const LOCAL_STORAGE_ON_TOAST_ID = 'rrtrlson'; 33 | 34 | export const LOCAL_STORAGE_OFF_TOAST_ID = 'rrtrlsoff'; 35 | -------------------------------------------------------------------------------- /src/components/Tools/Buttons/MoreVisibilityButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Button } from 'semantic-ui-react'; 5 | import { toggleMoreVisibility } from '../../../actions/app.actions'; 6 | 7 | function MoreVisibilityButton({ dispatch }) { 8 | return ( 9 | 27 | ); 28 | } 29 | 30 | ItemToggleButton.defaultProps = { 31 | onToggle: () => {}, 32 | status: false, 33 | name: '', 34 | label: 'No Label Set', 35 | disabled: false, 36 | }; 37 | 38 | ItemToggleButton.propTypes = { 39 | onToggle: PropTypes.func, 40 | status: PropTypes.bool, 41 | name: PropTypes.string, 42 | label: PropTypes.string, 43 | disabled: PropTypes.bool, 44 | }; 45 | 46 | export default ItemToggleButton; 47 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "JSON Resume", 3 | "name": "JSON Resume | code your resume", 4 | "icons": [ 5 | { 6 | "src": "\/android-icon-36x36.png", 7 | "sizes": "36x36", 8 | "type": "image\/png", 9 | "density": "0.75" 10 | }, 11 | { 12 | "src": "\/android-icon-48x48.png", 13 | "sizes": "48x48", 14 | "type": "image\/png", 15 | "density": "1.0" 16 | }, 17 | { 18 | "src": "\/android-icon-72x72.png", 19 | "sizes": "72x72", 20 | "type": "image\/png", 21 | "density": "1.5" 22 | }, 23 | { 24 | "src": "\/android-icon-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image\/png", 27 | "density": "2.0" 28 | }, 29 | { 30 | "src": "\/android-icon-144x144.png", 31 | "sizes": "144x144", 32 | "type": "image\/png", 33 | "density": "3.0" 34 | }, 35 | { 36 | "src": "\/android-icon-192x192.png", 37 | "sizes": "192x192", 38 | "type": "image\/png", 39 | "density": "4.0" 40 | } 41 | ], 42 | "start_url": ".", 43 | "display": "standalone", 44 | "theme_color": "#000000", 45 | "background_color": "#ffffff" 46 | } -------------------------------------------------------------------------------- /src/components/Resume/Education.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | function Education({ education, font }) { 7 | return ( 8 |
9 |

Education

10 |
11 |
    12 | {education.map( 13 | (ed) => ed.isVisible !== false && ( 14 |
  • 15 |

    {ed.site}

    16 | {ed.dateFrom && 17 |

    18 | {`${ed.dateFrom}${ed.dateTo ? ` - ${ed.dateTo}` : ''}`} 19 |

    } 20 | {ed.studyDegree} 21 |
  • 22 | ), 23 | )} 24 |
25 |
26 | ); 27 | } 28 | 29 | Education.defaultProps = { 30 | education: [], 31 | }; 32 | 33 | Education.propTypes = { 34 | education: PropTypes.arrayOf(PropTypes.shape({})), 35 | font: PropTypes.string.isRequired, 36 | }; 37 | 38 | const mapStateToProps = (state) => ({ 39 | education: state.resume.education, 40 | font: state.tools.font, 41 | }); 42 | 43 | export default connect(mapStateToProps)(Education); 44 | -------------------------------------------------------------------------------- /src/components/Resume/Stars.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import starsfilled from '../../icons/star-fill.svg'; 4 | import starsblank from '../../icons/star-blank.svg'; 5 | 6 | import starsfilleddark from '../../icons/dark/star-fill.svg'; 7 | import starsblankdark from '../../icons/dark/star-blank.svg'; 8 | 9 | const level = (lev) => { 10 | let a = parseInt(lev, 10); 11 | a = Number.isNaN(a) || a < 0 ? 0 : a; 12 | a = (a > 5) ? 5 : a; 13 | return a; 14 | }; 15 | 16 | function Stars({ lev }) { 17 | const stars = []; 18 | for (let i = 1; i <= level(lev); i += 1) { 19 | stars.push(filled-star); 20 | stars.push(filled-star); 21 | } 22 | for (let i = level(lev) + 1; i <= 5; i += 1) { 23 | stars.push(blank-star); 24 | stars.push(blank-star); 25 | } 26 | return ( 27 |
28 | {stars} 29 |
30 | ); 31 | } 32 | 33 | Stars.defaultProps = { 34 | lev: 0, 35 | }; 36 | 37 | Stars.propTypes = { 38 | lev: PropTypes.number, 39 | }; 40 | 41 | export default Stars; 42 | -------------------------------------------------------------------------------- /src/icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/icons/dark/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/Resume/Certifications.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | function Certifications({ certification, font }) { 7 | return ( 8 |
9 |

10 | Certifications 11 |

12 |
13 |
    14 | {certification.map( 15 | (cert) => cert.isVisible !== false && ( 16 |
  • 17 |

    18 | {cert.issuedBy} 19 |

    20 | {cert.dateFrom && ( 21 |

    22 | {`${cert.dateFrom}${cert.dateTo ? ` - ${cert.dateTo}` : ''}`} 23 |

    24 | )} 25 |

    {cert.id}

    26 |
  • 27 | ), 28 | )} 29 |
30 |
31 | ); 32 | } 33 | 34 | Certifications.defaultProps = { 35 | certification: [], 36 | }; 37 | 38 | Certifications.propTypes = { 39 | certification: PropTypes.arrayOf(PropTypes.shape({})), 40 | font: PropTypes.string.isRequired, 41 | }; 42 | 43 | const mapStateToProps = (state) => ({ 44 | certification: state.resume.certification, 45 | font: state.tools.font, 46 | }); 47 | 48 | export default connect(mapStateToProps)(Certifications); 49 | -------------------------------------------------------------------------------- /src/components/Navigation/SidebarCloseButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Menu } from 'semantic-ui-react'; 4 | 5 | function SidebarCloseButton({ 6 | closeToolbar, toolbarOpen, title, titleIcon, backgroundColor, statusMessage, statusMessageColor, 7 | }) { 8 | return ( 9 | 15 | {title 16 | && } 17 | {statusMessage 18 | && } 19 | 27 | 28 | ); 29 | } 30 | 31 | SidebarCloseButton.defaultProps = { 32 | closeToolbar: () => {}, 33 | toolbarOpen: false, 34 | title: undefined, 35 | titleIcon: undefined, 36 | backgroundColor: undefined, 37 | statusMessage: undefined, 38 | statusMessageColor: undefined, 39 | }; 40 | 41 | SidebarCloseButton.propTypes = { 42 | closeToolbar: PropTypes.func, 43 | toolbarOpen: PropTypes.bool, 44 | title: PropTypes.string, 45 | titleIcon: PropTypes.string, 46 | backgroundColor: PropTypes.string, 47 | statusMessage: PropTypes.string, 48 | statusMessageColor: PropTypes.string, 49 | }; 50 | 51 | export default SidebarCloseButton; 52 | -------------------------------------------------------------------------------- /src/helpers/app.helper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export function FocusTrap({ full, mobile }) { 5 | return ( 6 | <> 7 | {mobile && 8 | } 21 | {full && 22 | } 31 | 32 | ); 33 | } 34 | 35 | FocusTrap.defaultProps = { 36 | full: false, 37 | mobile: false, 38 | }; 39 | 40 | FocusTrap.propTypes = { 41 | full: PropTypes.bool, 42 | mobile: PropTypes.bool, 43 | }; 44 | 45 | export const debounceMap = new Map(); 46 | 47 | export const debounce = (func, wait, immediate, key) => { 48 | if (immediate) { 49 | func(); 50 | return; 51 | } 52 | const existing = debounceMap.get(key); 53 | if (existing) { 54 | clearTimeout(existing); 55 | } 56 | const timeout = setTimeout(func, wait); 57 | debounceMap.set(key, timeout); 58 | }; 59 | 60 | export const unbounce = (key) => { 61 | const existing = debounceMap.get(key); 62 | if (existing) { 63 | clearTimeout(existing); 64 | } 65 | debounceMap.clear(key); 66 | }; 67 | -------------------------------------------------------------------------------- /src/icons/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/icons/dark/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/actions/app.actions.js: -------------------------------------------------------------------------------- 1 | export const TOGGLE_TOOLBAR = 'TOGGLE_TOOLBAR'; 2 | export const toggleToolbar = () => ({ 3 | type: TOGGLE_TOOLBAR, 4 | }); 5 | 6 | export const TOGGLE_EDITOR = 'TOGGLE_EDITOR'; 7 | export const toggleEditor = () => ({ 8 | type: TOGGLE_EDITOR, 9 | }); 10 | 11 | export const NEW_RESUME = 'NEW_RESUME'; 12 | export const newResume = () => ({ 13 | type: NEW_RESUME, 14 | }); 15 | 16 | export const CHANGE_FONT = 'CHANGE_FONT'; 17 | export const changeFont = (font) => ({ 18 | type: CHANGE_FONT, 19 | font, 20 | }); 21 | 22 | export const TOGGLE_SHOW_ITEM = 'TOGGLE_SHOW_ITEM'; 23 | export const toggleShowItem = (item) => ({ 24 | type: TOGGLE_SHOW_ITEM, 25 | item, 26 | }); 27 | 28 | export const CHANGE_RESUME_ORDER = 'CHANGE_RESUME_ORDER'; 29 | export const changeResumeOrder = (order) => ({ 30 | type: CHANGE_RESUME_ORDER, 31 | order, 32 | }); 33 | 34 | export const UPDATE_RESUME = 'UPDATE_RESUME'; 35 | export const updateResume = (resume, autoSave) => ({ 36 | type: UPDATE_RESUME, 37 | resume, 38 | autoSave, 39 | }); 40 | 41 | export const UPDATE_EDITOR_STATUS = 'UPDATE_RESUME_STAUS'; 42 | export const updateResumeEditorStatus = (status) => ({ 43 | type: UPDATE_EDITOR_STATUS, 44 | status, 45 | }); 46 | 47 | export const TOGGLE_AUTO_SAVE = 'TOGGLE_AUTO_SAVE'; 48 | export const toggleAutoSave = () => ({ 49 | type: TOGGLE_AUTO_SAVE, 50 | }); 51 | 52 | export const CHANGE_PAPER_SIZE = 'CHANGE_PAPER_SIZE'; 53 | export const changePaperSize = (paperSize) => ({ 54 | type: CHANGE_PAPER_SIZE, 55 | paperSize, 56 | }); 57 | 58 | export const TOGGLE_MORE_VISIBILITY = 'TOGGLE_MORE_VISIBILITY'; 59 | export const toggleMoreVisibility = () => ({ 60 | type: TOGGLE_MORE_VISIBILITY, 61 | }); 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-resume", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "ace-builds": "^1.16.0", 7 | "axios": "^1.3.4", 8 | "classnames": "^2.3.2", 9 | "date-fns": "^2.29.3", 10 | "history": "^5.3.0", 11 | "prop-types": "^15.8.1", 12 | "react": "^18.2.0", 13 | "react-ace": "^10.1.0", 14 | "react-dom": "^18.2.0", 15 | "react-redux": "^8.0.5", 16 | "react-router": "^5.2.1", 17 | "react-scripts": "^5.0.1", 18 | "react-toastify": "^9.1.2", 19 | "redux": "^4.2.1", 20 | "redux-thunk": "^2.4.2", 21 | "semantic-ui-css": "^2.5.0", 22 | "semantic-ui-react": "^2.1.4", 23 | "sortablejs": "^1.15.0", 24 | "uuid": "^9.0.0" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "build": "react-scripts build", 29 | "test": "react-scripts test", 30 | "test:coverage": "react-scripts test --coverage --watchAll=false", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": "airbnb", 35 | "rules": { 36 | "react/jsx-filename-extension": 0, 37 | "operator-linebreak": 0, 38 | "react/jsx-wrap-multilines": 0 39 | }, 40 | "env": { 41 | "browser": true, 42 | "node": true, 43 | "jest": true 44 | } 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@testing-library/jest-dom": "^5.16.5", 60 | "@testing-library/react": "^14.0.0", 61 | "eslint-config-airbnb": "^19.0.4", 62 | "redux-logger": "^3.0.6" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Resume/Experience.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | function Experience({ experience, font }) { 7 | return ( 8 |
9 |

10 | Experience 11 |

12 |
13 |
    14 | {experience.map( 15 | (exp) => exp.isVisible !== false && ( 16 |
  • 17 | {' '} 18 |

    19 | {exp.position} 20 |

    21 | {exp.dateFrom && 22 |

    23 | {`${exp.dateFrom}${exp.dateTo ? ` - ${exp.dateTo}` : ''}`} 24 |

    } 25 | {`${exp.company}, ${exp.city}, ${exp.state}`} 26 |
      27 |
    • {exp.primaryWorkBrief}
    • 28 | {exp.impact1 &&
    • {exp.impact1}
    • } 29 | {exp.impact2 &&
    • {exp.impact2}
    • } 30 | {exp.impact3 &&
    • {exp.impact3}
    • } 31 | {exp.impact4 &&
    • {exp.impact4}
    • } 32 | {exp.impact5 &&
    • {exp.impact5}
    • } 33 |
    34 |
  • 35 | ), 36 | )} 37 |
38 |
39 | ); 40 | } 41 | 42 | Experience.defaultProps = { 43 | experience: [], 44 | }; 45 | 46 | Experience.propTypes = { 47 | experience: PropTypes.arrayOf(PropTypes.shape({})), 48 | font: PropTypes.string.isRequired, 49 | }; 50 | 51 | const mapStateToProps = (state) => ({ 52 | experience: state.resume.experience, 53 | font: state.tools.font, 54 | }); 55 | 56 | export default connect(mapStateToProps)(Experience); 57 | -------------------------------------------------------------------------------- /src/components/Tools/FontSelector.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Label, Icon, Button } from 'semantic-ui-react'; 5 | import { v4 as uuid } from 'uuid'; 6 | import { changeFont } from '../../actions/app.actions'; 7 | import fonts from '../../helpers/font.helper'; 8 | 9 | class FontSelector extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.handleFontChange = this.handleFontChange.bind(this); 13 | } 14 | 15 | handleFontChange(e) { 16 | const { dispatch } = this.props; 17 | const selection = e.target.selectedIndex; 18 | const selectedFont = fonts[selection].font; 19 | dispatch(changeFont(selectedFont)); 20 | } 21 | 22 | render() { 23 | const { selectedFont } = this.props; 24 | return ( 25 |
26 | 30 | 53 |
54 | ); 55 | } 56 | } 57 | 58 | FontSelector.defaultProps = { 59 | dispatch: () => {}, 60 | selectedFont: '', 61 | }; 62 | 63 | FontSelector.propTypes = { 64 | dispatch: PropTypes.func, 65 | selectedFont: PropTypes.string, 66 | }; 67 | 68 | const mapStateToProps = (state) => ({ 69 | selectedFont: state.tools.font, 70 | }); 71 | 72 | export default connect(mapStateToProps)(FontSelector); 73 | -------------------------------------------------------------------------------- /src/icons/internet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/Resume/TechnicalSkills.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { v4 as uuid } from 'uuid'; 5 | import Stars from './Stars'; 6 | 7 | const defaultLevel = 4; 8 | 9 | const retString = (kw) => (typeof kw === 'string' ? kw : kw.name); 10 | const retObject = (kw) => (typeof kw === 'string' ? { name: kw, level: defaultLevel } : kw); 11 | 12 | function TechnicalSkills({ techSkills, showSkillLevel, font }) { 13 | return ( 14 |
15 |

16 | Technical Skills 17 |

18 |
19 |
20 | {techSkills.map( 21 | (skill) => skill.isVisible !== false && ( 22 |
23 |

24 | {skill.category} 25 |

26 | {showSkillLevel 27 | ? skill.keywords.map((kw) => ( 28 |
29 |
{retObject(kw).name}
30 | 31 |
32 | )) 33 | : skill.keywords.map((kw, skillIndex) => (skillIndex === skill.keywords.length - 1 ? retString(kw) : `${retString(kw)}, `))} 34 |
), 35 | )} 36 |
37 |
38 | ); 39 | } 40 | 41 | TechnicalSkills.defaultProps = { 42 | techSkills: [], 43 | showSkillLevel: false, 44 | }; 45 | 46 | TechnicalSkills.propTypes = { 47 | techSkills: PropTypes.arrayOf(PropTypes.shape({})), 48 | showSkillLevel: PropTypes.bool, 49 | font: PropTypes.string.isRequired, 50 | }; 51 | 52 | const mapStateToProps = (state) => ({ 53 | techSkills: state.resume.technicalSkills, 54 | showSkillLevel: state.tools.showSkillLevel, 55 | font: state.tools.font, 56 | }); 57 | 58 | export default connect(mapStateToProps)(TechnicalSkills); 59 | -------------------------------------------------------------------------------- /src/icons/dark/internet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/Tools/Buttons/LocalStorageToggle.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | import React, { useState } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { connect } from 'react-redux'; 5 | import { 6 | Label, 7 | Icon, 8 | Button, 9 | Confirm, 10 | } from 'semantic-ui-react'; 11 | import ItemToggleButton from './ItemToggleButton'; 12 | import { toggleAutoSave, updateResume } from '../../../actions/app.actions'; 13 | import ls from '../../../helpers/localstorage.helper'; 14 | 15 | function LocalStorageToggle({ dispatch, status, resume }) { 16 | const [confirm, setConfirm] = useState(false); 17 | return ( 18 |
19 | 23 |
54 | ); 55 | } 56 | 57 | LocalStorageToggle.defaultProps = { 58 | dispatch: () => {}, 59 | status: false, 60 | }; 61 | 62 | LocalStorageToggle.propTypes = { 63 | dispatch: PropTypes.func, 64 | status: PropTypes.bool, 65 | resume: PropTypes.string.isRequired, 66 | }; 67 | 68 | const mapStateToProps = (state) => ({ 69 | resume: state.resume, 70 | }); 71 | 72 | export default connect(mapStateToProps)(LocalStorageToggle); 73 | -------------------------------------------------------------------------------- /src/components/Resume/Projects.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | function Projects({ projects, font }) { 7 | return ( 8 |
9 |

10 | Projects 11 |

12 |
13 |
    14 | {projects.map( 15 | (project) => project.isVisible !== false && ( 16 |
  • 17 |

    18 | {project.link ? ( 19 | 20 | {project.name} 21 | 22 | ) : ( 23 | project.name 24 | )} 25 |

    26 | {project.dateFrom && ( 27 |

    28 | {`${project.dateFrom}${project.dateTo ? ` - ${project.dateTo}` : ''}`} 29 |

    30 | )} 31 | {project.teamBrief} 32 |
      33 | {project.details.map( 34 | (detail) => detail && ( 35 |
    • 36 | {detail.search('http') > -1 ? ( 37 | 38 | {detail} 39 | 40 | ) : ( 41 | detail 42 | )} 43 |
    • 44 | ), 45 | )} 46 |
    47 |
  • 48 | ), 49 | )} 50 |
51 |
52 | ); 53 | } 54 | 55 | Projects.defaultProps = { 56 | projects: [], 57 | }; 58 | 59 | Projects.propTypes = { 60 | projects: PropTypes.arrayOf(PropTypes.shape({})), 61 | font: PropTypes.string.isRequired, 62 | }; 63 | 64 | const mapStateToProps = (state) => ({ 65 | projects: state.resume.projects, 66 | font: state.tools.font, 67 | }); 68 | 69 | export default connect(mapStateToProps)(Projects); 70 | -------------------------------------------------------------------------------- /src/components/Navigation/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Sidebar } from 'semantic-ui-react'; 5 | import { SidebarCloseButton, ToolbarHeader } from '.'; 6 | import { 7 | EditorButton, 8 | PrintButton, 9 | VisibilityChanger, 10 | FontSelector, 11 | OrderChanger, 12 | DownloadButton, 13 | LoadFromFileButton, 14 | SaveToCloudButtons, 15 | LocalStorageToggle, 16 | PaperSize, 17 | } from '../Tools'; 18 | import { toggleToolbar } from '../../actions/app.actions'; 19 | 20 | function Toolbar({ 21 | toolbarOpen, dispatch, resume, autoSave, 22 | }) { 23 | return ( 24 | 55 | ); 56 | } 57 | 58 | Toolbar.defaultProps = { 59 | dispatch: () => {}, 60 | toolbarOpen: false, 61 | resume: {}, 62 | autoSave: false, 63 | }; 64 | 65 | Toolbar.propTypes = { 66 | dispatch: PropTypes.func, 67 | toolbarOpen: PropTypes.bool, 68 | resume: PropTypes.shape({}), 69 | autoSave: PropTypes.bool, 70 | }; 71 | 72 | const mapStateToProps = (state) => ({ 73 | toolbarOpen: state.app.toolbarOpen, 74 | resume: state.resume, 75 | autoSave: state.tools.autoSave, 76 | }); 77 | 78 | export default connect(mapStateToProps)(Toolbar); 79 | -------------------------------------------------------------------------------- /src/components/Tools/PaperSize.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Label, Icon, Button } from 'semantic-ui-react'; 5 | import { v4 as uuid } from 'uuid'; 6 | import { changePaperSize } from '../../actions/app.actions'; 7 | 8 | export const paperSizes = [ 9 | { tag: 'letter', name: 'Letter (8.5" x 11")' }, 10 | { tag: 'a4', name: 'A4 (210mm × 297mm)' }, 11 | { tag: 'legal', name: 'Legal (8.5" x 14")' }, 12 | ]; 13 | 14 | class PaperSize extends Component { 15 | constructor(props) { 16 | super(props); 17 | this.handlePaperSizeChange = this.handlePaperSizeChange.bind(this); 18 | } 19 | 20 | handlePaperSizeChange(e) { 21 | const { dispatch } = this.props; 22 | const selection = e.target.selectedIndex; 23 | const paperSize = paperSizes[selection].tag; 24 | dispatch(changePaperSize(paperSize)); 25 | } 26 | 27 | render() { 28 | const { paperSize } = this.props; 29 | 30 | return ( 31 |
32 | 36 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | PaperSize.defaultProps = { 64 | dispatch: () => {}, 65 | paperSize: '', 66 | }; 67 | 68 | PaperSize.propTypes = { 69 | dispatch: PropTypes.func, 70 | paperSize: PropTypes.string, 71 | }; 72 | 73 | const mapStateToProps = (state) => ({ 74 | paperSize: state.tools.paperSize, 75 | }); 76 | 77 | export default connect(mapStateToProps)(PaperSize); 78 | -------------------------------------------------------------------------------- /src/icons/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/components/Tools/Buttons/LoadFromFileButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component, createRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Label, Icon, Button } from 'semantic-ui-react'; 4 | import { toast } from 'react-toastify'; 5 | import { updateResume } from '../../../actions/app.actions'; 6 | 7 | class LoadFromFileButton extends Component { 8 | fileRef = createRef(); 9 | 10 | onFileChange = ({ target }) => { 11 | const file = target.files && target.files.length && target.files[0]; 12 | 13 | if (file) { 14 | this.readFile(file); 15 | } 16 | } 17 | 18 | onFileError = () => { 19 | toast('😟 Something went wrong while loading from file!', { autoClose: false }); 20 | } 21 | 22 | onFileRead = ({ target }) => { 23 | const { dispatch, autoSave } = this.props; 24 | const { result } = target; 25 | 26 | if (!result) { 27 | toast('😟 No data loaded from file!', { autoClose: false }); 28 | return; 29 | } 30 | 31 | try { 32 | const resume = JSON.parse(result); 33 | 34 | dispatch(updateResume(resume, autoSave)); 35 | 36 | toast('🙌 Resume loaded from file!'); 37 | } catch (e) { 38 | toast('😟 Could not parse data file!', { autoClose: false }); 39 | } 40 | } 41 | 42 | readFile(file) { 43 | const fileReader = new FileReader(); 44 | 45 | fileReader.onload = this.onFileRead; 46 | fileReader.onerror = this.onFileError; 47 | fileReader.readAsText(file); 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 | 57 |
75 | ); 76 | } 77 | } 78 | 79 | LoadFromFileButton.defaultProps = { 80 | dispatch: () => {}, 81 | autoSave: false, 82 | }; 83 | 84 | LoadFromFileButton.propTypes = { 85 | dispatch: PropTypes.func, 86 | autoSave: PropTypes.bool, 87 | }; 88 | 89 | export default LoadFromFileButton; 90 | -------------------------------------------------------------------------------- /src/icons/dark/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/helpers/font.helper.js: -------------------------------------------------------------------------------- 1 | const fonts = [ 2 | { font: 'Roboto, sans-serif', name: 'Roboto' }, 3 | { font: 'Mukta, sans-serif', name: 'Mukta' }, 4 | { font: 'Encode Sans Expanded, sans-serif', name: 'Encode Sans Expanded' }, 5 | { font: 'Open Sans, sans-serif', name: 'Open Sans' }, 6 | { font: 'Oswald, sans-serif', name: 'Oswald' }, 7 | { font: "'Slabo 27px', serif", name: 'Slabo 27px' }, 8 | { font: 'Roboto Condensed, sans-serif', name: 'Roboto Condensed' }, 9 | { font: 'Lato, sans-serif', name: 'Lato' }, 10 | { font: 'Montserrat, sans-serif', name: 'Montserrat' }, 11 | { font: 'Source Sans Pro, sans-serif', name: 'Source Sans Pro' }, 12 | { font: 'Raleway, sans-serif', name: 'Raleway' }, 13 | { font: 'PT Sans, sans-serif', name: 'PT Sans' }, 14 | { font: 'Ubuntu, sans-serif', name: 'Ubuntu' }, 15 | { font: 'Open Sans Condensed, sans-serif', name: 'Open Sans Condensed' }, 16 | { font: 'Merriweather, serif', name: 'Merriweather' }, 17 | { font: 'Roboto Slab, serif', name: 'Roboto Slab' }, 18 | { font: 'Asap Condensed, sans-serif', name: 'Asap Condensed' }, 19 | { font: 'Noto Sans, sans-serif', name: 'Noto Sans' }, 20 | { font: 'Arimo, sans-serif', name: 'Arimo' }, 21 | { font: 'Poppins, sans-serif', name: 'Poppins' }, 22 | { font: 'Titillium Web, sans-serif', name: 'Titillium Web' }, 23 | { font: 'Muli, sans-serif', name: 'Muli' }, 24 | { font: 'PT Sans Narrow, sans-serif', name: 'PT Sans Narrow' }, 25 | { font: 'Oxygen, sans-serif', name: 'Oxygen' }, 26 | { font: 'Geo, sans-serif', name: 'Geo' }, 27 | { font: 'Inconsolata, monospace', name: 'Inconsolata' }, 28 | { font: 'Dosis, sans-serif', name: 'Dosis' }, 29 | { font: 'Cabin, sans-serif', name: 'Cabin' }, 30 | { font: 'Quicksand, sans-serif', name: 'Quicksand' }, 31 | { font: 'Abel, sans-serif', name: 'Abel' }, 32 | { font: 'Ubuntu Condensed, sans-serif', name: 'Ubuntu Condensed' }, 33 | { font: 'Varela Round, sans-serif', name: 'Varela Round' }, 34 | { font: 'Questrial, sans-serif', name: 'Questrial' }, 35 | { font: 'Maven Pro, sans-serif', name: 'Maven Pro' }, 36 | { font: 'Khula, sans-serif', name: 'Khula' }, 37 | { font: 'Rokkitt, serif', name: 'Rokkitt' }, 38 | { font: 'Pathway Gothic One, sans-serif', name: 'Pathway Gothic One' }, 39 | { font: 'Alegreya, serif', name: 'Alegreya' }, 40 | { font: 'News Cycle, sans-serif', name: 'News Cycle' }, 41 | { font: 'Cabin Condensed, sans-serif', name: 'Cabin Condensed' }, 42 | { font: 'Old Standard TT, serif', name: 'Old Standard TT' }, 43 | { font: 'Nunito Sans, sans-serif', name: 'Nunito Sans' }, 44 | { font: 'Quattrocento, serif', name: 'Quattrocento' }, 45 | { font: 'Didact Gothic, sans-serif', name: 'Didact Gothic' }, 46 | { font: 'Faustina, serif', name: 'Faustina' }, 47 | { font: 'Jura, sans-serif', name: 'Jura' }, 48 | { font: 'Khand, sans-serif', name: 'Khand' }, 49 | { font: 'Assistant, sans-serif', name: 'Assistant' }, 50 | { font: 'Antic, sans-serif', name: 'Antic' }, 51 | { font: 'Cutive Mono, monospace', name: 'Cutive Mono' }, 52 | { font: 'Source Code Pro, monospace', name: 'Source Code Pro' }, 53 | ]; 54 | 55 | export default fonts; 56 | -------------------------------------------------------------------------------- /src/reducers/tools.reducer.js: -------------------------------------------------------------------------------- 1 | /* eslint default-param-last: 0 */ 2 | import { 3 | CHANGE_FONT, 4 | TOGGLE_SHOW_ITEM, 5 | CHANGE_RESUME_ORDER, 6 | UPDATE_EDITOR_STATUS, 7 | TOGGLE_EDITOR, 8 | TOGGLE_AUTO_SAVE, 9 | CHANGE_PAPER_SIZE, 10 | } from '../actions/app.actions'; 11 | import { EDITOR_STATUS, saveTools, loadTools } from '../helpers/tools.helper'; 12 | import { defaultResumeOrder } from '../helpers/resume.helper'; 13 | 14 | const storedTools = loadTools(); 15 | const initialState = { 16 | font: 'Source Code Pro, monospace', 17 | showAddress: true, 18 | showEmail: true, 19 | showPhone: true, 20 | showGithub: false, 21 | order: defaultResumeOrder, 22 | showTechSkills: true, 23 | showSkillLevel: false, 24 | showProjects: true, 25 | showEducation: true, 26 | showCertification: true, 27 | showExperience: true, 28 | showLinkedIn: false, 29 | showWebsite: true, 30 | showProfessionalSummary: true, 31 | editorStatus: EDITOR_STATUS.WAITING, 32 | autoSave: false, 33 | paperSize: 'letter', 34 | showIcon: true, 35 | darkMode: false, 36 | }; 37 | 38 | const getItemToToggle = (state, action) => ({ 39 | [action.item]: !state[action.item], 40 | }); 41 | 42 | const changeFont = (state, action) => ({ 43 | ...state, 44 | font: action.font, 45 | }); 46 | 47 | const toggleShowItem = (state, action) => ({ 48 | ...state, 49 | ...getItemToToggle(state, action), 50 | }); 51 | 52 | const changeResumeOrder = (state, action) => ({ 53 | ...state, 54 | order: action.order, 55 | }); 56 | 57 | const updateResumeEditorStatus = (state, action) => ({ 58 | ...state, 59 | editorStatus: action.status, 60 | }); 61 | 62 | const toggleEditor = (state) => ({ 63 | ...state, 64 | editorStatus: EDITOR_STATUS.WAITING, 65 | }); 66 | 67 | const toggleAutoSave = (state) => ({ 68 | ...state, 69 | autoSave: !state.autoSave, 70 | }); 71 | 72 | const choosePaperSize = (state, paperSize) => ({ 73 | ...state, 74 | paperSize, 75 | }); 76 | 77 | export default (state = storedTools || initialState, action) => { 78 | switch (action.type) { 79 | case CHANGE_FONT: 80 | return (state.autoSave 81 | ? saveTools(changeFont(state, action)) 82 | : changeFont(state, action)); 83 | case TOGGLE_SHOW_ITEM: 84 | return (state.autoSave 85 | ? saveTools(toggleShowItem(state, action)) 86 | : toggleShowItem(state, action)); 87 | case CHANGE_RESUME_ORDER: 88 | return (state.autoSave 89 | ? saveTools(changeResumeOrder(state, action)) 90 | : changeResumeOrder(state, action)); 91 | case UPDATE_EDITOR_STATUS: 92 | return updateResumeEditorStatus(state, action); 93 | case TOGGLE_EDITOR: 94 | return toggleEditor(state); 95 | case TOGGLE_AUTO_SAVE: 96 | return saveTools(toggleAutoSave(state)); 97 | case CHANGE_PAPER_SIZE: 98 | return (state.autoSave 99 | ? saveTools(choosePaperSize(state, action.paperSize)) 100 | : choosePaperSize(state, action.paperSize)); 101 | default: 102 | return state; 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /src/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route, Switch, Router } from 'react-router'; 4 | import { connect } from 'react-redux'; 5 | import { toast, ToastContainer } from 'react-toastify'; 6 | import { createBrowserHistory } from 'history'; 7 | import classNames from 'classnames'; 8 | import { constants } from './config'; 9 | import { FocusTrap } from './helpers/app.helper'; 10 | import routes from './routes'; 11 | import './styles/App.css'; 12 | import './styles/darkmode.css'; 13 | 14 | const history = createBrowserHistory(); 15 | const { ROUTES } = constants; 16 | const { Home } = routes; 17 | 18 | function App({ 19 | editorOpen, 20 | toolbarOpen, 21 | moreVisibilityOpen, 22 | darkMode, 23 | autoSave, 24 | }) { 25 | const [autoSaveToastId, setToastId] = useState(''); 26 | useEffect(() => { 27 | if (!autoSave) { 28 | const id = toast.warn( 29 | 'To prevent data loss, download your resume using the Download button or turn on auto save!', 30 | { 31 | autoClose: false, 32 | position: 'bottom-right', 33 | closeOnClick: false, 34 | }, 35 | ); 36 | setToastId(id); 37 | } else { 38 | toast.dismiss(autoSaveToastId); 39 | setToastId(''); 40 | } 41 | }, [autoSave]); 42 | 43 | if (darkMode) { 44 | document.body.style.background = '#2d2d2d'; 45 | } else { 46 | document.body.style.background = '#fff'; 47 | } 48 | return ( 49 |
50 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | ); 76 | } 77 | 78 | App.defaultProps = { 79 | editorOpen: false, 80 | toolbarOpen: false, 81 | moreVisibilityOpen: false, 82 | darkMode: false, 83 | }; 84 | 85 | App.propTypes = { 86 | editorOpen: PropTypes.bool, 87 | toolbarOpen: PropTypes.bool, 88 | moreVisibilityOpen: PropTypes.bool, 89 | darkMode: PropTypes.bool, 90 | autoSave: PropTypes.bool.isRequired, 91 | }; 92 | 93 | const mapStateToProps = (state) => ({ 94 | editorOpen: state.app.editorOpen, 95 | toolbarOpen: state.app.toolbarOpen, 96 | moreVisibilityOpen: state.app.moreVisibilityOpen, 97 | darkMode: state.tools.darkMode, 98 | autoSave: state.tools.autoSave, 99 | }); 100 | 101 | export default connect(mapStateToProps)(App); 102 | -------------------------------------------------------------------------------- /src/icons/dark/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/helpers/tools.helper.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | import { debounce } from './app.helper'; 3 | import ls from './localstorage.helper'; 4 | import { 5 | SAVE_RESUME_ERROR_TOAST_ID, SAVE_RESUME_SUCCESS_TOAST_ID, 6 | LOCAL_STORAGE_ON_TOAST_ID, LOCAL_STORAGE_OFF_TOAST_ID, 7 | } 8 | from '../config/constants'; 9 | 10 | const STORED_TOOLS_KEY = 'rr-ls-tools-key'; 11 | const savedTools = ls.getItem(STORED_TOOLS_KEY); 12 | let prevLocalStorageState = savedTools ? savedTools.autoSave : false; 13 | 14 | if (!prevLocalStorageState) { 15 | debounce( 16 | () => toast(' ⚠️ Auto save to local storage is turned off!', { toastId: 'rrtrlsoffinit', position: 'top-right', autoClose: false }), 17 | 1000, 18 | false, 19 | 'rrtrlsoffinit', 20 | ); 21 | } else { 22 | debounce( 23 | () => toast(' 💾 Auto save to local storage is turned on!', { toastId: 'rrtrlsoninit', position: 'top-right', autoClose: 10000 }), 24 | 1000, 25 | false, 26 | 'rrtrlsoninit', 27 | ); 28 | } 29 | 30 | export const EDITOR_STATUS = { 31 | UPDATED: 'code updated', 32 | WAITING: 'waiting for changes', 33 | ERROR: 'invalid json', 34 | VALIDATING: 'validating', 35 | }; 36 | 37 | export const getStatusColor = (status) => { 38 | switch (status) { 39 | case EDITOR_STATUS.WAITING: 40 | return 'blue'; 41 | case EDITOR_STATUS.ERROR: 42 | return 'red'; 43 | case EDITOR_STATUS.VALIDATING: 44 | return 'yellow'; 45 | case EDITOR_STATUS.UPDATED: 46 | return 'green'; 47 | default: 48 | return undefined; 49 | } 50 | }; 51 | 52 | export const getDarkStatusColor = (status) => { 53 | switch (status) { 54 | case EDITOR_STATUS.WAITING: 55 | return 'teal'; 56 | case EDITOR_STATUS.ERROR: 57 | return 'pink'; 58 | case EDITOR_STATUS.VALIDATING: 59 | return 'yellow'; 60 | case EDITOR_STATUS.UPDATED: 61 | return 'green'; 62 | default: 63 | return undefined; 64 | } 65 | }; 66 | 67 | export const saveTools = (tools) => { 68 | if (!tools.autoSave && prevLocalStorageState) { 69 | toast(' ⚠️ Auto save to local storage is now off!', { toastId: LOCAL_STORAGE_OFF_TOAST_ID, position: 'top-right', autoClose: false }); 70 | } else if (tools.autoSave && !prevLocalStorageState) { 71 | toast.dismiss('rrtrlsoffinit'); 72 | toast.dismiss(LOCAL_STORAGE_OFF_TOAST_ID); 73 | toast(' 💾 Auto save to local storage is now on!', { toastId: LOCAL_STORAGE_ON_TOAST_ID, position: 'top-right', autoClose: 10000 }); 74 | } 75 | prevLocalStorageState = tools.autoSave; 76 | if (ls.setItem(STORED_TOOLS_KEY, tools)) { 77 | toast.dismiss(SAVE_RESUME_ERROR_TOAST_ID); 78 | debounce( 79 | () => toast(' 💾 saved to local storage...', { toastId: SAVE_RESUME_SUCCESS_TOAST_ID, position: 'top-right' }), 80 | 100, 81 | false, 82 | SAVE_RESUME_SUCCESS_TOAST_ID, 83 | ); 84 | } else { 85 | debounce( 86 | () => toast(' ⚠️ error saving to local storage...', { toastId: SAVE_RESUME_ERROR_TOAST_ID, position: 'top-right' }), 87 | 100, 88 | false, 89 | SAVE_RESUME_ERROR_TOAST_ID, 90 | ); 91 | } 92 | return tools; 93 | }; 94 | 95 | export const loadTools = () => ls.getItem(STORED_TOOLS_KEY); 96 | -------------------------------------------------------------------------------- /src/components/Tools/OrderChanger.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Button, Label, Icon } from 'semantic-ui-react'; 5 | import Sortable from 'sortablejs'; 6 | import { changeResumeOrder } from '../../actions/app.actions'; 7 | import { 8 | EDUCATION, 9 | TECH_SKILLS, 10 | PROJECTS, 11 | EXPERIENCE, 12 | CERTIFICATION, 13 | PROFESSIONAL_SUMMARY, 14 | } from '../../helpers/resume.helper'; 15 | 16 | class OrderChanger extends Component { 17 | constructor(props) { 18 | super(props); 19 | this.createSortableList = this.createSortableList.bind(this); 20 | this.onOrderChange = this.onOrderChange.bind(this); 21 | this.getResumeOrderHandles = this.getResumeOrderHandles.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | this.createSortableList(); 26 | } 27 | 28 | onOrderChange(e) { 29 | const { dispatch } = this.props; 30 | const newResumeOrder = Array.from(e.to.children) 31 | .map((item) => parseInt(item.getAttribute('data-id'), 10)); 32 | dispatch(changeResumeOrder(newResumeOrder)); 33 | } 34 | 35 | getResumeOrderHandles() { 36 | const { order } = this.props; 37 | let resumeSection = ''; 38 | return order.map((item) => { 39 | switch (item) { 40 | case PROFESSIONAL_SUMMARY: 41 | resumeSection = 'Proessional Summary'; 42 | break; 43 | case TECH_SKILLS: 44 | resumeSection = 'Technical Skills'; 45 | break; 46 | case EXPERIENCE: 47 | resumeSection = 'Experience'; 48 | break; 49 | case PROJECTS: 50 | resumeSection = 'Projects'; 51 | break; 52 | case EDUCATION: 53 | resumeSection = 'Education'; 54 | break; 55 | case CERTIFICATION: 56 | resumeSection = 'Certifications'; 57 | break; 58 | default: 59 | resumeSection = 'Unknown Section'; 60 | break; 61 | } 62 | return ( 63 |
  • 64 |
  • 77 | ); 78 | }); 79 | } 80 | 81 | createSortableList() { 82 | Sortable.create(this.orderList, { 83 | animation: 100, 84 | dataIdAttr: 'data-id', 85 | sort: true, 86 | onUpdate: this.onOrderChange, 87 | }); 88 | } 89 | 90 | render() { 91 | const currentOrder = this.getResumeOrderHandles(); 92 | return ( 93 |
    94 | 98 |
      { this.orderList = list; }} 101 | > 102 | {currentOrder} 103 |
    104 |
    105 | ); 106 | } 107 | } 108 | 109 | OrderChanger.defaultProps = { 110 | dispatch: () => {}, 111 | order: [], 112 | }; 113 | 114 | OrderChanger.propTypes = { 115 | dispatch: PropTypes.func, 116 | order: PropTypes.arrayOf(PropTypes.number), 117 | }; 118 | 119 | const mapStateToProps = (state) => ({ 120 | order: state.tools.order, 121 | }); 122 | 123 | export default connect(mapStateToProps)(OrderChanger); 124 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 | 34 | 43 | JSON Resume | code your resume 44 | 45 | 46 | 47 |
    48 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/Tools/MoreVisibilityModal.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classNames from 'classnames'; 4 | import { connect } from 'react-redux'; 5 | import { 6 | Modal, 7 | Icon, 8 | Accordion, 9 | } from 'semantic-ui-react'; 10 | import { SidebarCloseButton } from '../Navigation'; 11 | import { ItemToggleButton } from './Buttons'; 12 | import { toggleMoreVisibility, updateResume } from '../../actions/app.actions'; 13 | 14 | const sectionHeaderMap = { 15 | experience: 'Experience', 16 | education: 'Education', 17 | certification: 'Certification', 18 | technicalSkills: 'Technical Skills', 19 | projects: 'Projects', 20 | }; 21 | 22 | const itemNamePropMap = { 23 | experience: (item) => `${item.company} - ${item.position}`, 24 | education: (item) => `${item.site} - ${item.studyDegree}`, 25 | certification: (item) => item.issuedBy, 26 | technicalSkills: (item) => item.category, 27 | projects: (item) => item.name, 28 | }; 29 | 30 | function MoreVisibilityModal({ 31 | open, dispatch, resume, autoSave, darkMode, 32 | }) { 33 | const [activeAccordion, setActiveAccordion] = useState(''); 34 | return ( 35 | 45 | 46 | dispatch(toggleMoreVisibility())} 50 | toolbarOpen={open} 51 | backgroundColor="white" 52 | /> 53 | 54 | 55 |

    Use these toggle to enable and disable individual items in your resume.

    56 | 57 | {Object.keys(sectionHeaderMap).map((section) => ( 58 | 59 | { 63 | if (activeAccordion !== section) { 64 | setActiveAccordion(section); 65 | } else { 66 | setActiveAccordion(''); 67 | } 68 | }} 69 | > 70 | 71 | {sectionHeaderMap[section]} 72 | 73 | 74 |
    78 | {resume[section].map((sectionItem, itemIndex) => ( 79 | { 84 | const updatedResume = resume; 85 | updatedResume[section][itemIndex].isVisible = 86 | !(sectionItem.isVisible !== false); 87 | dispatch(updateResume(updatedResume, autoSave)); 88 | }} 89 | /> 90 | ))} 91 |
    92 |
    93 |
    94 | ))} 95 |
    96 |
    97 |
    98 | ); 99 | } 100 | 101 | MoreVisibilityModal.propTypes = { 102 | dispatch: PropTypes.func.isRequired, 103 | open: PropTypes.bool.isRequired, 104 | resume: PropTypes.shape({}).isRequired, 105 | autoSave: PropTypes.bool.isRequired, 106 | darkMode: PropTypes.bool.isRequired, 107 | }; 108 | 109 | const mapStateToProps = (state) => ({ 110 | open: state.app.moreVisibilityOpen, 111 | resume: state.resume, 112 | autoSave: state.tools.autoSave, 113 | darkMode: state.tools.darkMode, 114 | }); 115 | 116 | export default connect(mapStateToProps)(MoreVisibilityModal); 117 | -------------------------------------------------------------------------------- /src/components/Resume/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { v4 as uuid } from 'uuid'; 5 | import classNames from 'classnames'; 6 | import Certifications from './Certifications'; 7 | import Education from './Education'; 8 | import Experience from './Experience'; 9 | import ResumeHeader from './Header'; 10 | import Projects from './Projects'; 11 | import ProfessionalSummary from './ProfessionalSummary'; 12 | import TechnicalSkills from './TechnicalSkills'; 13 | import { paperSizes } from '../Tools/PaperSize'; 14 | import { 15 | EDUCATION, 16 | TECH_SKILLS, 17 | PROJECTS, 18 | EXPERIENCE, 19 | CERTIFICATION, 20 | PROFESSIONAL_SUMMARY, 21 | defaultResumeOrder, 22 | } from '../../helpers/resume.helper'; 23 | import '../../styles/Resume.css'; 24 | 25 | function Resume({ 26 | font, 27 | showEducation, 28 | showTechSkills, 29 | showProjects, 30 | showExperience, 31 | showCertification, 32 | showProfessionalSummary, 33 | order, 34 | paperSizeObj, 35 | darkMode, 36 | }) { 37 | return ( 38 | <> 39 |
    40 |
    44 | 45 | {order.map((item) => { 46 | switch (item) { 47 | case PROFESSIONAL_SUMMARY: 48 | return showProfessionalSummary 49 | && ; 50 | case TECH_SKILLS: 51 | return showTechSkills 52 | && ; 53 | case EXPERIENCE: 54 | return showExperience 55 | && ; 56 | case PROJECTS: 57 | return showProjects 58 | && ; 59 | case EDUCATION: 60 | return showEducation 61 | && ; 62 | case CERTIFICATION: 63 | return showCertification 64 | && ; 65 | default: 66 | return

    Error with order.

    ; 67 | } 68 | })} 69 |
    70 |
    71 |

    80 | ⬆ ️ 81 | {`bottom limit of ${paperSizeObj.name} size page`} 82 | ⬆ ️ 83 |

    84 | 85 | ); 86 | } 87 | 88 | Resume.defaultProps = { 89 | font: undefined, 90 | showEducation: true, 91 | showTechSkills: true, 92 | showProjects: true, 93 | showExperience: true, 94 | showCertification: true, 95 | showProfessionalSummary: true, 96 | order: defaultResumeOrder, 97 | paperSizeObj: paperSizes[0], 98 | darkMode: false, 99 | }; 100 | 101 | Resume.propTypes = { 102 | font: PropTypes.string, 103 | showEducation: PropTypes.bool, 104 | showTechSkills: PropTypes.bool, 105 | showProjects: PropTypes.bool, 106 | showExperience: PropTypes.bool, 107 | showCertification: PropTypes.bool, 108 | showProfessionalSummary: PropTypes.bool, 109 | order: PropTypes.arrayOf(PropTypes.number), 110 | paperSizeObj: PropTypes.shape({ 111 | name: PropTypes.string, 112 | tag: PropTypes.string, 113 | }), 114 | darkMode: PropTypes.bool, 115 | }; 116 | 117 | const mapStateToProps = (state) => ({ 118 | showEducation: state.tools.showEducation, 119 | showTechSkills: state.tools.showTechSkills, 120 | showProjects: state.tools.showProjects, 121 | showExperience: state.tools.showExperience, 122 | showCertification: state.tools.showCertification, 123 | showProfessionalSummary: state.tools.showProfessionalSummary, 124 | font: state.tools.font, 125 | order: state.tools.order, 126 | paperSizeObj: paperSizes.find((size) => size.tag === state.tools.paperSize), 127 | resume: state.resume, 128 | darkMode: state.tools.darkMode, 129 | }); 130 | 131 | export default connect(mapStateToProps)(Resume); 132 | -------------------------------------------------------------------------------- /src/styles/App.css: -------------------------------------------------------------------------------- 1 | *:not(#json-resume-editor, #json-resume-editor *, .resume, #resume-toast-container *){ 2 | -webkit-font-smoothing: antialiased; 3 | text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px; 4 | color: #263238 !important; 5 | scroll-behavior: smooth !important; 6 | } 7 | 8 | body { 9 | line-height: initial !important; 10 | } 11 | 12 | .json-resume-tool { 13 | margin-top: 20px; 14 | padding-left: 20px; 15 | padding-right: 20px; 16 | } 17 | 18 | .json-resume-tool > div { 19 | border: none !important; 20 | font-weight: 100 !important; 21 | background-color: transparent !important; 22 | } 23 | 24 | .json-resume-tool button:not(.circular), 25 | .json-resume-tool a { 26 | border-radius: 0 !important; 27 | border-top: 0.25px solid rgba(0,0,0,0.25) !important; 28 | box-shadow: 0 3px 3px rgba(0,0,0,0.16), 0 3px 3px rgba(0,0,0,0.23) !important; 29 | font-weight: 100 !important; 30 | } 31 | 32 | .json-resume-tool button label { 33 | font-size: 16px !important; 34 | } 35 | 36 | .json-resume-tool button:hover, 37 | .json-resume-tool a:hover { 38 | background-color: rgba(211, 211, 211, 0.493) !important; 39 | } 40 | 41 | .json-resume-tool button:active { 42 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24) !important; 43 | } 44 | 45 | #json-resume-editor { 46 | font-family: monospace; 47 | font-size: 16px; 48 | width: 100% !important; 49 | height: 100% !important; 50 | border: 1px solid black; 51 | } 52 | 53 | #root > div > div.ui.scale.down.right.very.wide.visible.sidebar > div.ui.massive.borderless.top.attached.menu > div:nth-child(1) { 54 | margin-right: 0 !important; 55 | } 56 | 57 | #root > div > div.ui.massive.top.attached.menu { 58 | position: fixed; 59 | top: 0; 60 | left: 0; 61 | height: 52px; 62 | background-color: transparent; 63 | border-bottom: none; 64 | } 65 | 66 | #root > div > div.ui.massive.top.attached.menu > a, 67 | #root > div > div.ui.massive.top.attached.menu { 68 | border: none !important; 69 | } 70 | 71 | #root > div > div.ui.massive.top.attached.menu > a { 72 | font-size: 24px !important; 73 | } 74 | 75 | #root > div > div.ui.massive.top.attached.menu > a:before { 76 | display: none; 77 | } 78 | 79 | #root > div > div.ui.scale.down.right.very.wide.visible.sidebar > div.ui.massive.borderless.top.attached.menu > div:nth-child(2) { 80 | font-family: monospace; 81 | font-style: italic; 82 | font-size: 14px; 83 | } 84 | 85 | .options-selector { 86 | font-weight: 100; 87 | -webkit-font-smoothing: antialiased; 88 | border: none; 89 | display: inline-block; 90 | border-radius: 0; 91 | -moz-border-radiu: 0; 92 | -webkit-border-radius: 0; 93 | -webkit-appearance: none; 94 | -moz-appearance: none; 95 | background-color: transparent; 96 | outline: none; 97 | width: 100%; 98 | height: 100%; 99 | text-align: center !important; 100 | padding: .78571429em 1.5em .78571429em; 101 | background: url(https://cdn3.iconfinder.com/data/icons/google-material-design-icons/48/ic_keyboard_arrow_down_48px-128.png) no-repeat right center; 102 | background-size: 40px 40px; 103 | cursor: pointer; 104 | } 105 | 106 | .json-resume-tool.font-selector button { 107 | padding: 0; 108 | } 109 | 110 | #\#resume-order-changer { 111 | list-style: none; 112 | appearance: none; 113 | width: 100%; 114 | padding: 0; 115 | margin-top: 0; 116 | } 117 | 118 | .resume-toast-body { 119 | padding-left: 10px; 120 | color: #263238; 121 | -webkit-font-smoothing: antialiased; 122 | text-shadow: rgba(0, 0, 0, 0.01) 0 0 1px; 123 | } 124 | 125 | .Toastify__toast-theme--dark .resume-toast-body { 126 | color: #fff; 127 | -webkit-text-fill-color: #fff; 128 | } 129 | 130 | .ui.attached.menu { 131 | margin: 0 !important; 132 | } 133 | 134 | @media only screen and (max-width: 500px) { 135 | #root > div > aside > div { 136 | width: 100vw; 137 | } 138 | 139 | #root > div > div.ui.scale.down.right.very.wide.visible.sidebar > div:nth-child(2) { 140 | padding-left: 5px !important; 141 | padding-right: 5px !important; 142 | } 143 | } 144 | 145 | @media only screen and (max-width: 767px) { 146 | .ui.modal>.header { 147 | padding: 0 !important; 148 | padding-right: 0 !important; 149 | margin: 0; 150 | } 151 | } 152 | 153 | @media print { 154 | .Toastify{ 155 | display: none; 156 | } 157 | body { 158 | background-color: #fff !important; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/styles/darkmode.css: -------------------------------------------------------------------------------- 1 | @media only screen { 2 | .react-resume.dark { 3 | background-color: #2d2d2d; 4 | color: #fff; 5 | -webkit-text-fill-color: #fff; 6 | } 7 | 8 | .react-resume.dark h1 { 9 | color: #f2bb13; 10 | -webkit-text-fill-color: #f2bb13; 11 | /* -webkit-opacity: 1; 12 | opacity: 1; */ 13 | 14 | font-weight: 1.1em; 15 | } 16 | 17 | .react-resume.dark h2 { 18 | color: #f2bb13; 19 | -webkit-text-fill-color: #f2bb13; 20 | } 21 | 22 | .react-resume.dark hr { 23 | border-color: #1f1f1f; 24 | } 25 | 26 | .react-resume.dark a { 27 | color: #51ffff; 28 | -webkit-text-fill-color: #51ffff; 29 | } 30 | 31 | .react-resume.dark img.normal-icon { 32 | display: none; 33 | } 34 | 35 | .react-resume.dark img.dark-icon { 36 | display: initial; 37 | } 38 | 39 | .App.darkMode { 40 | background: #2d2d2d !important; 41 | } 42 | 43 | .App.darkMode .ui.basic.label { 44 | -webkit-text-fill-color: #f2bb13; 45 | 46 | color: #f2bb13; 47 | } 48 | 49 | .App.darkMode .sidebar .ui.massive.attached.menu .item { 50 | -webkit-text-fill-color: #f2bb13; 51 | 52 | color: #f2bb13; 53 | } 54 | 55 | .App.darkMode > .ui.massive.attached.menu > .item { 56 | -webkit-text-fill-color: #f2bb13; 57 | 58 | color: #f2bb13; 59 | } 60 | 61 | .App.darkMode .json-resume-tool { 62 | background: #2d2d2d; 63 | } 64 | 65 | .App.darkMode .ui.scale.down.sidebar { 66 | background: #2d2d2d !important; 67 | } 68 | 69 | .App.darkMode .ui.scale.down.sidebar div > em, 70 | .App.darkMode .ui.scale.down.sidebar div > h1, 71 | .App.darkMode .ui.scale.down.sidebar h2 { 72 | color: #f2bb13 !important; 73 | -webkit-text-fill-color: #f2bb13; 74 | } 75 | 76 | .App.darkMode .ui.large.fluid.button { 77 | background: #1f1f1f !important; 78 | color: #fff !important; 79 | -webkit-text-fill-color: #fff; 80 | } 81 | 82 | .App.darkMode .ui.checkbox label { 83 | color: #fff !important; 84 | -webkit-text-fill-color: #fff; 85 | } 86 | 87 | .App.darkMode .ui.toggle.checked.checkbox input:checked ~ label { 88 | color: #fff !important; 89 | -webkit-text-fill-color: #fff; 90 | } 91 | 92 | .App.darkMode .ui.large.fluid.button .options-selector { 93 | color: #fff !important; 94 | -webkit-text-fill-color: #fff; 95 | 96 | background: url(https://cdnjs.cloudflare.com/ajax/libs/material-design-icons/3.0.1/hardware/1x_web/ic_keyboard_arrow_down_white_48dp.png) 97 | no-repeat right center; 98 | } 99 | 100 | .App.darkMode .ui.button.basic.circular { 101 | color: #fff !important; 102 | -webkit-text-fill-color: #fff; 103 | 104 | box-shadow: 0 0 1.5px rgba(31, 31, 31, 0.65) inset !important; 105 | background: transparent !important; 106 | } 107 | 108 | .App.darkMode .ui.button.basic.circular:hover { 109 | background: #000 !important; 110 | } 111 | 112 | .ui.modal.darkModal .ui.top.attached.menu { 113 | background: #2d2d2d !important; 114 | } 115 | 116 | .ui.modal.darkModal .ui.large.fluid.button { 117 | background: #1f1f1f !important; 118 | color: #fff !important; 119 | -webkit-text-fill-color: #fff; 120 | } 121 | 122 | .ui.modal.darkModal .ui.checkbox label { 123 | color: #fff !important; 124 | -webkit-text-fill-color: #fff; 125 | } 126 | 127 | .ui.modal.darkModal .ui.toggle.checked.checkbox input:checked ~ label { 128 | color: #fff !important; 129 | -webkit-text-fill-color: #fff; 130 | } 131 | 132 | .ui.modal.darkModal .ui.top.attached.menu .item { 133 | color: #f2bb13 !important; 134 | -webkit-text-fill-color: #f2bb13; 135 | } 136 | 137 | .ui.modal.darkModal .content { 138 | background: #2d2d2d !important; 139 | color: #fff; 140 | -webkit-text-fill-color: #fff; 141 | } 142 | 143 | .ui.modal.darkModal .content .ui.accordion.styled { 144 | background: #1f1f1f !important; 145 | } 146 | 147 | .ui.modal.darkModal .content .ui.accordion.styled .title { 148 | color: #fff !important; 149 | -webkit-text-fill-color: #fff; 150 | } 151 | 152 | .save-to-cloud.dark.ui.modal * { 153 | background: #2d2d2d !important; 154 | color: #fff !important; 155 | -webkit-text-fill-color: #fff; 156 | } 157 | 158 | .save-to-cloud.dark.ui.modal * > button, 159 | .save-to-cloud.dark.ui.modal * > button * { 160 | background: #1f1f1f !important; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/resume-data.js: -------------------------------------------------------------------------------- 1 | const Resume = { 2 | header: { 3 | name: 'Your Name', 4 | email: 'email@domain.com', 5 | phone: '123-456-7890', 6 | github: 'https://github.com/xxxxxxx', 7 | linkedin: 'https://linkedin.com/in/xxxxxx', 8 | address: '123 Main Street', 9 | website: 'https://website.com', 10 | city: 'City', 11 | state: 'CA', 12 | zip: '00000', 13 | country: 'USA', 14 | }, 15 | professionalSummary: { 16 | text: 'Your professional summary!', 17 | }, 18 | experience: [ 19 | { 20 | company: 'Experience 1', 21 | city: 'City', 22 | state: 'CA', 23 | position: 'Position 1', 24 | dateFrom: 'XX/XXXX', 25 | dateTo: '', 26 | primaryWorkBrief: 'Brief description of your main tasks.', 27 | impact1: 'Something awesome you did 1.', 28 | impact2: 'Something awesome you did 2.', 29 | impact3: '', 30 | impact4: '', 31 | isVisible: true, 32 | }, 33 | { 34 | company: 'Experience 2', 35 | city: 'City', 36 | state: 'CA', 37 | position: 'Position 2', 38 | dateFrom: 'XX/XXXX', 39 | dateTo: 'XX/XXXX', 40 | primaryWorkBrief: 'Brief description of your main tasks.', 41 | impact1: 'Something awesome you did 1.', 42 | impact2: 'Something awesome you did 2.', 43 | impact3: '', 44 | impact4: '', 45 | isVisible: true, 46 | }, 47 | { 48 | company: 'Experience 3', 49 | city: 'City', 50 | state: 'CA', 51 | position: 'Position 3', 52 | dateFrom: '', 53 | dateTo: '', 54 | primaryWorkBrief: 'Brief description of your main tasks.', 55 | impact1: 'Something awesome you did 1.', 56 | impact2: 'Something awesome you did 2.', 57 | impact3: '', 58 | impact4: '', 59 | isVisible: true, 60 | }, 61 | ], 62 | education: [ 63 | { 64 | site: 'School 1', 65 | dateFrom: 'XX/XXXX', 66 | dateTo: 'XX/XXXX', 67 | studyDegree: 'Subject, Degree/Certificate', 68 | isVisible: true, 69 | }, 70 | { 71 | site: 'School 2', 72 | dateFrom: 'XX/XXXX', 73 | dateTo: '', 74 | studyDegree: 'Subject, Degree/Certificate', 75 | isVisible: true, 76 | }, 77 | ], 78 | certification: [ 79 | { 80 | issuedBy: 'Issuer 1/Cert Name', 81 | id: '#12345', 82 | dateFrom: 'XX/XXXX', 83 | dateTo: '', 84 | isVisible: true, 85 | }, 86 | { 87 | issuedBy: 'Issuer 2/Cert Name', 88 | id: '#12345', 89 | dateFrom: 'XX/XXXX', 90 | dateTo: 'XX/XXXX', 91 | isVisible: true, 92 | }, 93 | { 94 | issuedBy: 'Issuer 3/Cert Name', 95 | id: '#12345', 96 | dateFrom: '', 97 | dateTo: '', 98 | isVisible: true, 99 | }, 100 | ], 101 | technicalSkills: [ 102 | { 103 | category: 'Development Languages', 104 | keywords: [ 105 | { name: 'JavaScript', level: 3 }, 106 | { name: 'HTML', level: 4 }, 107 | { name: 'CSS', level: 4 }, 108 | ], 109 | isVisible: true, 110 | }, 111 | { 112 | category: 'Technologies', 113 | keywords: [ 114 | { name: 'MongoDB', level: 2 }, 115 | { name: 'Express', level: 4 }, 116 | { name: 'React', level: 4 }, 117 | { name: 'Node.js', level: 4 }, 118 | { name: 'Mocha', level: 4 }, 119 | 'Passport', 120 | { name: 'JWT', level: 5 }, 121 | { name: 'Chai', level: 4 }, 122 | { name: 'Redux', level: 2 }, 123 | { name: 'Git', level: 4 }, 124 | { name: 'GitHub', level: 4 }, 125 | 'Gatsby', 126 | ], 127 | isVisible: true, 128 | }, 129 | { 130 | category: 'Custom Category', 131 | keywords: [ 132 | { 133 | name: 'Item 1', 134 | level: 3, 135 | }, 136 | { 137 | name: 'Item 2', 138 | level: 4, 139 | }, 140 | { 141 | name: 'Item 3', 142 | level: 4, 143 | }, 144 | ], 145 | isVisible: true, 146 | columnWidthPercent: '33.33%', 147 | }, 148 | ], 149 | projects: [ 150 | { 151 | name: 'Project 1', 152 | dateFrom: 'XX/XXXX', 153 | dateTo: '', 154 | link: 'http://website.com', 155 | teamBrief: '1-person project', 156 | details: ['Detail 1', 'Detail 2', 'http://projectLink.com'], 157 | isVisible: true, 158 | }, 159 | { 160 | name: 'Project 2', 161 | link: 'http://website.com', 162 | dateFrom: 'XX/XXXX', 163 | dateTo: 'XX/XXXX', 164 | teamBrief: '1-person project', 165 | details: ['Detail 1', 'Detail 2', 'https://google.com'], 166 | isVisible: true, 167 | }, 168 | ], 169 | }; 170 | 171 | export default Resume; 172 | -------------------------------------------------------------------------------- /src/components/Tools/CodeEditor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { connect } from 'react-redux'; 5 | import { Sidebar, Segment } from 'semantic-ui-react'; 6 | import AceEditor from 'react-ace'; 7 | import { SidebarCloseButton } from '../Navigation'; 8 | import { 9 | toggleEditor, 10 | updateResume, 11 | updateResumeEditorStatus, 12 | } from '../../actions/app.actions'; 13 | import { isValidJSON } from '../../helpers/resume.helper'; 14 | import { EDITOR_STATUS, getStatusColor, getDarkStatusColor } from '../../helpers/tools.helper'; 15 | 16 | import 'ace-builds/src-noconflict/mode-json'; 17 | import 'ace-builds/src-noconflict/theme-tomorrow_night_bright'; 18 | import 'ace-builds/src-noconflict/theme-tomorrow'; 19 | import 'ace-builds/src-noconflict/ext-language_tools'; 20 | 21 | class CodeEditor extends Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | editorValue: JSON.stringify(props.resume, null, '\t'), 26 | }; 27 | this.onResumeChange = this.onResumeChange.bind(this); 28 | } 29 | 30 | componentDidUpdate() { 31 | const { editorValue } = this.state; 32 | const { resume, editorOpen } = this.props; 33 | const resumeValue = JSON.stringify(resume, null, '\t'); 34 | if (!editorOpen && editorValue !== resumeValue) { 35 | // eslint-disable-next-line 36 | this.setState({ editorValue: resumeValue }); 37 | } 38 | } 39 | 40 | onResumeChange(data) { 41 | const { dispatch, autoSave } = this.props; 42 | this.setState({ 43 | editorValue: data, 44 | }); 45 | dispatch(updateResumeEditorStatus(EDITOR_STATUS.VALIDATING)); 46 | const updatedResume = isValidJSON(data); 47 | if (updatedResume) { 48 | dispatch(updateResume(updatedResume, autoSave)); 49 | dispatch(updateResumeEditorStatus(EDITOR_STATUS.UPDATED)); 50 | return; 51 | } 52 | dispatch(updateResumeEditorStatus(EDITOR_STATUS.ERROR)); 53 | } 54 | 55 | render() { 56 | const { 57 | editorOpen, 58 | dispatch, 59 | statusMessage, 60 | darkMode, 61 | } = this.props; 62 | const { editorValue } = this.state; 63 | const statusColor = darkMode ? 64 | getDarkStatusColor(statusMessage) : getStatusColor(statusMessage); 65 | return ( 66 | 79 | dispatch(toggleEditor())} 85 | toolbarOpen={editorOpen} 86 | backgroundColor={darkMode ? '#2d2d2d' : '#fff'} 87 | /> 88 |
    98 | 102 | 118 | 119 |
    120 |
    121 | ); 122 | } 123 | } 124 | 125 | CodeEditor.defaultProps = { 126 | dispatch: () => {}, 127 | editorOpen: false, 128 | resume: {}, 129 | statusMessage: EDITOR_STATUS.WAITING, 130 | autoSave: false, 131 | darkMode: false, 132 | }; 133 | 134 | CodeEditor.propTypes = { 135 | dispatch: PropTypes.func, 136 | editorOpen: PropTypes.bool, 137 | resume: PropTypes.shape({}), 138 | statusMessage: PropTypes.string, 139 | autoSave: PropTypes.bool, 140 | darkMode: PropTypes.bool, 141 | }; 142 | 143 | const mapStateToProps = (state) => ({ 144 | editorOpen: state.app.editorOpen, 145 | resume: state.resume, 146 | statusMessage: state.tools.editorStatus, 147 | autoSave: state.tools.autoSave, 148 | darkMode: state.tools.darkMode, 149 | }); 150 | 151 | export default connect(mapStateToProps)(CodeEditor); 152 | -------------------------------------------------------------------------------- /src/components/Resume/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import linkedinIcon from '../../icons/linkedin.svg'; 6 | import mailIcon from '../../icons/mail.svg'; 7 | import phoneIcon from '../../icons/phone.svg'; 8 | import websiteIcon from '../../icons/internet.svg'; 9 | import githubIcon from '../../icons/github.svg'; 10 | 11 | import linkedinDarkIcon from '../../icons/dark/linkedin.svg'; 12 | import mailDarkIcon from '../../icons/dark/mail.svg'; 13 | import phoneDarkIcon from '../../icons/dark/phone.svg'; 14 | import websiteDarkIcon from '../../icons/dark/internet.svg'; 15 | import githubDarkIcon from '../../icons/dark/github.svg'; 16 | 17 | export function Header({ 18 | header, showEmail, showPhone, showGithub, showLinkedIn, showWebsite, showAddress, font, showIcon, 19 | }) { 20 | return ( 21 |
    22 |

    {header.name}

    23 | 75 | {showAddress && ( 76 |
      77 |
    • {header.address}
    • 78 |
    • {header.city}
    • 79 |
    • {header.state}
    • 80 |
    • {header.zip}
    • 81 |
    • {header.country}
    • 82 |
    83 | )} 84 |
    85 | ); 86 | } 87 | 88 | Header.defaultProps = { 89 | header: undefined, 90 | showAddress: true, 91 | showEmail: true, 92 | showPhone: true, 93 | showGithub: true, 94 | showLinkedIn: true, 95 | showWebsite: true, 96 | showIcon: true, 97 | }; 98 | 99 | Header.propTypes = { 100 | header: PropTypes.shape({ 101 | name: PropTypes.string, 102 | email: PropTypes.string, 103 | github: PropTypes.string, 104 | linkedin: PropTypes.string, 105 | country: PropTypes.string, 106 | address: PropTypes.string, 107 | city: PropTypes.string, 108 | state: PropTypes.string, 109 | zip: PropTypes.string, 110 | phone: PropTypes.string, 111 | website: PropTypes.string, 112 | }), 113 | showEmail: PropTypes.bool, 114 | showPhone: PropTypes.bool, 115 | showGithub: PropTypes.bool, 116 | showLinkedIn: PropTypes.bool, 117 | showWebsite: PropTypes.bool, 118 | showAddress: PropTypes.bool, 119 | font: PropTypes.string.isRequired, 120 | showIcon: PropTypes.bool, 121 | }; 122 | 123 | const mapStateToProps = (state) => ({ 124 | header: state.resume.header, 125 | showAddress: state.tools.showAddress, 126 | showEmail: state.tools.showEmail, 127 | showPhone: state.tools.showPhone, 128 | showGithub: state.tools.showGithub, 129 | showLinkedIn: state.tools.showLinkedIn, 130 | showWebsite: state.tools.showWebsite, 131 | font: state.tools.font, 132 | showIcon: state.tools.showIcon, 133 | }); 134 | 135 | export default connect(mapStateToProps)(Header); 136 | -------------------------------------------------------------------------------- /src/helpers/resume.helper.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | import { debounce } from './app.helper'; 3 | import ls from './localstorage.helper'; 4 | import defaultResume from '../resume-data'; 5 | import { SAVE_RESUME_ERROR_TOAST_ID, SAVE_RESUME_SUCCESS_TOAST_ID } from '../config/constants'; 6 | 7 | export const isValidJSON = (data, parsed, autoFix) => { 8 | let cleanedResume; 9 | try { 10 | let newResume; 11 | if (!parsed) { 12 | newResume = JSON.parse(data); 13 | } else { 14 | newResume = data; 15 | } 16 | 17 | if (!newResume) { 18 | return false; 19 | } 20 | 21 | if (('header' in newResume) && typeof newResume.header !== 'object') { 22 | return false; 23 | } 24 | 25 | if (!('professionalSummary' in newResume) && typeof newResume.professionalSummary !== 'object') { 26 | return false; 27 | } 28 | 29 | let missingProp = false; 30 | [ 31 | 'name', 32 | 'email', 33 | 'phone', 34 | 'github', 35 | 'linkedin', 36 | 'address', 37 | 'city', 38 | 'state', 39 | 'zip', 40 | 'country', 41 | ].forEach((key) => { 42 | if (!(key in newResume.header)) { 43 | missingProp = true; 44 | } 45 | }); 46 | 47 | if (missingProp) { 48 | throw new Error(''); 49 | } 50 | 51 | const missingRootProps = []; 52 | 53 | if (!('experience' in newResume) && !Array.isArray(newResume.experience)) { 54 | if (!autoFix) { 55 | throw new Error(''); 56 | } else { 57 | missingRootProps.push('experience'); 58 | } 59 | } 60 | if (!('education' in newResume) && !Array.isArray(newResume.education)) { 61 | if (!autoFix) { 62 | throw new Error(''); 63 | } else { 64 | missingRootProps.push('education'); 65 | } 66 | } 67 | if ( 68 | !('technicalSkills' in newResume) && !Array.isArray(newResume.technicalSkills) 69 | ) { 70 | if (!autoFix) { 71 | throw new Error(''); 72 | } else { 73 | missingRootProps.push('technicalSkills'); 74 | } 75 | } 76 | if (!('projects' in newResume) && !Array.isArray(newResume.projects)) { 77 | if (!autoFix) { 78 | throw new Error(''); 79 | } else { 80 | missingRootProps.push('projects'); 81 | } 82 | } 83 | if ( 84 | !('certification' in newResume) && !Array.isArray(newResume.certification) 85 | ) { 86 | if (!autoFix) { 87 | throw new Error(''); 88 | } else { 89 | missingRootProps.push('certification'); 90 | } 91 | } 92 | if (!('professionalSummary' in newResume)) { 93 | if (!autoFix) { 94 | throw new Error(''); 95 | } else { 96 | missingRootProps.push('professionalSummary'); 97 | } 98 | } 99 | cleanedResume = { 100 | header: missingRootProps.includes('header') 101 | ? defaultResume.header 102 | : newResume.header, 103 | experience: missingRootProps.includes('experience') 104 | ? defaultResume.experience 105 | : newResume.experience, 106 | education: missingRootProps.includes('education') 107 | ? defaultResume.education 108 | : newResume.education, 109 | technicalSkills: missingRootProps.includes('technicalSkills') 110 | ? defaultResume.technicalSkills 111 | : newResume.technicalSkills, 112 | projects: missingRootProps.includes('projects') 113 | ? defaultResume.projects 114 | : newResume.projects, 115 | certification: missingRootProps.includes('certification') 116 | ? defaultResume.certification 117 | : newResume.certification, 118 | professionalSummary: missingRootProps.includes('professionalSummary') 119 | ? defaultResume.professionalSummary 120 | : newResume.professionalSummary, 121 | }; 122 | } catch (error) { 123 | // eslint-disable-next-line no-console 124 | console.error(error); 125 | return false; 126 | } 127 | return cleanedResume; 128 | }; 129 | 130 | export const PROFESSIONAL_SUMMARY = 0; 131 | export const TECH_SKILLS = 1; 132 | export const EXPERIENCE = 2; 133 | export const PROJECTS = 3; 134 | export const EDUCATION = 4; 135 | export const CERTIFICATION = 5; 136 | 137 | export const defaultResumeOrder = [ 138 | PROFESSIONAL_SUMMARY, 139 | TECH_SKILLS, 140 | EXPERIENCE, 141 | PROJECTS, 142 | EDUCATION, 143 | CERTIFICATION, 144 | ]; 145 | 146 | export const STORED_RESUME_KEY = 'rr-ls-resume-key'; 147 | 148 | export const saveResume = (resume) => { 149 | if (ls.setItem(STORED_RESUME_KEY, resume)) { 150 | debounce( 151 | () => toast(' 💾 saved to localStorage...', { toastId: SAVE_RESUME_SUCCESS_TOAST_ID }), 152 | 500, 153 | false, 154 | SAVE_RESUME_SUCCESS_TOAST_ID, 155 | ); 156 | } else { 157 | debounce( 158 | () => toast(' ⚠️ error saving to localStorage...', { toastId: SAVE_RESUME_ERROR_TOAST_ID }), 159 | 500, 160 | false, 161 | SAVE_RESUME_ERROR_TOAST_ID, 162 | ); 163 | } 164 | }; 165 | 166 | export const loadResume = () => { 167 | const resume = ls.getItem(STORED_RESUME_KEY); 168 | const cleaned = isValidJSON(resume, true, true); 169 | return cleaned; 170 | }; 171 | -------------------------------------------------------------------------------- /src/components/Tools/VisibilityChanger.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Label, Icon } from 'semantic-ui-react'; 5 | import { toggleShowItem } from '../../actions/app.actions'; 6 | import { ItemToggleButton, MoreVisibilityButton } from './Buttons'; 7 | 8 | const controlledToggleMap = { 9 | showTechSkills: ['showSkillLevel'], 10 | }; 11 | 12 | class VisibilityChanger extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.handleToggle = this.handleToggle.bind(this); 16 | } 17 | 18 | handleToggle(item, disabled) { 19 | // eslint-disable-next-line react/destructuring-assignment 20 | const itemOn = this.props[item]; 21 | if (disabled) return; 22 | const { dispatch } = this.props; 23 | const children = controlledToggleMap[item] || []; 24 | 25 | dispatch(toggleShowItem(item)); 26 | children.forEach((child) => { 27 | // eslint-disable-next-line react/destructuring-assignment 28 | const childOn = this.props[child]; 29 | if (itemOn && childOn) { 30 | dispatch(toggleShowItem(child)); 31 | } 32 | }); 33 | } 34 | 35 | render() { 36 | const { 37 | showAddress, 38 | showEmail, 39 | showPhone, 40 | showGithub, 41 | showTechSkills, 42 | showProjects, 43 | showEducation, 44 | showCertification, 45 | showExperience, 46 | showLinkedIn, 47 | showWebsite, 48 | showSkillLevel, 49 | showProfessionalSummary, 50 | showIcon, 51 | darkMode, 52 | } = this.props; 53 | return ( 54 |
    55 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
    76 | ); 77 | } 78 | } 79 | 80 | VisibilityChanger.defaultProps = { 81 | dispatch: () => { }, 82 | showAddress: true, 83 | showEmail: true, 84 | showPhone: true, 85 | showGithub: true, 86 | showTechSkills: true, 87 | showSkillLevel: false, 88 | showProjects: true, 89 | showEducation: true, 90 | showCertification: true, 91 | showExperience: true, 92 | showProfessionalSummary: true, 93 | showLinkedIn: true, 94 | showWebsite: true, 95 | showIcon: true, 96 | darkMode: false, 97 | }; 98 | 99 | VisibilityChanger.propTypes = { 100 | dispatch: PropTypes.func, 101 | showAddress: PropTypes.bool, 102 | showEmail: PropTypes.bool, 103 | showPhone: PropTypes.bool, 104 | showGithub: PropTypes.bool, 105 | showTechSkills: PropTypes.bool, 106 | showSkillLevel: PropTypes.bool, 107 | showProjects: PropTypes.bool, 108 | showEducation: PropTypes.bool, 109 | showCertification: PropTypes.bool, 110 | showExperience: PropTypes.bool, 111 | showLinkedIn: PropTypes.bool, 112 | showWebsite: PropTypes.bool, 113 | showProfessionalSummary: PropTypes.bool, 114 | showIcon: PropTypes.bool, 115 | darkMode: PropTypes.bool, 116 | }; 117 | 118 | const mapStateToProps = (state) => ({ 119 | showAddress: state.tools.showAddress, 120 | showEmail: state.tools.showEmail, 121 | showPhone: state.tools.showPhone, 122 | showGithub: state.tools.showGithub, 123 | showTechSkills: state.tools.showTechSkills, 124 | showSkillLevel: state.tools.showSkillLevel, 125 | showProjects: state.tools.showProjects, 126 | showEducation: state.tools.showEducation, 127 | showCertification: state.tools.showCertification, 128 | showExperience: state.tools.showExperience, 129 | showLinkedIn: state.tools.showLinkedIn, 130 | showWebsite: state.tools.showWebsite, 131 | showProfessionalSummary: state.tools.showProfessionalSummary, 132 | showIcon: state.tools.showIcon, 133 | darkMode: state.tools.darkMode, 134 | }); 135 | 136 | export default connect(mapStateToProps)(VisibilityChanger); 137 | -------------------------------------------------------------------------------- /src/styles/Resume.css: -------------------------------------------------------------------------------- 1 | .react-resume { 2 | margin-left: auto; 3 | margin-right: auto; 4 | margin-top: 15px; 5 | margin-bottom: 5px; 6 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); 7 | padding: 0.20in 0.50in 0.25in 0.50in; 8 | box-sizing: content-box; 9 | background-color: rgba(51, 47, 47, 0); 10 | text-overflow: clip; 11 | overflow-y: hidden; 12 | } 13 | 14 | .letter { 15 | max-width: 8.5in !important; 16 | min-width: 8.5in !important; 17 | min-height: 11in !important; 18 | max-height: 11in !important; 19 | } 20 | 21 | .a4 { 22 | max-width: 210mm !important; 23 | min-width: 210mm !important; 24 | min-height: 297mm !important; 25 | max-height: 297mm !important; 26 | } 27 | 28 | .legal { 29 | max-width: 8.5in !important; 30 | min-width: 8.5in !important; 31 | min-height: 14in !important; 32 | max-height: 14in !important; 33 | } 34 | 35 | .react-resume h1 { 36 | font-size: 22px; 37 | margin-top: 0; 38 | } 39 | 40 | .react-resume h2 { 41 | font-size: 15px; 42 | padding-bottom: 0; 43 | margin-bottom: 0; 44 | margin-top: 5px; 45 | } 46 | 47 | .react-resume h3 { 48 | font-size: 13px; 49 | margin: 0; 50 | } 51 | 52 | .resume { 53 | font-size: 11px; 54 | padding: 0; 55 | margin: 0; 56 | } 57 | 58 | .resume-header { 59 | text-align: center; 60 | } 61 | 62 | .resume-header h1 { 63 | margin-top: 0; 64 | margin-bottom: 0; 65 | } 66 | 67 | .resume-header hr:nth-of-type(1) { 68 | height: 1px; 69 | background-color: black; 70 | } 71 | 72 | .resume-header ul { 73 | text-align: center; 74 | margin-right: auto; 75 | margin-left: auto; 76 | margin-bottom: 15px; 77 | margin-top: 5px; 78 | padding: 0; 79 | } 80 | 81 | .resume-header li { 82 | list-style: none; 83 | display: inline; 84 | font-size: 13px; 85 | } 86 | 87 | .resume-header ul:first-of-type li::after { 88 | content: " | "; 89 | font-weight: bold; 90 | } 91 | 92 | .resume-header ul:first-of-type li:nth-of-type(3)::after { 93 | content: ""; 94 | display: block; 95 | } 96 | 97 | .resume-header ul:first-of-type li:last-child::after { 98 | content: ""; 99 | } 100 | 101 | .resume-header ul:nth-of-type(2) li::after { 102 | content: ", "; 103 | } 104 | 105 | .resume-header ul:nth-of-type(2) li:last-of-type::after { 106 | content: ""; 107 | } 108 | 109 | .resume-header .header-icon { 110 | position: relative; 111 | top: 0.2em; 112 | width: 1.2em; 113 | height: 1.2em; 114 | padding: 0 0.4em; 115 | } 116 | 117 | .resume-experience, 118 | .resume-professional-summary { 119 | text-align: left; 120 | } 121 | 122 | .resume-experience > ul, 123 | .resume-education > ul, 124 | .resume-projects > ul, 125 | .resume-certification > ul { 126 | margin-left: 0; 127 | padding-left: 0; 128 | } 129 | 130 | .resume-experience > ul > li, 131 | .resume-education > ul > li, 132 | .resume-projects > ul > li, 133 | .resume-certification > ul > li { 134 | list-style: none; 135 | margin-bottom: 10px; 136 | } 137 | 138 | .resume-experience h3:first-of-type, 139 | .resume-education h3:first-of-type, 140 | .resume-projects h3:first-of-type, 141 | .resume-certification h3:first-of-type { 142 | display: inline-block; 143 | width: 70%; 144 | } 145 | 146 | .resume-experience h3:nth-of-type(2), 147 | .resume-education h3:nth-of-type(2), 148 | .resume-projects h3:nth-of-type(2), 149 | .resume-certification h3:nth-of-type(2) { 150 | display: inline-block; 151 | width: 30%; 152 | text-align: right; 153 | vertical-align: top; 154 | font-size: 13px; 155 | font-weight: 400; 156 | } 157 | 158 | .resume-experience em, 159 | .resume-experience i { 160 | display: block; 161 | } 162 | 163 | .resume-experience i { 164 | font-size: small; 165 | margin-bottom: 5px; 166 | } 167 | 168 | .resume-experience em { 169 | margin-bottom: 5px; 170 | } 171 | 172 | .resume-tech-skills h3 { 173 | margin-bottom: 5px; 174 | } 175 | 176 | .resume-tech-skills .grid-container { 177 | display: flex; 178 | flex-direction: row; 179 | flex-wrap: wrap; 180 | justify-content: flex-start; 181 | } 182 | 183 | .resume-tech-skills .grid-column { 184 | flex-basis: 50%; 185 | margin-bottom: 0px; 186 | } 187 | 188 | .tech-skills-keyword { 189 | width: 100%; 190 | height: 1.4em; 191 | padding: 0.1em 0; 192 | box-sizing: border-box; 193 | } 194 | 195 | .tech-skills-keyword .keyword-name { 196 | width: 60%; 197 | display: inline-block; 198 | } 199 | 200 | .tech-skills-keyword .keyword-level { 201 | width: 40%; 202 | display: inline-block; 203 | } 204 | 205 | .tech-skills-keyword .keyword-level img{ 206 | height: 1.2em; 207 | width: 1.2em; 208 | margin: 0 0.2em; 209 | } 210 | 211 | .react-resume img.dark-icon { 212 | display: none; 213 | } 214 | 215 | @media (min-width: 500px) { 216 | .resume-tech-skills .grid-container { 217 | grid-template-columns: 50% 50%; 218 | grid-template-rows: 100%; 219 | margin-bottom: 0; 220 | } 221 | 222 | .resume-tech-skills .grid-column-1 { 223 | grid-column-start: 1; 224 | } 225 | 226 | .resume-tech-skills .grid-column-2 { 227 | grid-column-start: 2; 228 | } 229 | 230 | .resume-tech-skills .grid-column-1, 231 | .resume-tech-skills .grid-column-2 { 232 | margin-bottom: 0; 233 | } 234 | } 235 | 236 | @media print { 237 | .resume-tools { 238 | display: none; 239 | } 240 | 241 | .resume-edit-panel { 242 | display: none; 243 | } 244 | 245 | div.ad-area { 246 | display: none; 247 | margin-top: 75px; 248 | } 249 | 250 | aside, #root > div > div.ui.massive.top.attached.menu { 251 | display: none; 252 | } 253 | 254 | #root > .App .react-resume, #root > .App .react-resume > .resume { 255 | margin: 0 !important; 256 | padding: 0 !important; 257 | box-shadow: none; 258 | } 259 | 260 | #root > div > p { 261 | display: none; 262 | } 263 | 264 | * { 265 | overflow: visible !important; 266 | } 267 | 268 | @page { 269 | size: letter; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/tests/VisibilityChanger.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | import { render, fireEvent } from '@testing-library/react'; 4 | import { Provider } from 'react-redux'; 5 | import store from '../store'; 6 | import VisibilityChanger from '../components/Tools/VisibilityChanger'; 7 | import Resume from '../components/Resume'; 8 | 9 | // TODO: add tests for checking ItemToggleButton[data-testid='showSkillLevel'] button functioning 10 | // TODO: include MoreVisibilityButton within tests 11 | 12 | // TODO?: isolate assertions checking for specific tree structures, classes, etc. for easier testing 13 | 14 | describe('Toggle buttons render as expected, displayed under "Visibility" with eye icon', () => { 15 | test('Container for toggle buttons renders as expected', () => { 16 | const { getByTestId } = render( 17 | , 18 | ); 19 | 20 | // get root node created by VisibilityChanger 21 | const vc = getByTestId('VisibilityChanger'); 22 | 23 | // should have class "json-resume-tool" 24 | expect(vc.classList).toContain('json-resume-tool'); 25 | 26 | // should contain 17 child elements 27 | expect(vc.childElementCount).toBe(17); 28 | 29 | // first child should be a div with expected classes 30 | const label = vc.firstChild; 31 | expect(label.tagName).toBe('DIV'); 32 | expect(label.classList).toContain('ui', 'big', 'basic', 'label'); 33 | 34 | // first child should contain 2 nodes (icon and text) with expected attributes and classes 35 | expect(label.childNodes.length).toEqual(2); 36 | const [icon, text] = label.childNodes; 37 | expect(icon.tagName).toBe('I'); 38 | expect(icon.getAttribute('aria-hidden')).toBe('true'); 39 | expect(icon.classList).toContain('eye', 'icon'); 40 | expect(text.nodeName).toBe('#text'); 41 | 42 | // text node should have value "Visibility" 43 | expect(text.nodeValue).toEqual('Visibility'); 44 | }); 45 | 46 | test('Toggle buttons render as expected', () => { 47 | const { getByTestId } = render( 48 | , 49 | ); 50 | 51 | /* 52 | * data-testids for toggle-able resume elements 53 | * each corresponding toggle button has data-testid = 'show' + resume element's data-testid 54 | * (eg. "Email", "showEmail") 55 | */ 56 | const dataTestIDs = ['Email', 'Phone', 'Github', 'LinkedIn', 'Website', 'Address', 'Certification', 'Education', 'Experience', 'Projects', 'TechSkills', 'SkillLevel']; 57 | 58 | dataTestIDs.forEach((id) => { 59 | // get btn el 60 | const btn = getByTestId(`show${id}`); 61 | 62 | // expect each button to be a child of the VisibilityChanger container 63 | expect(btn.parentNode.isSameNode(getByTestId('VisibilityChanger'))).toBeTruthy(); 64 | 65 | // expect each button to be a button element, classlist containing "ui large fluid button" 66 | expect(btn.tagName).toBe('BUTTON'); 67 | expect(btn.classList).toContain('ui', 'large', 'fluid', 'button'); 68 | 69 | // expect it to contain a div with classlist "ui toggle checkbox" 70 | const div = btn.children[0]; 71 | expect(div.classList).toContain('ui', 'toggle', 'checkbox'); 72 | 73 | // expect the div to contain an input with class 'hidden' and type 'checkbox' 74 | const [input, label] = div.children; 75 | expect(input.tagName).toBe('INPUT'); 76 | expect(input.classList).toContain('hidden'); 77 | expect(input.getAttribute('type')).toBe('checkbox'); 78 | 79 | /* expect the div to also contain a label containing `Show ${id}` */ 80 | expect(label.tagName).toBe('LABEL'); 81 | 82 | // workaround for ids that do not match wording of their labels 83 | let textInLabel = ''; 84 | if (id === 'Certification') { 85 | textInLabel = 'Certifications'; 86 | } else if (id === 'TechSkills') { 87 | textInLabel = 'TechnicalSkills'; 88 | } else { 89 | textInLabel = id; 90 | } 91 | 92 | let wordsArry = ''; 93 | // ignore 'LinkedIn' when dividing words 94 | if (textInLabel !== 'LinkedIn') { 95 | wordsArry = textInLabel.match(/[A-Z][a-z]+/g); 96 | } else { 97 | wordsArry = [textInLabel]; 98 | } 99 | const wordsStr = wordsArry.join(' '); 100 | expect(label.innerHTML).toBe(`Show ${wordsStr}`); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('Toggle buttons work as expected', () => { 106 | test('Resume content visibility matches corresponding toggle button status on load', () => { 107 | const { getByTestId } = render( 108 | , 109 | ); 110 | 111 | render(); 112 | 113 | // note: "showSkillLevel" toggle button not being tested yet 114 | const dataTestIDs = ['Email', 'Phone', 'Github', 'LinkedIn', 'Website', 'Address', 'Certification', 'Education', 'Experience', 'Projects', 'TechSkills', 'SkillLevel']; 115 | 116 | dataTestIDs.forEach((id) => { 117 | // if toggle button status is on, corresponding resume content should be visible 118 | if (getByTestId(`show${id}`).children[0].classList.contains('checked')) { 119 | expect(getByTestId(id)).toBeInTheDocument(); 120 | } else { 121 | // if toggle button status is off, corresponding resume content should not be visible 122 | expect(() => { 123 | getByTestId(id); 124 | }).toThrow(); 125 | } 126 | }); 127 | }); 128 | 129 | test('Toggle button click updates toggle button status and corresponding resume content visibility', () => { 130 | const { getByTestId } = render( 131 | , 132 | ); 133 | 134 | render(); 135 | 136 | // note: "showSkillLevel" toggle button not being tested yet 137 | const dataTestIDs = ['Email', 'Phone', 'Github', 'LinkedIn', 'Website', 'Address', 'Certification', 'Education', 'Experience', 'Projects', 'TechSkills']; 138 | 139 | dataTestIDs.forEach((id) => { 140 | // toggling off should update toggle button status from on to off 141 | // corresponding resume content should disappear 142 | if (getByTestId(`show${id}`).children[0].classList.contains('checked')) { 143 | fireEvent.click(getByTestId(`show${id}`)); 144 | expect(getByTestId(`show${id}`).children[0].classList).not.toContain('checked'); 145 | expect(() => { 146 | getByTestId(id); 147 | }).toThrow(); 148 | } else { 149 | // toggling on should update toggle button status from off to on 150 | // corresponding resume content should appear 151 | fireEvent.click(getByTestId(`show${id}`)); 152 | expect(getByTestId(`show${id}`).children[0].classList).toContain('checked'); 153 | expect(getByTestId(id)).toBeInTheDocument(); 154 | } 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /src/components/Tools/Buttons/SaveToCloudButtons.js: -------------------------------------------------------------------------------- 1 | /* global gapi Dropbox OneDrive */ 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { connect } from 'react-redux'; 5 | import { 6 | Label, 7 | Icon, 8 | Button, 9 | Modal, 10 | Segment, 11 | Dimmer, 12 | Loader, 13 | } from 'semantic-ui-react'; 14 | import axios from 'axios'; 15 | import { v4 as uuid } from 'uuid'; 16 | import { toast } from 'react-toastify'; 17 | import classNames from 'classnames'; 18 | import settings from '../../../config/settings'; 19 | import { toggleToolbar } from '../../../actions/app.actions'; 20 | import { FocusTrap } from '../../../helpers/app.helper'; 21 | 22 | const { API } = settings; 23 | const { SAVE_KEY, FILE_KEY } = API; 24 | 25 | const CLOUDS = { 26 | GOOGLE: 'gd', 27 | DROPBOX: 'db', 28 | ONEDRIVE: 'od', 29 | }; 30 | 31 | class SaveToCloudButtons extends Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = { 35 | preparingFile: false, 36 | fileReady: false, 37 | cloudSelection: undefined, 38 | fileSource: undefined, 39 | savingToCloud: false, 40 | saveComplete: false, 41 | }; 42 | this.prepareFile = this.prepareFile.bind(this); 43 | this.onCloseModal = this.onCloseModal.bind(this); 44 | this.onSaveToDropBox = this.onSaveToDropBox.bind(this); 45 | this.onSaveToOneDrive = this.onSaveToOneDrive.bind(this); 46 | } 47 | 48 | onCloseModal() { 49 | this.setState({ 50 | preparingFile: false, 51 | fileReady: false, 52 | cloudSelection: undefined, 53 | fileSource: undefined, 54 | savingToCloud: false, 55 | saveComplete: false, 56 | }); 57 | } 58 | 59 | onSaveToDropBox() { 60 | const { fileSource } = this.state; 61 | this.setState({ 62 | savingToCloud: true, 63 | }); 64 | Dropbox.save( 65 | fileSource, 66 | 'resume.json', 67 | { 68 | success: () => { 69 | this.setState({ 70 | savingToCloud: false, 71 | saveComplete: true, 72 | }); 73 | toast('🙌 Resume saved to Dropbox!'); 74 | }, 75 | cancel: () => { 76 | this.setState({ 77 | savingToCloud: false, 78 | saveComplete: false, 79 | }); 80 | toast('✋ Save to Dropbox was cancelled!', { autoClose: 5000 }); 81 | }, 82 | error: () => { 83 | this.setState({ 84 | savingToCloud: false, 85 | saveComplete: false, 86 | }); 87 | toast('😟 Something went wrong while saving to Dropbox!', { autoClose: false }); 88 | }, 89 | }, 90 | ); 91 | } 92 | 93 | onSaveToOneDrive() { 94 | const { fileSource } = this.state; 95 | this.setState({ 96 | savingToCloud: true, 97 | }); 98 | const odOptions = { 99 | clientId: 'f95c8ea4-b187-408f-9778-912af37c3b74', 100 | action: 'save', 101 | sourceUri: fileSource, 102 | fileName: 'resume.json', 103 | openInNewWindow: true, 104 | viewType: 'folders', 105 | success: () => { 106 | this.setState({ 107 | savingToCloud: false, 108 | saveComplete: true, 109 | }); 110 | toast('🙌 Resume saved to OneDrive!'); 111 | }, 112 | cancel: () => { 113 | this.setState({ 114 | savingToCloud: false, 115 | saveComplete: false, 116 | }); 117 | toast('✋ Save to OneDrive was cancelled!', { autoClose: 5000 }); 118 | }, 119 | error: () => { 120 | this.setState({ 121 | savingToCloud: false, 122 | saveComplete: false, 123 | }); 124 | toast('😟 Something went wrong while saving to OneDrive!', { autoClose: false }); 125 | }, 126 | }; 127 | OneDrive.save(odOptions); 128 | } 129 | 130 | prepareFile(selection) { 131 | const { resume, dispatch } = this.props; 132 | dispatch(toggleToolbar()); 133 | this.setState({ 134 | preparingFile: true, 135 | fileReady: false, 136 | cloudSelection: selection, 137 | }); 138 | const dataId = uuid(); 139 | axios.post(API.URL + API.SAVE(SAVE_KEY), { content: resume, jsId: dataId }) 140 | .then((response) => { 141 | const { data } = response; 142 | const fileSource = API.URL + API.FILE(data, dataId, FILE_KEY); 143 | this.setState({ 144 | preparingFile: false, 145 | fileReady: true, 146 | fileSource, 147 | }, () => { 148 | if (selection === CLOUDS.GOOGLE) { 149 | gapi.savetodrive.render( 150 | 'gdrive-container', 151 | { 152 | src: fileSource, 153 | filename: 'resume.json', 154 | sitename: 'JSON Resume', 155 | }, 156 | ); 157 | } 158 | }); 159 | }) 160 | .catch((error) => { 161 | this.setState({ 162 | preparingFile: false, 163 | }); 164 | // eslint-disable-next-line no-console 165 | console.log(error); 166 | toast('⚠️ Error preparing file, Try again later!', { toastId: 'rrterrprepfile', autoClose: false }); 167 | }); 168 | } 169 | 170 | render() { 171 | const { 172 | preparingFile, 173 | fileReady, 174 | cloudSelection, 175 | savingToCloud, 176 | saveComplete, 177 | } = this.state; 178 | const { darkMode } = this.props; 179 | return ( 180 |
    181 | 185 |
    186 |
    211 | 212 | 213 | 214 | {preparingFile && 215 | 216 | 217 | } 218 | {fileReady && cloudSelection === CLOUDS.GOOGLE && 219 |
    220 |

    221 | Your file is ready to save to Google Drive. Click the button below to save. 222 |

    223 |
    } 240 | {fileReady && cloudSelection === CLOUDS.DROPBOX && 241 |
    242 |

    243 | Your file is ready to save to Dropbox. Click the button below to save. 244 |

    245 |
    } 262 | {fileReady && cloudSelection === CLOUDS.ONEDRIVE && 263 |
    264 |

    265 | Your file is ready to save to OneDrive. Click the button below to save. 266 |

    267 |
    } 284 |
    285 |
    286 | {fileReady && 287 | 288 |
    299 | ); 300 | } 301 | } 302 | 303 | SaveToCloudButtons.defaultProps = { 304 | dispatch: () => {}, 305 | resume: {}, 306 | darkMode: false, 307 | }; 308 | 309 | SaveToCloudButtons.propTypes = { 310 | dispatch: PropTypes.func, 311 | resume: PropTypes.shape({}), 312 | darkMode: PropTypes.bool, 313 | }; 314 | 315 | const mapStateToProps = (state) => ({ 316 | resume: state.resume, 317 | darkMode: state.tools.darkMode, 318 | }); 319 | 320 | export default connect(mapStateToProps)(SaveToCloudButtons); 321 | --------------------------------------------------------------------------------