mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 01:15:26 +10:00
clear everything
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
/* eslint-disable react/no-danger */
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { graphql } from 'gatsby';
|
||||
import React from 'react';
|
||||
import Hero from './landing/Hero';
|
||||
import * as styles from './Blog.module.css';
|
||||
import Wrapper from './shared/Wrapper';
|
||||
|
||||
export default function Template({ data }) {
|
||||
const { markdownRemark } = data;
|
||||
const { frontmatter, html } = markdownRemark;
|
||||
return (
|
||||
<Wrapper>
|
||||
<div className="my-24 container">
|
||||
<Hero />
|
||||
|
||||
<Helmet>
|
||||
<title>{frontmatter.title} | Reactive Resume</title>
|
||||
<link
|
||||
rel="canonical"
|
||||
href={`https://rxresu.me/blog/${frontmatter.slug}`}
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
<h1 className="mt-16 text-3xl">{frontmatter.title}</h1>
|
||||
<h2>{frontmatter.date}</h2>
|
||||
<div
|
||||
className={styles.container}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query ($slug: String!) {
|
||||
markdownRemark(frontmatter: { slug: { eq: $slug } }) {
|
||||
html
|
||||
frontmatter {
|
||||
date(formatString: "MMMM DD, YYYY")
|
||||
slug
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,11 +0,0 @@
|
||||
.container {
|
||||
@apply leading-loose my-8;
|
||||
}
|
||||
|
||||
.container p {
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
.container h4 {
|
||||
@apply font-medium text-xl;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import * as styles from './Artboard.module.css';
|
||||
import { useSelector } from '../../../contexts/ResumeContext';
|
||||
import Castform from '../../../templates/Castform';
|
||||
import Celebi from '../../../templates/Celebi';
|
||||
import Gengar from '../../../templates/Gengar';
|
||||
import Glalie from '../../../templates/Glalie';
|
||||
import Onyx from '../../../templates/Onyx';
|
||||
import Pikachu from '../../../templates/Pikachu';
|
||||
|
||||
const Artboard = () => {
|
||||
const state = useSelector();
|
||||
const { t } = useTranslation();
|
||||
const { id, name, metadata } = state;
|
||||
const { template } = metadata;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{name} | {t('shared.appName')}
|
||||
</title>
|
||||
<link rel="canonical" href={`https://rxresu.me/app/builder/${id}`} />
|
||||
</Helmet>
|
||||
|
||||
<div id="page" className={styles.container}>
|
||||
{template === 'onyx' && <Onyx data={state} />}
|
||||
{template === 'pikachu' && <Pikachu data={state} />}
|
||||
{template === 'gengar' && <Gengar data={state} />}
|
||||
{template === 'castform' && <Castform data={state} />}
|
||||
{template === 'glalie' && <Glalie data={state} />}
|
||||
{template === 'celebi' && <Celebi data={state} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Artboard);
|
||||
@@ -1,11 +0,0 @@
|
||||
@media screen {
|
||||
.container {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
overflow: scroll;
|
||||
margin: 2rem auto;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
@apply bg-white rounded;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Link } from 'gatsby';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Avatar from '../../shared/Avatar';
|
||||
import Logo from '../../shared/Logo';
|
||||
import * as styles from './LeftNavbar.module.css';
|
||||
import SectionIcon from '../../shared/SectionIcon';
|
||||
import sections from '../../../data/leftSections';
|
||||
|
||||
const LeftNavbar = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Tooltip title={t('builder.tooltips.backToDashboard')} placement="right">
|
||||
<div>
|
||||
<Link to="/app/dashboard">
|
||||
<Logo size="40px" />
|
||||
</Link>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<hr className="my-6" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 text-primary-500">
|
||||
{sections.map((x) => (
|
||||
<SectionIcon
|
||||
key={x.id}
|
||||
section={x}
|
||||
containerId="LeftSidebar"
|
||||
tooltipPlacement="right"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<hr className="mt-auto my-6" />
|
||||
|
||||
<Avatar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LeftNavbar);
|
||||
@@ -1,7 +0,0 @@
|
||||
.container {
|
||||
width: 75px;
|
||||
z-index: 20;
|
||||
box-shadow: var(--left-shadow);
|
||||
|
||||
@apply px-4 py-6 h-screen flex flex-col items-center;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { Element } from 'react-scroll';
|
||||
import React, { Fragment, memo, useContext } from 'react';
|
||||
import * as styles from './LeftSidebar.module.css';
|
||||
import Awards from './sections/Awards';
|
||||
import Certifications from './sections/Certifications';
|
||||
import Education from './sections/Education';
|
||||
import Hobbies from './sections/Hobbies';
|
||||
import Languages from './sections/Languages';
|
||||
import LeftNavbar from './LeftNavbar';
|
||||
import Objective from './sections/Objective';
|
||||
import Profile from './sections/Profile';
|
||||
import Projects from './sections/Projects';
|
||||
import References from './sections/References';
|
||||
import Skills from './sections/Skills';
|
||||
import Social from './sections/Social';
|
||||
import Work from './sections/Work';
|
||||
import sections from '../../../data/leftSections';
|
||||
import SettingsContext from '../../../contexts/SettingsContext';
|
||||
|
||||
const getComponent = (id) => {
|
||||
switch (id) {
|
||||
case 'profile':
|
||||
return Profile;
|
||||
case 'social':
|
||||
return Social;
|
||||
case 'objective':
|
||||
return Objective;
|
||||
case 'work':
|
||||
return Work;
|
||||
case 'education':
|
||||
return Education;
|
||||
case 'projects':
|
||||
return Projects;
|
||||
case 'awards':
|
||||
return Awards;
|
||||
case 'certifications':
|
||||
return Certifications;
|
||||
case 'skills':
|
||||
return Skills;
|
||||
case 'hobbies':
|
||||
return Hobbies;
|
||||
case 'languages':
|
||||
return Languages;
|
||||
case 'references':
|
||||
return References;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
const SidebarSection = ({ id, event }) => {
|
||||
const Component = getComponent(id);
|
||||
|
||||
return (
|
||||
<Fragment key={id}>
|
||||
<Element name={id}>
|
||||
<Component id={id} event={event} />
|
||||
</Element>
|
||||
<hr />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const LeftSidebar = () => {
|
||||
const { isSideBarOpen } = useContext(SettingsContext);
|
||||
return (
|
||||
<div className="flex">
|
||||
<LeftNavbar />
|
||||
{isSideBarOpen && (
|
||||
<div id="LeftSidebar" className={styles.container}>
|
||||
{sections.map(SidebarSection)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default memo(LeftSidebar);
|
||||
@@ -1,6 +0,0 @@
|
||||
.container {
|
||||
z-index: 10;
|
||||
|
||||
@apply w-full h-screen overflow-scroll p-8;
|
||||
@apply grid gap-8;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Awards = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List
|
||||
path={path}
|
||||
event={event}
|
||||
titlePath="title"
|
||||
subtitlePath="awarder"
|
||||
textPath="summary"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Awards);
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Certifications = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List
|
||||
path={path}
|
||||
event={event}
|
||||
titlePath="title"
|
||||
subtitlePath="issuer"
|
||||
textPath="summary"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Certifications);
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Education = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List
|
||||
hasDate
|
||||
path={path}
|
||||
event={event}
|
||||
titlePath="institution"
|
||||
textPath="field"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Education);
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Hobbies = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List path={path} event={event} titlePath="name" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Hobbies);
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Languages = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List path={path} event={event} titlePath="name" subtitlePath="fluency" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Languages);
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
|
||||
const Objective = ({ id }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="textarea"
|
||||
label={t('shared.forms.summary')}
|
||||
path="objective.body"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Objective);
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import PhotoUpload from '../../../shared/PhotoUpload';
|
||||
|
||||
const Profile = ({ id }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<PhotoUpload />
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Input
|
||||
name="firstName"
|
||||
label={t('builder.profile.firstName')}
|
||||
path="profile.firstName"
|
||||
/>
|
||||
<Input
|
||||
name="lastName"
|
||||
label={t('builder.profile.lastName')}
|
||||
path="profile.lastName"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
name="subtitle"
|
||||
label={t('shared.forms.subtitle')}
|
||||
path="profile.subtitle"
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="date"
|
||||
name="birthDate"
|
||||
label={t('builder.profile.birthDate')}
|
||||
path="profile.birthDate"
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<Input
|
||||
name="addressLine1"
|
||||
label={t('builder.profile.address.line1')}
|
||||
path="profile.address.line1"
|
||||
/>
|
||||
<Input
|
||||
name="addressLine2"
|
||||
label={t('builder.profile.address.line2')}
|
||||
path="profile.address.line2"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<Input
|
||||
name="city"
|
||||
label={t('builder.profile.address.city')}
|
||||
path="profile.address.city"
|
||||
/>
|
||||
<Input
|
||||
name="pincode"
|
||||
label={t('builder.profile.address.pincode')}
|
||||
path="profile.address.pincode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<Input
|
||||
name="phone"
|
||||
label={t('shared.forms.phone')}
|
||||
path="profile.phone"
|
||||
/>
|
||||
<Input
|
||||
name="website"
|
||||
label={t('shared.forms.website')}
|
||||
path="profile.website"
|
||||
/>
|
||||
<Input
|
||||
name="email"
|
||||
label={t('shared.forms.email')}
|
||||
path="profile.email"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Profile);
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Projects = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List
|
||||
path={path}
|
||||
event={event}
|
||||
titlePath="title"
|
||||
subtitlePath="link"
|
||||
textPath="summary"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Projects);
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const References = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List
|
||||
path={path}
|
||||
event={event}
|
||||
titlePath="name"
|
||||
subtitlePath="position"
|
||||
textPath="summary"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(References);
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Skills = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List path={path} event={event} titlePath="name" subtitlePath="level" />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Skills);
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Social = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List
|
||||
path={path}
|
||||
event={event}
|
||||
titlePath="network"
|
||||
subtitlePath="username"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Social);
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import List from '../../lists/List';
|
||||
|
||||
const Work = ({ id, event }) => {
|
||||
const path = `${id}.items`;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
name="heading"
|
||||
label={t('builder.sections.heading')}
|
||||
path={`${id}.heading`}
|
||||
/>
|
||||
|
||||
<List
|
||||
hasDate
|
||||
path={path}
|
||||
event={event}
|
||||
titlePath="company"
|
||||
textPath="summary"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Work);
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
const EmptyList = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="py-6 opacity-75 text-center">{t('builder.emptyList')}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(EmptyList);
|
||||
@@ -1,115 +0,0 @@
|
||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||
import { MdAdd } from 'react-icons/md';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import * as styles from './List.module.css';
|
||||
import { formatDateRange, reorder } from '../../../utils';
|
||||
import { useDispatch, useSelector } from '../../../contexts/ResumeContext';
|
||||
import Button from '../../shared/Button';
|
||||
import EmptyList from './EmptyList';
|
||||
import ListItem from './ListItem';
|
||||
import ModalContext from '../../../contexts/ModalContext';
|
||||
|
||||
const List = ({
|
||||
path,
|
||||
title,
|
||||
titlePath,
|
||||
subtitle,
|
||||
subtitlePath,
|
||||
text,
|
||||
textPath,
|
||||
hasDate,
|
||||
event,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const items = useSelector(path, []);
|
||||
const { emitter } = useContext(ModalContext);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleAdd = () => emitter.emit(event);
|
||||
|
||||
const handleEdit = (data) => emitter.emit(event, data);
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
const { source, destination } = result;
|
||||
|
||||
if (!destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceDroppableId = source.droppableId;
|
||||
const destinationDroppableId = destination.droppableId;
|
||||
if (sourceDroppableId !== destinationDroppableId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.index === destination.index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsReordered = reorder(items, source.index, destination.index);
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path,
|
||||
value: itemsReordered,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className={styles.list}>
|
||||
{isEmpty(items) ? (
|
||||
<EmptyList />
|
||||
) : (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId={path}>
|
||||
{(dropProvided) => (
|
||||
<div
|
||||
ref={dropProvided.innerRef}
|
||||
{...dropProvided.droppableProps}
|
||||
>
|
||||
{items.map((x, i) => (
|
||||
<ListItem
|
||||
key={x.id}
|
||||
data={x}
|
||||
path={path}
|
||||
title={title || get(x, titlePath, '')}
|
||||
subtitle={
|
||||
subtitle ||
|
||||
get(x, subtitlePath, '') ||
|
||||
(hasDate &&
|
||||
formatDateRange(
|
||||
{
|
||||
startDate: x.startDate,
|
||||
endDate: x.endDate,
|
||||
language: i18n.language,
|
||||
},
|
||||
t,
|
||||
))
|
||||
}
|
||||
text={text || get(x, textPath, '')}
|
||||
onEdit={() => handleEdit(x)}
|
||||
isFirst={i === 0}
|
||||
isLast={i === items.length - 1}
|
||||
index={i}
|
||||
/>
|
||||
))}
|
||||
{dropProvided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button outline icon={MdAdd} onClick={handleAdd} className="mt-8 ml-auto">
|
||||
{t('shared.buttons.add')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(List);
|
||||
@@ -1,3 +0,0 @@
|
||||
.list {
|
||||
@apply flex flex-col border border-primary-200 rounded;
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { IoIosArrowDown, IoIosArrowUp } from 'react-icons/io';
|
||||
import { MdMoreVert } from 'react-icons/md';
|
||||
import { Menu, MenuItem } from '@material-ui/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useState } from 'react';
|
||||
import Switch from '@material-ui/core/Switch';
|
||||
import { useDispatch } from '../../../contexts/ResumeContext';
|
||||
import * as styles from './ListItem.module.css';
|
||||
|
||||
const dataTestIdPrefix = 'list-item-';
|
||||
|
||||
const ListItem = ({
|
||||
title,
|
||||
subtitle,
|
||||
text,
|
||||
path,
|
||||
data,
|
||||
isFirst,
|
||||
isLast,
|
||||
onEdit,
|
||||
index,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [deleteClickCount, setDeleteClickCount] = useState(0);
|
||||
|
||||
const handleClick = (event) => setAnchorEl(event.currentTarget);
|
||||
const toggleSwitchState = 'isVisible' in data ? data.isVisible : true;
|
||||
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMoveUp = () => {
|
||||
dispatch({
|
||||
type: 'on_move_item_up',
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMoveDown = () => {
|
||||
dispatch({
|
||||
type: 'on_move_item_down',
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch({
|
||||
type: 'on_delete_item',
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleToggleUse = () => {
|
||||
dispatch({
|
||||
type: 'on_toggle_use_item',
|
||||
payload: {
|
||||
path,
|
||||
value: data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const checkConfirmationAndDelte = () => {
|
||||
if (deleteClickCount === 1) {
|
||||
handleDelete();
|
||||
} else if (deleteClickCount < 1) {
|
||||
setDeleteClickCount(deleteClickCount + 1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable draggableId={data.id} index={index}>
|
||||
{(dragProvided) => (
|
||||
<div
|
||||
ref={dragProvided.innerRef}
|
||||
className={styles.listItem}
|
||||
data-testid={`${dataTestIdPrefix}${path}`}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
<div className="grid">
|
||||
<span className="font-medium truncate">{title}</span>
|
||||
|
||||
{subtitle && (
|
||||
<span className="mt-1 text-sm opacity-75 truncate">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{text && (
|
||||
<span className="w-4/5 mt-5 text-sm opacity-75 truncate">
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.toggleButton}>
|
||||
<Switch
|
||||
checked={toggleSwitchState}
|
||||
onChange={handleToggleUse}
|
||||
color="primary"
|
||||
name="toggleSwitch"
|
||||
className="toggle-button"
|
||||
inputProps={{ 'aria-label': 'primary checkbox' }}
|
||||
/>
|
||||
|
||||
<div className={styles.menu}>
|
||||
<MdMoreVert
|
||||
size="18px"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
className="cursor-context-menu"
|
||||
/>
|
||||
<Menu
|
||||
keepMounted
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => {
|
||||
setDeleteClickCount(0);
|
||||
handleClose();
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
>
|
||||
<div className="flex items-center space-around">
|
||||
<MenuItem disabled={isFirst} onClick={handleMoveUp}>
|
||||
<IoIosArrowUp size="18px" />
|
||||
</MenuItem>
|
||||
<MenuItem disabled={isLast} onClick={handleMoveDown}>
|
||||
<IoIosArrowDown size="18px" />
|
||||
</MenuItem>
|
||||
</div>
|
||||
<MenuItem onClick={handleEdit}>
|
||||
{t('shared.buttons.edit')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={checkConfirmationAndDelte}>
|
||||
<span className="text-red-600 font-medium">
|
||||
{deleteClickCount
|
||||
? t('shared.buttons.confirmation')
|
||||
: t('shared.buttons.delete')}
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ListItem);
|
||||
|
||||
export { dataTestIdPrefix };
|
||||
@@ -1,21 +0,0 @@
|
||||
.list-item {
|
||||
@apply flex items-center justify-between border-t border-primary-200 px-6 py-5;
|
||||
}
|
||||
|
||||
.list-item:first-child {
|
||||
@apply border-t-0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
@apply opacity-0;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.list-item:hover .menu {
|
||||
@apply opacity-100;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
@apply flex items-center justify-center
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { BsBoxArrowRight, BsBoxArrowLeft } from 'react-icons/bs';
|
||||
import * as styles from './RightNavbar.module.css';
|
||||
import SectionIcon from '../../shared/SectionIcon';
|
||||
import SyncIndicator from './SyncIndicator';
|
||||
import sections from '../../../data/rightSections';
|
||||
import SettingsContext from '../../../contexts/SettingsContext';
|
||||
|
||||
const SideBarToggleIcon = ({ className }) => {
|
||||
const { isSideBarOpen, setSideBarToggle } = useContext(SettingsContext);
|
||||
const ToggleIcon = isSideBarOpen ? BsBoxArrowRight : BsBoxArrowLeft;
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={() => setSideBarToggle(!isSideBarOpen)}
|
||||
>
|
||||
<ToggleIcon size="18px" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
const RightNavbar = () => (
|
||||
<div className={styles.container}>
|
||||
<div className="grid grid-cols-1 gap-4 text-primary-500">
|
||||
{sections.map((x) => (
|
||||
<SectionIcon
|
||||
key={x.id}
|
||||
section={x}
|
||||
containerId="RightSidebar"
|
||||
tooltipPlacement="left"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<SideBarToggleIcon className="mt-auto" />
|
||||
<hr className=" my-6" />
|
||||
<SyncIndicator />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default memo(RightNavbar);
|
||||
@@ -1,7 +0,0 @@
|
||||
.container {
|
||||
width: 75px;
|
||||
z-index: 20;
|
||||
box-shadow: var(--right-shadow);
|
||||
|
||||
@apply px-4 py-6 h-screen flex flex-col items-center;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Element } from 'react-scroll';
|
||||
import React, { Fragment, memo, useContext } from 'react';
|
||||
import * as styles from './RightSidebar.module.css';
|
||||
import About from './sections/About';
|
||||
import Actions from './sections/Actions';
|
||||
import Colors from './sections/Colors';
|
||||
import FontSize from './sections/FontSize';
|
||||
import Fonts from './sections/Fonts';
|
||||
import Layout from './sections/Layout';
|
||||
import RightNavbar from './RightNavbar';
|
||||
import Settings from './sections/Settings';
|
||||
import Templates from './sections/Templates';
|
||||
import sections from '../../../data/rightSections';
|
||||
import SettingsContext from '../../../contexts/SettingsContext';
|
||||
|
||||
const getComponent = (id) => {
|
||||
switch (id) {
|
||||
case 'templates':
|
||||
return Templates;
|
||||
case 'layout':
|
||||
return Layout;
|
||||
case 'colors':
|
||||
return Colors;
|
||||
case 'fonts':
|
||||
return Fonts;
|
||||
case 'actions':
|
||||
return Actions;
|
||||
case 'settings':
|
||||
return Settings;
|
||||
case 'about':
|
||||
return About;
|
||||
case 'font-size':
|
||||
return FontSize;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
const SidebarSection = ({ id, event }) => {
|
||||
const Component = getComponent(id);
|
||||
return (
|
||||
<Fragment key={id}>
|
||||
<Element name={id}>
|
||||
<Component id={id} event={event} />
|
||||
</Element>
|
||||
<hr />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const RightSidebar = () => {
|
||||
const { isSideBarOpen } = useContext(SettingsContext);
|
||||
return (
|
||||
<div className="flex">
|
||||
{isSideBarOpen && (
|
||||
<div id="RightSidebar" className={styles.container}>
|
||||
{sections.map(SidebarSection)}
|
||||
</div>
|
||||
)}
|
||||
<RightNavbar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RightSidebar);
|
||||
@@ -1,6 +0,0 @@
|
||||
.container {
|
||||
z-index: 10;
|
||||
|
||||
@apply w-full h-screen overflow-scroll p-8;
|
||||
@apply grid gap-8;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { MdSync } from 'react-icons/md';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import cx from 'classnames';
|
||||
import DatabaseContext from '../../../contexts/DatabaseContext';
|
||||
|
||||
const SyncIndicator = () => {
|
||||
const { isUpdating } = useContext(DatabaseContext);
|
||||
|
||||
return (
|
||||
<div className="text-4xl">
|
||||
<MdSync className={cx({ spin: isUpdating })} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SyncIndicator);
|
||||
@@ -1,107 +0,0 @@
|
||||
import { FaBug, FaCoffee, FaExternalLinkAlt } from 'react-icons/fa';
|
||||
import { MdCode } from 'react-icons/md';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { graphql, useStaticQuery } from 'gatsby';
|
||||
import React, { memo } from 'react';
|
||||
import Button from '../../../shared/Button';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import * as styles from './About.module.css';
|
||||
|
||||
const About = ({ id }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { site } = useStaticQuery(graphql`
|
||||
query {
|
||||
site {
|
||||
siteMetadata {
|
||||
version
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.about.donate.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">
|
||||
<Trans t={t} i18nKey="builder.about.donate.text">
|
||||
A<span className="font-bold">B</span>C
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<a
|
||||
href="https://www.buymeacoffee.com/AmruthPillai"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Button icon={FaCoffee}>{t('builder.about.donate.button')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.about.bugFeature.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">{t('builder.about.bugFeature.text')}</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<a
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume/issues/new"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Button icon={FaBug}>{t('builder.about.bugFeature.button')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.about.appreciate.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">{t('builder.about.appreciate.text')}</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<a href="https://amruthpillai.com" rel="noreferrer" target="_blank">
|
||||
<Button icon={FaExternalLinkAlt}>amruthpillai.com</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.about.sourceCode.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">{t('builder.about.sourceCode.text')}</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<a
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Button icon={MdCode}>
|
||||
{t('builder.about.sourceCode.button')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-4 text-center opacity-50 text-sm flex flex-col items-center justify-center">
|
||||
<Trans t={t} i18nKey="builder.about.footer">
|
||||
A
|
||||
<a href="https://amruthpillai.com" rel="noreferrer" target="_blank">
|
||||
B
|
||||
</a>
|
||||
</Trans>
|
||||
|
||||
<span className="mt-2 font-medium">v{site.siteMetadata.version}</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(About);
|
||||
@@ -1,11 +0,0 @@
|
||||
.container {
|
||||
@apply bg-primary-100 rounded grid gap-5 p-8;
|
||||
}
|
||||
|
||||
.container h5 {
|
||||
@apply text-lg font-semibold;
|
||||
}
|
||||
|
||||
.container p {
|
||||
@apply text-sm font-medium;
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { FaFileExport, FaFileImport } from 'react-icons/fa';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useContext, useState } from 'react';
|
||||
import * as styles from './Actions.module.css';
|
||||
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
|
||||
import Button from '../../../shared/Button';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import ModalContext from '../../../../contexts/ModalContext';
|
||||
|
||||
const Actions = ({ id }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loadDemoText, setLoadDemoText] = useState(
|
||||
t('builder.actions.loadDemoData.button'),
|
||||
);
|
||||
const [resetText, setResetText] = useState(
|
||||
t('builder.actions.resetEverything.button'),
|
||||
);
|
||||
|
||||
const state = useSelector();
|
||||
const dispatch = useDispatch();
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
|
||||
const handleImport = () => emitter.emit(events.IMPORT_MODAL);
|
||||
|
||||
const handleExport = () => emitter.emit(events.EXPORT_MODAL);
|
||||
|
||||
const getSharableUrl = () => {
|
||||
const shareId = state.id;
|
||||
return `https://rxresu.me/r/${shareId}`;
|
||||
};
|
||||
|
||||
const handleOpenLink = () => {
|
||||
if (typeof window !== `undefined`) {
|
||||
window && window.open(getSharableUrl());
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadDemo = () => {
|
||||
if (loadDemoText === t('builder.actions.loadDemoData.button')) {
|
||||
setLoadDemoText(t('shared.buttons.confirmation'));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'load_demo_data' });
|
||||
setLoadDemoText(t('builder.actions.loadDemoData.button'));
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (resetText === t('builder.actions.resetEverything.button')) {
|
||||
setResetText(t('shared.buttons.confirmation'));
|
||||
return;
|
||||
}
|
||||
|
||||
setResetText(t('builder.actions.resetEverything.button'));
|
||||
dispatch({ type: 'reset_data' });
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.actions.import.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">{t('builder.actions.import.text')}</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button icon={FaFileImport} onClick={handleImport}>
|
||||
{t('builder.actions.import.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.actions.export.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">{t('builder.actions.export.text')}</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button icon={FaFileExport} onClick={handleExport}>
|
||||
{t('builder.actions.export.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.actions.share.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">{t('builder.actions.share.text')}</p>
|
||||
|
||||
<div>
|
||||
<Input
|
||||
type="action"
|
||||
value={getSharableUrl()}
|
||||
onClick={handleOpenLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.actions.loadDemoData.button')}</h5>
|
||||
|
||||
<p className="leading-loose">
|
||||
{t('builder.actions.loadDemoData.text')}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button onClick={handleLoadDemo}>{loadDemoText}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.actions.resetEverything.button')}</h5>
|
||||
|
||||
<p className="leading-loose">
|
||||
{t('builder.actions.resetEverything.text')}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button onClick={handleReset}>{resetText}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Actions);
|
||||
@@ -1,11 +0,0 @@
|
||||
.container {
|
||||
@apply bg-primary-100 rounded grid gap-5 p-8;
|
||||
}
|
||||
|
||||
.container h5 {
|
||||
@apply text-lg font-semibold;
|
||||
}
|
||||
|
||||
.container p {
|
||||
@apply text-sm font-medium;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import * as styles from './Colors.module.css';
|
||||
import { handleKeyUp } from '../../../../utils';
|
||||
import { useDispatch } from '../../../../contexts/ResumeContext';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import colorOptions from '../../../../data/colorOptions';
|
||||
|
||||
const Colors = ({ id }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleClick = (value) => {
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path: 'metadata.colors.primary',
|
||||
value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<div className="mb-6 grid grid-cols-8 gap-x-2 gap-y-6">
|
||||
{colorOptions.map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={styles.circle}
|
||||
style={{ backgroundColor: color }}
|
||||
onKeyUp={(e) => handleKeyUp(e, () => handleClick(color))}
|
||||
onClick={() => handleClick(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
type="color"
|
||||
name="primary"
|
||||
label={t('builder.colors.primary')}
|
||||
placeholder="#FF4081"
|
||||
path="metadata.colors.primary"
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="color"
|
||||
name="text"
|
||||
label={t('builder.colors.text')}
|
||||
placeholder="#444444"
|
||||
path="metadata.colors.text"
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="color"
|
||||
name="background"
|
||||
label={t('builder.colors.background')}
|
||||
placeholder="#FFFFFF"
|
||||
path="metadata.colors.background"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Colors);
|
||||
@@ -1,11 +0,0 @@
|
||||
.circle {
|
||||
@apply cursor-pointer rounded-full h-6 w-6;
|
||||
}
|
||||
|
||||
.circle:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.circle:hover {
|
||||
@apply opacity-75;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/control-has-associated-label */
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import { scaler } from '../../../../utils';
|
||||
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import fontSizeOptions from '../../../../data/fontSizeOptions';
|
||||
|
||||
const FontSizes = ({ id }) => {
|
||||
const steps = 20;
|
||||
const min = 0;
|
||||
const max = min + steps - 1;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const fontSize = useSelector('metadata.fontSize');
|
||||
const [scale, setScale] = useState(fontSize || 7);
|
||||
|
||||
useEffect(() => {
|
||||
for (const [key, sizeDefault] of Object.entries(fontSizeOptions)) {
|
||||
document.documentElement.style.setProperty(
|
||||
key,
|
||||
`${scaler(scale) * sizeDefault}rem`,
|
||||
);
|
||||
}
|
||||
}, [scale]);
|
||||
|
||||
const onChange = (event) => {
|
||||
const { value } = event.target;
|
||||
|
||||
setScale(value);
|
||||
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path: 'metadata.fontSize',
|
||||
value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<input
|
||||
step={1}
|
||||
min={min}
|
||||
max={max}
|
||||
type="range"
|
||||
onChange={onChange}
|
||||
defaultValue={scale}
|
||||
className="rounded-lg overflow-hidden appearance-none bg-gray-400 h-4 w-full"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(FontSizes);
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import * as styles from './Fonts.module.css';
|
||||
import { handleKeyUp } from '../../../../utils';
|
||||
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import fontOptions from '../../../../data/fontOptions';
|
||||
|
||||
const Fonts = ({ id }) => {
|
||||
const dispatch = useDispatch();
|
||||
const font = useSelector('metadata.font');
|
||||
|
||||
const handleClick = (value) => {
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path: 'metadata.font',
|
||||
value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{fontOptions.map((x) => (
|
||||
<div
|
||||
key={x}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
style={{ fontFamily: x }}
|
||||
className={cx(styles.font, { [styles.selected]: font === x })}
|
||||
onKeyUp={(e) => handleKeyUp(e, () => handleClick(x))}
|
||||
onClick={() => handleClick(x)}
|
||||
>
|
||||
{x}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Fonts);
|
||||
@@ -1,15 +0,0 @@
|
||||
.font {
|
||||
@apply px-6 py-6 text-xl font-medium truncate text-center border border-transparent bg-primary-100 rounded;
|
||||
}
|
||||
|
||||
.font:focus {
|
||||
@apply outline-none border border-primary-400;
|
||||
}
|
||||
|
||||
.font:hover {
|
||||
@apply border border-primary-400;
|
||||
}
|
||||
|
||||
.font.selected {
|
||||
@apply outline-none border border-primary-600;
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useState } from 'react';
|
||||
import * as styles from './Layout.module.css';
|
||||
import { move, reorder } from '../../../../utils';
|
||||
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
|
||||
import Button from '../../../shared/Button';
|
||||
import Heading from '../../../shared/Heading';
|
||||
|
||||
const Layout = ({ id }) => {
|
||||
const { t } = useTranslation();
|
||||
const [resetLayoutText, setResetLayoutText] = useState(
|
||||
t('builder.layout.reset'),
|
||||
);
|
||||
|
||||
const template = useSelector('metadata.template');
|
||||
const blocks = useSelector(`metadata.layout.${template}`, [[]]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onDragEnd = (result) => {
|
||||
const { source, destination } = result;
|
||||
|
||||
if (!destination) {
|
||||
return;
|
||||
}
|
||||
const sInd = +source.droppableId;
|
||||
const dInd = +destination.droppableId;
|
||||
|
||||
if (sInd === dInd) {
|
||||
const items = reorder(blocks[sInd], source.index, destination.index);
|
||||
const newState = [...blocks];
|
||||
newState[sInd] = items;
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path: `metadata.layout.${template}`,
|
||||
value: newState,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const newResult = move(blocks[sInd], blocks[dInd], source, destination);
|
||||
const newState = [...blocks];
|
||||
newState[sInd] = newResult[sInd];
|
||||
newState[dInd] = newResult[dInd];
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path: `metadata.layout.${template}`,
|
||||
value: newState,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetLayout = () => {
|
||||
if (resetLayoutText === t('builder.layout.reset')) {
|
||||
setResetLayoutText(t('shared.buttons.confirmation'));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'reset_layout' });
|
||||
setResetLayoutText(t('builder.layout.reset'));
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<p className="leading-loose">
|
||||
{t('builder.layout.text', { count: blocks.length })}
|
||||
</p>
|
||||
|
||||
<div className={`grid gap-8 grid-cols-${blocks.length}`}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
{blocks.map((el, ind) => (
|
||||
<Droppable key={ind} droppableId={`${ind}`}>
|
||||
{(dropProvided) => (
|
||||
<div
|
||||
ref={dropProvided.innerRef}
|
||||
className={styles.droppable}
|
||||
{...dropProvided.droppableProps}
|
||||
>
|
||||
<div className="grid gap-3">
|
||||
<span className="uppercase font-semibold text-xs">
|
||||
{t('builder.layout.block')} {ind + 1}
|
||||
</span>
|
||||
{el.map((item, index) => (
|
||||
<Draggable key={item} index={index} draggableId={item}>
|
||||
{(dragProvided) => (
|
||||
<div
|
||||
ref={dragProvided.innerRef}
|
||||
className={styles.draggable}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
{t(`builder.sections.${item}`)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
{dropProvided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<Button onClick={handleResetLayout}>{resetLayoutText}</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Layout);
|
||||
@@ -1,7 +0,0 @@
|
||||
.droppable {
|
||||
@apply p-6 bg-primary-100 col-span-1 rounded;
|
||||
}
|
||||
|
||||
.draggable {
|
||||
@apply px-4 py-2 font-medium rounded bg-primary-200;
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { FaAngleDown } from 'react-icons/fa';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { toast } from 'react-toastify';
|
||||
import React, { memo, useContext, useState } from 'react';
|
||||
import * as styles from './Settings.module.css';
|
||||
import { languages } from '../../../../i18n';
|
||||
import { useDispatch } from '../../../../contexts/ResumeContext';
|
||||
import Button from '../../../shared/Button';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import Input from '../../../shared/Input';
|
||||
import SettingsContext from '../../../../contexts/SettingsContext';
|
||||
import UserContext from '../../../../contexts/UserContext';
|
||||
import themeConfig from '../../../../data/themeConfig';
|
||||
|
||||
const Settings = ({ id }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [deleteText, setDeleteText] = useState(
|
||||
t('builder.settings.dangerZone.button'),
|
||||
);
|
||||
const [isDeleteAccountInProgress, setDeleteAccountInProgress] =
|
||||
useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { deleteAccount } = useContext(UserContext);
|
||||
const { theme, setTheme, language, setLanguage } =
|
||||
useContext(SettingsContext);
|
||||
|
||||
const handleChangeTheme = (e) => {
|
||||
setTheme(e.target.value);
|
||||
};
|
||||
|
||||
const handleChangeLanguage = (e) => {
|
||||
const lang = e.target.value;
|
||||
setLanguage(lang);
|
||||
dispatch({ type: 'change_language', payload: lang });
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
if (deleteText === t('builder.settings.dangerZone.button')) {
|
||||
setDeleteText(t('shared.buttons.confirmation'));
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleteAccountInProgress(true);
|
||||
|
||||
try {
|
||||
await deleteAccount();
|
||||
} catch (error) {
|
||||
toast.error('An error occurred deleting your account.');
|
||||
setDeleteAccountInProgress(false);
|
||||
setDeleteText(t('builder.settings.dangerZone.button'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<Input
|
||||
label={t('builder.settings.theme')}
|
||||
type="dropdown"
|
||||
options={Object.keys(themeConfig)}
|
||||
value={theme}
|
||||
onChange={handleChangeTheme}
|
||||
/>
|
||||
|
||||
<label>
|
||||
<span>{t('builder.settings.language')}</span>
|
||||
<div className="relative grid items-center">
|
||||
<select onChange={handleChangeLanguage} value={language}>
|
||||
{languages.map((x) => (
|
||||
<option key={x.code} value={x.code}>
|
||||
{x.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<FaAngleDown
|
||||
size="16px"
|
||||
className="absolute right-0 opacity-50 hover:opacity-75 mx-4"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<p className="text-sm leading-loose">
|
||||
<Trans t={t} i18nKey="builder.settings.translate">
|
||||
A
|
||||
<a
|
||||
href="https://github.com/AmruthPillai/Reactive-Resume#translation"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
B
|
||||
</a>
|
||||
C
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className={styles.container}>
|
||||
<h5>{t('builder.settings.dangerZone.heading')}</h5>
|
||||
|
||||
<p className="leading-loose">{t('builder.settings.dangerZone.text')}</p>
|
||||
|
||||
<div className="mt-4 flex">
|
||||
<Button
|
||||
isDelete
|
||||
onClick={handleDeleteAccount}
|
||||
isLoading={isDeleteAccountInProgress}
|
||||
>
|
||||
{deleteText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Settings);
|
||||
@@ -1,11 +0,0 @@
|
||||
.container {
|
||||
@apply bg-primary-100 rounded grid gap-5 p-8;
|
||||
}
|
||||
|
||||
.container h5 {
|
||||
@apply text-lg font-semibold;
|
||||
}
|
||||
|
||||
.container p {
|
||||
@apply text-sm font-medium;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { graphql, useStaticQuery } from 'gatsby';
|
||||
import { GatsbyImage } from 'gatsby-plugin-image';
|
||||
import React, { memo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import * as styles from './Templates.module.css';
|
||||
import { handleKeyUp } from '../../../../utils';
|
||||
import { useDispatch, useSelector } from '../../../../contexts/ResumeContext';
|
||||
import Heading from '../../../shared/Heading';
|
||||
import templateOptions from '../../../../data/templateOptions';
|
||||
|
||||
const Templates = ({ id }) => {
|
||||
const dispatch = useDispatch();
|
||||
const template = useSelector('metadata.template');
|
||||
|
||||
const previews = useStaticQuery(graphql`
|
||||
query {
|
||||
onyx: file(relativePath: { eq: "templates/onyx.png" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: FIXED, height: 240)
|
||||
}
|
||||
}
|
||||
pikachu: file(relativePath: { eq: "templates/pikachu.png" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: FIXED, height: 240)
|
||||
}
|
||||
}
|
||||
gengar: file(relativePath: { eq: "templates/gengar.png" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: FIXED, height: 240)
|
||||
}
|
||||
}
|
||||
castform: file(relativePath: { eq: "templates/castform.png" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: FIXED, height: 240)
|
||||
}
|
||||
}
|
||||
glalie: file(relativePath: { eq: "templates/glalie.png" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: FIXED, height: 240)
|
||||
}
|
||||
}
|
||||
celebi: file(relativePath: { eq: "templates/celebi.png" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: FIXED, height: 240)
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const handleClick = (value) => {
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path: 'metadata.template',
|
||||
value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Heading id={id} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{templateOptions.map((x) => (
|
||||
<div
|
||||
key={x.id}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
onKeyUp={(e) => handleKeyUp(e, () => handleClick(x.id))}
|
||||
onClick={() => handleClick(x.id)}
|
||||
className={cx(styles.template, {
|
||||
[styles.selected]: template === x.id,
|
||||
})}
|
||||
>
|
||||
<GatsbyImage
|
||||
alt={x.name}
|
||||
image={previews[x.id].childImageSharp.gatsbyImageData}
|
||||
/>
|
||||
<span>{x.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Templates);
|
||||
@@ -1,27 +0,0 @@
|
||||
.template {
|
||||
@apply w-full rounded flex flex-col items-center;
|
||||
}
|
||||
|
||||
.template:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.template img {
|
||||
height: 240px;
|
||||
|
||||
@apply w-full object-cover border border-transparent rounded;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.template.selected img {
|
||||
@apply border-primary-600;
|
||||
}
|
||||
|
||||
.template:hover img {
|
||||
@apply cursor-pointer opacity-75;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.template span {
|
||||
@apply mt-1 text-center text-sm font-semibold;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { MdAdd } from 'react-icons/md';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import * as styles from './CreateResume.module.css';
|
||||
import { handleKeyUp } from '../../utils';
|
||||
import ModalContext from '../../contexts/ModalContext';
|
||||
|
||||
const createResumeButtonDataTestId = 'create-resume-button';
|
||||
|
||||
const CreateResume = () => {
|
||||
const { t } = useTranslation();
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
|
||||
const handleClick = () => emitter.emit(events.CREATE_RESUME_MODAL);
|
||||
|
||||
return (
|
||||
<div className={styles.resume}>
|
||||
<div className={styles.backdrop}>
|
||||
<MdAdd size="48" />
|
||||
</div>
|
||||
<div
|
||||
data-testid={createResumeButtonDataTestId}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className={styles.page}
|
||||
onClick={handleClick}
|
||||
onKeyUp={(e) => handleKeyUp(e, handleClick)}
|
||||
>
|
||||
<MdAdd size="48" />
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
<p>{t('dashboard.createResume')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CreateResume);
|
||||
|
||||
export { createResumeButtonDataTestId };
|
||||
@@ -1,34 +0,0 @@
|
||||
.resume {
|
||||
@apply relative flex flex-col items-center;
|
||||
}
|
||||
|
||||
.resume > .backdrop {
|
||||
max-width: 184px;
|
||||
height: 260px;
|
||||
|
||||
@apply rounded absolute w-full bg-black text-white shadow;
|
||||
@apply absolute flex justify-center items-center;
|
||||
}
|
||||
|
||||
.resume > .page {
|
||||
max-width: 184px;
|
||||
height: 260px;
|
||||
|
||||
@apply rounded absolute w-full bg-primary-100 text-primary-400;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
@apply cursor-pointer absolute flex justify-center items-center;
|
||||
}
|
||||
|
||||
.resume > .page:hover {
|
||||
@apply transition-opacity duration-200 ease-in-out opacity-25;
|
||||
}
|
||||
|
||||
.resume > .meta {
|
||||
margin-top: 260px;
|
||||
|
||||
@apply text-center;
|
||||
}
|
||||
|
||||
.resume > .meta p {
|
||||
@apply mt-3 font-medium leading-normal;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { MdMoreHoriz, MdOpenInNew } from 'react-icons/md';
|
||||
import { Menu, MenuItem } from '@material-ui/core';
|
||||
import { navigate } from 'gatsby';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import DatabaseContext from '../../contexts/DatabaseContext';
|
||||
import ModalContext from '../../contexts/ModalContext';
|
||||
import * as styles from './ResumePreview.module.css';
|
||||
|
||||
const menuToggleDataTestIdPrefix = 'resume-preview-menu-toggle-';
|
||||
|
||||
const ResumePreview = ({ resume }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
const { duplicateResume, deleteResume } = useContext(DatabaseContext);
|
||||
|
||||
const handleOpen = () => navigate(`/app/builder/${resume.id}`);
|
||||
|
||||
const handleMenuClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleDuplicate = () => {
|
||||
duplicateResume(resume);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleRename = () => {
|
||||
emitter.emit(events.CREATE_RESUME_MODAL, resume);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteResume(resume.id);
|
||||
toast(t('dashboard.toasts.deleted', { name: resume.name }));
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.resume}>
|
||||
<div className={styles.backdrop}>
|
||||
<img src={resume.preview} alt={resume.name} />
|
||||
</div>
|
||||
<div className={styles.page}>
|
||||
<MdOpenInNew
|
||||
color="#fff"
|
||||
size="48"
|
||||
className="cursor-pointer"
|
||||
onClick={handleOpen}
|
||||
/>
|
||||
<MdMoreHoriz
|
||||
data-testid={`${menuToggleDataTestIdPrefix}${resume.id}`}
|
||||
color="#fff"
|
||||
size="48"
|
||||
className="cursor-pointer"
|
||||
aria-haspopup="true"
|
||||
onClick={handleMenuClick}
|
||||
/>
|
||||
<Menu
|
||||
keepMounted
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
<MenuItem onClick={handleDuplicate}>
|
||||
{t('dashboard.buttons.duplicate')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleRename}>
|
||||
{t('dashboard.buttons.rename')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<span className="text-red-600 font-medium">
|
||||
{t('shared.buttons.delete')}
|
||||
</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
<span>{resume.name}</span>
|
||||
{resume.updatedAt && (
|
||||
<span>
|
||||
{t('dashboard.lastUpdated', {
|
||||
timestamp: dayjs(resume.updatedAt)
|
||||
.locale(i18n.language.substr(0, 2))
|
||||
.fromNow(),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResumePreview;
|
||||
|
||||
export { menuToggleDataTestIdPrefix };
|
||||
@@ -1,44 +0,0 @@
|
||||
.resume {
|
||||
@apply relative flex flex-col items-center;
|
||||
}
|
||||
|
||||
.resume > .backdrop {
|
||||
max-width: 184px;
|
||||
height: 260px;
|
||||
|
||||
@apply rounded absolute w-full bg-black shadow;
|
||||
}
|
||||
|
||||
.resume > .backdrop img {
|
||||
max-width: 184px;
|
||||
height: 260px;
|
||||
|
||||
@apply w-full object-cover rounded;
|
||||
}
|
||||
|
||||
.resume > .page {
|
||||
max-width: 184px;
|
||||
height: 260px;
|
||||
|
||||
@apply rounded absolute w-full bg-black;
|
||||
@apply opacity-0 transition-opacity duration-200 ease-in-out;
|
||||
@apply absolute text-primary-500 flex flex-col justify-evenly items-center;
|
||||
}
|
||||
|
||||
.resume > .page:hover {
|
||||
@apply opacity-75 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.resume > .meta {
|
||||
margin-top: 260px;
|
||||
|
||||
@apply flex flex-col text-center items-center;
|
||||
}
|
||||
|
||||
.resume > .meta span:first-child {
|
||||
@apply mt-3 font-medium leading-normal;
|
||||
}
|
||||
|
||||
.resume > .meta span:last-child {
|
||||
font-size: 10px;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Link } from 'gatsby';
|
||||
import React, { memo } from 'react';
|
||||
import * as styles from './TopNavbar.module.css';
|
||||
import Avatar from '../shared/Avatar';
|
||||
import Logo from '../shared/Logo';
|
||||
|
||||
const TopNavbar = () => (
|
||||
<div className={styles.navbar}>
|
||||
<div className="container">
|
||||
<Link to="/">
|
||||
<Logo size="40px" />
|
||||
</Link>
|
||||
|
||||
<Avatar className="ml-8" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default memo(TopNavbar);
|
||||
@@ -1,10 +0,0 @@
|
||||
.navbar {
|
||||
height: 65px;
|
||||
box-shadow: var(--bottom-shadow);
|
||||
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.navbar > div {
|
||||
@apply h-full flex items-center justify-between;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Link } from '@reach/router';
|
||||
import { navigate } from 'gatsby';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import Button from '../shared/Button';
|
||||
import Logo from '../shared/Logo';
|
||||
import ModalContext from '../../contexts/ModalContext';
|
||||
import UserContext from '../../contexts/UserContext';
|
||||
|
||||
const Hero = () => {
|
||||
const { t } = useTranslation();
|
||||
const { emitter, events } = useContext(ModalContext);
|
||||
const { user, loading } = useContext(UserContext);
|
||||
|
||||
const handleLogin = () => emitter.emit(events.AUTH_MODAL);
|
||||
|
||||
const handleGotoApp = () => navigate('/app/dashboard');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row items-center">
|
||||
<Link to="/">
|
||||
<span className="sr-only">Reactive Resume</span>
|
||||
<Logo className="shadow-lg" size="256px" />
|
||||
</Link>
|
||||
|
||||
<div className="mt-12 md:mt-0 md:ml-12">
|
||||
<h1 className="text-5xl font-bold">{t('shared.appName')}</h1>
|
||||
<h2 className="mt-1 text-3xl text-primary-500">
|
||||
{t('shared.shortDescription')}
|
||||
</h2>
|
||||
|
||||
<div className="mt-12">
|
||||
{user ? (
|
||||
<Button
|
||||
onClick={handleGotoApp}
|
||||
isLoading={loading}
|
||||
className="mx-auto md:mx-0"
|
||||
>
|
||||
{t('landing.hero.goToApp')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
isLoading={loading}
|
||||
className="mx-auto md:mx-0"
|
||||
>
|
||||
{t('shared.buttons.login')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Hero);
|
||||
@@ -1,77 +0,0 @@
|
||||
import { graphql, useStaticQuery } from 'gatsby';
|
||||
import { GatsbyImage } from 'gatsby-plugin-image';
|
||||
import React from 'react';
|
||||
import * as styles from './Screenshots.module.css';
|
||||
|
||||
const Screenshots = () => {
|
||||
const screenshots = useStaticQuery(graphql`
|
||||
query {
|
||||
screen1: file(relativePath: { eq: "screenshots/screen-1.png" }) {
|
||||
childImageSharp {
|
||||
original {
|
||||
src
|
||||
}
|
||||
gatsbyImageData(layout: FIXED, width: 400)
|
||||
}
|
||||
}
|
||||
screen2: file(relativePath: { eq: "screenshots/screen-2.png" }) {
|
||||
childImageSharp {
|
||||
original {
|
||||
src
|
||||
}
|
||||
gatsbyImageData(layout: FIXED, width: 400)
|
||||
}
|
||||
}
|
||||
screen3: file(relativePath: { eq: "screenshots/screen-3.png" }) {
|
||||
childImageSharp {
|
||||
original {
|
||||
src
|
||||
}
|
||||
gatsbyImageData(layout: FIXED, width: 400)
|
||||
}
|
||||
}
|
||||
screen4: file(relativePath: { eq: "screenshots/screen-4.png" }) {
|
||||
childImageSharp {
|
||||
original {
|
||||
src
|
||||
}
|
||||
gatsbyImageData(layout: FIXED, width: 400)
|
||||
}
|
||||
}
|
||||
screen5: file(relativePath: { eq: "screenshots/screen-5.png" }) {
|
||||
childImageSharp {
|
||||
original {
|
||||
src
|
||||
}
|
||||
gatsbyImageData(layout: FIXED, width: 400)
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return (
|
||||
<div className="mt-24 mb-4">
|
||||
<div className="text-xl uppercase font-bold mb-8">Screenshots</div>
|
||||
|
||||
<div className="flex w-full overflow-x-scroll">
|
||||
{Object.keys(screenshots).map((x) => (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.screenshot}
|
||||
key={screenshots[x].childImageSharp.original.src}
|
||||
href={screenshots[x].childImageSharp.original.src}
|
||||
>
|
||||
<span className="sr-only">Reactive Resume Screenshot</span>
|
||||
<GatsbyImage
|
||||
image={screenshots[x].childImageSharp.gatsbyImageData}
|
||||
alt="Reactive Resume Screenshot"
|
||||
/>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Screenshots;
|
||||
@@ -1,16 +0,0 @@
|
||||
.screenshot {
|
||||
filter: grayscale(100%);
|
||||
|
||||
@apply shadow-xl rounded border-2 border-primary-100 ml-10;
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.screenshot:hover {
|
||||
filter: grayscale(0%);
|
||||
|
||||
@apply transition-all duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.screenshot:first-child {
|
||||
@apply ml-0;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Fade, Modal } from '@material-ui/core';
|
||||
import React, { memo } from 'react';
|
||||
import { getRandomTip } from '../../data/tips';
|
||||
import Logo from '../shared/Logo';
|
||||
|
||||
const dataTestId = 'loading-screen';
|
||||
|
||||
const LoadingScreen = () => (
|
||||
<Modal data-testid={dataTestId} open hideBackdrop>
|
||||
<Fade in>
|
||||
<div className="w-screen h-screen flex justify-center items-center outline-none">
|
||||
<div className="flex flex-col items-center">
|
||||
<Logo size="48px" className="mb-4" />
|
||||
<span className="font-medium opacity-75">{getRandomTip()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export default memo(LoadingScreen);
|
||||
export { dataTestId };
|
||||
@@ -1,21 +0,0 @@
|
||||
import { navigate } from 'gatsby';
|
||||
import React, { memo, useContext } from 'react';
|
||||
import LoadingScreen from './LoadingScreen';
|
||||
import UserContext from '../../contexts/UserContext';
|
||||
|
||||
const PrivateRoute = ({ component: Component, ...props }) => {
|
||||
const { user, loading } = useContext(UserContext);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
navigate('/');
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Component user={user} {...props} />;
|
||||
};
|
||||
|
||||
export default memo(PrivateRoute);
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Menu, MenuItem } from '@material-ui/core';
|
||||
import { toUrl } from 'gatsby-source-gravatar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useContext, useMemo, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import UserContext from '../../contexts/UserContext';
|
||||
import { handleKeyUp } from '../../utils';
|
||||
import * as styles from './Avatar.module.css';
|
||||
|
||||
const Avatar = ({ className }) => {
|
||||
const { t } = useTranslation();
|
||||
const { user, logout } = useContext(UserContext);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const handleClick = (event) => setAnchorEl(event.currentTarget);
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const photoURL = useMemo(
|
||||
() => toUrl(user.email || '', 'size=128&d=retro'),
|
||||
[user.email],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
className="flex focus:outline-none"
|
||||
onClick={handleClick}
|
||||
onKeyUp={(e) => handleKeyUp(e, handleClick)}
|
||||
>
|
||||
<img
|
||||
src={photoURL}
|
||||
alt={user.displayName || 'Anonymous User'}
|
||||
className={cx(styles.container, className)}
|
||||
/>
|
||||
</div>
|
||||
<Menu
|
||||
keepMounted
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
open={Boolean(anchorEl)}
|
||||
>
|
||||
<MenuItem onClick={handleLogout}>{t('shared.buttons.logout')}</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Avatar);
|
||||
@@ -1,3 +0,0 @@
|
||||
.container {
|
||||
@apply w-12 h-12 rounded-full;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { handleKeyUp } from '../../utils';
|
||||
import * as styles from './Button.module.css';
|
||||
|
||||
const Button = ({
|
||||
icon,
|
||||
onClick,
|
||||
outline,
|
||||
children,
|
||||
className,
|
||||
isLoading,
|
||||
isDelete,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const Icon = icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
onKeyUp={(e) => handleKeyUp(e, onClick)}
|
||||
onClick={isLoading ? undefined : onClick}
|
||||
className={cx(styles.container, className, {
|
||||
[styles.outline]: outline,
|
||||
[styles.remove]: isDelete,
|
||||
})}
|
||||
>
|
||||
{icon && <Icon size="14" className="mr-3" />}
|
||||
{isLoading ? t('shared.buttons.loading') : children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Button);
|
||||
@@ -1,44 +0,0 @@
|
||||
.container {
|
||||
font-size: 11px;
|
||||
padding: 6px 18px;
|
||||
|
||||
@apply relative flex items-center cursor-pointer rounded font-semibold bg-primary-900 border border-primary-900 text-primary-50;
|
||||
@apply transition-colors duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
@apply bg-primary-700 border-primary-700;
|
||||
@apply transition-colors duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.container:active {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.container:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.container.outline {
|
||||
@apply border border-primary-900 bg-transparent text-primary-900;
|
||||
}
|
||||
|
||||
.container.outline:hover {
|
||||
@apply bg-primary-200;
|
||||
}
|
||||
|
||||
.container.outline:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.container.remove {
|
||||
@apply bg-red-600 border-red-600 text-white;
|
||||
}
|
||||
|
||||
.container.remove:hover {
|
||||
@apply bg-red-700 border-red-700;
|
||||
}
|
||||
|
||||
.container.remove:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from '../../contexts/ResumeContext';
|
||||
|
||||
const Heading = ({ id }) => {
|
||||
const { t } = useTranslation();
|
||||
const heading = useSelector(`${id}.heading`, t(`builder.sections.${id}`));
|
||||
|
||||
return <h2 className="text-4xl focus:outline-none">{heading}</h2>;
|
||||
};
|
||||
|
||||
export default memo(Heading);
|
||||
@@ -1,164 +0,0 @@
|
||||
import { FaAngleDown } from 'react-icons/fa';
|
||||
import { MdOpenInNew } from 'react-icons/md';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { isFunction } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import React, { memo, useEffect, useState } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { useDispatch, useSelector } from '../../contexts/ResumeContext';
|
||||
import { handleKeyUp } from '../../utils';
|
||||
import * as styles from './Input.module.css';
|
||||
|
||||
const Input = ({
|
||||
name,
|
||||
path,
|
||||
label,
|
||||
error,
|
||||
value,
|
||||
onBlur,
|
||||
options,
|
||||
touched,
|
||||
onClick,
|
||||
onChange,
|
||||
className,
|
||||
isRequired,
|
||||
placeholder,
|
||||
type = 'text',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [uuid, setUuid] = useState(null);
|
||||
const stateValue = useSelector(path, '');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
setUuid(uuidv4());
|
||||
}, []);
|
||||
|
||||
value = path ? stateValue : value;
|
||||
onChange = isFunction(onChange)
|
||||
? onChange
|
||||
: (e) => {
|
||||
dispatch({
|
||||
type: 'on_input',
|
||||
payload: {
|
||||
path,
|
||||
value: e.target.value,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<label htmlFor={uuid}>
|
||||
<span>
|
||||
{label}{' '}
|
||||
{isRequired && (
|
||||
<span className="opacity-75 font-normal lowercase">
|
||||
({t('shared.forms.required')})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{(type === 'text' || type === 'date') && (
|
||||
<div className="relative grid items-center">
|
||||
<input
|
||||
id={uuid}
|
||||
name={name}
|
||||
type={type}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'textarea' && (
|
||||
<div className="flex flex-col">
|
||||
<textarea
|
||||
id={uuid}
|
||||
rows="4"
|
||||
name={name}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
<p className="mt-2 text-sm opacity-75">
|
||||
<Trans t={t} i18nKey="shared.forms.markdown">
|
||||
A
|
||||
<a
|
||||
href="https://www.markdownguide.org/basic-syntax/"
|
||||
className="text-blue-600"
|
||||
target="blank"
|
||||
>
|
||||
B
|
||||
</a>
|
||||
C
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'dropdown' && (
|
||||
<div className="relative grid items-center">
|
||||
<select
|
||||
id={uuid}
|
||||
name={name}
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
>
|
||||
{options.map((x) => (
|
||||
<option key={x} value={x}>
|
||||
{x}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<FaAngleDown
|
||||
size="16px"
|
||||
className="absolute right-0 opacity-50 hover:opacity-75 mx-4"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'color' && (
|
||||
<div className="relative grid items-center">
|
||||
<div className={styles.circle} style={{ backgroundColor: value }} />
|
||||
|
||||
<input
|
||||
id={uuid}
|
||||
name={name}
|
||||
type="text"
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'action' && (
|
||||
<div className={cx('relative grid items-center', styles.readOnly)}>
|
||||
<input readOnly id={uuid} name={name} type="text" value={value} />
|
||||
|
||||
<div
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
onKeyUp={(e) => handleKeyUp(e, onClick)}
|
||||
>
|
||||
<MdOpenInNew size="16px" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && touched && <p>{error}</p>}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Input);
|
||||
@@ -1,30 +0,0 @@
|
||||
.circle {
|
||||
left: 14px;
|
||||
|
||||
@apply absolute bg-primary-900 rounded-full w-6 h-6;
|
||||
}
|
||||
|
||||
div.circle + input {
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.read-only input:hover {
|
||||
cursor: default;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.read-only input:focus {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.read-only div {
|
||||
@apply absolute rounded-r top-0 right-0 bottom-0 w-20 flex justify-center items-center bg-primary-50;
|
||||
}
|
||||
|
||||
.read-only div:hover {
|
||||
@apply bg-primary-100;
|
||||
}
|
||||
|
||||
.read-only div:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { graphql, useStaticQuery } from 'gatsby';
|
||||
import { GatsbyImage } from 'gatsby-plugin-image';
|
||||
import React, { memo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import * as styles from './Logo.module.css';
|
||||
|
||||
const Logo = ({ size = '256px', className }) => {
|
||||
const { file } = useStaticQuery(graphql`
|
||||
query {
|
||||
file(relativePath: { eq: "logo.png" }) {
|
||||
childImageSharp {
|
||||
gatsbyImageData(layout: FIXED)
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return (
|
||||
<GatsbyImage
|
||||
loading="eager"
|
||||
alt="Reactive Resume"
|
||||
className={cx(styles.logo, className)}
|
||||
style={{ width: size, height: size }}
|
||||
image={file.childImageSharp.gatsbyImageData}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Logo);
|
||||
@@ -1,9 +0,0 @@
|
||||
.logo {
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
@apply rounded;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
box-shadow: var(--shadow-strong);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React, { memo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import gfm from 'remark-gfm';
|
||||
import cx from 'classnames';
|
||||
|
||||
const Markdown = ({ children, className, ...props }) => (
|
||||
<ReactMarkdown
|
||||
className={cx('markdown', className)}
|
||||
remarkPlugins={[gfm]}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
||||
export default memo(Markdown);
|
||||
@@ -1,58 +0,0 @@
|
||||
import { MdFileUpload } from 'react-icons/md';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo, useContext, useRef } from 'react';
|
||||
import { handleKeyUp } from '../../utils';
|
||||
import Input from './Input';
|
||||
import * as styles from './PhotoUpload.module.css';
|
||||
import StorageContext from '../../contexts/StorageContext';
|
||||
|
||||
const PhotoUpload = () => {
|
||||
const fileInputRef = useRef(null);
|
||||
const { t } = useTranslation();
|
||||
const { uploadPhotograph } = useContext(StorageContext);
|
||||
|
||||
const handleIconClick = () => {
|
||||
fileInputRef.current.click();
|
||||
};
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
uploadPhotograph(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Tooltip
|
||||
title={t('builder.tooltips.uploadPhotograph')}
|
||||
placement="right-start"
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className={styles.circle}
|
||||
onClick={handleIconClick}
|
||||
onKeyUp={(e) => handleKeyUp(e, handleIconClick)}
|
||||
>
|
||||
<MdFileUpload size="22px" />
|
||||
<input
|
||||
name="file"
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
onChange={handleImageUpload}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Input
|
||||
name="photograph"
|
||||
label={t('builder.profile.photograph')}
|
||||
className="pl-6 w-full"
|
||||
path="profile.photograph"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PhotoUpload);
|
||||
@@ -1,17 +0,0 @@
|
||||
.circle {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
flex: 0 0 60px;
|
||||
|
||||
@apply flex items-center justify-center cursor-pointer bg-primary-200 text-primary-500 rounded-full;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.circle:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
.circle:hover {
|
||||
@apply opacity-75;
|
||||
@apply transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Link } from 'react-scroll';
|
||||
import { Tooltip } from '@material-ui/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { memo } from 'react';
|
||||
import * as styles from './SectionIcon.module.css';
|
||||
|
||||
const SectionIcon = ({ section, containerId, tooltipPlacement }) => {
|
||||
const { t } = useTranslation();
|
||||
const { id, icon: Icon } = section;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={t(`builder.sections.${id}`)}
|
||||
placement={tooltipPlacement}
|
||||
arrow
|
||||
>
|
||||
<Link
|
||||
spy
|
||||
smooth
|
||||
to={id}
|
||||
offset={-18}
|
||||
duration={500}
|
||||
containerId={containerId}
|
||||
activeClass="text-primary-900"
|
||||
className={styles.icon}
|
||||
>
|
||||
<Icon size="18px" />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SectionIcon);
|
||||
@@ -1,11 +0,0 @@
|
||||
.icon {
|
||||
@apply py-2 cursor-pointer;
|
||||
}
|
||||
|
||||
.icon:focus {
|
||||
@apply outline-none text-primary-900;
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
@apply text-primary-900;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Slide, toast } from 'react-toastify';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import ModalRegistrar from '../../modals/ModalRegistrar';
|
||||
|
||||
const Wrapper = ({ children }) => {
|
||||
useEffect(() => {
|
||||
toast.configure({
|
||||
role: 'alert',
|
||||
hideProgressBar: true,
|
||||
transition: Slide,
|
||||
closeButton: false,
|
||||
position: 'bottom-right',
|
||||
pauseOnFocusLoss: false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<html lang="en" />
|
||||
<title>Reactive Resume</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A free and open source resume builder that’s built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3."
|
||||
/>
|
||||
<link rel="canonical" href="https://rxresu.me" />
|
||||
<meta property="og:url" content="https://rxresu.me" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="A free and open source resume builder that’s built to make the mundane tasks of creating, updating and sharing your resume as easy as 1, 2, 3."
|
||||
/>
|
||||
<meta property="og:image" content="http://rxresu.me/images/share.png" />
|
||||
</Helmet>
|
||||
|
||||
{children}
|
||||
|
||||
<ModalRegistrar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Wrapper);
|
||||
Reference in New Issue
Block a user