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 ( +
+
+ {data.picture.alt} +
+ + {data.download.label} + + {displayProfileName && } +
+ ); +}; + +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 ( + <> +
+
+

{name}

+
{time}
+
+
{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}} +

+ + + + ); +}; + +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 };