90% completed, only final touches left

This commit is contained in:
Amruth Pillai
2020-03-25 16:08:42 +05:30
parent 36800a944e
commit d5dcd38edb
21 changed files with 462 additions and 24 deletions

16
package-lock.json generated
View File

@ -3400,6 +3400,11 @@
}
}
},
"classnames": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
},
"clean-css": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz",
@ -11617,6 +11622,17 @@
}
}
},
"react-toastify": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-5.5.0.tgz",
"integrity": "sha512-jsVme7jALIFGRyQsri/g4YTsRuaaGI70T6/ikjwZMB4mwTZaCWqj5NqxhGrRStKlJc5npXKKvKeqTiRGQl78LQ==",
"requires": {
"@babel/runtime": "^7.4.2",
"classnames": "^2.2.6",
"prop-types": "^15.7.2",
"react-transition-group": "^4"
}
},
"react-transition-group": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz",

View File

@ -11,6 +11,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"react-toastify": "^5.5.0",
"uuid": "^7.0.2"
},
"scripts": {

View File

@ -4,7 +4,8 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport"
content="width=device-width, initial-scale=.5, maximum-scale=12.0, minimum-scale=.25, user-scalable=yes" />
<meta name="theme-color" content="#000000" />
<meta name="description"
content="The resume generator you've been waiting for. Completely private, secure and customizable. Pick a layout, pick colors, enter your information and voila!" />
@ -13,7 +14,9 @@
<title>Reactive Resume</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
</head>

View File

@ -1,10 +1,11 @@
{
"layout": "Onyx",
"font": {
"family": "Montserrat"
},
"colors": {
"background": "#FFF",
"accent": "#FF5722",
"body": "#414141"
"background": "#FFFFFF",
"primary": "#414141",
"accent": "#FF5722"
}
}

View File

@ -1,23 +1,31 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Onyx from '../../templates/onyx/Onyx';
import Onyx from '../../templates/onyx';
import LeftSidebar from '../LeftSidebar/LeftSidebar';
import RightSidebar from '../RightSidebar/RightSidebar';
toast.configure({
autoClose: 3000,
closeButton: false,
hideProgressBar: true,
position: toast.POSITION.BOTTOM_RIGHT,
});
const App = () => {
return (
<div className="grid grid-cols-5 items-center">
<div className="h-screen overflow-hidden grid grid-cols-5 items-center">
<LeftSidebar />
<div className="col-span-3">
<div id="page" className="p-12 my-auto mx-auto shadow-2xl">
<div className="z-0 h-screen col-span-3 flex justify-center items-center overflow-scroll">
<div id="page" className="p-10 my-auto shadow-2xl overflow-scroll">
<Onyx />
</div>
</div>
<div id="rightSidebar" className="h-screen bg-white col-span-1 shadow-2xl overflow-scroll">
This is the right sidebar
</div>
<RightSidebar />
</div>
);
};

View File

@ -1,10 +1,10 @@
import React, { useState, useEffect, useContext } from 'react';
import AppContext from '../../context/AppContext';
import TabBar from '../../shared/TabBar';
import ProfileTab from './tabs/Profile';
import ObjectiveTab from './tabs/Objective';
import WorkTab from './tabs/Work';
import AppContext from '../../context/AppContext';
import EducationTab from './tabs/Education';
import AwardsTab from './tabs/Awards';
import CertificationsTab from './tabs/Certifications';
@ -28,7 +28,7 @@ const LeftSidebar = () => {
const { data } = state;
const [currentTab, setCurrentTab] = useState('Profile');
const onChange = (key, value) => {
const onChange = (key, value) =>
dispatch({
type: 'on_input',
payload: {
@ -36,8 +36,8 @@ const LeftSidebar = () => {
value,
},
});
};
// TODO: Remove this in production environment
useEffect(() => {
dispatch({ type: 'populate_starter' });
}, [dispatch]);
@ -66,9 +66,12 @@ const LeftSidebar = () => {
};
return (
<div id="leftSidebar" className="h-screen bg-white col-span-1 shadow-2xl overflow-y-scroll">
<div
id="leftSidebar"
className="z-10 py-6 h-screen bg-white col-span-1 shadow-2xl overflow-y-scroll"
>
<TabBar tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
<div className="px-6 pb-6">{renderTabs()}</div>
<div className="px-6">{renderTabs()}</div>
</div>
);
};

View File

@ -41,6 +41,7 @@ const AwardsTab = ({ data, onChange }) => {
last={index === data.awards.items.length - 1}
/>
))}
<AddItem dispatch={dispatch} />
</>
);

View File

@ -42,6 +42,7 @@ const EducationTab = ({ data, onChange }) => {
last={index === data.education.items.length - 1}
/>
))}
<AddItem dispatch={dispatch} />
</>
);

View File

@ -0,0 +1,53 @@
import React, { useState, useContext } from 'react';
import AppContext from '../../context/AppContext';
import TabBar from '../../shared/TabBar';
import LayoutTab from './tabs/Layout';
import ColorsTab from './tabs/Colors';
import FontsTab from './tabs/Fonts';
import ActionsTab from './tabs/Actions';
const tabs = ['Layout', 'Colors', 'Fonts', 'Actions'];
const RightSidebar = () => {
const context = useContext(AppContext);
const { state, dispatch } = context;
const { data, theme } = state;
const [currentTab, setCurrentTab] = useState('Actions');
const onChange = (key, value) =>
dispatch({
type: 'on_input',
payload: {
key,
value,
},
});
const renderTabs = () => {
switch (currentTab) {
case 'Layout':
return <LayoutTab theme={theme} />;
case 'Colors':
return <ColorsTab theme={theme} onChange={onChange} />;
case 'Fonts':
return <FontsTab theme={theme} onChange={onChange} />;
case 'Actions':
return <ActionsTab data={data} theme={theme} dispatch={dispatch} />;
default:
return null;
}
};
return (
<div
id="rightSidebar"
className="z-10 py-6 h-screen bg-white col-span-1 shadow-2xl overflow-y-scroll"
>
<TabBar tabs={tabs} currentTab={currentTab} setCurrentTab={setCurrentTab} />
<div className="px-6">{renderTabs()}</div>
</div>
);
};
export default RightSidebar;

View File

@ -0,0 +1,124 @@
/* eslint-disable jsx-a11y/anchor-has-content */
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { useRef } from 'react';
const ActionsTab = ({ data, theme, dispatch }) => {
const fileInputRef = useRef(null);
const importJson = event => {
const fr = new FileReader();
fr.addEventListener('load', () => {
const importedObject = JSON.parse(fr.result);
dispatch({ type: 'import_data', payload: importedObject });
});
fr.readAsText(event.target.files[0]);
};
const exportToJson = () => {
const backupObj = { data, theme };
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(backupObj))}`;
const dlAnchor = document.getElementById('downloadAnchor');
dlAnchor.setAttribute('href', dataStr);
dlAnchor.setAttribute('download', `RxResumeBackup_${Date.now()}.json`);
dlAnchor.click();
};
const resetEverything = () => {
dispatch({ type: 'reset' });
};
return (
<div>
<div className="shadow text-center text-sm p-5">
Changes you make to your resume are saved automatically to your browser&apos;s local
storage. No data gets out, hence your information is completely secure.
</div>
<hr className="my-6" />
<div className="shadow text-center p-5">
<h6 className="font-bold text-sm mb-2">Import/Export</h6>
<p className="text-sm">
You can import or export your data in JSON format. With this, you can edit and print your
resume from any device. Save this file for later use.
</p>
<input ref={fileInputRef} type="file" className="hidden" onChange={importJson} />
<a id="downloadAnchor" className="hidden" />
<div className="mt-4 grid grid-cols-2 col-gap-6">
<button
type="button"
onClick={() => fileInputRef.current.click()}
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-5 rounded"
>
<div className="flex justify-center items-center">
<i className="material-icons mr-2 font-bold text-base">publish</i>
<span className="text-sm">Import</span>
</div>
</button>
<button
type="button"
onClick={exportToJson}
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-5 rounded"
>
<div className="flex justify-center items-center">
<i className="material-icons mr-2 font-bold text-base">get_app</i>
<span className="text-sm">Export</span>
</div>
</button>
</div>
</div>
<hr className="my-6" />
<div className="shadow text-center p-5">
<h6 className="font-bold text-sm mb-2">Print Your Resume</h6>
<div className="text-sm">
You can simply press <pre className="inline font-bold">Cmd/Ctrl + P</pre> at any time
while you&apos;re in the app to print your resume, but here&apos;s a fancy button to do
the same thing, just &apos;cause.
</div>
<button
type="button"
onClick={() => window.print()}
className="mt-4 w-1/2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-2 px-5 rounded"
>
<div className="flex justify-center items-center">
<i className="material-icons mr-2 font-bold text-base">print</i>
<span className="text-sm">Print</span>
</div>
</button>
</div>
<hr className="my-6" />
<div className="shadow text-center p-5">
<h6 className="font-bold text-sm mb-2">Reset Everything!</h6>
<div className="text-sm">
This action will reset all your data and remove backups made to your browser&apos;s local
storage as well, so please make sure you have exported your information before you reset
everything.
</div>
<button
type="button"
onClick={resetEverything}
className="mt-4 w-1/2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2 px-5 rounded"
>
<div className="flex justify-center items-center">
<i className="material-icons mr-2 font-bold text-base">refresh</i>
<span className="text-sm">Reset</span>
</div>
</button>
</div>
</div>
);
};
export default ActionsTab;

View File

@ -0,0 +1,108 @@
import React from 'react';
import { toast } from 'react-toastify';
import TextField from '../../../shared/TextField';
import { copyToClipboard } from '../../../utils';
const colorOptions = [
'#f44336',
'#E91E63',
'#9C27B0',
'#673AB7',
'#3F51B5',
'#2196F3',
'#03A9F4',
'#00BCD4',
'#009688',
'#4CAF50',
'#8BC34A',
'#CDDC39',
'#FFEB3B',
'#FFC107',
'#FF9800',
'#FF5722',
'#795548',
'#9E9E9E',
'#607D8B',
'#FAFAFA',
'#212121',
'#263238',
];
const ColorsTab = ({ theme, onChange }) => {
const copyColorToClipboard = color => {
copyToClipboard(color);
toast(`Color ${color} copied to clipboard.`, {
bodyClassName: 'text-center text-gray-800 py-2',
});
onChange('theme.colors.accent', color);
};
return (
<div>
<div className="uppercase tracking-wide text-gray-600 text-xs font-semibold mb-4">
Color Options
</div>
<div className="mb-6 grid grid-cols-8 col-gap-2 row-gap-3">
{colorOptions.map(color => (
<div
key={color}
className="cursor-pointer rounded-full border border-gray-200 h-6 w-6 hover:opacity-75"
style={{ backgroundColor: color }}
onClick={() => copyColorToClipboard(color)}
/>
))}
</div>
<hr className="my-6" />
<div className="my-6 grid grid-cols-6 items-end">
<div
className="rounded-full w-8 h-8 mb-2 border-2"
style={{ backgroundColor: theme.colors.background }}
/>
<div className="col-span-5">
<TextField
disabled
label="Background Color"
placeholder="#FFFFFF"
value={theme.colors.background}
onChange={v => onChange('theme.colors.background', v)}
/>
</div>
</div>
<div className="my-6 grid grid-cols-6 items-end">
<div
className="rounded-full w-8 h-8 mb-2 border-2"
style={{ backgroundColor: theme.colors.primary }}
/>
<div className="col-span-5">
<TextField
label="Primary Color"
placeholder="#FFFFFF"
value={theme.colors.primary}
onChange={v => onChange('theme.colors.primary', v)}
/>
</div>
</div>
<div className="my-6 grid grid-cols-6 items-end">
<div
className="rounded-full w-8 h-8 mb-2 border-2"
style={{ backgroundColor: theme.colors.accent }}
/>
<div className="col-span-5">
<TextField
label="Accent Color"
placeholder="#FFFFFF"
value={theme.colors.accent}
onChange={v => onChange('theme.colors.accent', v)}
/>
</div>
</div>
</div>
);
};
export default ColorsTab;

View File

@ -0,0 +1,35 @@
import React from 'react';
const fontOptions = [
'Lato',
'Merriweather',
'Montserrat',
'Open Sans',
'Raleway',
'Roboto',
'Rubik',
'Source Sans Pro',
'Titillium Web',
'Ubuntu',
];
const FontsTab = ({ theme, onChange }) => {
return (
<div className="grid grid-cols-1 gap-6">
{fontOptions.map(x => (
<div
key={x}
style={{ fontFamily: x }}
onClick={() => onChange('theme.font.family', x)}
className={`w-full rounded border py-4 shadow text-xl text-center ${
theme.font.family === x ? 'border-gray-500' : 'border-transparent'
} hover:border-gray-400 cursor-pointer`}
>
{x}
</div>
))}
</div>
);
};
export default FontsTab;

View File

@ -0,0 +1,31 @@
import React from 'react';
import Onyx, { Image as OnyxPreview } from '../../../templates/onyx';
const layouts = [
{
name: 'Onyx',
component: Onyx,
preview: OnyxPreview,
},
];
const LayoutTab = ({ theme }) => {
return (
<div className="grid grid-cols-2 gap-6">
{layouts.map(x => (
<div key={x.name} className="text-center">
<img
className={`rounded cursor-pointer object-cover border shadow hover:shadow-md ${
theme.layout === x.name ? 'border-gray-500' : 'border-transparent '
} hover:border-gray-400 cursor-pointer`}
src={x.preview}
alt={x.name}
/>
<p className="mt-1 text-sm font-medium">{x.name}</p>
</div>
))}
</div>
);
};
export default LayoutTab;

View File

@ -62,13 +62,14 @@ const initialState = {
},
},
theme: {
layout: 'Onyx',
font: {
family: '',
},
colors: {
background: '',
primary: '',
accent: '',
body: '',
},
},
};
@ -95,6 +96,12 @@ const reducer = (state, { type, payload }) => {
return set({ ...state }, `data.${payload.key}.items`, items);
case 'on_input':
return set({ ...state }, payload.key, payload.value);
case 'import_data':
return {
...state,
data: payload.data,
theme: payload.theme,
};
case 'populate_starter':
return {
...state,

View File

@ -1,3 +1,14 @@
/* Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Merriweather:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Rubik:wght@400;500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Titillium+Web:wght@400;600;700&display=swap');
html,
body {
height: 100%;
@ -33,10 +44,14 @@ body {
height: 29.7cm;
zoom: 0.8;
background-color: white;
overflow: scroll;
}
}
@page {
size: A4;
margin: 0;
}
@media print {
html,
body,
@ -52,8 +67,9 @@ body {
}
#page {
width: 21cm;
height: 29.7cm;
margin: 0;
padding: 0;
box-shadow: none;
position: absolute;
left: 0;

View File

@ -17,7 +17,7 @@ const TabBar = ({ tabs, currentTab, setCurrentTab }) => {
};
return (
<div className="my-6 mx-4 flex items-center">
<div className="mx-4 mb-6 flex items-center">
<div
className="flex mr-1 cursor-pointer select-none text-gray-600 hover:text-gray-800"
onClick={() => scrollBy(-100)}

View File

@ -1,6 +1,6 @@
import React from 'react';
const TextField = ({ label, placeholder, value, onChange, className }) => (
const TextField = ({ label, placeholder, value, onChange, className, disabled }) => (
<div className={`w-full flex flex-col ${className}`}>
{label && (
<label className="uppercase tracking-wide text-gray-600 text-xs font-semibold mb-2">
@ -10,6 +10,7 @@ const TextField = ({ label, placeholder, value, onChange, className }) => (
<input
className="appearance-none block w-full bg-gray-200 text-gray-800 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
type="text"
disabled={disabled}
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}

View File

@ -11,7 +11,7 @@ const Onyx = () => {
style={{
fontFamily: theme.font.family,
backgroundColor: theme.colors.background,
color: theme.colors.body,
color: theme.colors.primary,
}}
>
<div className="grid grid-cols-4 items-center">
@ -181,7 +181,10 @@ const Onyx = () => {
<span
key={x}
className="text-xs rounded-full px-3 py-1 font-medium my-2 mr-2"
style={{ backgroundColor: theme.colors.body, color: theme.colors.background }}
style={{
backgroundColor: theme.colors.primary,
color: theme.colors.background,
}}
>
{x}
</span>

View File

@ -0,0 +1,5 @@
import Onyx from './Onyx';
import image from './preview.png';
export const Image = image;
export default Onyx;

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

View File

@ -8,4 +8,25 @@ const move = (array, element, delta) => {
array.splice(indexes[0], 2, array[indexes[1]], array[indexes[0]]);
};
export { move };
const copyToClipboard = text => {
const textArea = document.createElement('textarea');
textArea.style.position = 'fixed';
textArea.style.top = 0;
textArea.style.left = 0;
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = 0;
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
return successful;
};
export { move, copyToClipboard };