mirror of
https://dev.azure.com/tstanciu94/Packages/_git/standard-cv
synced 2025-08-10 18:32:25 +03:00
move standard cv to npm package
This commit is contained in:
parent
1542a72462
commit
2c4fea7bf7
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
||||
@flare:registry=https://toodle.ddns.net/public-node-registry
|
10
package.json
10
package.json
@ -52,15 +52,17 @@
|
||||
"lint-staged": "^12.2.2",
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.10.2",
|
||||
"bootstrap": "^4.3.1",
|
||||
"jquery": "^3.3.1",
|
||||
"popper.js": "^1.14.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "none",
|
||||
|
40
src/components/Footer.js
Normal file
40
src/components/Footer.js
Normal file
@ -0,0 +1,40 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Footer = ({ data }) => {
|
||||
return (
|
||||
<footer>
|
||||
<small className="copyright">
|
||||
{data.custom ? (
|
||||
data.custom
|
||||
) : (
|
||||
<>
|
||||
<a href={data.project.repository} target="_blank" rel="noreferrer">
|
||||
{data.project.name}
|
||||
</a>
|
||||
{` | ${data.owner.message} `}
|
||||
<a href={data.owner.url} target="_blank" rel="noreferrer">
|
||||
{data.owner.name}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</small>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
Footer.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
project: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
repository: PropTypes.string
|
||||
}),
|
||||
owner: PropTypes.shape({
|
||||
message: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
url: PropTypes.string
|
||||
}),
|
||||
custom: PropTypes.node
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Footer;
|
66
src/components/Layout.js
Normal file
66
src/components/Layout.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import HeaderLayout from "./header/HeaderLayout";
|
||||
import Education from "./section/Education";
|
||||
import Work from "./section/Work";
|
||||
import Projects from "./section/Projects";
|
||||
import Skills from "./section/Skills";
|
||||
import Honors from "./section/Honors";
|
||||
import Conferences from "./section/Conferences";
|
||||
import Footer from "./Footer";
|
||||
|
||||
const Layout = ({ data }) => {
|
||||
return (
|
||||
<main className="container">
|
||||
<div className="row">
|
||||
<div className="col-lg-4 left-side">
|
||||
<HeaderLayout data={data.header} />
|
||||
</div>
|
||||
|
||||
<div className="col-lg-8 right-side">
|
||||
<article>
|
||||
{data.article.education.visible && (
|
||||
<Education data={data.article.education} />
|
||||
)}
|
||||
|
||||
{data.article.work.visible && <Work data={data.article.work} />}
|
||||
|
||||
{data.article.projects.visible && (
|
||||
<Projects data={data.article.projects} />
|
||||
)}
|
||||
|
||||
{data.article.skills.visible && (
|
||||
<Skills data={data.article.skills} />
|
||||
)}
|
||||
|
||||
{data.article.honors.visible && (
|
||||
<Honors data={data.article.honors} />
|
||||
)}
|
||||
|
||||
{data.article.conferences.visible && (
|
||||
<Conferences data={data.article.conferences} />
|
||||
)}
|
||||
</article>
|
||||
{data.footer.visible && <Footer data={data.footer} />}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
header: PropTypes.object.isRequired,
|
||||
article: PropTypes.shape({
|
||||
education: PropTypes.object.isRequired,
|
||||
work: PropTypes.object.isRequired,
|
||||
projects: PropTypes.object,
|
||||
skills: PropTypes.object.isRequired,
|
||||
honors: PropTypes.object,
|
||||
conferences: PropTypes.object
|
||||
}).isRequired,
|
||||
footer: PropTypes.object.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Layout;
|
13
src/components/header/About.js
Normal file
13
src/components/header/About.js
Normal file
@ -0,0 +1,13 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const About = ({ data }) => {
|
||||
return <p className="justify-text">{data.content}</p>;
|
||||
};
|
||||
|
||||
About.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default About;
|
58
src/components/header/HeaderLayout.js
Normal file
58
src/components/header/HeaderLayout.js
Normal file
@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Profile from "./Profile";
|
||||
import ProfileName from "./ProfileName";
|
||||
import SocialNetworks from "./SocialNetworks";
|
||||
import About from "./About";
|
||||
import { useWindowSize } from "@flare/react-hooks";
|
||||
|
||||
const TabletHeader = ({ data }) => {
|
||||
return (
|
||||
<header>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<Profile
|
||||
data={data.profile}
|
||||
displayProfileName={false}
|
||||
className="profile-inline"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="profile-inline">
|
||||
<ProfileName data={data.profile} />
|
||||
</div>
|
||||
<SocialNetworks data={data.networks} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md about-flat">
|
||||
<About data={data.about} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ data }) => {
|
||||
return (
|
||||
<header>
|
||||
<Profile data={data.profile} />
|
||||
<SocialNetworks data={data.networks} />
|
||||
<About data={data.about} />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderLayout = ({ data }) => {
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
if (isTablet) return <TabletHeader data={data} />;
|
||||
return <Header data={data} />;
|
||||
};
|
||||
|
||||
HeaderLayout.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
profile: PropTypes.object.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default HeaderLayout;
|
50
src/components/header/Profile.js
Normal file
50
src/components/header/Profile.js
Normal file
@ -0,0 +1,50 @@
|
||||
import PropTypes from "prop-types";
|
||||
import ProfileName from "./ProfileName";
|
||||
|
||||
const Profile = ({ data, displayProfileName, className }) => {
|
||||
const handleDownloadClick = () => {
|
||||
window.umami && window.umami("cv-download");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="picture">
|
||||
<img src={data.picture.src} alt={data.picture.alt} />
|
||||
</div>
|
||||
<a
|
||||
className="btn btn-cv"
|
||||
href={data.download.src}
|
||||
role="button"
|
||||
download
|
||||
onClick={handleDownloadClick}
|
||||
>
|
||||
<i className="fas fa-file-alt"></i> {data.download.label}
|
||||
</a>
|
||||
{displayProfileName && <ProfileName data={data} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Profile.defaultProps = {
|
||||
displayProfileName: true,
|
||||
className: "profile"
|
||||
};
|
||||
|
||||
Profile.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
position: PropTypes.string.isRequired,
|
||||
picture: PropTypes.shape({
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string
|
||||
}),
|
||||
download: PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
src: PropTypes.string.isRequired
|
||||
})
|
||||
}).isRequired,
|
||||
displayProfileName: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
export default Profile;
|
19
src/components/header/ProfileName.js
Normal file
19
src/components/header/ProfileName.js
Normal file
@ -0,0 +1,19 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ProfileName = ({ data }) => {
|
||||
return (
|
||||
<>
|
||||
<h1>{data.name}</h1>
|
||||
<span>{data.position}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileName.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
position: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default ProfileName;
|
50
src/components/header/SocialNetworks.js
Normal file
50
src/components/header/SocialNetworks.js
Normal file
@ -0,0 +1,50 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SocialNetworks = ({ data }) => {
|
||||
const networks = data.sort((a, b) => a.id - b.id);
|
||||
const handleSocialNetworkClick = event => () => {
|
||||
event && window.umami && window.umami(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<ul className="social">
|
||||
{networks.map(network => (
|
||||
<li key={`social-network-${network.id}`}>
|
||||
<i className={network.icon}></i>{" "}
|
||||
{network.sameTab ? (
|
||||
<a
|
||||
href={network.url}
|
||||
onClick={handleSocialNetworkClick(network.event)}
|
||||
>
|
||||
{network.label}
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
href={network.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={handleSocialNetworkClick(network.event)}
|
||||
>
|
||||
{network.label}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
SocialNetworks.propTypes = {
|
||||
data: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.number.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
sameTab: PropTypes.bool,
|
||||
event: PropTypes.string
|
||||
})
|
||||
).isRequired
|
||||
};
|
||||
|
||||
export default SocialNetworks;
|
28
src/components/section/Conferences.js
Normal file
28
src/components/section/Conferences.js
Normal file
@ -0,0 +1,28 @@
|
||||
import PropTypes from "prop-types";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
import { composeKey } from "../../utils/textUtils";
|
||||
|
||||
const Conferences = ({ data }) => {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle icon={data.icon} label={data.label} />
|
||||
<div className="justify-text">
|
||||
<ul style={{ marginBottom: "-5px" }}>
|
||||
{data.elements.map(conference => (
|
||||
<li key={composeKey(conference)}>{conference}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Conferences.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
elements: PropTypes.array.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Conferences;
|
59
src/components/section/Education.js
Normal file
59
src/components/section/Education.js
Normal file
@ -0,0 +1,59 @@
|
||||
import PropTypes from "prop-types";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
|
||||
const School = ({ name, time, title, courses, last }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="school justify-text">
|
||||
<div className="upper-row">
|
||||
<h3 className="school-name">{name}</h3>
|
||||
<div className="time">{time}</div>
|
||||
</div>
|
||||
<div className="school-title">{title}</div>
|
||||
{courses && (
|
||||
<>
|
||||
<b>{courses.label}:</b> {courses.content}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!last && <br />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Education = ({ data }) => {
|
||||
const _schools = [...data.elements.sort((a, b) => a.id - b.id)];
|
||||
const last = _schools.pop();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle icon={data.icon} label={data.label} />
|
||||
|
||||
{_schools.map(school => (
|
||||
<School key={`school-${school.id}`} {...school} />
|
||||
))}
|
||||
|
||||
<School {...last} last />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Education.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
elements: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
time: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
courses: PropTypes.shape({
|
||||
label: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired
|
||||
})
|
||||
})
|
||||
).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Education;
|
58
src/components/section/Honors.js
Normal file
58
src/components/section/Honors.js
Normal file
@ -0,0 +1,58 @@
|
||||
import PropTypes from "prop-types";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
import { composeKey } from "../../utils/textUtils";
|
||||
|
||||
const HonorComponent = ({ name, abbreviation, bullets, last }) => {
|
||||
const ulProps = last ? { style: { marginBottom: "-5px" } } : {};
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
{abbreviation ? <abbr title={abbreviation}>{name}</abbr> : <>{name}</>}
|
||||
</h3>
|
||||
|
||||
<ul {...ulProps}>
|
||||
{bullets.map(bullet => (
|
||||
<li key={composeKey(bullet)}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
HonorComponent.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
abbreviation: PropTypes.string,
|
||||
bullets: PropTypes.array.isRequired,
|
||||
last: PropTypes.bool
|
||||
};
|
||||
|
||||
const Honors = ({ data }) => {
|
||||
const _honors = [...data.elements.sort((a, b) => a.id - b.id)];
|
||||
const last = _honors.pop();
|
||||
|
||||
return (
|
||||
<section className="honors">
|
||||
<SectionTitle icon={data.icon} label={data.label} />
|
||||
{_honors.map(honor => (
|
||||
<HonorComponent key={`honor-${honor.id}`} {...honor} />
|
||||
))}
|
||||
<HonorComponent {...last} last={true} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Honors.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
elements: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
abbreviation: PropTypes.string,
|
||||
bullets: PropTypes.array.isRequired
|
||||
})
|
||||
).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Honors;
|
36
src/components/section/Projects.js
Normal file
36
src/components/section/Projects.js
Normal file
@ -0,0 +1,36 @@
|
||||
import PropTypes from "prop-types";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
import Job from "./job/Job";
|
||||
import { composeKey } from "../../utils/textUtils";
|
||||
|
||||
const Projects = ({ data }) => {
|
||||
const _projects = [...data.elements.sort((a, b) => a.id - b.id)];
|
||||
const last = _projects.pop();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle icon={data.icon} label={data.label} />
|
||||
{_projects.map(project => (
|
||||
<Job key={composeKey(project.name)} {...project} />
|
||||
))}
|
||||
<Job {...last} last />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Projects.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
elements: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
bullets: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
)
|
||||
})
|
||||
).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Projects;
|
20
src/components/section/SectionTitle.js
Normal file
20
src/components/section/SectionTitle.js
Normal file
@ -0,0 +1,20 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SectionTitle = ({ icon, label }) => {
|
||||
return (
|
||||
<h2 className="section-title">
|
||||
<span className="fa-stack fa-xs">
|
||||
<i className="fas fa-circle fa-stack-2x"></i>
|
||||
<i className={`fas ${icon} fa-stack-1x fa-inverse`}></i>
|
||||
</span>{" "}
|
||||
{label}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
|
||||
SectionTitle.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default SectionTitle;
|
51
src/components/section/Skills.js
Normal file
51
src/components/section/Skills.js
Normal file
@ -0,0 +1,51 @@
|
||||
import PropTypes from "prop-types";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
|
||||
const Skill = ({ type, description, last }) => {
|
||||
return (
|
||||
<>
|
||||
<b>{type}:</b> {description}
|
||||
{!last && <br />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Skill.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
.isRequired,
|
||||
last: PropTypes.bool
|
||||
};
|
||||
|
||||
const Skills = ({ data }) => {
|
||||
const _skills = [...data.elements.sort((a, b) => a.id - b.id)];
|
||||
const last = _skills.pop();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle icon={data.icon} label={data.label} />
|
||||
<div className="justify-text">
|
||||
{_skills.map(skill => (
|
||||
<Skill key={`skill-${skill.id}`} {...skill} />
|
||||
))}
|
||||
<Skill {...last} last={true} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Skills.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
elements: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
type: PropTypes.string.isRequired,
|
||||
description: PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
.isRequired
|
||||
})
|
||||
).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Skills;
|
82
src/components/section/Work.js
Normal file
82
src/components/section/Work.js
Normal file
@ -0,0 +1,82 @@
|
||||
import PropTypes from "prop-types";
|
||||
import SectionTitle from "./SectionTitle";
|
||||
import Job from "./job/Job";
|
||||
import JobChapter from "./job/JobChapter";
|
||||
import { composeKey } from "../../utils/textUtils";
|
||||
|
||||
const MultiplePositionsWork = ({ data, last }) => {
|
||||
const divProps = last
|
||||
? { className: "job justify-text", style: { marginBottom: "-15px" } }
|
||||
: { className: "job justify-text" };
|
||||
|
||||
return (
|
||||
<div {...divProps}>
|
||||
<h3 className="company">{data.name}</h3>
|
||||
{data.positions.map(position => (
|
||||
<JobChapter key={composeKey(position.name)} {...position} trivial />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkPoint = ({ data, last }) =>
|
||||
data.positions ? (
|
||||
<MultiplePositionsWork data={data} last={last} />
|
||||
) : (
|
||||
<Job {...data} last={last} />
|
||||
);
|
||||
|
||||
const Work = ({ data }) => {
|
||||
const _work = [...data.elements];
|
||||
const last = _work.pop();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle icon={data.icon} label={data.label} />
|
||||
|
||||
{_work.map(point => (
|
||||
<WorkPoint key={composeKey(point.name)} data={point} />
|
||||
))}
|
||||
|
||||
<WorkPoint data={last} last={true} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Work.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
icon: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
elements: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
time: PropTypes.string,
|
||||
position: PropTypes.string,
|
||||
positions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
time: PropTypes.string,
|
||||
content: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
bullets: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
)
|
||||
})
|
||||
).isRequired
|
||||
})
|
||||
),
|
||||
content: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
bullets: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
).isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Work;
|
33
src/components/section/job/Job.js
Normal file
33
src/components/section/job/Job.js
Normal file
@ -0,0 +1,33 @@
|
||||
import PropTypes from "prop-types";
|
||||
import JobChapter from "./JobChapter";
|
||||
|
||||
const Job = ({ last, ...props }) => {
|
||||
// style="margin-bottom: -15px" must pe used on last element of section
|
||||
const divProps = last
|
||||
? { className: "job justify-text", style: { marginBottom: "-15px" } }
|
||||
: { className: "job justify-text" };
|
||||
|
||||
return (
|
||||
<div {...divProps}>
|
||||
<JobChapter {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Job.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
time: PropTypes.string,
|
||||
position: PropTypes.string,
|
||||
content: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
bullets: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
)
|
||||
})
|
||||
).isRequired,
|
||||
chapter: PropTypes.bool,
|
||||
last: PropTypes.bool
|
||||
};
|
||||
|
||||
export default Job;
|
62
src/components/section/job/JobChapter.js
Normal file
62
src/components/section/job/JobChapter.js
Normal file
@ -0,0 +1,62 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { composeKey } from "../../../utils/textUtils";
|
||||
|
||||
const JobFragment = ({ text, bullets }) => {
|
||||
return (
|
||||
<>
|
||||
{text && text}
|
||||
{bullets ? (
|
||||
<ul>
|
||||
{bullets.map(bullet => (
|
||||
<li key={composeKey(bullet)}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<ul></ul>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JobFragment.propTypes = {
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
bullets: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
)
|
||||
};
|
||||
|
||||
const JobChapter = ({ name, time, position, content, trivial }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="upper-row">
|
||||
{trivial ? (
|
||||
<div className="job-title">{name}</div>
|
||||
) : (
|
||||
<h3 className="company">{name}</h3>
|
||||
)}
|
||||
<div className="time">{time}</div>
|
||||
</div>
|
||||
{position && <div className="job-title">{position}</div>}
|
||||
{content.map(fragment => (
|
||||
<JobFragment key={`job-fragment-${fragment.id}`} {...fragment} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
JobChapter.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
time: PropTypes.string,
|
||||
position: PropTypes.string,
|
||||
content: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
bullets: PropTypes.arrayOf(
|
||||
PropTypes.oneOfType([PropTypes.string, PropTypes.node])
|
||||
)
|
||||
})
|
||||
).isRequired,
|
||||
trivial: PropTypes.bool
|
||||
};
|
||||
|
||||
export default JobChapter;
|
57
src/index.js
57
src/index.js
@ -1,2 +1,55 @@
|
||||
const empty = "";
|
||||
export { empty };
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
// styles
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
import "./styles/main.css";
|
||||
|
||||
// jQuery first, then Popper.js, then Bootstrap JS
|
||||
import "jquery/dist/jquery.slim.min.js";
|
||||
import "popper.js/dist/popper.min.js";
|
||||
import "bootstrap/dist/js/bootstrap.min.js";
|
||||
|
||||
import Layout from "./components/Layout";
|
||||
import { useLink } from "@flare/react-hooks";
|
||||
import { constants, useTheme, useFavicon, utils } from "./theme";
|
||||
|
||||
const StandardCV = ({ data }) => {
|
||||
const { theme, favicon, options } = data.configuration;
|
||||
const { urlTheme: enableUrlTheme } = options;
|
||||
|
||||
useLink(
|
||||
"https://fonts.googleapis.com/css?family=Noto+Sans&display=swap",
|
||||
"stylesheet"
|
||||
);
|
||||
|
||||
const themeFromUrl = enableUrlTheme ? utils.getThemeFromUrl() : null;
|
||||
const _theme = themeFromUrl ?? theme ?? constants.defaultTheme;
|
||||
useTheme(_theme);
|
||||
useFavicon(_theme, favicon);
|
||||
|
||||
return <Layout data={data} />;
|
||||
};
|
||||
|
||||
StandardCV.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
configuration: PropTypes.shape({
|
||||
theme: PropTypes.oneOf(constants.themes),
|
||||
favicon: PropTypes.shape({
|
||||
use: PropTypes.bool.isRequired,
|
||||
id: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
href: PropTypes.string
|
||||
}).isRequired,
|
||||
options: PropTypes.shape({
|
||||
urlTheme: PropTypes.bool
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
header: PropTypes.object.isRequired,
|
||||
article: PropTypes.object.isRequired,
|
||||
footer: PropTypes.object.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default StandardCV;
|
||||
|
200
src/styles/main.css
Normal file
200
src/styles/main.css
Normal file
@ -0,0 +1,200 @@
|
||||
:root {
|
||||
--primary-color: #71c9ce;
|
||||
--secondary-color: #64b2b6;
|
||||
--text-color: #3d3d3f;
|
||||
--border-color: #ddd;
|
||||
--background-color: #fff;
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Noto Sans", sans-serif;
|
||||
padding: 15px 0px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: var(--background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 0 25px 0 var(--shadow-color);
|
||||
}
|
||||
|
||||
a,
|
||||
a:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.left-side {
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.profile {
|
||||
text-align: center;
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.profile h1 {
|
||||
padding-top: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.profile-inline {
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.profile-inline h1 {
|
||||
text-transform: uppercase;
|
||||
font-size: 2.3rem;
|
||||
}
|
||||
|
||||
.picture {
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.picture img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.about-flat {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.justify-text {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.btn-cv {
|
||||
padding: 10px;
|
||||
color: white;
|
||||
background-color: var(--primary-color);
|
||||
width: 100%;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.btn-cv:hover {
|
||||
color: var(--background-color);
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn:focus,
|
||||
.btn:active {
|
||||
outline: none !important;
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.social {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.social i {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.right-side {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 40px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
text-transform: uppercase;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.upper-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.company,
|
||||
.school-name {
|
||||
color: var(--primary-color);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.job-title,
|
||||
.school-title {
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.job li,
|
||||
section {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.honors h3 {
|
||||
color: var(--primary-color);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
border-radius: 0px;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.right-side {
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.left-side {
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
header {
|
||||
/* fixed sidebar */
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.time {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
29
src/theme/constants.js
Normal file
29
src/theme/constants.js
Normal file
@ -0,0 +1,29 @@
|
||||
const theme = {
|
||||
TURQUOISE: "turquoise",
|
||||
BLUE: "blue",
|
||||
GREEN: "green",
|
||||
BROWN: "brown",
|
||||
ORANGE: "orange",
|
||||
PURPLE: "purple",
|
||||
PINK: "pink",
|
||||
CORAL: "coral",
|
||||
NUDE: "nude",
|
||||
RAINBOW: "rainbow"
|
||||
};
|
||||
|
||||
const themes = [
|
||||
theme.TURQUOISE,
|
||||
theme.BLUE,
|
||||
theme.GREEN,
|
||||
theme.BROWN,
|
||||
theme.ORANGE,
|
||||
theme.PURPLE,
|
||||
theme.PINK,
|
||||
theme.CORAL,
|
||||
theme.NUDE,
|
||||
theme.RAINBOW
|
||||
];
|
||||
|
||||
const defaultTheme = theme.TURQUOISE;
|
||||
|
||||
export { defaultTheme, theme, themes };
|
21
src/theme/hooks/useFavicon.js
Normal file
21
src/theme/hooks/useFavicon.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useFavicon = (themeName, configuration) => {
|
||||
const { use, id: faviconElementId, placeholder, href } = configuration;
|
||||
if (
|
||||
use &&
|
||||
(!faviconElementId || !placeholder || !href || !href.includes(placeholder))
|
||||
) {
|
||||
throw Error("Invalid favicon configuration.");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!use) return;
|
||||
const favicon = document.getElementById(faviconElementId);
|
||||
if (favicon) {
|
||||
favicon.href = href.replace(placeholder, themeName);
|
||||
}
|
||||
}, [use, themeName, faviconElementId, placeholder, href]);
|
||||
};
|
||||
|
||||
export default useFavicon;
|
71
src/theme/hooks/useTheme.js
Normal file
71
src/theme/hooks/useTheme.js
Normal file
@ -0,0 +1,71 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import {
|
||||
turquoise,
|
||||
blue,
|
||||
brown,
|
||||
pink,
|
||||
purple,
|
||||
orange,
|
||||
coral,
|
||||
nude,
|
||||
green
|
||||
} from "../variants";
|
||||
import { theme } from "../constants";
|
||||
|
||||
const getTheme = name => {
|
||||
switch (name) {
|
||||
case theme.TURQUOISE:
|
||||
return turquoise;
|
||||
case theme.BLUE:
|
||||
return blue;
|
||||
case theme.BROWN:
|
||||
return brown;
|
||||
case theme.PINK:
|
||||
return pink;
|
||||
case theme.PURPLE:
|
||||
return purple;
|
||||
case theme.ORANGE:
|
||||
return orange;
|
||||
case theme.CORAL:
|
||||
return coral;
|
||||
case theme.NUDE:
|
||||
return nude;
|
||||
case theme.GREEN:
|
||||
return green;
|
||||
default:
|
||||
return turquoise;
|
||||
}
|
||||
};
|
||||
|
||||
const useTheme = name => {
|
||||
useEffect(() => {
|
||||
const theme = getTheme(name);
|
||||
document.documentElement.style.setProperty(
|
||||
"--primary-color",
|
||||
theme.colors.primary
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--secondary-color",
|
||||
theme.colors.secondary
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--text-color",
|
||||
theme.colors.text
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--border-color",
|
||||
theme.colors.border
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--background-color",
|
||||
theme.colors.background
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--shadow-color",
|
||||
theme.colors.shadow
|
||||
);
|
||||
}, [name]);
|
||||
};
|
||||
|
||||
export default useTheme;
|
6
src/theme/index.js
Normal file
6
src/theme/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import useTheme from "./hooks/useTheme";
|
||||
import useFavicon from "./hooks/useFavicon";
|
||||
import * as constants from "./constants";
|
||||
import * as utils from "./utils";
|
||||
|
||||
export { constants, useTheme, useFavicon, utils };
|
6
src/theme/utils.js
Normal file
6
src/theme/utils.js
Normal file
@ -0,0 +1,6 @@
|
||||
export const getThemeFromUrl = () => {
|
||||
const urlString = window.location.href;
|
||||
const url = new URL(urlString);
|
||||
const theme = url.searchParams.get("theme");
|
||||
return theme;
|
||||
};
|
12
src/theme/variants/blue.js
Normal file
12
src/theme/variants/blue.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#65a3cc",
|
||||
secondary: "#5499c7",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
12
src/theme/variants/brown.js
Normal file
12
src/theme/variants/brown.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#d27065",
|
||||
secondary: "#cd6155",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
12
src/theme/variants/coral.js
Normal file
12
src/theme/variants/coral.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#ff9483",
|
||||
secondary: "#F1948A",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
12
src/theme/variants/green.js
Normal file
12
src/theme/variants/green.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#3d884d",
|
||||
secondary: "#367a45",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
11
src/theme/variants/index.js
Normal file
11
src/theme/variants/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import turquoise from "./turquoise";
|
||||
import blue from "./blue";
|
||||
import brown from "./brown";
|
||||
import pink from "./pink";
|
||||
import purple from "./purple";
|
||||
import orange from "./orange";
|
||||
import coral from "./coral";
|
||||
import nude from "./nude";
|
||||
import green from "./green";
|
||||
|
||||
export { turquoise, blue, brown, pink, purple, orange, coral, nude, green };
|
12
src/theme/variants/nude.js
Normal file
12
src/theme/variants/nude.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#f29e95",
|
||||
secondary: "#f1948a",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
12
src/theme/variants/orange.js
Normal file
12
src/theme/variants/orange.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#f49f2b",
|
||||
secondary: "#f39514",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
12
src/theme/variants/pink.js
Normal file
12
src/theme/variants/pink.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#d5819b",
|
||||
secondary: "#D17390",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
12
src/theme/variants/purple.js
Normal file
12
src/theme/variants/purple.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#c19ad2",
|
||||
secondary: "#BB8FCE",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
12
src/theme/variants/turquoise.js
Normal file
12
src/theme/variants/turquoise.js
Normal file
@ -0,0 +1,12 @@
|
||||
const theme = {
|
||||
colors: {
|
||||
primary: "#71c9ce",
|
||||
secondary: "#64b2b6",
|
||||
text: "#3d3d3f",
|
||||
border: "#ddd",
|
||||
background: "#fff",
|
||||
shadow: "rgba(0, 0, 0, 0.1)"
|
||||
}
|
||||
};
|
||||
|
||||
export default theme;
|
9
src/utils/textUtils.js
Normal file
9
src/utils/textUtils.js
Normal file
@ -0,0 +1,9 @@
|
||||
const composeKey = input => {
|
||||
if (typeof input === "string" || input instanceof String) {
|
||||
return input.replace(/\s+/g, "-").toLowerCase(); //replace spaces with dashes
|
||||
}
|
||||
const key = Date.now();
|
||||
return `generated-key-${key}`;
|
||||
};
|
||||
|
||||
export { composeKey };
|
Loading…
x
Reference in New Issue
Block a user