move standard cv to npm package

This commit is contained in:
Tudor Stanciu 2022-09-07 19:34:36 +03:00
parent 1542a72462
commit 2c4fea7bf7
36 changed files with 1248 additions and 6 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
@flare:registry=https://toodle.ddns.net/public-node-registry

View File

@ -52,15 +52,17 @@
"lint-staged": "^12.2.2", "lint-staged": "^12.2.2",
"prettier": "^2.5.1" "prettier": "^2.5.1"
}, },
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
},
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.10.2", "@fortawesome/fontawesome-free": "^5.10.2",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"popper.js": "^1.14.7", "popper.js": "^1.14.7",
"prop-types": "^15.8.1", "prop-types": "^15.8.1"
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1"
}, },
"prettier": { "prettier": {
"trailingComma": "none", "trailingComma": "none",

40
src/components/Footer.js Normal file
View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -1,2 +1,55 @@
const empty = ""; import React from "react";
export { empty }; 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
View 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
View 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 };

View 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;

View 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
View 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
View 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;
};

View 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;

View 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;

View 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;

View 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;

View 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 };

View 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;

View 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;

View 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;

View 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;

View 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
View 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 };