mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-16 17:51:43 +10:00
- implement actions section
This commit is contained in:
@ -53,13 +53,9 @@ const List = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
outline
|
||||
icon={MdAdd}
|
||||
title="Add New"
|
||||
onClick={handleAdd}
|
||||
className="mt-8 ml-auto"
|
||||
/>
|
||||
<Button outline icon={MdAdd} onClick={handleAdd} className="mt-8 ml-auto">
|
||||
Add New
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,25 +1,52 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { MdImportExport } from 'react-icons/md';
|
||||
import { clone } from 'lodash';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Button from '../../../shared/Button';
|
||||
import styles from './Actions.module.css';
|
||||
import Input from '../../../shared/Input';
|
||||
import ModalContext from '../../../../contexts/ModalContext';
|
||||
import { useSelector } from '../../../../contexts/ResumeContext';
|
||||
|
||||
const Actions = () => {
|
||||
const state = useSelector();
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
|
||||
const handleImport = () => emitter.emit(events.IMPORT_MODAL);
|
||||
|
||||
const handleExportToJson = () => {
|
||||
const backupObj = clone(state);
|
||||
delete backupObj.id;
|
||||
const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(backupObj),
|
||||
)}`;
|
||||
const dlAnchor = document.getElementById('downloadAnchor');
|
||||
dlAnchor.setAttribute('href', dataStr);
|
||||
dlAnchor.setAttribute('download', `RxResume_${state.id}.json`);
|
||||
dlAnchor.click();
|
||||
};
|
||||
|
||||
const getSharableUrl = () => {
|
||||
const shareId = state.id.split('-')[0];
|
||||
return `https://rxresu.me/r/${shareId}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading>Actions</Heading>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>Import from Other Sources</h5>
|
||||
<h5>Import Your Resume</h5>
|
||||
|
||||
<p>
|
||||
You can import your information from various sources like JSON Resume
|
||||
or your LinkedIn profile to autofill most of the data for your resume.
|
||||
or your LinkedIn to autofill most of the data for your resume.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button icon={MdImportExport} title="Import" />
|
||||
<Button icon={MdImportExport} onClick={handleImport}>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -32,21 +59,27 @@ const Actions = () => {
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button title="Save as PDF" />
|
||||
<Button outline title="Export as JSON" className="ml-6" />
|
||||
<Button>Save as PDF</Button>
|
||||
<Button outline className="ml-6" onClick={handleExportToJson}>
|
||||
Export as JSON
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<a id="downloadAnchor" className="hidden">
|
||||
Download Exported JSON
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>Share Your Resume</h5>
|
||||
|
||||
<p>
|
||||
The link below will be accessible publicly if you choose, and you can
|
||||
share the latest version of your resume to anyone in the world.
|
||||
The link below will be accessible publicly if you choose to share it,
|
||||
and viewers would see the latest version of your resume at any time.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Input type="action" value="https://google.com" onClick={() => {}} />
|
||||
<Input type="action" value={getSharableUrl()} onClick={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -59,7 +92,7 @@ const Actions = () => {
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button title="Load Demo Data" />
|
||||
<Button>Load Demo Data</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -73,7 +106,7 @@ const Actions = () => {
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button title="Delete Account" />
|
||||
<Button isDelete>Delete Account</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
import React, { memo } from 'react';
|
||||
import { useDispatch } from '../../../../contexts/ResumeContext';
|
||||
import colors from '../../../../data/colors';
|
||||
import colorOptions from '../../../../data/colorOptions';
|
||||
import { handleKeyUp } from '../../../../utils';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
@ -25,7 +25,7 @@ const Colors = () => {
|
||||
<Heading>Colors</Heading>
|
||||
|
||||
<div className="mb-6 grid grid-cols-8 col-gap-2 row-gap-6">
|
||||
{colors.map((color) => (
|
||||
{colorOptions.map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
tabIndex="0"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import cx from 'classnames';
|
||||
import React, { memo } from 'react';
|
||||
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
|
||||
import fonts from '../../../../data/fonts';
|
||||
import fontOptions from '../../../../data/fontOptions';
|
||||
import { handleKeyUp } from '../../../../utils';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import styles from './Fonts.module.css';
|
||||
@ -25,7 +25,7 @@ const Fonts = () => {
|
||||
<Heading>Fonts</Heading>
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{fonts.map((x) => (
|
||||
{fontOptions.map((x) => (
|
||||
<div
|
||||
key={x}
|
||||
tabIndex="0"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import cx from 'classnames';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
|
||||
import templates from '../../../../data/templates';
|
||||
import templateOptions from '../../../../data/templateOptions';
|
||||
import { handleKeyUp } from '../../../../utils';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import styles from './Templates.module.css';
|
||||
@ -28,7 +28,7 @@ const Templates = () => {
|
||||
<Heading>Templates</Heading>
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{templates.map((x) => (
|
||||
{templateOptions.map((x) => (
|
||||
<div
|
||||
key={x.id}
|
||||
tabIndex="0"
|
||||
|
||||
@ -28,21 +28,20 @@ const Hero = () => {
|
||||
|
||||
<div className="mt-12 flex">
|
||||
{user ? (
|
||||
<Button
|
||||
title="Go to App"
|
||||
onClick={handleGotoApp}
|
||||
isLoading={loading}
|
||||
/>
|
||||
<Button onClick={handleGotoApp} isLoading={loading}>
|
||||
Go to App
|
||||
</Button>
|
||||
) : (
|
||||
<Button title="Login" onClick={handleLogin} isLoading={loading} />
|
||||
)}
|
||||
<Button
|
||||
outline
|
||||
className="ml-8"
|
||||
title="GitHub"
|
||||
icon={FaGithub}
|
||||
onClick={toggleDarkMode}
|
||||
/>
|
||||
>
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,15 @@ import React, { memo } from 'react';
|
||||
import { handleKeyUp } from '../../utils';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
const Button = ({ icon, title, onClick, outline, className, isLoading }) => {
|
||||
const Button = ({
|
||||
icon,
|
||||
onClick,
|
||||
outline,
|
||||
children,
|
||||
className,
|
||||
isLoading,
|
||||
isDelete,
|
||||
}) => {
|
||||
const Icon = icon;
|
||||
|
||||
return (
|
||||
@ -12,10 +20,11 @@ const Button = ({ icon, title, onClick, outline, className, isLoading }) => {
|
||||
onClick={isLoading ? undefined : onClick}
|
||||
className={cx(styles.container, className, {
|
||||
[styles.outline]: outline,
|
||||
[styles.delete]: isDelete,
|
||||
})}
|
||||
>
|
||||
{icon && <Icon size="14" className="mr-3" />}
|
||||
{isLoading ? 'Loading...' : title}
|
||||
{isLoading ? 'Loading...' : children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@ -29,3 +29,15 @@
|
||||
.container.outline:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.container.delete {
|
||||
@apply bg-red-600 border-red-600 text-white;
|
||||
}
|
||||
|
||||
.container.delete:hover {
|
||||
@apply bg-red-700 border-red-700;
|
||||
}
|
||||
|
||||
.container.delete:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
@ -4,7 +4,6 @@ import React, { memo, useEffect, useState } from 'react';
|
||||
import { FaAngleDown } from 'react-icons/fa';
|
||||
import { MdClose, MdOpenInNew } from 'react-icons/md';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { IoIosCopy } from 'react-icons/io';
|
||||
import { useDispatch, useSelector } from '../../contexts/ResumeContext';
|
||||
import { handleKeyUp } from '../../utils';
|
||||
import styles from './Input.module.css';
|
||||
|
||||
@ -10,6 +10,7 @@ const ModalEvents = {
|
||||
HOBBY_MODAL: 'hobby_modal',
|
||||
LANGUAGE_MODAL: 'language_modal',
|
||||
REFERENCE_MODAL: 'reference_modal',
|
||||
IMPORT_MODAL: 'import_modal',
|
||||
};
|
||||
|
||||
export default ModalEvents;
|
||||
|
||||
@ -14,18 +14,18 @@ const DEBOUNCE_WAIT_TIME = 4000;
|
||||
const defaultState = {
|
||||
isOffline: false,
|
||||
isUpdating: false,
|
||||
createResume: () => {},
|
||||
deleteResume: () => {},
|
||||
getResume: async () => {},
|
||||
getResumes: async () => {},
|
||||
createResume: () => {},
|
||||
updateResume: async () => {},
|
||||
debouncedUpdateResume: async () => {},
|
||||
debouncedUpdateMetadata: async () => {},
|
||||
deleteResume: () => {},
|
||||
};
|
||||
|
||||
const DatabaseContext = createContext(defaultState);
|
||||
|
||||
const DatabaseProvider = ({ children }) => {
|
||||
const [resumeId, setResumeId] = useState(false);
|
||||
const [isOffline, setOffline] = useState(false);
|
||||
const [isUpdating, setUpdating] = useState(false);
|
||||
const { user } = useContext(UserContext);
|
||||
@ -38,6 +38,7 @@ const DatabaseProvider = ({ children }) => {
|
||||
}, []);
|
||||
|
||||
const getResume = async (id) => {
|
||||
setResumeId(id);
|
||||
const snapshot = await firebase
|
||||
.database()
|
||||
.ref(`users/${user.uid}/resumes/${id}`)
|
||||
@ -71,13 +72,11 @@ const DatabaseProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
const updateResume = async (resume) => {
|
||||
const { id } = resume;
|
||||
|
||||
setUpdating(true);
|
||||
|
||||
await firebase
|
||||
.database()
|
||||
.ref(`users/${user.uid}/resumes/${id}`)
|
||||
.ref(`users/${user.uid}/resumes/${resumeId}`)
|
||||
.update({
|
||||
...resume,
|
||||
updatedAt: firebase.database.ServerValue.TIMESTAMP,
|
||||
@ -88,22 +87,6 @@ const DatabaseProvider = ({ children }) => {
|
||||
|
||||
const debouncedUpdateResume = debounce(updateResume, DEBOUNCE_WAIT_TIME);
|
||||
|
||||
const updateMetadata = async (resumeId, metadata) => {
|
||||
setUpdating(true);
|
||||
|
||||
await firebase
|
||||
.database()
|
||||
.ref(`users/${user.uid}/resumes/${resumeId}`)
|
||||
.update({
|
||||
metadata,
|
||||
updatedAt: firebase.database.ServerValue.TIMESTAMP,
|
||||
});
|
||||
|
||||
setUpdating(false);
|
||||
};
|
||||
|
||||
const debouncedUpdateMetadata = debounce(updateMetadata, DEBOUNCE_WAIT_TIME);
|
||||
|
||||
const deleteResume = (id) => {
|
||||
firebase.database().ref(`users/${user.uid}/resumes/${id}`).remove();
|
||||
};
|
||||
@ -116,9 +99,8 @@ const DatabaseProvider = ({ children }) => {
|
||||
getResume,
|
||||
createResume,
|
||||
updateResume,
|
||||
debouncedUpdateResume,
|
||||
debouncedUpdateMetadata,
|
||||
deleteResume,
|
||||
debouncedUpdateResume,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -90,6 +90,11 @@ const ResumeProvider = ({ children }) => {
|
||||
debouncedUpdateResume(newState);
|
||||
return newState;
|
||||
|
||||
case 'on_import':
|
||||
newState = { id: state.id, ...payload };
|
||||
debouncedUpdateResume(newState);
|
||||
return newState;
|
||||
|
||||
case 'set_data':
|
||||
return payload;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const colors = [
|
||||
const colorOptions = [
|
||||
'#f44336',
|
||||
'#E91E63',
|
||||
'#9C27B0',
|
||||
@ -17,4 +17,4 @@ const colors = [
|
||||
'#FF5722',
|
||||
];
|
||||
|
||||
export default colors;
|
||||
export default colorOptions;
|
||||
@ -1,4 +1,4 @@
|
||||
const fonts = [
|
||||
const fontOptions = [
|
||||
'Lato',
|
||||
'Montserrat',
|
||||
'Nunito',
|
||||
@ -9,4 +9,4 @@ const fonts = [
|
||||
'Titillium Web',
|
||||
];
|
||||
|
||||
export default fonts;
|
||||
export default fontOptions;
|
||||
@ -1,4 +1,4 @@
|
||||
const templates = [
|
||||
const templateOptions = [
|
||||
{
|
||||
id: 'onyx',
|
||||
name: 'Onyx',
|
||||
@ -11,4 +11,4 @@ const templates = [
|
||||
},
|
||||
];
|
||||
|
||||
export default templates;
|
||||
export default templateOptions;
|
||||
@ -39,23 +39,25 @@ const AuthModal = () => {
|
||||
|
||||
const loggedInAction = (
|
||||
<>
|
||||
<Button outline className="mr-8" title="Logout" onClick={logout} />
|
||||
<Button title="Go to App" onClick={handleGotoApp} />
|
||||
<Button outline className="mr-8" onClick={logout}>
|
||||
Logout
|
||||
</Button>
|
||||
<Button title="" onClick={handleGotoApp}>
|
||||
Go to App
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
const loggedOutAction = (
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
title="Sign in with Google"
|
||||
onClick={handleSignInWithGoogle}
|
||||
/>
|
||||
<Button isLoading={isLoading} onClick={handleSignInWithGoogle}>
|
||||
Sign in with Google
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
state={[open, setOpen]}
|
||||
title={getTitle()}
|
||||
state={[open, setOpen]}
|
||||
action={user ? loggedInAction : loggedOutAction}
|
||||
>
|
||||
<p>{getMessage()}</p>
|
||||
|
||||
@ -9,7 +9,7 @@ import { handleKeyUp } from '../utils';
|
||||
import styles from './BaseModal.module.css';
|
||||
|
||||
const BaseModal = forwardRef(
|
||||
({ title, state, children, action, onDestroy }, ref) => {
|
||||
({ title, state, children, action, hideActions = false, onDestroy }, ref) => {
|
||||
const [open, setOpen] = state;
|
||||
|
||||
const handleClose = () => {
|
||||
@ -44,16 +44,15 @@ const BaseModal = forwardRef(
|
||||
|
||||
<div className={styles.body}>{children}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
outline
|
||||
title="Cancel"
|
||||
className="mr-8"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
{!hideActions && (
|
||||
<div className={styles.actions}>
|
||||
<Button outline className="mr-8" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{action}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fade>
|
||||
</Modal>
|
||||
|
||||
@ -79,7 +79,9 @@ const DataModal = ({
|
||||
: title.create;
|
||||
|
||||
const submitAction = (
|
||||
<Button type="submit" title={getTitle} onClick={() => onSubmit(values)} />
|
||||
<Button type="submit" onClick={() => onSubmit(values)}>
|
||||
{getTitle}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const onDestroy = () => {
|
||||
|
||||
@ -5,6 +5,7 @@ import AwardModal from './sections/AwardModal';
|
||||
import CertificateModal from './sections/CertificateModal';
|
||||
import EducationModal from './sections/EducationModal';
|
||||
import HobbyModal from './sections/HobbyModal';
|
||||
import ImportModal from './sections/ImportModal';
|
||||
import LanguageModal from './sections/LanguageModal';
|
||||
import ReferenceModal from './sections/ReferenceModal';
|
||||
import SkillModal from './sections/SkillModal';
|
||||
@ -25,6 +26,7 @@ const ModalRegistrar = () => {
|
||||
<HobbyModal />
|
||||
<LanguageModal />
|
||||
<ReferenceModal />
|
||||
<ImportModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
95
src/modals/sections/ImportModal.js
Normal file
95
src/modals/sections/ImportModal.js
Normal file
@ -0,0 +1,95 @@
|
||||
import React, { memo, useContext, useEffect, useState, useRef } from 'react';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import ModalContext from '../../contexts/ModalContext';
|
||||
import BaseModal from '../BaseModal';
|
||||
import Button from '../../components/shared/Button';
|
||||
import { useDispatch } from '../../contexts/ResumeContext';
|
||||
|
||||
const ImportModal = () => {
|
||||
const fileInputRef = useRef(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
|
||||
useEffect(() => {
|
||||
const unbind = emitter.on(events.IMPORT_MODAL, () => setOpen(true));
|
||||
|
||||
return () => unbind();
|
||||
}, [emitter, events]);
|
||||
|
||||
const importReactiveResumeJson = (event) => {
|
||||
const fr = new FileReader();
|
||||
fr.addEventListener('load', () => {
|
||||
const payload = JSON.parse(fr.result);
|
||||
dispatch({ type: 'on_import', payload });
|
||||
setOpen(false);
|
||||
});
|
||||
fr.readAsText(event.target.files[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal hideActions state={[open, setOpen]} title="Import Data">
|
||||
<div>
|
||||
<h5 className="text-xl font-semibold mb-4">
|
||||
Import from Reactive Resume
|
||||
</h5>
|
||||
|
||||
<p>
|
||||
Reactive Resume has it's own schema format to make the most of
|
||||
all the customizable capabilities it has to offer. If you'd like
|
||||
to import a backup of your resume made with this app, just upload the
|
||||
file using the button below.
|
||||
</p>
|
||||
|
||||
<Button className="mt-5" onClick={() => fileInputRef.current.click()}>
|
||||
Select File
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={importReactiveResumeJson}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<div>
|
||||
<h5 className="text-xl font-semibold mb-4">Import from JSON Resume</h5>
|
||||
|
||||
<p>
|
||||
<a href="https://jsonresume.org/">JSON Resume</a> is an open standard
|
||||
for resume schema structure. If you are one of the many enthusiasts
|
||||
who have their resume ready in this format, all it takes it just one
|
||||
click to get started with Reactive Resume.
|
||||
</p>
|
||||
|
||||
<Tooltip title="Coming Soon" placement="right" arrow>
|
||||
<div className="mt-5 inline-block">
|
||||
<Button className="opacity-50">Select File</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<hr className="my-8" />
|
||||
|
||||
<div>
|
||||
<h5 className="text-xl font-semibold mb-4">Import from LinkedIn</h5>
|
||||
|
||||
<p>
|
||||
You can import a JSON that was exported from Reactive Resume by
|
||||
clicking on the button below and selecting the appropriate file.
|
||||
</p>
|
||||
|
||||
<Tooltip title="Coming Soon" placement="right" arrow>
|
||||
<div className="mt-5 inline-block">
|
||||
<Button className="opacity-50">Select File</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ImportModal);
|
||||
Reference in New Issue
Block a user