diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..2e8ff6c
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+@flare:registry=https://toodle.ddns.net/public-node-registry
\ No newline at end of file
diff --git a/package.json b/package.json
index 5a26a06..b638cb6 100644
--- a/package.json
+++ b/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",
diff --git a/src/components/Footer.js b/src/components/Footer.js
new file mode 100644
index 0000000..2dbb390
--- /dev/null
+++ b/src/components/Footer.js
@@ -0,0 +1,40 @@
+import PropTypes from "prop-types";
+
+const Footer = ({ data }) => {
+ return (
+
+ );
+};
+
+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;
diff --git a/src/components/Layout.js b/src/components/Layout.js
new file mode 100644
index 0000000..3f44e87
--- /dev/null
+++ b/src/components/Layout.js
@@ -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 (
+
+
+
+
+
+
+
+
+ {data.article.education.visible && (
+
+ )}
+
+ {data.article.work.visible && }
+
+ {data.article.projects.visible && (
+
+ )}
+
+ {data.article.skills.visible && (
+
+ )}
+
+ {data.article.honors.visible && (
+
+ )}
+
+ {data.article.conferences.visible && (
+
+ )}
+
+ {data.footer.visible &&
}
+
+
+
+ );
+};
+
+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;
diff --git a/src/components/header/About.js b/src/components/header/About.js
new file mode 100644
index 0000000..6e1475f
--- /dev/null
+++ b/src/components/header/About.js
@@ -0,0 +1,13 @@
+import PropTypes from "prop-types";
+
+const About = ({ data }) => {
+ return
{data.content}
;
+};
+
+About.propTypes = {
+ data: PropTypes.shape({
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
+ }).isRequired
+};
+
+export default About;
diff --git a/src/components/header/HeaderLayout.js b/src/components/header/HeaderLayout.js
new file mode 100644
index 0000000..7b68796
--- /dev/null
+++ b/src/components/header/HeaderLayout.js
@@ -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 (
+
+ );
+};
+
+const Header = ({ data }) => {
+ return (
+
+ );
+};
+
+const HeaderLayout = ({ data }) => {
+ const { isTablet } = useWindowSize();
+
+ if (isTablet) return ;
+ return ;
+};
+
+HeaderLayout.propTypes = {
+ data: PropTypes.shape({
+ profile: PropTypes.object.isRequired
+ }).isRequired
+};
+
+export default HeaderLayout;
diff --git a/src/components/header/Profile.js b/src/components/header/Profile.js
new file mode 100644
index 0000000..681a98d
--- /dev/null
+++ b/src/components/header/Profile.js
@@ -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 (
+
+ );
+};
+
+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;
diff --git a/src/components/header/ProfileName.js b/src/components/header/ProfileName.js
new file mode 100644
index 0000000..a2c872d
--- /dev/null
+++ b/src/components/header/ProfileName.js
@@ -0,0 +1,19 @@
+import PropTypes from "prop-types";
+
+const ProfileName = ({ data }) => {
+ return (
+ <>
+ {data.name}
+ {data.position}
+ >
+ );
+};
+
+ProfileName.propTypes = {
+ data: PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ position: PropTypes.string.isRequired
+ }).isRequired
+};
+
+export default ProfileName;
diff --git a/src/components/header/SocialNetworks.js b/src/components/header/SocialNetworks.js
new file mode 100644
index 0000000..feeca2e
--- /dev/null
+++ b/src/components/header/SocialNetworks.js
@@ -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 (
+
+ );
+};
+
+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;
diff --git a/src/components/section/Conferences.js b/src/components/section/Conferences.js
new file mode 100644
index 0000000..116f5d9
--- /dev/null
+++ b/src/components/section/Conferences.js
@@ -0,0 +1,28 @@
+import PropTypes from "prop-types";
+import SectionTitle from "./SectionTitle";
+import { composeKey } from "../../utils/textUtils";
+
+const Conferences = ({ data }) => {
+ return (
+
+
+
+
+ {data.elements.map(conference => (
+ - {conference}
+ ))}
+
+
+
+ );
+};
+
+Conferences.propTypes = {
+ data: PropTypes.shape({
+ icon: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ elements: PropTypes.array.isRequired
+ }).isRequired
+};
+
+export default Conferences;
diff --git a/src/components/section/Education.js b/src/components/section/Education.js
new file mode 100644
index 0000000..609b0fb
--- /dev/null
+++ b/src/components/section/Education.js
@@ -0,0 +1,59 @@
+import PropTypes from "prop-types";
+import SectionTitle from "./SectionTitle";
+
+const School = ({ name, time, title, courses, last }) => {
+ return (
+ <>
+
+
+
{title}
+ {courses && (
+ <>
+
{courses.label}: {courses.content}
+ >
+ )}
+
+ {!last &&
}
+ >
+ );
+};
+
+const Education = ({ data }) => {
+ const _schools = [...data.elements.sort((a, b) => a.id - b.id)];
+ const last = _schools.pop();
+
+ return (
+
+
+
+ {_schools.map(school => (
+
+ ))}
+
+
+
+ );
+};
+
+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;
diff --git a/src/components/section/Honors.js b/src/components/section/Honors.js
new file mode 100644
index 0000000..aa7dd46
--- /dev/null
+++ b/src/components/section/Honors.js
@@ -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 (
+ <>
+
+ {abbreviation ? {name} : <>{name}>}
+
+
+
+ {bullets.map(bullet => (
+ - {bullet}
+ ))}
+
+ >
+ );
+};
+
+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 (
+
+
+ {_honors.map(honor => (
+
+ ))}
+
+
+ );
+};
+
+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;
diff --git a/src/components/section/Projects.js b/src/components/section/Projects.js
new file mode 100644
index 0000000..10e2a0b
--- /dev/null
+++ b/src/components/section/Projects.js
@@ -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 (
+
+
+ {_projects.map(project => (
+
+ ))}
+
+
+ );
+};
+
+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;
diff --git a/src/components/section/SectionTitle.js b/src/components/section/SectionTitle.js
new file mode 100644
index 0000000..78464f1
--- /dev/null
+++ b/src/components/section/SectionTitle.js
@@ -0,0 +1,20 @@
+import PropTypes from "prop-types";
+
+const SectionTitle = ({ icon, label }) => {
+ return (
+
+
+
+
+ {" "}
+ {label}
+
+ );
+};
+
+SectionTitle.propTypes = {
+ icon: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired
+};
+
+export default SectionTitle;
diff --git a/src/components/section/Skills.js b/src/components/section/Skills.js
new file mode 100644
index 0000000..03a22f4
--- /dev/null
+++ b/src/components/section/Skills.js
@@ -0,0 +1,51 @@
+import PropTypes from "prop-types";
+import SectionTitle from "./SectionTitle";
+
+const Skill = ({ type, description, last }) => {
+ return (
+ <>
+ {type}: {description}
+ {!last &&
}
+ >
+ );
+};
+
+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 (
+
+
+
+ {_skills.map(skill => (
+
+ ))}
+
+
+
+ );
+};
+
+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;
diff --git a/src/components/section/Work.js b/src/components/section/Work.js
new file mode 100644
index 0000000..90318ac
--- /dev/null
+++ b/src/components/section/Work.js
@@ -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 (
+
+
{data.name}
+ {data.positions.map(position => (
+
+ ))}
+
+ );
+};
+
+const WorkPoint = ({ data, last }) =>
+ data.positions ? (
+
+ ) : (
+
+ );
+
+const Work = ({ data }) => {
+ const _work = [...data.elements];
+ const last = _work.pop();
+
+ return (
+
+
+
+ {_work.map(point => (
+
+ ))}
+
+
+
+ );
+};
+
+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;
diff --git a/src/components/section/job/Job.js b/src/components/section/job/Job.js
new file mode 100644
index 0000000..aa9ef5a
--- /dev/null
+++ b/src/components/section/job/Job.js
@@ -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 (
+
+
+
+ );
+};
+
+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;
diff --git a/src/components/section/job/JobChapter.js b/src/components/section/job/JobChapter.js
new file mode 100644
index 0000000..08113cd
--- /dev/null
+++ b/src/components/section/job/JobChapter.js
@@ -0,0 +1,62 @@
+import PropTypes from "prop-types";
+import { composeKey } from "../../../utils/textUtils";
+
+const JobFragment = ({ text, bullets }) => {
+ return (
+ <>
+ {text && text}
+ {bullets ? (
+
+ {bullets.map(bullet => (
+ - {bullet}
+ ))}
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+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 (
+ <>
+
+ {trivial ? (
+
{name}
+ ) : (
+
{name}
+ )}
+
{time}
+
+ {position && {position}
}
+ {content.map(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;
diff --git a/src/index.js b/src/index.js
index e441dd0..45ccefc 100644
--- a/src/index.js
+++ b/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 ;
+};
+
+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;
diff --git a/src/styles/main.css b/src/styles/main.css
new file mode 100644
index 0000000..d079de4
--- /dev/null
+++ b/src/styles/main.css
@@ -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;
+ }
+}
diff --git a/src/theme/constants.js b/src/theme/constants.js
new file mode 100644
index 0000000..2ffaa59
--- /dev/null
+++ b/src/theme/constants.js
@@ -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 };
diff --git a/src/theme/hooks/useFavicon.js b/src/theme/hooks/useFavicon.js
new file mode 100644
index 0000000..21160cc
--- /dev/null
+++ b/src/theme/hooks/useFavicon.js
@@ -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;
diff --git a/src/theme/hooks/useTheme.js b/src/theme/hooks/useTheme.js
new file mode 100644
index 0000000..e38f3f1
--- /dev/null
+++ b/src/theme/hooks/useTheme.js
@@ -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;
diff --git a/src/theme/index.js b/src/theme/index.js
new file mode 100644
index 0000000..d3864e1
--- /dev/null
+++ b/src/theme/index.js
@@ -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 };
diff --git a/src/theme/utils.js b/src/theme/utils.js
new file mode 100644
index 0000000..f4b42dc
--- /dev/null
+++ b/src/theme/utils.js
@@ -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;
+};
diff --git a/src/theme/variants/blue.js b/src/theme/variants/blue.js
new file mode 100644
index 0000000..0216c58
--- /dev/null
+++ b/src/theme/variants/blue.js
@@ -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;
diff --git a/src/theme/variants/brown.js b/src/theme/variants/brown.js
new file mode 100644
index 0000000..b7beebb
--- /dev/null
+++ b/src/theme/variants/brown.js
@@ -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;
diff --git a/src/theme/variants/coral.js b/src/theme/variants/coral.js
new file mode 100644
index 0000000..e9a3827
--- /dev/null
+++ b/src/theme/variants/coral.js
@@ -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;
diff --git a/src/theme/variants/green.js b/src/theme/variants/green.js
new file mode 100644
index 0000000..2c2ee2c
--- /dev/null
+++ b/src/theme/variants/green.js
@@ -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;
diff --git a/src/theme/variants/index.js b/src/theme/variants/index.js
new file mode 100644
index 0000000..142c656
--- /dev/null
+++ b/src/theme/variants/index.js
@@ -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 };
diff --git a/src/theme/variants/nude.js b/src/theme/variants/nude.js
new file mode 100644
index 0000000..4018f06
--- /dev/null
+++ b/src/theme/variants/nude.js
@@ -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;
diff --git a/src/theme/variants/orange.js b/src/theme/variants/orange.js
new file mode 100644
index 0000000..b13ab8b
--- /dev/null
+++ b/src/theme/variants/orange.js
@@ -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;
diff --git a/src/theme/variants/pink.js b/src/theme/variants/pink.js
new file mode 100644
index 0000000..072b3c2
--- /dev/null
+++ b/src/theme/variants/pink.js
@@ -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;
diff --git a/src/theme/variants/purple.js b/src/theme/variants/purple.js
new file mode 100644
index 0000000..598b0f2
--- /dev/null
+++ b/src/theme/variants/purple.js
@@ -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;
diff --git a/src/theme/variants/turquoise.js b/src/theme/variants/turquoise.js
new file mode 100644
index 0000000..b542a33
--- /dev/null
+++ b/src/theme/variants/turquoise.js
@@ -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;
diff --git a/src/utils/textUtils.js b/src/utils/textUtils.js
new file mode 100644
index 0000000..371c1df
--- /dev/null
+++ b/src/utils/textUtils.js
@@ -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 };