initial commit, completed 40% of first milestone
This commit is contained in:
parent
dfeb67cda9
commit
25a6740c24
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": false,
|
||||
"codeFrame": false
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:react/recommended",
|
||||
"prettier",
|
||||
"prettier/react"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"jsx-a11y/no-static-element-interactions": 0,
|
||||
"jsx-a11y/click-events-have-key-events": 0,
|
||||
"jsx-a11y/label-has-associated-control": 0,
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react/no-array-index-key": 0,
|
||||
"no-param-reassign": 0,
|
||||
"react/prop-types": 0,
|
||||
"no-plusplus": 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
|
@ -3,15 +3,21 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.9.7",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"lodash": "^4.17.15",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-scripts": "3.4.1"
|
||||
"react-scripts": "3.4.1",
|
||||
"uuid": "^7.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"css": "postcss src/assets/tailwind/tailwind.src.css -o src/assets/tailwind/tailwind.css",
|
||||
"prestart": "npm run css",
|
||||
"start": "react-scripts start",
|
||||
"prebuild": "npm run css",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
|
@ -30,5 +36,18 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^9.7.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "^18.1.0",
|
||||
"eslint-config-prettier": "^6.10.1",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^2.5.1",
|
||||
"postcss-cli": "^7.1.0",
|
||||
"tailwindcss": "^1.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
const tailwindcss = require('tailwindcss');
|
||||
|
||||
module.exports = {
|
||||
plugins: [tailwindcss('./tailwind.js'), require('autoprefixer')],
|
||||
};
|
|
@ -1,43 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
<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="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!" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
<title>Reactive Resume</title>
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
<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>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
38
src/App.css
38
src/App.css
|
@ -1,38 +0,0 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
26
src/App.js
26
src/App.js
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,138 @@
|
|||
{
|
||||
"profile": {
|
||||
"heading": "Profile",
|
||||
"photo": "https://i.imgur.com/PziAhpG.png",
|
||||
"firstName": "Amruth",
|
||||
"lastName": "Pillai",
|
||||
"subtitle": "UI/UX Designer & Full Stack Developer",
|
||||
"address": {
|
||||
"line1": "#5/A, Banashankari Nivas,",
|
||||
"line2": "Brindavan Layout, Padmanabhanagar,",
|
||||
"line3": "Bangalore, India - 560061"
|
||||
},
|
||||
"phone": "+91 98453 36113",
|
||||
"website": "amruthpillai.com",
|
||||
"email": "hello@amruthpillai.com"
|
||||
},
|
||||
"objective": {
|
||||
"heading": "Objective",
|
||||
"body": "Lorem ipsum dolor sit amet consectetur adipisicing elit. Quis deserunt fuga qui perspiciatis magni obcaecati libero ullam lorem, ipsum dolor sit amet consectetur adipisicing elit, voluptate dolores. Lorem ipsum dolor sit amet consectetur adipisicing elit. Assumenda maiores reiciendis, iusto cupiditate, laborum optio nesciunt corrupti, libero numquam a impedit totam magnam ipsum ducimus quas labore ea? Labore, eum."
|
||||
},
|
||||
"work": {
|
||||
"heading": "Work Experience",
|
||||
"items": [
|
||||
{
|
||||
"id": "e737444e-cad1-4862-a5ec-26b3103a2a9f",
|
||||
"title": "GoDhiyo Solutions Pvt. Ltd.",
|
||||
"role": "Full Stack Web Developer",
|
||||
"start": "July 2018",
|
||||
"end": "current",
|
||||
"description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consectetur tempore mollitia perspiciatis quisquam quis obcaecati ducimus architecto temporibus, optio natus fuga maxime beatae consequuntur quae a vero. Molestias, distinctio vero."
|
||||
},
|
||||
{
|
||||
"id": "24ada227-3f81-49f9-897e-210d0c407011",
|
||||
"title": "Grabhouse Pvt Ltd.",
|
||||
"role": "Full Stack Web Developer",
|
||||
"start": "July 2018",
|
||||
"end": "current",
|
||||
"description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consectetur tempore mollitia perspiciatis quisquam quis obcaecati ducimus architecto temporibus, optio natus fuga maxime beatae consequuntur quae a vero. Molestias, distinctio vero."
|
||||
}
|
||||
]
|
||||
},
|
||||
"education": {
|
||||
"heading": "Education",
|
||||
"items": [
|
||||
{
|
||||
"id": "a5995314-0026-44c6-8505-0eed5f59dcd1",
|
||||
"name": "Dayananda Sagar College of Engineering",
|
||||
"major": "Bachelor of Engineering in Computer Science & Engineering",
|
||||
"start": "2015",
|
||||
"end": "2018",
|
||||
"grade": "62%",
|
||||
"description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consectetur tempore mollitia perspiciatis quisquam quis obcaecati ducimus architecto temporibus, optio natus fuga maxime beatae consequuntur quae a vero. Molestias, distinctio vero."
|
||||
},
|
||||
{
|
||||
"id": "ce613329-246f-446d-bad1-3a46368e3f7f",
|
||||
"name": "Dayananda Sagar Institute of Technology",
|
||||
"major": "Diploma in Computer Science & Engineering",
|
||||
"start": "2012",
|
||||
"end": "2015",
|
||||
"grade": "80%",
|
||||
"description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consectetur tempore mollitia perspiciatis quisquam quis obcaecati ducimus architecto temporibus, optio natus fuga maxime beatae consequuntur quae a vero. Molestias, distinctio vero."
|
||||
}
|
||||
]
|
||||
},
|
||||
"awards": {
|
||||
"heading": "Honors & Awards",
|
||||
"items": [
|
||||
{
|
||||
"id": "011d4061-b675-412d-8943-19f2c4271099",
|
||||
"title": "International Flutter Hackathon '19",
|
||||
"subtitle": "1st Place, International Level"
|
||||
},
|
||||
{
|
||||
"id": "340a979f-72e8-406d-aa4f-cc99b98e1e68",
|
||||
"title": "Smart India Hackathon '17",
|
||||
"subtitle": "1st Place, National Level"
|
||||
}
|
||||
]
|
||||
},
|
||||
"certifications": {
|
||||
"heading": "Certifications",
|
||||
"items": [
|
||||
{
|
||||
"id": "868a8e11-3f0f-4ab2-877d-9334edfc25cf",
|
||||
"title": "The Complete React Native and Redux Course",
|
||||
"subtitle": "Udemy"
|
||||
},
|
||||
{
|
||||
"id": "056b8c6b-a376-4a9e-8008-4cc61d051c34",
|
||||
"title": "Applied CS with Android",
|
||||
"subtitle": "Google"
|
||||
}
|
||||
]
|
||||
},
|
||||
"skills": {
|
||||
"heading": "Skills & Hobbies",
|
||||
"items": [
|
||||
"Photography",
|
||||
"Music",
|
||||
"Travel",
|
||||
"Adobe Photoshop",
|
||||
"Figma",
|
||||
"React",
|
||||
"React Native",
|
||||
"NodeJS"
|
||||
]
|
||||
},
|
||||
"extras": {
|
||||
"heading": "Personal Information",
|
||||
"items": [
|
||||
{
|
||||
"id": "b94ad832-df86-4b6c-b1ee-8f955f72b891",
|
||||
"key": "Date of Birth",
|
||||
"value": "6th August 1995"
|
||||
},
|
||||
{
|
||||
"id": "76ee0ca4-2beb-49ba-9fb5-8dd7fe8d3b80",
|
||||
"key": "Father's Name",
|
||||
"value": "Ashokan Ramalingam"
|
||||
},
|
||||
{
|
||||
"id": "cc159f26-07f5-4f57-9cce-1cb7346e9ea5",
|
||||
"key": "Languages Known",
|
||||
"value": "English, Kannada, Tamil, Hindi, German"
|
||||
},
|
||||
{
|
||||
"id": "055ef7c2-f984-45e5-9f2b-8b2300de6be1",
|
||||
"key": "Nationality",
|
||||
"value": "Indian"
|
||||
},
|
||||
{
|
||||
"id": "b36abc3e-46d0-477b-808b-8a73037dc251",
|
||||
"key": "Passport Number",
|
||||
"value": "R0337285"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"font": {
|
||||
"family": "Montserrat"
|
||||
},
|
||||
"colors": {
|
||||
"background": "#FFF",
|
||||
"accent": "#FF5722",
|
||||
"body": "#414141"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
@tailwind base;
|
||||
|
||||
@tailwind components;
|
||||
|
||||
@tailwind utilities;
|
|
@ -0,0 +1,25 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
|
||||
import Onyx from '../../templates/onyx/Onyx';
|
||||
import LeftSidebar from '../LeftSidebar/LeftSidebar';
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-5 items-center">
|
||||
<LeftSidebar />
|
||||
|
||||
<div className="col-span-3">
|
||||
<div id="page" className="p-12 my-auto mx-auto shadow-2xl">
|
||||
<Onyx />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="rightSidebar" className="h-screen bg-white col-span-1 shadow-2xl overflow-scroll">
|
||||
This is the right sidebar
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,76 @@
|
|||
import React, { useState, useEffect, useContext } from 'react';
|
||||
|
||||
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';
|
||||
import SkillsTab from './tabs/Skills';
|
||||
import ExtrasTab from './tabs/Extras';
|
||||
|
||||
const tabs = [
|
||||
'Profile',
|
||||
'Objective',
|
||||
'Work Experience',
|
||||
'Education',
|
||||
'Awards',
|
||||
'Certifications',
|
||||
'Skills',
|
||||
'Extras',
|
||||
];
|
||||
|
||||
const LeftSidebar = () => {
|
||||
const context = useContext(AppContext);
|
||||
const { state, dispatch } = context;
|
||||
const { data } = state;
|
||||
|
||||
const [currentTab, setCurrentTab] = useState('Extras');
|
||||
const onChange = (key, value) => {
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: 'populate_starter' });
|
||||
}, [dispatch]);
|
||||
|
||||
const renderTabs = () => {
|
||||
switch (currentTab) {
|
||||
case 'Profile':
|
||||
return <ProfileTab data={data} onChange={onChange} />;
|
||||
case 'Objective':
|
||||
return <ObjectiveTab data={data} onChange={onChange} />;
|
||||
case 'Work Experience':
|
||||
return <WorkTab data={data} onChange={onChange} />;
|
||||
case 'Education':
|
||||
return <EducationTab data={data} onChange={onChange} />;
|
||||
case 'Awards':
|
||||
return <AwardsTab data={data} onChange={onChange} />;
|
||||
case 'Certifications':
|
||||
return <CertificationsTab data={data} onChange={onChange} />;
|
||||
case 'Skills':
|
||||
return <SkillsTab data={data} onChange={onChange} />;
|
||||
case 'Extras':
|
||||
return <ExtrasTab data={data} onChange={onChange} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="leftSidebar" className="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>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeftSidebar;
|
|
@ -0,0 +1,197 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import set from 'lodash/set';
|
||||
|
||||
import TextField from '../../../shared/TextField';
|
||||
import AppContext from '../../../context/AppContext';
|
||||
|
||||
const AwardsTab = ({ data, onChange }) => {
|
||||
const context = useContext(AppContext);
|
||||
const { dispatch } = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.awards.items.map((x, index) => (
|
||||
<Item
|
||||
item={x}
|
||||
key={x.id}
|
||||
index={index}
|
||||
onChange={onChange}
|
||||
dispatch={dispatch}
|
||||
first={index === 0}
|
||||
last={index === data.awards.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
<AddItem dispatch={dispatch} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddItem = ({ dispatch }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [item, setItem] = useState({
|
||||
id: uuidv4(),
|
||||
title: '',
|
||||
subtitle: '',
|
||||
});
|
||||
|
||||
const onChange = (key, value) => setItem(set({ ...item }, key, value));
|
||||
|
||||
const addItem = () => {
|
||||
dispatch({
|
||||
type: 'add_item',
|
||||
payload: {
|
||||
key: 'awards',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
setItem({
|
||||
id: uuidv4(),
|
||||
title: '',
|
||||
subtitle: '',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">Add Award</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Math & Science Olympiad"
|
||||
value={item.title}
|
||||
onChange={v => onChange('title', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Subtitle"
|
||||
placeholder="First Place, International Level"
|
||||
value={item.subtitle}
|
||||
onChange={v => onChange('subtitle', v)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
className="mt-4 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">add</i>
|
||||
<span className="text-sm">Add</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ item, index, onChange, dispatch, first, last }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const identifier = `data.awards.items[${index}]`;
|
||||
|
||||
const deleteItem = () =>
|
||||
dispatch({
|
||||
type: 'delete_item',
|
||||
payload: {
|
||||
key: 'awards',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemUp = () =>
|
||||
dispatch({
|
||||
type: 'move_item_up',
|
||||
payload: {
|
||||
key: 'awards',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemDown = () =>
|
||||
dispatch({
|
||||
type: 'move_item_down',
|
||||
payload: {
|
||||
key: 'awards',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="my-4 border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">{item.title}</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Math & Science Olympiad"
|
||||
value={item.title}
|
||||
onChange={v => onChange(`${identifier}.title`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Subtitle"
|
||||
placeholder="First Place, International Level"
|
||||
value={item.subtitle}
|
||||
onChange={v => onChange(`${identifier}.subtitle`, v)}
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteItem}
|
||||
className="bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">delete</i>
|
||||
<span className="text-sm">Delete</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex">
|
||||
{!first && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemUp}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_upward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!last && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemDown}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_downward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AwardsTab;
|
|
@ -0,0 +1,197 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import set from 'lodash/set';
|
||||
|
||||
import TextField from '../../../shared/TextField';
|
||||
import AppContext from '../../../context/AppContext';
|
||||
|
||||
const CertificationsTab = ({ data, onChange }) => {
|
||||
const context = useContext(AppContext);
|
||||
const { dispatch } = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.certifications.items.map((x, index) => (
|
||||
<Item
|
||||
item={x}
|
||||
key={x.id}
|
||||
index={index}
|
||||
onChange={onChange}
|
||||
dispatch={dispatch}
|
||||
first={index === 0}
|
||||
last={index === data.certifications.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
<AddItem dispatch={dispatch} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddItem = ({ dispatch }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [item, setItem] = useState({
|
||||
id: uuidv4(),
|
||||
title: '',
|
||||
subtitle: '',
|
||||
});
|
||||
|
||||
const onChange = (key, value) => setItem(set({ ...item }, key, value));
|
||||
|
||||
const addItem = () => {
|
||||
dispatch({
|
||||
type: 'add_item',
|
||||
payload: {
|
||||
key: 'certifications',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
setItem({
|
||||
id: uuidv4(),
|
||||
title: '',
|
||||
subtitle: '',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">Add Certification</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Android Development Nanodegree"
|
||||
value={item.title}
|
||||
onChange={v => onChange('title', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Subtitle"
|
||||
placeholder="Udacity"
|
||||
value={item.subtitle}
|
||||
onChange={v => onChange('subtitle', v)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
className="mt-4 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">add</i>
|
||||
<span className="text-sm">Add</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ item, index, onChange, dispatch, first, last }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const identifier = `data.certifications.items[${index}]`;
|
||||
|
||||
const deleteItem = () =>
|
||||
dispatch({
|
||||
type: 'delete_item',
|
||||
payload: {
|
||||
key: 'certifications',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemUp = () =>
|
||||
dispatch({
|
||||
type: 'move_item_up',
|
||||
payload: {
|
||||
key: 'certifications',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemDown = () =>
|
||||
dispatch({
|
||||
type: 'move_item_down',
|
||||
payload: {
|
||||
key: 'certifications',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="my-4 border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">{item.title}</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Title"
|
||||
placeholder="Android Development Nanodegree"
|
||||
value={item.title}
|
||||
onChange={v => onChange(`${identifier}.title`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Subtitle"
|
||||
placeholder="Udacity"
|
||||
value={item.subtitle}
|
||||
onChange={v => onChange(`${identifier}.subtitle`, v)}
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteItem}
|
||||
className="bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">delete</i>
|
||||
<span className="text-sm">Delete</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex">
|
||||
{!first && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemUp}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_upward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!last && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemDown}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_downward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificationsTab;
|
|
@ -0,0 +1,268 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import set from 'lodash/set';
|
||||
|
||||
import TextField from '../../../shared/TextField';
|
||||
import TextArea from '../../../shared/TextArea';
|
||||
import AppContext from '../../../context/AppContext';
|
||||
|
||||
const EducationTab = ({ data, onChange }) => {
|
||||
const context = useContext(AppContext);
|
||||
const { dispatch } = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.education.items.map((x, index) => (
|
||||
<Item
|
||||
item={x}
|
||||
key={x.id}
|
||||
index={index}
|
||||
onChange={onChange}
|
||||
dispatch={dispatch}
|
||||
first={index === 0}
|
||||
last={index === data.education.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
<AddItem dispatch={dispatch} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddItem = ({ dispatch }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [item, setItem] = useState({
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
major: '',
|
||||
start: '',
|
||||
end: '',
|
||||
grade: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const onChange = (key, value) => setItem(set({ ...item }, key, value));
|
||||
|
||||
const addItem = () => {
|
||||
dispatch({
|
||||
type: 'add_item',
|
||||
payload: {
|
||||
key: 'education',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
setItem({
|
||||
id: uuidv4(),
|
||||
title: '',
|
||||
role: '',
|
||||
start: '',
|
||||
end: '',
|
||||
grade: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">Add Education</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Name"
|
||||
placeholder="Harvard University"
|
||||
value={item.name}
|
||||
onChange={v => onChange('name', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Major"
|
||||
placeholder="Masters in Computer Science"
|
||||
value={item.major}
|
||||
onChange={v => onChange('major', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Grade"
|
||||
placeholder="7.2 CGPA"
|
||||
value={item.grade}
|
||||
onChange={v => onChange('grade', v)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 col-gap-4">
|
||||
<TextField
|
||||
label="Start Date"
|
||||
placeholder="March 2018"
|
||||
value={item.start}
|
||||
onChange={v => onChange('start', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="End Date"
|
||||
placeholder="May 2020"
|
||||
value={item.end}
|
||||
onChange={v => onChange('end', v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
rows="5"
|
||||
label="Description"
|
||||
placeholder="You can write about projects or special credit classes that you took while studying at this school."
|
||||
value={item.description}
|
||||
onChange={v => onChange('description', v)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
className="mt-4 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">add</i>
|
||||
<span className="text-sm">Add</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ item, index, onChange, dispatch, first, last }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const identifier = `data.education.items[${index}]`;
|
||||
|
||||
const deleteItem = () =>
|
||||
dispatch({
|
||||
type: 'delete_item',
|
||||
payload: {
|
||||
key: 'education',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemUp = () =>
|
||||
dispatch({
|
||||
type: 'move_item_up',
|
||||
payload: {
|
||||
key: 'education',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemDown = () =>
|
||||
dispatch({
|
||||
type: 'move_item_down',
|
||||
payload: {
|
||||
key: 'education',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="my-4 border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">{item.name}</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Name"
|
||||
placeholder="Harvard University"
|
||||
value={item.name}
|
||||
onChange={v => onChange(`${identifier}.name`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Major"
|
||||
placeholder="Masters in Computer Science"
|
||||
value={item.major}
|
||||
onChange={v => onChange(`${identifier}.major`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Grade"
|
||||
placeholder="7.2 CGPA"
|
||||
value={item.grade}
|
||||
onChange={v => onChange(`${identifier}.grade`, v)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 col-gap-4">
|
||||
<TextField
|
||||
label="Start Date"
|
||||
placeholder="March 2018"
|
||||
value={item.start}
|
||||
onChange={v => onChange(`${identifier}.start`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="End Date"
|
||||
placeholder="May 2020"
|
||||
value={item.end}
|
||||
onChange={v => onChange(`${identifier}.end`, v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
rows="5"
|
||||
label="Description"
|
||||
placeholder="You can write about projects or special credit classes that you took while studying at this school."
|
||||
value={item.description}
|
||||
onChange={v => onChange(`${identifier}.description`, v)}
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteItem}
|
||||
className="bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">delete</i>
|
||||
<span className="text-sm">Delete</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex">
|
||||
{!first && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemUp}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_upward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!last && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemDown}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_downward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EducationTab;
|
|
@ -0,0 +1,197 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import set from 'lodash/set';
|
||||
|
||||
import TextField from '../../../shared/TextField';
|
||||
import AppContext from '../../../context/AppContext';
|
||||
|
||||
const ExtrasTab = ({ data, onChange }) => {
|
||||
const context = useContext(AppContext);
|
||||
const { dispatch } = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.extras.items.map((x, index) => (
|
||||
<Item
|
||||
item={x}
|
||||
key={x.id}
|
||||
index={index}
|
||||
onChange={onChange}
|
||||
dispatch={dispatch}
|
||||
first={index === 0}
|
||||
last={index === data.extras.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
<AddItem dispatch={dispatch} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddItem = ({ dispatch }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [item, setItem] = useState({
|
||||
id: uuidv4(),
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
|
||||
const onChange = (key, value) => setItem(items => set({ ...items }, key, value));
|
||||
|
||||
const addItem = () => {
|
||||
dispatch({
|
||||
type: 'add_item',
|
||||
payload: {
|
||||
key: 'extras',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
setItem({
|
||||
id: uuidv4(),
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">Add Item</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Key"
|
||||
placeholder="Date of Birth"
|
||||
value={item.key}
|
||||
onChange={v => onChange('key', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Value"
|
||||
placeholder="6th August 1995"
|
||||
value={item.value}
|
||||
onChange={v => onChange('value', v)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
className="mt-4 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">add</i>
|
||||
<span className="text-sm">Add</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ item, index, onChange, dispatch, first, last }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const identifier = `data.extras.items[${index}]`;
|
||||
|
||||
const deleteItem = () =>
|
||||
dispatch({
|
||||
type: 'delete_item',
|
||||
payload: {
|
||||
key: 'extras',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemUp = () =>
|
||||
dispatch({
|
||||
type: 'move_item_up',
|
||||
payload: {
|
||||
key: 'extras',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemDown = () =>
|
||||
dispatch({
|
||||
type: 'move_item_down',
|
||||
payload: {
|
||||
key: 'extras',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="my-4 border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">{item.key}</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Key"
|
||||
placeholder="Date of Birth"
|
||||
value={item.key}
|
||||
onChange={v => onChange(`${identifier}.key`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Value"
|
||||
placeholder="6th August 1995"
|
||||
value={item.value}
|
||||
onChange={v => onChange(`${identifier}.value`, v)}
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteItem}
|
||||
className="bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">delete</i>
|
||||
<span className="text-sm">Delete</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex">
|
||||
{!first && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemUp}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_upward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!last && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemDown}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_downward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtrasTab;
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import TextArea from '../../../shared/TextArea';
|
||||
|
||||
const ObjectiveTab = ({ data, onChange }) => {
|
||||
return (
|
||||
<div>
|
||||
<TextArea
|
||||
rows="15"
|
||||
label="Objective"
|
||||
placeholder="Looking for a challenging role in a reputable organization to utilize my technical, database, and management skills for the growth of the organization as well as to enhance my knowledge about new and emerging trends in the IT sector."
|
||||
value={data.objective.body}
|
||||
onChange={v => onChange('data.objective.body', v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectiveTab;
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import TextField from '../../../shared/TextField';
|
||||
|
||||
const ProfileTab = ({ data, onChange }) => (
|
||||
<div>
|
||||
<TextField
|
||||
label="Photo URL"
|
||||
placeholder="https://i.imgur.com/..."
|
||||
value={data.profile.photo}
|
||||
onChange={v => onChange('data.profile.photo', v)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 col-gap-4">
|
||||
<TextField
|
||||
label="First Name"
|
||||
placeholder="Jane"
|
||||
value={data.profile.firstName}
|
||||
onChange={v => onChange('data.profile.firstName', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Last Name"
|
||||
placeholder="Doe"
|
||||
value={data.profile.lastName}
|
||||
onChange={v => onChange('data.profile.lastName', v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
label="Subtitle"
|
||||
placeholder="Full Stack Web Developer"
|
||||
value={data.profile.subtitle}
|
||||
onChange={v => onChange('data.profile.subtitle', v)}
|
||||
/>
|
||||
|
||||
<hr className="my-6" />
|
||||
|
||||
<TextField
|
||||
label="Address Line 1"
|
||||
placeholder="Palladium Complex"
|
||||
value={data.profile.address.line1}
|
||||
onChange={v => onChange('data.profile.address.line1', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Address Line 2"
|
||||
placeholder="140 E 14th St"
|
||||
value={data.profile.address.line2}
|
||||
onChange={v => onChange('data.profile.address.line2', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Address Line 3"
|
||||
placeholder="New York, NY 10003 USA"
|
||||
value={data.profile.address.line3}
|
||||
onChange={v => onChange('data.profile.address.line3', v)}
|
||||
/>
|
||||
|
||||
<hr className="my-6" />
|
||||
|
||||
<TextField
|
||||
label="Phone Number"
|
||||
placeholder="+1 541 754 3010"
|
||||
value={data.profile.phone}
|
||||
onChange={v => onChange('data.profile.phone', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Website"
|
||||
placeholder="google.com"
|
||||
value={data.profile.website}
|
||||
onChange={v => onChange('data.profile.website', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Email Address"
|
||||
placeholder="john.doe@example.com"
|
||||
value={data.profile.email}
|
||||
onChange={v => onChange('data.profile.email', v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
export default ProfileTab;
|
|
@ -0,0 +1,110 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
|
||||
import AppContext from '../../../context/AppContext';
|
||||
|
||||
const SkillsTab = ({ data, onChange }) => {
|
||||
const context = useContext(AppContext);
|
||||
const { dispatch } = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.skills.items.map((x, index) => (
|
||||
<Item item={x} key={x} index={index} onChange={onChange} dispatch={dispatch} />
|
||||
))}
|
||||
<AddItem dispatch={dispatch} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddItem = ({ dispatch }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [item, setItem] = useState('');
|
||||
|
||||
const addItem = () => {
|
||||
dispatch({
|
||||
type: 'add_item',
|
||||
payload: {
|
||||
key: 'skills',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
setItem('');
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">Add Skill</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<div className="mt-4 grid grid-cols-4 col-gap-4">
|
||||
<div className="col-span-3">
|
||||
<input
|
||||
className="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
placeholder="Cooking"
|
||||
value={item}
|
||||
onChange={e => setItem(e.target.value)}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
className="col-span-1 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium rounded"
|
||||
>
|
||||
<div className="flex justify-center items-center">
|
||||
<i className="material-icons font-bold text-lg">add</i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ item, index, onChange, dispatch }) => {
|
||||
const identifier = `data.skills.items[${index}]`;
|
||||
|
||||
const deleteItem = () =>
|
||||
dispatch({
|
||||
type: 'delete_item',
|
||||
payload: {
|
||||
key: 'skills',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-4 grid grid-cols-6">
|
||||
<div className="col-span-5">
|
||||
<input
|
||||
className="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
placeholder="Cooking"
|
||||
value={item}
|
||||
onChange={e => onChange(`${identifier}`, e.target.value)}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteItem}
|
||||
className="col-span-1 text-gray-600 hover:text-red-600 text-sm font-medium"
|
||||
>
|
||||
<div className="flex justify-end items-center">
|
||||
<i className="material-icons font-bold text-lg pr-4">close</i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkillsTab;
|
|
@ -0,0 +1,252 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import set from 'lodash/set';
|
||||
|
||||
import TextField from '../../../shared/TextField';
|
||||
import TextArea from '../../../shared/TextArea';
|
||||
import AppContext from '../../../context/AppContext';
|
||||
|
||||
const WorkTab = ({ data, onChange }) => {
|
||||
const context = useContext(AppContext);
|
||||
const { dispatch } = context;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.work.items.map((x, index) => (
|
||||
<Item
|
||||
item={x}
|
||||
key={x.id}
|
||||
index={index}
|
||||
onChange={onChange}
|
||||
dispatch={dispatch}
|
||||
first={index === 0}
|
||||
last={index === data.work.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
<AddItem dispatch={dispatch} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AddItem = ({ dispatch }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const [item, setItem] = useState({
|
||||
id: uuidv4(),
|
||||
title: '',
|
||||
role: '',
|
||||
start: '',
|
||||
end: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const onChange = (key, value) => setItem(set({ ...item }, key, value));
|
||||
|
||||
const addItem = () => {
|
||||
dispatch({
|
||||
type: 'add_item',
|
||||
payload: {
|
||||
key: 'work',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
setItem({
|
||||
id: uuidv4(),
|
||||
title: '',
|
||||
role: '',
|
||||
start: '',
|
||||
end: '',
|
||||
description: '',
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">Add Work Experience</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Name"
|
||||
placeholder="Amazon US"
|
||||
value={item.title}
|
||||
onChange={v => onChange('title', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Role"
|
||||
placeholder="Frontend Web Developer"
|
||||
value={item.role}
|
||||
onChange={v => onChange('role', v)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 col-gap-4">
|
||||
<TextField
|
||||
label="Start Date"
|
||||
placeholder="March 2018"
|
||||
value={item.start}
|
||||
onChange={v => onChange('start', v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="End Date"
|
||||
placeholder="current"
|
||||
value={item.end}
|
||||
onChange={v => onChange('end', v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
rows="5"
|
||||
label="Description"
|
||||
placeholder="You can write about what you specialized in while working at the company and what projects you were a part of."
|
||||
value={item.description}
|
||||
onChange={v => onChange('description', v)}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addItem}
|
||||
className="mt-4 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">add</i>
|
||||
<span className="text-sm">Add</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ item, index, onChange, dispatch, first, last }) => {
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
const identifier = `data.work.items[${index}]`;
|
||||
|
||||
const deleteItem = () =>
|
||||
dispatch({
|
||||
type: 'delete_item',
|
||||
payload: {
|
||||
key: 'work',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemUp = () =>
|
||||
dispatch({
|
||||
type: 'move_item_up',
|
||||
payload: {
|
||||
key: 'work',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
const moveItemDown = () =>
|
||||
dispatch({
|
||||
type: 'move_item_down',
|
||||
payload: {
|
||||
key: 'work',
|
||||
value: item,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="my-4 border border-gray-200 rounded p-5">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<h6 className="text-sm font-medium">{item.title}</h6>
|
||||
<i className="material-icons">{isOpen ? 'expand_less' : 'expand_more'}</i>
|
||||
</div>
|
||||
|
||||
<div className={`mt-6 ${isOpen ? 'block' : 'hidden'}`}>
|
||||
<TextField
|
||||
label="Name"
|
||||
placeholder="Amazon US"
|
||||
value={item.title}
|
||||
onChange={v => onChange(`${identifier}.title`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Role"
|
||||
placeholder="Frontend Web Developer"
|
||||
value={item.role}
|
||||
onChange={v => onChange(`${identifier}.role`, v)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 col-gap-4">
|
||||
<TextField
|
||||
label="Start Date"
|
||||
placeholder="March 2018"
|
||||
value={item.start}
|
||||
onChange={v => onChange(`${identifier}.start`, v)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="End Date"
|
||||
placeholder="current"
|
||||
value={item.end}
|
||||
onChange={v => onChange(`${identifier}.end`, v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
rows="5"
|
||||
label="Description"
|
||||
placeholder="You can write about what you specialized in while working at the company and what projects you were a part of."
|
||||
value={item.description}
|
||||
onChange={v => onChange(`${identifier}.description`, v)}
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={deleteItem}
|
||||
className="bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2 px-5 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons mr-2 font-bold text-base">delete</i>
|
||||
<span className="text-sm">Delete</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex">
|
||||
{!first && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemUp}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded mr-2"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_upward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!last && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={moveItemDown}
|
||||
className="bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium py-2 px-4 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<i className="material-icons font-bold text-base">arrow_downward</i>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkTab;
|
|
@ -0,0 +1,115 @@
|
|||
/* eslint-disable no-case-declarations */
|
||||
import React, { createContext, useReducer } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import remove from 'lodash/remove';
|
||||
|
||||
import initialData from '../assets/starter/data.json';
|
||||
import initialTheme from '../assets/starter/theme.json';
|
||||
import { move } from '../utils';
|
||||
|
||||
const initialState = {
|
||||
data: {
|
||||
profile: {
|
||||
heading: 'Profile',
|
||||
photo: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
subtitle: '',
|
||||
address: {
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
},
|
||||
phone: '',
|
||||
website: '',
|
||||
email: '',
|
||||
},
|
||||
objective: {
|
||||
heading: 'Objective',
|
||||
body: '',
|
||||
},
|
||||
work: {
|
||||
heading: 'Work Experience',
|
||||
items: [],
|
||||
},
|
||||
education: {
|
||||
heading: 'Education',
|
||||
items: [],
|
||||
},
|
||||
awards: {
|
||||
heading: 'Honors & Awards',
|
||||
items: [],
|
||||
},
|
||||
certifications: {
|
||||
heading: 'Certifications',
|
||||
items: [],
|
||||
},
|
||||
skills: {
|
||||
heading: 'Skills & Hobbies',
|
||||
items: [],
|
||||
},
|
||||
extras: {
|
||||
heading: 'Personal Information',
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
theme: {
|
||||
font: {
|
||||
family: '',
|
||||
},
|
||||
colors: {
|
||||
background: '',
|
||||
accent: '',
|
||||
body: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state, { type, payload }) => {
|
||||
let items;
|
||||
|
||||
switch (type) {
|
||||
case 'add_item':
|
||||
items = get({ ...state }, `data.${payload.key}.items`, []);
|
||||
items.push(payload.value);
|
||||
return set({ ...state }, `data.${payload.key}.items`, items);
|
||||
case 'delete_item':
|
||||
items = get({ ...state }, `data.${payload.key}.items`, []);
|
||||
remove(items, x => x === payload.value);
|
||||
return set({ ...state }, `data.${payload.key}.items`, items);
|
||||
case 'move_item_up':
|
||||
items = get({ ...state }, `data.${payload.key}.items`, []);
|
||||
move(items, payload.value, -1);
|
||||
return set({ ...state }, `data.${payload.key}.items`, items);
|
||||
case 'move_item_down':
|
||||
items = get({ ...state }, `data.${payload.key}.items`, []);
|
||||
move(items, payload.value, 1);
|
||||
return set({ ...state }, `data.${payload.key}.items`, items);
|
||||
case 'on_input':
|
||||
return set({ ...state }, payload.key, payload.value);
|
||||
case 'populate_starter':
|
||||
return {
|
||||
...state,
|
||||
data: initialData,
|
||||
theme: initialTheme,
|
||||
};
|
||||
case 'reset':
|
||||
return initialState;
|
||||
default:
|
||||
throw state;
|
||||
}
|
||||
};
|
||||
|
||||
const AppContext = createContext(initialState);
|
||||
const { Provider } = AppContext;
|
||||
|
||||
const StateProvider = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
return <Provider value={{ state, dispatch }}>{children}</Provider>;
|
||||
};
|
||||
|
||||
export const AppProvider = StateProvider;
|
||||
export const AppConsumer = AppContext.Consumer;
|
||||
|
||||
export default AppContext;
|
|
@ -1,13 +1,61 @@
|
|||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100%;
|
||||
color: #2d3748;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 14px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
@media screen {
|
||||
#sidebar {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#tabs::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
background: transparent; /* make scrollbar transparent */
|
||||
}
|
||||
|
||||
#tabs li:first-child {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
#tabs li:last-child {
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
#page {
|
||||
width: 21cm;
|
||||
height: 29.7cm;
|
||||
zoom: 0.8;
|
||||
background-color: white;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
html,
|
||||
body,
|
||||
body * {
|
||||
-webkit-print-color-adjust: exact;
|
||||
background-color: white;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#page,
|
||||
#page * {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#page {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
|
16
src/index.js
16
src/index.js
|
@ -1,17 +1,19 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
import './assets/tailwind/tailwind.css';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import './index.css';
|
||||
import { AppProvider } from './context/AppContext';
|
||||
import App from './components/App/App';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<AppProvider>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -15,9 +15,7 @@ const isLocalhost = Boolean(
|
|||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
|
@ -43,7 +41,7 @@ export function register(config) {
|
|||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA',
|
||||
);
|
||||
});
|
||||
} else {
|
||||
|
@ -71,7 +69,7 @@ function registerValidSW(swUrl, config) {
|
|||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
|
@ -122,9 +120,7 @@ function checkValidServiceWorker(swUrl, config) {
|
|||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
console.log('No internet connection found. App is running in offline mode.');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect';
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
const TabBar = ({ tabs, currentTab, setCurrentTab }) => {
|
||||
return (
|
||||
<ul id="tabs" className="my-4 flex items-center overflow-x-scroll">
|
||||
{tabs.map(tab =>
|
||||
currentTab === tab ? (
|
||||
<li key={tab} className="mx-1">
|
||||
<div className="whitespace-no-wrap bg-gray-700 text-white rounded-md text-sm py-2 px-6 font-medium">
|
||||
{tab}
|
||||
</div>
|
||||
</li>
|
||||
) : (
|
||||
<li key={tab} className="mx-1">
|
||||
<div
|
||||
className="bg-white whitespace-no-wrap rounded-md cursor-pointer text-sm py-2 px-6 font-medium hover:bg-gray-200"
|
||||
onClick={() => setCurrentTab(tab)}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabBar;
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
const TextArea = ({ label, placeholder, value, onChange, rows = 5 }) => (
|
||||
<div className="my-4 w-full flex flex-col">
|
||||
<label className="uppercase tracking-wide text-gray-600 text-xs font-semibold mb-2">
|
||||
{label}
|
||||
</label>
|
||||
<textarea
|
||||
className="appearance-none block leading-7 w-full bg-gray-200 text-gray-800 border border-gray-200 rounded py-3 px-4 focus:outline-none focus:bg-white focus:border-gray-500"
|
||||
rows={rows}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TextArea;
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
const TextField = ({ label, placeholder, value, onChange }) => (
|
||||
<div className="my-4 w-full flex flex-col">
|
||||
<label className="uppercase tracking-wide text-gray-600 text-xs font-semibold mb-2">
|
||||
{label}
|
||||
</label>
|
||||
<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"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TextField;
|
|
@ -0,0 +1,172 @@
|
|||
import React, { useContext } from 'react';
|
||||
import AppContext from '../../context/AppContext';
|
||||
|
||||
const Onyx = () => {
|
||||
const context = useContext(AppContext);
|
||||
const { state } = context;
|
||||
const { data, theme } = state;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: theme.font.family,
|
||||
backgroundColor: theme.colors.background,
|
||||
color: theme.colors.body,
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-4 items-center">
|
||||
<div className="col-span-3 flex items-center">
|
||||
{data.profile.photo && (
|
||||
<img
|
||||
className="rounded object-cover mr-4"
|
||||
src={data.profile.photo}
|
||||
alt="Resume Photograph"
|
||||
style={{ width: '120px', height: '120px' }}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="font-bold text-4xl" style={{ color: theme.colors.accent }}>
|
||||
{data.profile.firstName} {data.profile.lastName}
|
||||
</h1>
|
||||
<h6 className="font-medium text-sm">{data.profile.subtitle}</h6>
|
||||
|
||||
<div className="flex flex-col mt-4 text-xs">
|
||||
<span>{data.profile.address.line1}</span>
|
||||
<span>{data.profile.address.line2}</span>
|
||||
<span>{data.profile.address.line3}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 text-xs">
|
||||
<div className="flex items-center my-3">
|
||||
<span className="material-icons text-lg mr-2" style={{ color: theme.colors.accent }}>
|
||||
phone
|
||||
</span>
|
||||
<span className="font-medium">{data.profile.phone}</span>
|
||||
</div>
|
||||
<div className="flex items-center my-3">
|
||||
<span className="material-icons text-lg mr-2" style={{ color: theme.colors.accent }}>
|
||||
language
|
||||
</span>
|
||||
<span className="font-medium">{data.profile.website}</span>
|
||||
</div>
|
||||
<div className="flex items-center my-3">
|
||||
<span className="material-icons text-lg mr-2" style={{ color: theme.colors.accent }}>
|
||||
alternate_email
|
||||
</span>
|
||||
<span className="font-medium">{data.profile.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="my-6" />
|
||||
|
||||
<h6 className="text-xs font-bold uppercase mt-6 mb-2" style={{ color: theme.colors.accent }}>
|
||||
{data.objective.heading}
|
||||
</h6>
|
||||
<p className="text-sm">{data.objective.body}</p>
|
||||
|
||||
<h6 className="text-xs font-bold uppercase mt-6 mb-2" style={{ color: theme.colors.accent }}>
|
||||
{data.work.heading}
|
||||
</h6>
|
||||
{data.work.items.map(exp => (
|
||||
<div key={exp.title} className="mt-3">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h6 className="font-semibold">{exp.title}</h6>
|
||||
<p className="text-xs">{exp.role}</p>
|
||||
</div>
|
||||
<span className="text-xs font-medium">
|
||||
({exp.start} - {exp.end})
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm">{exp.description}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<h6 className="text-xs font-bold uppercase mt-6 mb-2" style={{ color: theme.colors.accent }}>
|
||||
{data.education.heading}
|
||||
</h6>
|
||||
{data.education.items.map(edu => (
|
||||
<div key={edu.name} className="mt-3">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h6 className="font-semibold">{edu.name}</h6>
|
||||
<p className="text-xs">{edu.major}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-sm font-bold">{edu.grade}</span>
|
||||
<span className="text-xs font-medium">
|
||||
({edu.start} - {edu.end})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-sm">{edu.description}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="grid grid-cols-2">
|
||||
<div>
|
||||
<h6
|
||||
className="text-xs font-bold uppercase mt-6 mb-2"
|
||||
style={{ color: theme.colors.accent }}
|
||||
>
|
||||
{data.awards.heading}
|
||||
</h6>
|
||||
{data.awards.items.map(x => (
|
||||
<div key={x.title} className="mt-3">
|
||||
<h6 className="font-semibold">{x.title}</h6>
|
||||
<p className="text-xs">{x.subtitle}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6
|
||||
className="text-xs font-bold uppercase mt-6 mb-2"
|
||||
style={{ color: theme.colors.accent }}
|
||||
>
|
||||
{data.certifications.heading}
|
||||
</h6>
|
||||
{data.certifications.items.map(x => (
|
||||
<div key={x.title} className="mt-3">
|
||||
<h6 className="font-semibold">{x.title}</h6>
|
||||
<p className="text-xs">{x.subtitle}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 className="text-xs font-bold uppercase mt-6 mb-2" style={{ color: theme.colors.accent }}>
|
||||
{data.skills.heading}
|
||||
</h6>
|
||||
<div className="mt-1 flex flex-wrap">
|
||||
{data.skills.items.map(x => (
|
||||
<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 }}
|
||||
>
|
||||
{x}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h6 className="text-xs font-bold uppercase mt-6 mb-2" style={{ color: theme.colors.accent }}>
|
||||
{data.extras.heading}
|
||||
</h6>
|
||||
<table className="w-2/3 table-auto">
|
||||
<tbody>
|
||||
{data.extras.items.map(x => (
|
||||
<tr key={x.key}>
|
||||
<td className="border font-medium px-4 py-2 text-sm">{x.key}</td>
|
||||
<td className="border px-4 py-2 text-sm">{x.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Onyx;
|
|
@ -0,0 +1,11 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
const move = (array, element, delta) => {
|
||||
const index = array.indexOf(element);
|
||||
const newIndex = index + delta;
|
||||
if (newIndex < 0 || newIndex === array.length) return;
|
||||
const indexes = [index, newIndex].sort((a, b) => a - b);
|
||||
array.splice(indexes[0], 2, array[indexes[1]], array[indexes[0]]);
|
||||
};
|
||||
|
||||
export { move };
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [],
|
||||
};
|
Loading…
Reference in New Issue