- implement gengar template

This commit is contained in:
Amruth Pillai
2020-07-12 19:31:22 +05:30
parent 5ccc360345
commit 41e708e302
24 changed files with 677 additions and 550 deletions

View File

@ -2,7 +2,9 @@ import React, { memo } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { useSelector } from '../../../contexts/ResumeContext'; import { useSelector } from '../../../contexts/ResumeContext';
import Onyx from '../../../templates/Onyx'; import Onyx from '../../../templates/Onyx';
import Pikachu from '../../../templates/Pikachu';
import styles from './Artboard.module.css'; import styles from './Artboard.module.css';
import Gengar from '../../../templates/Gengar';
const Artboard = () => { const Artboard = () => {
const state = useSelector(); const state = useSelector();
@ -18,6 +20,8 @@ const Artboard = () => {
<div id="artboard" className={styles.container}> <div id="artboard" className={styles.container}>
{template === 'onyx' && <Onyx data={state} />} {template === 'onyx' && <Onyx data={state} />}
{template === 'pikachu' && <Pikachu data={state} />}
{template === 'gengar' && <Gengar data={state} />}
</div> </div>
</div> </div>
); );

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { memo, useState } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd'; import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext'; import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
import { move, reorder } from '../../../../utils'; import { move, reorder } from '../../../../utils';
@ -9,7 +9,8 @@ import styles from './Layout.module.css';
const Layout = () => { const Layout = () => {
const [resetLayoutText, setResetLayoutText] = useState('Reset Layout'); const [resetLayoutText, setResetLayoutText] = useState('Reset Layout');
const blocks = useSelector('metadata.layout'); const template = useSelector('metadata.template');
const blocks = useSelector(`metadata.layout.${template}`, [[]]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const onDragEnd = (result) => { const onDragEnd = (result) => {
@ -28,7 +29,7 @@ const Layout = () => {
dispatch({ dispatch({
type: 'on_input', type: 'on_input',
payload: { payload: {
path: 'metadata.layout', path: `metadata.layout.${template}`,
value: newState, value: newState,
}, },
}); });
@ -40,7 +41,7 @@ const Layout = () => {
dispatch({ dispatch({
type: 'on_input', type: 'on_input',
payload: { payload: {
path: 'metadata.layout', path: `metadata.layout.${template}`,
value: newState, value: newState,
}, },
}); });
@ -110,4 +111,4 @@ const Layout = () => {
); );
}; };
export default Layout; export default memo(Layout);

View File

@ -2,8 +2,8 @@ import firebase from 'gatsby-plugin-firebase';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import React, { createContext, memo, useContext, useState } from 'react'; import React, { createContext, memo, useContext, useState } from 'react';
import initialState from '../data/initialState';
import UserContext from './UserContext'; import UserContext from './UserContext';
import initialState from '../data/initialState.json';
const DEBOUNCE_WAIT_TIME = 4000; const DEBOUNCE_WAIT_TIME = 4000;
@ -68,6 +68,8 @@ const DatabaseProvider = ({ children }) => {
}; };
const duplicateResume = (originalResume) => { const duplicateResume = (originalResume) => {
console.trace();
const id = uuid(); const id = uuid();
const createdAt = firebase.database.ServerValue.TIMESTAMP; const createdAt = firebase.database.ServerValue.TIMESTAMP;

View File

@ -1,15 +1,5 @@
import arrayMove from 'array-move'; import arrayMove from 'array-move';
import { import { clone, findIndex, get, isUndefined, merge, setWith } from 'lodash';
clone,
concat,
findIndex,
flatten,
get,
isUndefined,
merge,
setWith,
times,
} from 'lodash';
import React, { import React, {
createContext, createContext,
memo, memo,
@ -18,9 +8,8 @@ import React, {
useReducer, useReducer,
} from 'react'; } from 'react';
import demoState from '../data/demoState.json'; import demoState from '../data/demoState.json';
import initialState from '../data/initialState'; import initialState from '../data/initialState.json';
import DatabaseContext from './DatabaseContext'; import DatabaseContext from './DatabaseContext';
import leftSections from '../data/leftSections';
const ResumeContext = createContext({}); const ResumeContext = createContext({});
@ -32,7 +21,6 @@ const ResumeProvider = ({ children }) => {
let newState; let newState;
let index; let index;
let items; let items;
let diff;
let temp; let temp;
switch (type) { switch (type) {
@ -85,46 +73,15 @@ const ResumeProvider = ({ children }) => {
debouncedUpdateResume(newState); debouncedUpdateResume(newState);
return newState; return newState;
case 'set_block_count':
items = get(state, 'metadata.layout');
if (items.length === payload) return state;
if (payload === 1) {
items = flatten(items);
}
if (items.length > payload) {
diff = items.length - payload;
temp = items.splice(Math.max(items.length - diff, 1));
items[0] = concat(items[0], flatten(temp));
}
if (items.length < payload) {
diff = payload - items.length;
times(diff, () => items.push([]));
}
newState = setWith(clone(state), 'metadata.layout', items, clone);
debouncedUpdateResume(newState);
return newState;
case 'set_fixed_sections':
items = get(state, 'metadata.layout');
items = items.map((x) => {
return x.filter((y) => {
return !payload.includes(y);
});
});
newState = setWith(clone(state), 'metadata.layout', items, clone);
debouncedUpdateResume(newState);
return newState;
case 'reset_layout': case 'reset_layout':
items = [leftSections.filter((x) => !x.fixed).map((x) => x.id)]; temp = get(state, 'metadata.template');
newState = setWith(clone(state), 'metadata.layout', items, clone); items = get(initialState, `metadata.layout.${temp}`);
newState = setWith(
clone(state),
`metadata.layout.${temp}`,
items,
clone,
);
debouncedUpdateResume(newState); debouncedUpdateResume(newState);
return newState; return newState;
@ -144,7 +101,13 @@ const ResumeProvider = ({ children }) => {
return newState; return newState;
case 'reset_data': case 'reset_data':
newState = merge(clone(state), initialState); temp = clone(state);
newState = initialState;
newState.id = temp.id;
newState.user = temp.user;
newState.name = temp.name;
newState.createdAt = temp.createdAt;
newState.updatedAt = temp.updatedAt;
debouncedUpdateResume(newState); debouncedUpdateResume(newState);
return newState; return newState;

View File

@ -7,14 +7,21 @@
"date": "2019-04-01", "date": "2019-04-01",
"id": "6f857f2b-6312-4a0d-907d-2e17991954eb", "id": "6f857f2b-6312-4a0d-907d-2e17991954eb",
"summary": "", "summary": "",
"title": "International Flutter Hackathon '19" "title": "International Flutter Hackathon"
}, },
{ {
"awarder": "Venturesity", "awarder": "Venturesity",
"date": "2016-06-01", "date": "2016-06-01",
"id": "f6efa3f9-9741-4e36-a538-ba0d9779bc61", "id": "f6efa3f9-9741-4e36-a538-ba0d9779bc61",
"summary": "", "summary": "",
"title": "Venturesity Banyan Hack '16" "title": "Venturesity Banyan Hack"
},
{
"title": "Smart India Hackathon",
"awarder": "Govt. of India",
"date": "2017-04-01",
"summary": "",
"id": "89c0171a-eae9-403e-9f4c-a757fb535c2b"
} }
], ],
"visible": true "visible": true
@ -25,16 +32,23 @@
{ {
"date": "2018-02-01", "date": "2018-02-01",
"id": "d2ec12bc-7876-46bc-afd4-11ae06faf3bd", "id": "d2ec12bc-7876-46bc-afd4-11ae06faf3bd",
"issuer": "Google", "issuer": "Cisco Systems",
"summary": "", "summary": "",
"title": "Applied CS with Android" "title": "CCNP"
}, },
{ {
"date": "2019-06-01", "date": "2019-06-01",
"id": "f8312288-53ae-4504-a768-4b67aea95926", "id": "f8312288-53ae-4504-a768-4b67aea95926",
"issuer": "Udemy", "issuer": "VMWare",
"summary": "", "summary": "",
"title": "Data Science & Machine Learning using Python" "title": "VCP6-DCV"
},
{
"title": "DCUCI 642-999",
"issuer": "Cisco Systems",
"date": "2014-04-01",
"summary": "",
"id": "11107df6-5f3c-49ae-bcd4-62b8baa181a1"
} }
], ],
"visible": true "visible": true
@ -43,23 +57,23 @@
"heading": "Education", "heading": "Education",
"items": [ "items": [
{ {
"degree": "Bachelor's Degree", "degree": "Masters",
"endDate": "2018-04-01", "endDate": "2002-08-01",
"field": "Computer Science & Engineering", "field": "Computer Science",
"gpa": "9.2", "gpa": "7.2 CGPA",
"id": "c42e2a5a-3f0d-497e-838b-ac2019dcf045", "id": "c42e2a5a-3f0d-497e-838b-ac2019dcf045",
"institution": "Dayananda Sagar College of Engineering", "institution": "The City College of New York, NYC, NY",
"startDate": "2015-04-01", "startDate": "2001-09-01",
"summary": "" "summary": ""
}, },
{ {
"degree": "Diploma", "degree": "Bachelors",
"endDate": "2015-04-01", "endDate": "2001-08-01",
"field": "Computer Science", "field": "Computer Science",
"gpa": "9.8", "gpa": "8.4 CGPA",
"id": "278490a2-c327-4e83-8be8-adf913a9b36c", "id": "278490a2-c327-4e83-8be8-adf913a9b36c",
"institution": "Dayananda Sagar Institute of Technology", "institution": "University of California, Berkeley, CA",
"startDate": "2012-04-01", "startDate": "1997-09-01",
"summary": "" "summary": ""
} }
], ],
@ -68,6 +82,8 @@
"hobbies": { "hobbies": {
"heading": "Hobbies", "heading": "Hobbies",
"items": [ "items": [
{ "name": "Poetry", "id": "788dcf5a-78ca-4866-8397-c7a29073d9a1" },
{ "name": "Travelling", "id": "e3523371-f50c-4348-8c5e-35fe84c0006d" },
{ "id": "92c35e3b-6cd7-4cea-b505-61347ec61b68", "name": "Photography" }, { "id": "92c35e3b-6cd7-4cea-b505-61347ec61b68", "name": "Photography" },
{ {
"id": "d36f2089-93a9-4f30-a425-3dd81c6b89df", "id": "d36f2089-93a9-4f30-a425-3dd81c6b89df",
@ -75,7 +91,7 @@
}, },
{ {
"id": "d1da41a9-ae83-48fb-8047-d45ebd869a69", "id": "d1da41a9-ae83-48fb-8047-d45ebd869a69",
"name": "Working on Personal Projects" "name": "Developing Reactive Resume"
} }
], ],
"visible": true "visible": true
@ -113,15 +129,26 @@
"text": "#212121" "text": "#212121"
}, },
"font": "Open Sans", "font": "Open Sans",
"layout": [ "layout": {
"onyx": [
["objective", "work", "education", "projects"], ["objective", "work", "education", "projects"],
["hobbies", "languages"], ["hobbies", "languages", "awards", "certifications"],
["skills", "certifications", "awards", "references"] ["skills", "references"]
], ],
"template": "onyx" "pikachu": [
["skills", "languages", "hobbies", "awards", "certifications"],
["work", "education", "projects", "references"]
],
"gengar": [
["objective", "skills"],
["awards", "certifications", "languages", "references", "hobbies"],
["work", "education", "projects"]
]
},
"template": "pikachu"
}, },
"objective": { "objective": {
"body": "I'm Amruth Pillai, and as you might have already read, I'm a designer, developer, photographer and a writer. This website was made to showcase all of what I can do and plan to do. Don't judge my writing based on this section though, this is by far my shoddiest work yet.\n&nbsp;\nI got into design because I consider myself a pseudo-perfectionist, if that's even a word? As in, I hate to see things 'not look good'. So I set out on a journey to make products that people use that 'look great', and I'm forever on that path.", "body": "To obtain a job within my chosen field that will challenge me and allow me to use my education, skills and past experiences in a way that is mutually beneficial to both myself and my employer and allow for future growth and advancement.",
"heading": "Objective", "heading": "Objective",
"visible": true "visible": true
}, },
@ -137,7 +164,7 @@
"heading": "Profile", "heading": "Profile",
"lastName": "Pillai", "lastName": "Pillai",
"phone": "+91 98453 36113", "phone": "+91 98453 36113",
"photograph": "https://firebasestorage.googleapis.com/v0/b/rx-resume.appspot.com/o/users%2FNriQrOfocnfTtoRIpR3qEtHNxYq1%2Fphotographs%2Fx7vvg8?alt=media&token=99df9c05-f5e1-4360-b1e8-5c13ddd8cd84", "photograph": "https://i.imgur.com/2dmLSCT.jpg",
"profile": "", "profile": "",
"subtitle": "Full Stack Web Developer", "subtitle": "Full Stack Web Developer",
"website": "amruthpillai.com" "website": "amruthpillai.com"
@ -181,6 +208,14 @@
"phone": "+91 93893 34353", "phone": "+91 93893 34353",
"position": "CEO at Newton Motors", "position": "CEO at Newton Motors",
"summary": "" "summary": ""
},
{
"name": "Lorraine Beasley",
"position": "Head of HR, Carson Logistics",
"phone": "+1 661-808-4188",
"email": "l.beasley@carsonlogistics.com",
"summary": "",
"id": "94e3447b-0a78-4fb7-b14d-591982d35320"
} }
], ],
"visible": true "visible": true
@ -191,22 +226,32 @@
{ {
"id": "54e5bceb-d0e9-4f04-98d1-48a34f7cf920", "id": "54e5bceb-d0e9-4f04-98d1-48a34f7cf920",
"level": "Advanced", "level": "Advanced",
"name": "ReactJS" "name": "Customer Service Expertise"
}, },
{ {
"id": "f0274f62-2252-4cc0-bf12-9e1070942c50", "id": "f0274f62-2252-4cc0-bf12-9e1070942c50",
"level": "Advanced", "level": "Intermediate",
"name": "Angular" "name": "High-Volume Call Center"
}, },
{ {
"id": "689e2852-df1b-4d41-bda8-c41c88196264", "id": "689e2852-df1b-4d41-bda8-c41c88196264",
"level": "Advanced", "level": "Intermediate",
"name": "Flutter" "name": "Team Leader/Problem Solver"
}, },
{ {
"id": "3a4f73b1-50c1-4a85-a4b0-2a55dfe5053a", "id": "3a4f73b1-50c1-4a85-a4b0-2a55dfe5053a",
"level": "Novice", "level": "Novice",
"name": "Machine Learning" "name": "Call Center Management"
},
{
"name": "Teambuilding & Training",
"level": "Novice",
"id": "08d6c739-1465-41f7-8825-b8d94faa38d6"
},
{
"name": "Continuous Improvement",
"level": "Fundamental Awareness",
"id": "261b8fc3-aeec-4347-88a8-bcacb1a17aa3"
} }
], ],
"visible": true "visible": true
@ -214,6 +259,12 @@
"social": { "social": {
"heading": "Social", "heading": "Social",
"items": [ "items": [
{
"url": "https://pillai.xyz/instagram",
"network": "Instagram",
"username": "AmruthPillai",
"id": "a832b37d-f11d-4a80-8b4d-24796e571b17"
},
{ {
"id": "a72107fa-a4a5-407d-9e85-39bdb9c0b11a", "id": "a72107fa-a4a5-407d-9e85-39bdb9c0b11a",
"network": "Twitter", "network": "Twitter",
@ -233,22 +284,31 @@
"heading": "Work Experience", "heading": "Work Experience",
"items": [ "items": [
{ {
"company": "Postdot Technologies Pvt. Ltd.", "company": "On Point Electronics, NYC, NY",
"endDate": "", "endDate": "2018-07-01",
"id": "d7c64937-0cb9-41b1-a3a6-0679c882fe63", "id": "d7c64937-0cb9-41b1-a3a6-0679c882fe63",
"position": "Full Stack Web Developer", "position": "Customer Service Representative",
"startDate": "2020-06-08", "startDate": "2013-01-01",
"summary": "Postman is a great tool when trying to dissect RESTful APIs made by others or test ones you have made yourself. It offers a sleek user interface with which to make HTML requests, without the hassle of writing a bunch of code just to test an API's functionality.", "summary": "- Organized customer information and account data for business planning and customer service purposes.\n- Created excel spreadsheets to track customer data and perform intense reconciliation process.\n- Received 97% positive customer survey results.\n- Speed on calls was 10% above team average. \n**Key Achievement:** Designed and executed an automatized system for following up with customers, increasing customer retention by 22%.",
"website": "https://postman.com" "website": "https://onpoint.com"
}, },
{ {
"company": "GoDhiyo Solutions Pvt. Ltd.", "company": "Excelsior Communications, NYC, NY",
"endDate": "2020-04-01", "endDate": "2012-12-01",
"id": "f5c5dcfe-2a60-4169-a2f1-b305355518ea", "id": "f5c5dcfe-2a60-4169-a2f1-b305355518ea",
"position": "Full Stack Web Developer", "position": "Customer Service Representative",
"startDate": "2018-07-01", "startDate": "2009-10-01",
"summary": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi laoreet volutpat lacus, sed tempor lacus eleifend feugiat. Pellentesque molestie libero ac varius finibus. Fusce convallis, arcu sit amet lacinia vehicula, nisl justo egestas tortor.\n\n- In vestibulum eros a enim rhoncus\n- Phasellus ullamcorper magna quis est sagittis", "summary": "- Worked as a full time customer service rep in a high volume call center.\n- Received \"Associate of the Month\" award six times.\n- Chosen as an example for other associates in trainings. \n**Key Achievement:** Received Customer Appreciation bonus in three of four years.",
"website": "https://dhiyo.ai" "website": "https://excelsior.com"
},
{
"company": "Pizza Hut, Newark, NJ",
"position": "Waiter",
"website": "https://pizzahut.com",
"startDate": "2005-08-01",
"endDate": "2009-09-01",
"summary": "- Worked passionately in customer service in a high volume restaurant.\n- Completed the FAST customer service training class.\n- Maintained a high tip average thanks to consistent customer satisfaction.",
"id": "dd935088-6fe7-4a4b-8ff5-7417c32d2add"
} }
], ],
"visible": true "visible": true

View File

@ -1,89 +0,0 @@
import leftSections from './leftSections';
const initialState = {
profile: {
heading: 'Profile',
photograph: '',
firstName: '',
lastName: '',
subtitle: '',
address: {
line1: '',
line2: '',
city: '',
pincode: '',
},
profile: '',
website: '',
email: '',
},
social: {
heading: 'Social',
visible: true,
items: [],
},
objective: {
heading: 'Objective',
visible: true,
body: '',
},
work: {
heading: 'Work Experience',
visible: true,
items: [],
},
education: {
heading: 'Education',
visible: true,
items: [],
},
projects: {
heading: 'Projects',
visible: true,
items: [],
},
awards: {
heading: 'Awards',
visible: true,
items: [],
},
certifications: {
heading: 'Certifications',
visible: true,
items: [],
},
skills: {
heading: 'Skills',
visible: true,
items: [],
},
hobbies: {
heading: 'Hobbies',
visible: true,
items: [],
},
languages: {
heading: 'Languages',
visible: true,
items: [],
},
references: {
heading: 'References',
visible: true,
items: [],
},
metadata: {
template: 'onyx',
font: 'Montserrat',
layout: [leftSections.filter((x) => !x.fixed).map((x) => x.id)],
colors: {
text: '#444444',
primary: '#5875DB',
background: '#FFFFFF',
},
},
public: true,
updatedAt: new Date(),
};
export default initialState;

View File

@ -0,0 +1,99 @@
{
"profile": {
"heading": "Profile",
"photograph": "",
"firstName": "",
"lastName": "",
"subtitle": "",
"address": {
"line1": "",
"line2": "",
"city": "",
"pincode": ""
},
"profile": "",
"website": "",
"email": ""
},
"social": {
"heading": "Social",
"visible": true,
"items": []
},
"objective": {
"heading": "Objective",
"visible": true,
"body": ""
},
"work": {
"heading": "Work Experience",
"visible": true,
"items": []
},
"education": {
"heading": "Education",
"visible": true,
"items": []
},
"projects": {
"heading": "Projects",
"visible": true,
"items": []
},
"awards": {
"heading": "Awards",
"visible": true,
"items": []
},
"certifications": {
"heading": "Certifications",
"visible": true,
"items": []
},
"skills": {
"heading": "Skills",
"visible": true,
"items": []
},
"hobbies": {
"heading": "Hobbies",
"visible": true,
"items": []
},
"languages": {
"heading": "Languages",
"visible": true,
"items": []
},
"references": {
"heading": "References",
"visible": true,
"items": []
},
"metadata": {
"template": "onyx",
"font": "Montserrat",
"layout": {
"onyx": [
["objective", "work", "education", "projects"],
["hobbies", "languages", "awards", "certifications"],
["skills", "references"]
],
"pikachu": [
["skills", "languages", "hobbies", "awards", "certifications"],
["work", "education", "projects", "references"]
],
"gengar": [
["objective", "skills"],
["awards", "certifications", "languages", "references", "hobbies"],
["work", "education", "projects"]
]
},
"colors": {
"text": "#444444",
"primary": "#5875DB",
"background": "#FFFFFF"
}
},
"public": true
}

View File

@ -9,6 +9,11 @@ const templateOptions = [
name: 'Pikachu', name: 'Pikachu',
preview: 'https://source.unsplash.com/random/301x501', preview: 'https://source.unsplash.com/random/301x501',
}, },
{
id: 'gengar',
name: 'Gengar',
preview: 'https://source.unsplash.com/random/302x502',
},
]; ];
export default templateOptions; export default templateOptions;

View File

@ -10,6 +10,7 @@ const ExportModal = () => {
const state = useSelector(); const state = useSelector();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [isLoadingSingle, setLoadingSingle] = useState(false); const [isLoadingSingle, setLoadingSingle] = useState(false);
const [isLoadingMulti, setLoadingMulti] = useState(false);
const functionsUrl = 'http://localhost:5001/rx-resume/us-central1'; const functionsUrl = 'http://localhost:5001/rx-resume/us-central1';
const { emitter, events } = useContext(ModalContext); const { emitter, events } = useContext(ModalContext);
@ -26,19 +27,30 @@ const ExportModal = () => {
} }
}; };
const openFile = (blob) => {
if (typeof window !== `undefined`) {
const url = window.URL.createObjectURL(blob, { oneTimeOnly: true });
window && window.open(url);
setLoadingSingle(false);
}
};
const handleSinglePageDownload = async () => { const handleSinglePageDownload = async () => {
setLoadingSingle(true); setLoadingSingle(true);
fetch(`${functionsUrl}/printSinglePageResume?id=${state.id}`, { fetch(`${functionsUrl}/printSinglePageResume?id=${state.id}`, {
method: 'POST', method: 'POST',
}) })
.then((response) => response.blob()) .then((response) => response.blob())
.then((data) => { .then(openFile);
if (typeof window !== `undefined`) { };
const url = window.URL.createObjectURL(data, { oneTimeOnly: true });
window && window.open(url); const handleMultiPageDownload = async () => {
setLoadingSingle(false); setLoadingMulti(true);
} fetch(`${functionsUrl}/printMultiPageResume?id=${state.id}`, {
}); method: 'POST',
})
.then((response) => response.blob())
.then(openFile);
}; };
const handleExportToJson = () => { const handleExportToJson = () => {
@ -99,7 +111,13 @@ const ExportModal = () => {
> >
Single Page Resume Single Page Resume
</Button> </Button>
<Button className="ml-8">Multi Page Resume</Button> <Button
className="ml-8"
isLoading={isLoadingMulti}
onClick={handleMultiPageDownload}
>
Multi Page Resume
</Button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,6 +6,8 @@ import LoadingScreen from '../../components/router/LoadingScreen';
import DatabaseContext from '../../contexts/DatabaseContext'; import DatabaseContext from '../../contexts/DatabaseContext';
import Onyx from '../../templates/Onyx'; import Onyx from '../../templates/Onyx';
import styles from './view.module.css'; import styles from './view.module.css';
import Pikachu from '../../templates/Pikachu';
import Gengar from '../../templates/Gengar';
const ResumeViewer = ({ id }) => { const ResumeViewer = ({ id }) => {
const [resume, setResume] = useState(null); const [resume, setResume] = useState(null);
@ -46,6 +48,8 @@ const ResumeViewer = ({ id }) => {
style={{ backgroundColor: resume.metadata.colors.background }} style={{ backgroundColor: resume.metadata.colors.background }}
> >
{resume.metadata.template === 'onyx' && <Onyx data={resume} />} {resume.metadata.template === 'onyx' && <Onyx data={resume} />}
{resume.metadata.template === 'pikachu' && <Pikachu data={resume} />}
{resume.metadata.template === 'gengar' && <Gengar data={resume} />}
</div> </div>
<p className={styles.footer}> <p className={styles.footer}>

131
src/templates/Gengar.js Normal file
View File

@ -0,0 +1,131 @@
import React from 'react';
import PageContext from '../contexts/PageContext';
import { hexToRgb } from '../utils';
import AwardsA from './blocks/Awards/AwardsA';
import CertificationsA from './blocks/Certifications/CertificationsA';
import ContactB from './blocks/Contact/ContactB';
import EducationA from './blocks/Education/EducationA';
import HeadingC from './blocks/Heading/HeadingC';
import HobbiesA from './blocks/Hobbies/HobbiesA';
import LanguagesA from './blocks/Languages/LanguagesA';
import ObjectiveA from './blocks/Objective/ObjectiveA';
import ProjectsA from './blocks/Projects/ProjectsA';
import ReferencesB from './blocks/References/ReferencesB';
import SkillsA from './blocks/Skills/SkillsA';
import WorkA from './blocks/Work/WorkA';
const Blocks = {
objective: ObjectiveA,
work: WorkA,
education: EducationA,
projects: ProjectsA,
awards: AwardsA,
certifications: CertificationsA,
skills: SkillsA,
hobbies: HobbiesA,
languages: LanguagesA,
references: ReferencesB,
};
const Gengar = ({ data }) => {
const layout = data.metadata.layout.gengar;
const { r, g, b } = hexToRgb(data.metadata.colors.primary) || {};
const Photo = () =>
data.profile.photograph !== '' && (
<img
className="w-24 h-24 rounded-full mr-4 object-cover border-4"
style={{
borderColor: data.metadata.colors.background,
}}
src={data.profile.photograph}
alt={data.profile.firstName}
/>
);
const Profile = () => (
<div>
<h1 className="text-2xl font-bold leading-tight">
{data.profile.firstName}
</h1>
<h1 className="text-2xl font-bold leading-tight">
{data.profile.lastName}
</h1>
<div className="text-xs font-medium mt-2">{data.profile.subtitle}</div>
</div>
);
return (
<PageContext.Provider value={{ data, heading: HeadingC }}>
<div
id="page"
className="rounded"
style={{
fontFamily: data.metadata.font,
color: data.metadata.colors.text,
backgroundColor: data.metadata.colors.background,
}}
>
<div className="grid grid-cols-12">
<div
className="col-span-4 px-6 py-8"
style={{
backgroundColor: data.metadata.colors.primary,
color: data.metadata.colors.background,
}}
>
<div className="flex items-center">
<Photo />
<Profile />
</div>
<hr
className="w-1/4 my-5 opacity-25"
style={{ borderColor: data.metadata.colors.background }}
/>
<ContactB />
</div>
<div
className="col-span-8 px-6 py-8"
style={{ backgroundColor: `rgba(${r}, ${g}, ${b}, 0.1)` }}
>
<div className="grid gap-6 items-center">
{layout[0] &&
layout[0].map((x) => {
const Component = Blocks[x];
return Component && <Component key={x} />;
})}
</div>
</div>
<div
className="col-span-4 px-6 py-8"
style={{ backgroundColor: `rgba(${r}, ${g}, ${b}, 0.1)` }}
>
<div className="grid gap-6">
{layout[1] &&
layout[1].map((x) => {
const Component = Blocks[x];
return Component && <Component key={x} />;
})}
</div>
</div>
<div className="col-span-8 px-6 py-8">
<div className="grid gap-6">
{layout[2] &&
layout[2].map((x) => {
const Component = Blocks[x];
return Component && <Component key={x} />;
})}
</div>
</div>
</div>
</div>
</PageContext.Provider>
);
};
export default Gengar;

View File

@ -1,11 +1,10 @@
import React, { memo, useEffect } from 'react'; import React, { memo } from 'react';
import PageContext from '../contexts/PageContext'; import PageContext from '../contexts/PageContext';
import { useDispatch } from '../contexts/ResumeContext';
import AwardsA from './blocks/Awards/AwardsA'; import AwardsA from './blocks/Awards/AwardsA';
import CertificationsA from './blocks/Certifications/CertificationsA'; import CertificationsA from './blocks/Certifications/CertificationsA';
import Contact from './blocks/Contact/ContactA'; import Contact from './blocks/Contact/ContactA';
import EducationA from './blocks/Education/EducationA'; import EducationA from './blocks/Education/EducationA';
import Heading from './blocks/Heading/HeadingA'; import HeadingA from './blocks/Heading/HeadingA';
import HobbiesA from './blocks/Hobbies/HobbiesA'; import HobbiesA from './blocks/Hobbies/HobbiesA';
import LanguagesA from './blocks/Languages/LanguagesA'; import LanguagesA from './blocks/Languages/LanguagesA';
import ObjectiveA from './blocks/Objective/ObjectiveA'; import ObjectiveA from './blocks/Objective/ObjectiveA';
@ -28,18 +27,10 @@ const Blocks = {
}; };
const Onyx = ({ data }) => { const Onyx = ({ data }) => {
const dispatch = useDispatch(); const layout = data.metadata.layout.onyx;
useEffect(() => {
dispatch({
type: 'set_fixed_sections',
payload: ['profile', 'social'],
});
dispatch({ type: 'set_block_count', payload: 3 });
}, []);
return ( return (
<PageContext.Provider value={{ data, heading: Heading }}> <PageContext.Provider value={{ data, heading: HeadingA }}>
<div <div
id="page" id="page"
className="p-10 rounded" className="p-10 rounded"
@ -88,22 +79,22 @@ const Onyx = ({ data }) => {
/> />
<div className="grid gap-4"> <div className="grid gap-4">
{data.metadata.layout[0] && {layout[0] &&
data.metadata.layout[0].map((x) => { layout[0].map((x) => {
const Component = Blocks[x]; const Component = Blocks[x];
return Component && <Component key={x} />; return Component && <Component key={x} />;
})} })}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{data.metadata.layout[1] && {layout[1] &&
data.metadata.layout[1].map((x) => { layout[1].map((x) => {
const Component = Blocks[x]; const Component = Blocks[x];
return Component && <Component key={x} />; return Component && <Component key={x} />;
})} })}
</div> </div>
{data.metadata.layout[2] && {layout[2] &&
data.metadata.layout[2].map((x) => { layout[2].map((x) => {
const Component = Blocks[x]; const Component = Blocks[x];
return Component && <Component key={x} />; return Component && <Component key={x} />;
})} })}

View File

@ -1,28 +1,65 @@
import React, { useContext } from 'react'; import React from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import PageContext from '../contexts/PageContext';
import ContactA from './blocks/Contact/ContactA';
import HeadingB from './blocks/Heading/HeadingB';
import AwardsA from './blocks/Awards/AwardsA';
import CertificationsA from './blocks/Certifications/CertificationsA';
import EducationA from './blocks/Education/EducationA';
import HobbiesA from './blocks/Hobbies/HobbiesA';
import LanguagesA from './blocks/Languages/LanguagesA';
import ProjectsA from './blocks/Projects/ProjectsA';
import ReferencesA from './blocks/References/ReferencesA';
import SkillsA from './blocks/Skills/SkillsA';
import WorkA from './blocks/Work/WorkA';
const Pikachu = () => { const Blocks = {
const context = useContext(AppContext); work: WorkA,
const { state } = context; education: EducationA,
const { data, theme } = state; projects: ProjectsA,
awards: AwardsA,
certifications: CertificationsA,
skills: SkillsA,
hobbies: HobbiesA,
languages: LanguagesA,
references: ReferencesA,
};
const Photo = () => const Pikachu = ({ data }) => {
data.profile.photo !== '' && ( const layout = data.metadata.layout.pikachu;
return (
<PageContext.Provider value={{ data, heading: HeadingB }}>
<div
id="page"
className="p-10 rounded"
style={{
fontFamily: data.metadata.font,
color: data.metadata.colors.text,
backgroundColor: data.metadata.colors.background,
}}
>
<div className="grid grid-cols-12 col-gap-6 row-gap-8">
{data.profile.photograph && (
<div className="self-center col-span-4"> <div className="self-center col-span-4">
<img <img
className="w-48 h-48 rounded-full mx-auto object-cover" className="w-48 h-48 rounded-full mx-auto object-cover"
src={data.profile.photo} src={data.profile.photograph}
alt="" alt={data.profile.firstName}
/> />
</div> </div>
); )}
const Header = () => ( <div
className={`${
data.profile.photograph !== '' ? 'col-span-8' : 'col-span-12'
}`}
>
<div <div
className="h-48 rounded flex flex-col justify-center" className="h-48 rounded flex flex-col justify-center"
style={{ style={{
backgroundColor: theme.colors.accent, backgroundColor: data.metadata.colors.primary,
color: theme.colors.background, color: data.metadata.colors.background,
}} }}
> >
<div className="flex flex-col justify-center mx-8 my-6"> <div className="flex flex-col justify-center mx-8 my-6">
@ -33,307 +70,47 @@ const Pikachu = () => {
{data.profile.subtitle} {data.profile.subtitle}
</div> </div>
<hr className="my-4 opacity-50" /> {data.objective.body && (
<ReactMarkdown className="text-sm" source={data.objective.body} />
</div>
</div>
);
const ContactItem = ({ icon, value, link = '#' }) =>
value && (
<div className="flex items-center my-3">
<span
className="material-icons text-lg mr-2"
style={{ color: theme.colors.accent }}
>
{icon}
</span>
<a href={link}>
<span className="font-medium break-all">{value}</span>
</a>
</div>
);
const Heading = ({ title }) => (
<div
className="mb-2 border-b-2 pb-1 font-bold uppercase tracking-wide text-sm"
style={{ color: theme.colors.accent, borderColor: theme.colors.accent }}
>
{title}
</div>
);
const SkillItem = (x) => (
<span
key={x.id}
className="leading-none rounded-lg text-sm font-medium bg-gray-300 py-3 my-1 px-4"
>
{x.skill}
</span>
);
const Skills = () =>
data.skills &&
data.skills.enable && (
<div> <div>
<Heading title={data.skills.heading} /> <hr
<div className="flex flex-col mb-6"> className="my-5 opacity-25"
{data.skills.items.map(SkillItem)} style={{ borderColor: data.metadata.colors.background }}
</div> />
</div>
);
const HobbyItem = (x) => ( <ReactMarkdown
<span className="text-sm"
key={x.id} source={data.objective.body}
className="leading-none rounded-lg text-sm font-medium bg-gray-300 py-3 my-1 px-4" />
>
{x.hobby}
</span>
);
const Hobbies = () =>
data.hobbies &&
data.hobbies.enable && (
<div>
<Heading title={data.hobbies.heading} />
<div className="flex flex-col mb-6">
{data.hobbies.items.map(HobbyItem)}
</div>
</div>
);
const ReferenceItem = (x) => (
<div key={x.id} className="flex flex-col">
<h6 className="text-sm font-medium">{x.name}</h6>
<span className="text-xs">{x.position}</span>
<span className="text-xs">{x.phone}</span>
<span className="text-xs">{x.email}</span>
<ReactMarkdown className="mt-2 text-sm" source={x.description} />
</div>
);
const References = () =>
data.references &&
data.references.enable && (
<div>
<Heading title={data.references.heading} />
<div className="grid grid-cols-2 gap-2 mb-6">
{data.references.items.filter((x) => x.enable).map(ReferenceItem)}
</div>
</div>
);
const LanguageItem = (x) => (
<div key={x.id} className="grid grid-cols-2 items-center py-2">
<h6 className="text-sm font-medium">{x.key}</h6>
<div className="flex">
{x.level && <div className="font-bold text-sm mr-2">{x.level}</div>}
{x.rating !== 0 && (
<div className="flex">
{Array.from(Array(x.rating)).map((_, i) => (
<i
key={i}
className="material-icons text-lg"
style={{ color: theme.colors.accent }}
>
star
</i>
))}
</div> </div>
)} )}
</div> </div>
</div> </div>
);
const Languages = () =>
data.languages &&
data.languages.enable && (
<div>
<Heading title={data.languages.heading} />
<div className="mb-6">
{data.languages.items.filter((x) => x.enable).map(LanguageItem)}
</div>
</div>
);
const ExtraItem = (x) => (
<div key={x.id} className="text-sm my-1">
<h6 className="text-xs font-bold">{x.key}</h6>
<h6 className="">{x.value}</h6>
</div>
);
const Extras = () =>
data.extras &&
data.extras.enable && (
<div>
<Heading title={data.extras.heading} />
<div className="grid grid-cols-2">
{data.extras.items.filter((x) => x.enable).map(ExtraItem)}
</div>
</div>
);
const WorkItem = (x) => (
<div key={x.id} className="mb-3">
<div className="flex justify-between items-center">
<div>
<h6 className="font-semibold">{x.title}</h6>
<p className="text-xs">{x.role}</p>
</div>
<span className="text-xs font-medium">
({x.start} - {x.end})
</span>
</div>
<ReactMarkdown className="mt-2 text-sm" source={x.description} />
</div>
);
const Work = () =>
data.work &&
data.work.enable && (
<div>
<Heading title={data.work.heading} />
<div className="flex flex-col mb-4">
{data.work.items.filter((x) => x.enable).map(WorkItem)}
</div>
</div>
);
const EducationItem = (x) => (
<div key={x.id} className="mb-2">
<div className="flex justify-between items-center">
<div>
<h6 className="font-semibold">{x.name}</h6>
<p className="text-xs">{x.major}</p>
</div>
<div className="flex flex-col text-right items-end">
<span
className="text-sm font-bold"
style={{ color: theme.colors.accent }}
>
{x.grade}
</span>
<span className="text-xs font-medium">
({x.start} - {x.end})
</span>
</div>
</div>
<ReactMarkdown className="mt-2 text-sm" source={x.description} />
</div>
);
const Education = () =>
data.education &&
data.education.enable && (
<div>
<Heading title={data.education.heading} />
<div className="flex flex-col mb-4">
{data.education.items.filter((x) => x.enable).map(EducationItem)}
</div>
</div>
);
const AwardItem = (x) => (
<div key={x.id} className="mb-2">
<h6 className="font-semibold">{x.title}</h6>
<p className="text-xs">{x.subtitle}</p>
<ReactMarkdown className="mt-2 text-sm" source={x.description} />
</div>
);
const Awards = () =>
data.awards &&
data.awards.enable && (
<div>
<Heading title={data.awards.heading} />
<div className="flex flex-col mb-2">
{data.awards.items.filter((x) => x.enable).map(AwardItem)}
</div>
</div>
);
const CertificationItem = (x) => (
<div key={x.id} className="mb-3">
<h6 className="font-semibold">{x.title}</h6>
<p className="text-xs">{x.subtitle}</p>
<ReactMarkdown className="mt-2 text-sm" source={x.description} />
</div>
);
const Certifications = () =>
data.certifications &&
data.certifications.enable && (
<div>
<Heading title={data.certifications.heading} />
<div className="flex flex-col mb-2">
{data.certifications.items
.filter((x) => x.enable)
.map(CertificationItem)}
</div>
</div>
);
return (
<div
className="p-10"
style={{
fontFamily: theme.font.family,
backgroundColor: theme.colors.background,
color: theme.colors.primary,
}}
>
<div className="grid grid-cols-12 col-gap-6 row-gap-8">
<Photo />
<div
className={`${
data.profile.photo !== '' ? 'col-span-8' : 'col-span-12'
}`}
>
<Header />
</div> </div>
<div className="col-span-4 overflow-hidden"> <div className="col-span-4">
<div className="text-sm mb-6"> <div className="grid gap-4">
<ContactItem <ContactA />
icon="phone"
value={data.profile.phone}
link={`tel:${data.profile.phone}`}
/>
<ContactItem
icon="language"
value={data.profile.website}
link={`http://${data.profile.website}`}
/>
<ContactItem
icon="email"
value={data.profile.email}
link={`mailto:${data.profile.email}`}
/>
<ContactItem
icon="location_on"
value={data.profile.address.line3}
/>
</div>
<Skills /> {layout[0] &&
<Hobbies /> layout[0].map((x) => {
<Languages /> const Component = Blocks[x];
<Certifications /> return Component && <Component key={x} />;
})}
</div>
</div> </div>
<div className="col-span-8"> <div className="col-span-8">
<Work /> <div className="grid gap-4">
<Education /> {layout[1] &&
<Awards /> layout[1].map((x) => {
<References /> const Component = Blocks[x];
<Extras /> return Component && <Component key={x} />;
})}
</div> </div>
</div> </div>
</div> </div>
</div>
</PageContext.Provider>
); );
}; };

View File

@ -12,7 +12,7 @@ const AwardItem = (x) => (
<span className="text-xs">{x.awarder}</span> <span className="text-xs">{x.awarder}</span>
</div> </div>
{x.date && ( {x.date && (
<h6 className="text-xs font-medium"> <h6 className="text-xs font-medium text-right">
{moment(x.date).format('MMMM YYYY')} {moment(x.date).format('MMMM YYYY')}
</h6> </h6>
)} )}

View File

@ -12,7 +12,7 @@ const CertificationItem = (x) => (
<span className="text-xs">{x.issuer}</span> <span className="text-xs">{x.issuer}</span>
</div> </div>
{x.date && ( {x.date && (
<h6 className="text-xs font-medium"> <h6 className="text-xs font-medium text-right">
{moment(x.date).format('MMMM YYYY')} {moment(x.date).format('MMMM YYYY')}
</h6> </h6>
)} )}

View File

@ -0,0 +1,64 @@
import { get } from 'lodash';
import React, { memo, useContext } from 'react';
import { FaCaretRight } from 'react-icons/fa';
import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
import Icons from '../Icons';
const ContactItem = ({ value, icon, link }) => {
const { data } = useContext(PageContext);
const Icon = get(Icons, icon.toLowerCase(), FaCaretRight);
return value ? (
<div className="flex items-center">
<Icon
size="10px"
className="mr-2"
style={{ color: data.metadata.colors.background }}
/>
{link ? (
<a href={link} target="_blank" rel="noopener noreferrer">
<span className="font-medium break-all">{value}</span>
</a>
) : (
<span className="font-medium break-all">{value}</span>
)}
</div>
) : null;
};
const ContactA = () => {
const { data } = useContext(PageContext);
return (
<div className="text-xs grid gap-2">
<ContactItem
icon="phone"
value={data.profile.phone}
link={`tel:${data.profile.phone}`}
/>
<ContactItem
icon="website"
value={data.profile.website}
link={`http://${data.profile.website}`}
/>
<ContactItem
icon="email"
value={data.profile.email}
link={`mailto:${data.profile.email}`}
/>
{safetyCheck(data.social) &&
data.social.items.map((x) => (
<ContactItem
key={x.id}
value={x.username}
icon={x.network}
link={x.url}
/>
))}
</div>
);
};
export default memo(ContactA);

View File

@ -12,9 +12,9 @@ const EducationItem = (x) => (
<strong>{x.degree}</strong> {x.field} <strong>{x.degree}</strong> {x.field}
</span> </span>
</div> </div>
<div className="flex flex-col items-end"> <div className="flex flex-col items-end text-right">
{x.startDate && ( {x.startDate && (
<h6 className="text-xs font-medium"> <h6 className="text-xs font-medium mb-1">
({formatDateRange({ startDate: x.startDate, endDate: x.endDate })}) ({formatDateRange({ startDate: x.startDate, endDate: x.endDate })})
</h6> </h6>
)} )}

View File

@ -0,0 +1,20 @@
import React, { memo, useContext } from 'react';
import PageContext from '../../../contexts/PageContext';
const HeadingB = ({ children }) => {
const { data } = useContext(PageContext);
return (
<h6
className="mb-2 border-b-2 pb-1 font-bold uppercase tracking-wide text-sm"
style={{
color: data.metadata.colors.primary,
borderColor: data.metadata.colors.primary,
}}
>
{children}
</h6>
);
};
export default memo(HeadingB);

View File

@ -0,0 +1,11 @@
import React, { memo } from 'react';
const HeadingC = ({ children }) => {
return (
<h6 className="font-bold text-xs uppercase tracking-wide mb-1">
{children}
</h6>
);
};
export default memo(HeadingC);

View File

@ -16,7 +16,7 @@ const ProjectItem = (x) => (
)} )}
</div> </div>
{x.date && ( {x.date && (
<h6 className="text-xs font-medium"> <h6 className="text-xs font-medium text-right">
{moment(x.date).format('MMMM YYYY')} {moment(x.date).format('MMMM YYYY')}
</h6> </h6>
)} )}

View File

@ -0,0 +1,31 @@
import React, { memo, useContext } from 'react';
import ReactMarkdown from 'react-markdown';
import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const ReferenceItem = (x) => (
<div key={x.id} className="flex flex-col">
<h6 className="font-semibold">{x.name}</h6>
<span className="text-xs">{x.position}</span>
<span className="text-xs">{x.phone}</span>
<span className="text-xs">{x.email}</span>
{x.summary && (
<ReactMarkdown className="markdown mt-2 text-sm" source={x.summary} />
)}
</div>
);
const ReferencesB = () => {
const { data, heading: Heading } = useContext(PageContext);
return safetyCheck(data.references) ? (
<div>
<Heading>{data.references.heading}</Heading>
<div className="grid gap-4">
{data.references.items.map(ReferenceItem)}
</div>
</div>
) : null;
};
export default memo(ReferencesB);

View File

@ -0,0 +1,22 @@
import React, { memo, useContext } from 'react';
import PageContext from '../../../contexts/PageContext';
import { safetyCheck } from '../../../utils';
const SkillItem = (x) => (
<li key={x.id} className="text-sm py-1">
{x.skill}
</li>
);
const SkillsA = () => {
const { data, heading: Heading } = useContext(PageContext);
return safetyCheck(data.skills) ? (
<div>
<Heading>{data.skills.heading}</Heading>
<ul>{data.skills.items.map(SkillItem)}</ul>
</div>
) : null;
};
export default memo(SkillsA);

View File

@ -11,7 +11,7 @@ const WorkItem = (x) => (
<span className="text-xs">{x.position}</span> <span className="text-xs">{x.position}</span>
</div> </div>
{x.startDate && ( {x.startDate && (
<h6 className="text-xs font-medium"> <h6 className="text-xs font-medium text-right">
({formatDateRange({ startDate: x.startDate, endDate: x.endDate })}) ({formatDateRange({ startDate: x.startDate, endDate: x.endDate })})
</h6> </h6>
)} )}

View File

@ -30,6 +30,19 @@ export const getFieldProps = (formik, schema, name) => ({
...formik.getFieldProps(name), ...formik.getFieldProps(name),
}); });
export const hexToRgb = (hex) => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
export const reorder = (list, startIndex, endIndex) => { export const reorder = (list, startIndex, endIndex) => {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);