diff --git a/README.md b/README.md index 200f4282..dc1a35e9 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ # Portfolio +A portfolio showcasing projects, built with React. + +# Netlify link: +https://gabriellaberkowicz.netlify.app/ + +# Tech +- JavaScript +- React & Styled components +- CSS + HTML + diff --git a/index.html b/index.html index 6676fb2d..0ea90208 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,18 @@ - + - Portfolio + Portfolio - Gabriella Berkowicz + + + + + + + + +
diff --git a/package.json b/package.json index 48911600..5de0c4fc 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,16 @@ }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "styled-components": "^6.1.19", + "swiper": "^12.0.3" }, "devDependencies": { "@eslint/js": "^9.21.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.7.0", + "babel-plugin-styled-components": "^2.1.4", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", diff --git a/public/cross-white.svg b/public/cross-white.svg new file mode 100644 index 00000000..eacf48c9 --- /dev/null +++ b/public/cross-white.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 00000000..a9363e96 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/happy-thoughts.png b/public/happy-thoughts.png new file mode 100644 index 00000000..0e8c805f Binary files /dev/null and b/public/happy-thoughts.png differ diff --git a/public/og.image.2.png b/public/og.image.2.png new file mode 100644 index 00000000..044f1095 Binary files /dev/null and b/public/og.image.2.png differ diff --git a/public/og.image.png b/public/og.image.png new file mode 100644 index 00000000..4e8cb0e3 Binary files /dev/null and b/public/og.image.png differ diff --git a/public/profile-filled.png b/public/profile-filled.png new file mode 100644 index 00000000..de6d0855 Binary files /dev/null and b/public/profile-filled.png differ diff --git a/public/profile.png b/public/profile.png new file mode 100644 index 00000000..dc3aeb16 Binary files /dev/null and b/public/profile.png differ diff --git a/public/quiz-game.png b/public/quiz-game.png new file mode 100644 index 00000000..2d851e74 Binary files /dev/null and b/public/quiz-game.png differ diff --git a/public/recipe-library.png b/public/recipe-library.png new file mode 100644 index 00000000..87702238 Binary files /dev/null and b/public/recipe-library.png differ diff --git a/public/weather-app.png b/public/weather-app.png new file mode 100644 index 00000000..44cac4ef Binary files /dev/null and b/public/weather-app.png differ diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..5b67efaa 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,29 @@ +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { theme } from './style/Theme.styled'; +import { GlobalStyle } from './style/GlobalStyle'; +import { IntroSection } from './components/sections/IntroSection'; +import { ProjectSection } from './components/sections/ProjectSection'; +import { SkillsSection } from './components/sections/SkillsSection'; +import { ContactSection } from './components/sections/ContactSection'; +import { TechSection } from './components/sections/TechSection'; +import { ArticleSection } from './components/sections/ArticleSection'; + + export const App = () => { return ( <> -

Portfolio

-

Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptatem, laborum! Maxime animi nostrum facilis distinctio neque labore consectetur beatae eum ipsum excepturi voluptatum, dicta repellendus incidunt fugiat, consequatur rem aperiam.

+ + + +
+ + + + +
+ +
) } diff --git a/src/components/animations/AnimatedProjectSection.jsx b/src/components/animations/AnimatedProjectSection.jsx new file mode 100644 index 00000000..2c740ac4 --- /dev/null +++ b/src/components/animations/AnimatedProjectSection.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import { AnimatedSection } from "./AnimatedSection"; +import { theme } from "../../style/Theme.styled"; + +export const AnimatedProjectSection = ({ children }) => { + + const isBigScreen = window.matchMedia(theme.media.desktop).matches; + + return ( + !isBigScreen + ? ({children}) + : (children) + ); +} diff --git a/src/components/animations/AnimatedSection.jsx b/src/components/animations/AnimatedSection.jsx new file mode 100644 index 00000000..da5c7306 --- /dev/null +++ b/src/components/animations/AnimatedSection.jsx @@ -0,0 +1,38 @@ +import React, { useRef, useEffect, useState } from "react"; +import { StyledAnimatedSection } from "./AnimatedSection.styled"; +import { theme } from "../../style/Theme.styled"; + + +export const AnimatedSection = ({ children, direction = "up" }) => { + + // create a reference to a DOM element. Becomes the actual DOM node so the IntersectionObserver can observe it. + const ref = useRef(); + + // the visibility control (defaulting to false), making the component dynamic. When we update the value of visible by calling setVisible, React will re-render the component + const [visible, setVisible] = useState(false); + + // to change threshold value depending on screen size + const isBigScreen = window.matchMedia(theme.media.tablet).matches; + const threshold = isBigScreen ? 0.3 : 0.1; + + + // observes if the referenced element comes into view (becomes visible) - then call setVisible(true); + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) setVisible(true); + }, + { threshold } + ); + if (ref.current) observer.observe(ref.current); + + return () => observer.disconnect(); + }, []); + + + return ( + + {children} + + ); +}; diff --git a/src/components/animations/AnimatedSection.styled.js b/src/components/animations/AnimatedSection.styled.js new file mode 100644 index 00000000..fa35dd25 --- /dev/null +++ b/src/components/animations/AnimatedSection.styled.js @@ -0,0 +1,14 @@ +import styled from "styled-components"; + +const slideDirections = { + up: 'translateY(50px)', + down: 'translateY(-50px)', + left: 'translateX(-50px)', + right: 'translateX(50px)', +}; + +export const StyledAnimatedSection = styled.div` + opacity: ${({ visible }) => (visible ? 1 : 0)}; + transform: ${({ direction, visible }) => visible ? 'translate(0, 0)' : slideDirections[direction] || 'translateY(50px)'}; + transition: all 1s cubic-bezier(0.25, 0.1, 0.25, 1); +`; diff --git a/src/components/buttons/Button.jsx b/src/components/buttons/Button.jsx new file mode 100644 index 00000000..260b2bc4 --- /dev/null +++ b/src/components/buttons/Button.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { StyledButton, StyledLinkButton } from './Button.styled'; + + +export const LinkButton = ({ variant="primaryBtn", link, children, ...props }) => { + return ( + + {children} + + ); +} + +export const Button = ({ variant="secondaryBtn", children, ...props} ) => { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/components/buttons/Button.styled.js b/src/components/buttons/Button.styled.js new file mode 100644 index 00000000..8724f807 --- /dev/null +++ b/src/components/buttons/Button.styled.js @@ -0,0 +1,43 @@ +import styled from "styled-components"; + +export const StyledLinkButton = styled.a` + color: ${(props) => props.theme.colors[props.variant]?.text}; + background-color: ${(props) => props.theme.colors[props.variant]?.bg}; + outline: 2px solid ${(props) => props.theme.colors[props.variant]?.outline}; + border-radius: 12px; + padding: 8px 16px; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: transform 0.25s cubic-bezier(.2,.8,.2,1), + box-shadow 0.25s cubic-bezier(.2,.8,.2,1); + + &:hover{ + transform: translateY(-3px) scale(1.03); + box-shadow: 0 4px 6px rgba(0,0,0,0.12); + } + + @media ${(props) => props.theme.media.desktop} { + flex: 1; + } +`; + + +export const StyledButton = styled.button` + color: ${(props) => props.theme.colors[props.variant]?.text}; + background-color: ${(props) => props.theme.colors[props.variant]?.bg}; + outline: 2px solid ${(props) => props.theme.colors[props.variant]?.outline}; + border:none; + border-radius: 12px; + padding: 8px 16px; + transition: transform 0.25s cubic-bezier(.2,.8,.2,1), + box-shadow 0.25s cubic-bezier(.2,.8,.2,1); + cursor: pointer; + + &:hover{ + transform: translateY(-3px) scale(1.03); + box-shadow: 0 4px 6px rgba(0,0,0,0.12); + } +`; \ No newline at end of file diff --git a/src/components/cards/Article.jsx b/src/components/cards/Article.jsx new file mode 100644 index 00000000..4e81e7bb --- /dev/null +++ b/src/components/cards/Article.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Typography } from '../typography/CustomTypography'; +import { Button } from '../buttons/Button'; +import { Img } from '../images/Img'; +import { StyledCardDiv, StyledTextDivFaded } from './Card.styled'; + + +export const Article = ({ article, onOpen }) => { + return ( + + article image +
+ {article.tag} +
+ + {article.title} + {article.sections.map((section, index) => ( + {section} + ))} + + +
+ ); +} \ No newline at end of file diff --git a/src/components/cards/Card.styled.js b/src/components/cards/Card.styled.js new file mode 100644 index 00000000..47e94eff --- /dev/null +++ b/src/components/cards/Card.styled.js @@ -0,0 +1,94 @@ +import styled from "styled-components"; + +export const StyledCardDiv = styled.div` + background-color: #ffffff; + border-radius: 12px; + padding: 16px; + overflow: hidden; + display: flex; + flex-direction: column; + flex: 1; + gap: 24px; + transition: transform 0.25s ease; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + max-width: 400px; + /* width: 100%; */ + + @media ${(props) => props.theme.media.desktop} { + width: initial; + } + + &:hover { + transform: scale(1.02); + box-shadow: + 0 0 6px rgba(253, 111, 0, 0.15), + 0 8px 20px rgba(0, 0, 0, 0.10); + } +`; + +export const StyledProjectCardDiv = styled(StyledCardDiv)` + max-width: initial; + width: initial; + + @media ${(props) => props.theme.media.desktop} { + flex-direction: row; + justify-content: space-around; + gap: 12px; + box-shadow: none; + padding: 32px 12px; + flex-direction: ${({ reverse }) => (reverse ? "row-reverse" : "row")}; //reverse the content for every second project + } +`; + + +export const StyledContentDiv = styled.div` + display: flex; + flex-direction: column; + gap: 18px; + flex: 1; +`; + +export const StyledTextDiv = styled.div ` + overflow: hidden; + padding-bottom: 1em; + position: relative; +`; + +export const StyledTextDivFaded = styled(StyledTextDiv)` + max-height: 270px; + + // fade end of text content + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 3em; + pointer-events: none; + background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%); + } +`; + +export const StyledButtonDiv = styled.div ` + display: flex; + flex-direction: column; + align-items: column; + gap: 16px; + + @media ${(props) => props.theme.media.desktop} { + flex-direction: row; + width: 70%; + } +`; + +export const StyledProjectContentWrapper = styled.div` + display:flex; + flex-direction: column; + flex: 1; + justify-content: space-between; + @media ${(props) => props.theme.media.desktop} { + max-width: 50%; + margin: 12px 0; + } +`; \ No newline at end of file diff --git a/src/components/cards/CardContainer.styled.js b/src/components/cards/CardContainer.styled.js new file mode 100644 index 00000000..c92554ec --- /dev/null +++ b/src/components/cards/CardContainer.styled.js @@ -0,0 +1,16 @@ +import styled from "styled-components"; + +export const StyledCardContainer = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + grid-gap: 16px; +`; + +export const StyledProjectCardContainer = styled(StyledCardContainer)` + @media ${(props) => props.theme.media.desktop} { + display: flex; + flex-direction: column; + gap: 42px; + margin-top: 24px; + } +`; \ No newline at end of file diff --git a/src/components/cards/Project.jsx b/src/components/cards/Project.jsx new file mode 100644 index 00000000..b72e9fd4 --- /dev/null +++ b/src/components/cards/Project.jsx @@ -0,0 +1,73 @@ +import React, { useState, useEffect } from 'react'; +import { Tags } from './Tags'; +import { StyledTagContainer } from './Tags.styled'; +import { Img } from '../images/Img'; +import { LinkButton } from '../buttons/Button'; +import { GlobeIcon } from '../icons/Globe'; +import { GithubIcon } from '../icons/Github'; +import { Typography } from '../typography/CustomTypography'; +import { StyledProjectCardDiv, StyledProjectContentWrapper, StyledContentDiv, StyledTextDiv, StyledButtonDiv } from './Card.styled' +import { AnimatedSection } from '../animations/AnimatedSection'; +import { theme } from '../../style/Theme.styled'; + +const useMediaQuery = (query) => { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + // set initial value + setMatches(media.matches); + const listener = (e) => setMatches(e.matches); + media.addEventListener('change', listener); + // clean-up + return () => media.removeEventListener('change', listener); + }, [query]); + + return matches; +}; + +export const Project = ({ project, index }) => { + + // update reactively when window size changes + const isBigScreen = useMediaQuery(theme.media.desktop); + + const direction = index % 2 === 0 ? "right" : "left"; // even → right, odd → left + + const projectComponent = ( + + project + + + + {project.tags.map((tag, index) => ( + + ))} + + + {project.title} + {project.description} + + + + + + Live Demo + + + + View Code + + + + + ); + + return ( + <> + {isBigScreen + ? ({projectComponent}) + : projectComponent + } + + ); +} \ No newline at end of file diff --git a/src/components/cards/Tags.jsx b/src/components/cards/Tags.jsx new file mode 100644 index 00000000..44589d3b --- /dev/null +++ b/src/components/cards/Tags.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { StyledTag } from './Tags.styled'; + +export const Tags = ({ children }) => { + return ( + +

{children}

+
+ ); +} \ No newline at end of file diff --git a/src/components/cards/Tags.styled.js b/src/components/cards/Tags.styled.js new file mode 100644 index 00000000..6517d373 --- /dev/null +++ b/src/components/cards/Tags.styled.js @@ -0,0 +1,19 @@ +import styled from "styled-components"; + +export const StyledTag = styled.div` + background-color: transparent; + color: #4a4a4a; + border: 1px solid #888888; + border-radius: 12px; + padding: 2px 6px; + display: flex; + flex-direction: column; + align-items: center; +`; + +export const StyledTagContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 4px; + margin: 8px 0; +`; \ No newline at end of file diff --git a/src/components/icons/Github.jsx b/src/components/icons/Github.jsx new file mode 100644 index 00000000..27a10c7b --- /dev/null +++ b/src/components/icons/Github.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { theme } from '../../style/Theme.styled'; + +export const GithubIcon = (props) => { + return ( + + + + ); +} + + diff --git a/src/components/icons/Globe.jsx b/src/components/icons/Globe.jsx new file mode 100644 index 00000000..3b98a37b --- /dev/null +++ b/src/components/icons/Globe.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +export const GlobeIcon = () => { + return ( + + + + ); +} + + diff --git a/src/components/icons/IconsContainer.jsx b/src/components/icons/IconsContainer.jsx new file mode 100644 index 00000000..c71ebff6 --- /dev/null +++ b/src/components/icons/IconsContainer.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { GithubIcon } from './Github'; +import { LinkedinIcon } from './Linkedin'; +import { LinkIcon } from '../icons/LinkIcon'; +import styled from 'styled-components'; + +export const IconsContainer = () => { + return ( + + + + + + + ); +} + + +export const StyledWrapper = styled.div ` + display: flex; + gap: 24px; + margin-top: 32px; + align-items: center; +`; + diff --git a/src/components/icons/LinkIcon.jsx b/src/components/icons/LinkIcon.jsx new file mode 100644 index 00000000..80657d29 --- /dev/null +++ b/src/components/icons/LinkIcon.jsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export const LinkIcon = ({ children, link, ariaLabel }) => { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/components/icons/Linkedin.jsx b/src/components/icons/Linkedin.jsx new file mode 100644 index 00000000..7b37f8e6 --- /dev/null +++ b/src/components/icons/Linkedin.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { theme } from '../../style/Theme.styled'; + +export const LinkedinIcon = (props) => { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/components/images/Img.jsx b/src/components/images/Img.jsx new file mode 100644 index 00000000..caafe140 --- /dev/null +++ b/src/components/images/Img.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import styled from 'styled-components'; + +export const Img = ({ src, alt, height="280px" }) => { + return ( + + ); +} + +const StyledImg = styled.img` + height: ${({ height }) => height}; + object-fit: contain; +`; \ No newline at end of file diff --git a/src/components/modal/Modal.jsx b/src/components/modal/Modal.jsx new file mode 100644 index 00000000..73019d6b --- /dev/null +++ b/src/components/modal/Modal.jsx @@ -0,0 +1,20 @@ +import React from "react"; +import { StyledCrossIcon, StyledModal, StyledModalOverlay, StyledModalContent } from "./Modal.styled"; +import { Img } from "../images/Img"; + + + +export const Modal = ({ onClose, children }) => { + return ( + + + cross icon + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/src/components/modal/Modal.styled.js b/src/components/modal/Modal.styled.js new file mode 100644 index 00000000..966484c6 --- /dev/null +++ b/src/components/modal/Modal.styled.js @@ -0,0 +1,53 @@ +import styled from "styled-components"; + +export const StyledModalOverlay = styled.div` + display: flex; + flex-direction: column; + position: fixed; /* stay in place */ + z-index: 1; /* sit on top */ + left: 0; + top: 0; + width: 100vw; /* full width */ + height: 100vh; /* full height */ + background-color: rgba(0,0,0,0.4); +`; + +export const StyledModal = styled.div` + display: flex; + flex-direction: column; + align-items: center; + position: fixed; /* stay in place */ + z-index: 1; /* sit on top */ + left: 10%; + right: 10%; + height: 80vh; + top: 0; + margin: 70px auto; + overflow: auto; /* enable scroll if needed */ + border-radius: 5px; + background-color: white; + padding: 20px; + max-width: 700px; + border-radius: 6px; +`; + +export const StyledModalContent = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 20px; + padding: 12px 20px 20px 20px; +`; + +export const StyledModalImg = styled.img` + width: 80%; + box-shadow: 0 0 2px grey; + align-self: center; + margin-bottom: 20px; +`; + +export const StyledCrossIcon = styled.div` + align-self: end; + margin-right: 24px; +`; \ No newline at end of file diff --git a/src/components/sections/ArticleSection.jsx b/src/components/sections/ArticleSection.jsx new file mode 100644 index 00000000..6b8f7b6a --- /dev/null +++ b/src/components/sections/ArticleSection.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useState } from 'react'; +import articleData from '../../data/articles.json'; +import { Article } from '../cards/Article'; +import { Typography } from '../typography/CustomTypography'; +import { StyledCardContainer } from '../cards/CardContainer.styled'; +import { StyledSection } from './Section.styled'; +import { AnimatedSection } from '../animations/AnimatedSection'; +import { Modal } from '../modal/Modal'; +import { StyledModalImg } from '../modal/Modal.styled'; + + +export const ArticleSection = () => { + + // functionality to open/close article popup modal + const [selectedArticle, setSelectedArticle] = useState(null); + + const openModal= article => { + setSelectedArticle(article); + } + + const closeModal = () => { + setSelectedArticle(null); + } + + + return ( + <> + + + My Journey + + {articleData.articles.map((article, index) => ( +
openModal(article)} + /> + ))} + + + + + {selectedArticle && ( + + + {selectedArticle.title} + {selectedArticle.sections.map((section, index) => ( + {section} + ))} + + )} + + ) +} \ No newline at end of file diff --git a/src/components/sections/ContactSection.jsx b/src/components/sections/ContactSection.jsx new file mode 100644 index 00000000..fe3334f4 --- /dev/null +++ b/src/components/sections/ContactSection.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { IconsContainer } from '../icons/IconsContainer'; +import { Typography } from '../typography/CustomTypography'; +import { Img } from '../images/Img'; +import { StyledContactSection, StyledContent } from './Section.styled'; + + +export const ContactSection = () => { + return ( + + profile picture + + Let's Talk! + Gabriella Berkowicz + +46(0) 736 37 46 46 + gabriellaberko@live.se + + + + ); +} \ No newline at end of file diff --git a/src/components/sections/IntroSection.jsx b/src/components/sections/IntroSection.jsx new file mode 100644 index 00000000..01e8c80c --- /dev/null +++ b/src/components/sections/IntroSection.jsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import { Typography } from '../typography/CustomTypography'; +import { StyledIntroSection } from './Section.styled'; +import { IconsContainer } from '../icons/IconsContainer'; +import { Img } from '../images/Img'; +import { theme } from '../../style/Theme.styled'; + +const useMediaQuery = (query) => { + const [matches, setMatches] = useState(false); + + useEffect(() => { + const media = window.matchMedia(query); + // set initial value + setMatches(media.matches); + const listener = (e) => setMatches(e.matches); + media.addEventListener('change', listener); + // clean-up + return () => media.removeEventListener('change', listener); + }, [query]); + + return matches; +}; + + +export const IntroSection = () => { + + // update reactively when window size changes + const isBigScreen = useMediaQuery(theme.media.desktop); + + const smallScreenIntro = ( + + I am Gabriella Berkowicz +

+ profile picture +

+ Frontend Developer + With a Background in Web Analytics +

+ I'm passionate about building user-focused and data-informed web experiences with JavaScript and React. My goal is to continue to grow as a developer while bringing curiosity, creativity, and an analytical mindset to every project I take on. + My background as a Technical Web Analyst has given me a strong foundation in understanding user behavior, collaborating across teams, and making data-driven decisions in product development. I consider myself an adaptable, fast learner who enjoys problem solving, collaboration, and values continuous learning. + + + +
+ ) + + const bigScreenIntro = ( + +
+ I am Gabriella Berkowicz +

+ Frontend Developer + With a Background in Web Analytics +

+ I'm passionate about building user-focused and data-informed web experiences with JavaScript and React. My goal is to continue to grow as a developer while bringing curiosity, creativity, and an analytical mindset to every project I take on. + My background as a Technical Web Analyst has given me a strong foundation in understanding user behavior, collaborating across teams, and making data-driven decisions in product development. I consider myself an adaptable, fast learner who enjoys problem solving, collaboration, and values continuous learning. + + +
+ profile picture +
+ ) + + + return ( + !isBigScreen + ? smallScreenIntro + : bigScreenIntro + ); +} diff --git a/src/components/sections/ProjectSection.jsx b/src/components/sections/ProjectSection.jsx new file mode 100644 index 00000000..ea5c0fef --- /dev/null +++ b/src/components/sections/ProjectSection.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import projectData from '../../data/projects.json'; +import { Project } from '../cards/Project'; +import { Typography } from '../typography/CustomTypography'; +import { StyledProjectCardContainer } from '../cards/CardContainer.styled'; +import { StyledSection } from './Section.styled'; +import { AnimatedProjectSection } from '../animations/AnimatedProjectSection'; +import styled from 'styled-components'; + + +export const ProjectSection = () => { + + return ( + + + + Featured Projects + + + {projectData.projects.map((project, index) => ( + + ))} + + + + ); +} + +const StyledWrapper = styled.div` + text-align: center; +`; \ No newline at end of file diff --git a/src/components/sections/Section.styled.js b/src/components/sections/Section.styled.js new file mode 100644 index 00000000..b89ed02f --- /dev/null +++ b/src/components/sections/Section.styled.js @@ -0,0 +1,63 @@ +import styled from "styled-components"; + + +export const StyledSection = styled.section` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 32px; + margin: 100px 0; +`; + + +/*--- intro section ---*/ + +export const StyledIntroSection = styled.header` + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + + @media ${(props) => props.theme.media.desktop} { + flex-direction: row; + gap: 32px; + justify-content: space-evenly; + margin-top: 24px; + text-align: initial; + } +`; + +export const StyledWrapper = styled.div` + text-align: center; + + @media ${(props) => props.theme.media.desktop} { + text-align: initial; + } +`; + + +/*--- contact section ---*/ + +export const StyledContactSection = styled.footer` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 32px; + margin: 100px 0; + + @media ${(props) => props.theme.media.desktop} { + flex-direction: row; + } +`; + +export const StyledContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + @media ${(props) => props.theme.media.desktop} { + align-items: flex-start; + } +` \ No newline at end of file diff --git a/src/components/sections/SkillsSection.jsx b/src/components/sections/SkillsSection.jsx new file mode 100644 index 00000000..64538cbb --- /dev/null +++ b/src/components/sections/SkillsSection.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import skillsData from '../../data/skills.json'; +import { SkillsBox } from '../skills/SkillsBox'; +import { Typography } from '../typography/CustomTypography'; +import { StyledSection } from './Section.styled'; +import { StyledSkillsContainer } from '../skills/SkillsContainer.styled'; +import { AnimatedSection } from '../animations/AnimatedSection'; + + + +export const SkillsSection = () => { + return ( + + + Skills + + {skillsData.skills.map((skillObj, index) => ( + + ))} + + + + ); +} diff --git a/src/components/sections/TechSection.jsx b/src/components/sections/TechSection.jsx new file mode 100644 index 00000000..5da37ce4 --- /dev/null +++ b/src/components/sections/TechSection.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Typography } from '../typography/CustomTypography'; +import { StyledSection } from './Section.styled'; +import styled from 'styled-components'; +import { AnimatedSection } from '../animations/AnimatedSection'; + + + +export const TechSection = () => { + return ( + + + + Tech & Workflow + I work with modern development workflows using Git for version control, including structured branching and clear commit history. I value collaboration and enjoy working in pair- or mob-programming settings to build better solutions, share knowledge, and solve problems more effciently. I like to keep exploring new tools and approaches to create smarter solutions while continuously growing as a developer. + + + + ); +} + +export const StyledWrapper = styled.div ` + max-width: 780px; + text-align: center; +`; \ No newline at end of file diff --git a/src/components/skills/Skills.styled.js b/src/components/skills/Skills.styled.js new file mode 100644 index 00000000..3a7c443d --- /dev/null +++ b/src/components/skills/Skills.styled.js @@ -0,0 +1,34 @@ +import styled from "styled-components"; + + +export const StyledSkillsBoxDiv = styled.div ` + flex: 1; + text-align: center; + padding: 0 7px; + + @media ${(props) => props.theme.media.desktop} { + width: 200px; + } +`; + + +export const StyledSkillsList = styled.ul ` + list-style-type: none; + padding-left: 0; +`; + + +export const StyledLineDivider = styled.div` + &:not(:last-child) { + border-bottom: 2px solid ${(props) => props.theme.colors.main.accent}; /* horizontal line */ + } + + @media ${(props) => props.theme.media.desktop} { + height: auto; + + &:not(:last-child) { + border-bottom: none; + border-right: 2px solid ${(props) => props.theme.colors.main.accent}; /* vertical line */ + } + } +`; \ No newline at end of file diff --git a/src/components/skills/SkillsBox.jsx b/src/components/skills/SkillsBox.jsx new file mode 100644 index 00000000..2421c835 --- /dev/null +++ b/src/components/skills/SkillsBox.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { SkillsList } from './SkillsList'; +import { Typography } from '../typography/CustomTypography'; +import { StyledSkillsBoxDiv, StyledLineDivider } from './Skills.styled'; + + +export const SkillsBox = ({ skillObj } ) => { + return ( + <> + + {skillObj.title} +
+ {skillObj.skills.map((skill, index) => ( + + ))} +
+
+ + + ); +} \ No newline at end of file diff --git a/src/components/skills/SkillsContainer.styled.js b/src/components/skills/SkillsContainer.styled.js new file mode 100644 index 00000000..bc491be2 --- /dev/null +++ b/src/components/skills/SkillsContainer.styled.js @@ -0,0 +1,14 @@ +import styled from "styled-components"; + + +export const StyledSkillsContainer = styled.div` + display: flex; + flex-direction: column; + gap: 18px; + + @media ${(props) => props.theme.media.desktop} { + flex-direction: row; + justify-content: center; + + } +`; \ No newline at end of file diff --git a/src/components/skills/SkillsList.jsx b/src/components/skills/SkillsList.jsx new file mode 100644 index 00000000..230d6b02 --- /dev/null +++ b/src/components/skills/SkillsList.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { StyledSkillsList } from './Skills.styled'; + +export const SkillsList = ({ skill }) => { + return ( + +
  • {skill}
  • +
    + ); +} \ No newline at end of file diff --git a/src/components/typography/CustomTypography.jsx b/src/components/typography/CustomTypography.jsx new file mode 100644 index 00000000..18f7b549 --- /dev/null +++ b/src/components/typography/CustomTypography.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { StyledTypography } from './Typography.styled.js'; + + +export const Typography = ({ as="p", size, weight, children }) => { + return ( + + {children} + + ); +} diff --git a/src/components/typography/Typography.styled.js b/src/components/typography/Typography.styled.js new file mode 100644 index 00000000..3bcec927 --- /dev/null +++ b/src/components/typography/Typography.styled.js @@ -0,0 +1,60 @@ +import styled from "styled-components"; + + +// map the size and weight values +const sizeMapping = { + xs: "16px", + s: "22px", + m: "30px", + l: "64px", + xl: "80px", + xxl: "100px" +}; + +const weightMapping = { + xlight: 200, + light: 300, + regular: 400, + medium: 600, + bold: 700, + xbold: 800 +}; + +const typographyConfig = { + h1: { + mobile: { size: "48px", weight: 700 }, + desktop: { size: "52px", weight: 700 } + }, + h2: { + mobile: { size: "48px", weight: 700 }, + desktop: { size: "58px", weight: 700 } + }, + h3: { + mobile: { size: "24px", weight: 600 }, + desktop: { size: "24px", weight: 600 } + }, + p: { + mobile: { size: "16px", weight: 400 }, + desktop: { size: "18px", weight: 400 } + } +} + + +export const StyledTypography = styled.div` + margin: 0 0 4px 0; + padding: 0; + + // firsly check if size/weight values have been put manually, otherwise use pre-defined values for media size: + + // mobile (as default) + font-size: ${({ as, size }) => size ? sizeMapping[size] : typographyConfig[as]?.mobile.size}; + + font-weight: ${({ as, weight }) => weight ? weightMapping[weight] : typographyConfig[as]?.mobile.weight}; + + // desktop + @media ${(props) => props.theme.media.desktop} { + font-size: ${({ as, size }) => size ? sizeMapping[size] : typographyConfig[as]?.desktop.size}; + + font-weight: ${({ as, weight }) => weight ? weightMapping[weight] : typographyConfig[as]?.desktop.weight}; + } +`; diff --git a/src/data/articles.json b/src/data/articles.json new file mode 100644 index 00000000..5d9d0524 --- /dev/null +++ b/src/data/articles.json @@ -0,0 +1,14 @@ +{ + "articles": [ + { + "title": "My Motivation For Career Change", + "image": "https://images.unsplash.com/photo-1520792532857-293bd046307a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2370&q=80", + "sections": [ + "My journey into web development began with a desire to move from analyzing products to building them - to create the experiences that shape how users behave. Having been part of a product team for several years, I have always admired the developers' inspiration and creativity in their daily work. When I started coding real projects myself, I finally understood that drive - there's something magical about bringing ideas to life, and nothing like that superhero feeling when you finally solve a bug you struggled with for hours, or even days.", + "The first time I tried coding was during my higher vocational studies in Web Analytics, where we started learning JavaScript, HTML and CSS. I realized coding is actually really fun, but kept going with my career in web analytics. With time I grew into a more technical role where I had more insight in a product team, but felt the need to increase my technical understanding to better communicate with our developers and work more efficiently. I, therefore, set goals to learn more about web development for upskilling in my current role. I began some courses at freeCodeCamp and The Odin Project, and realized that it is what I want to do full-time. In August 2025 I officially started my developer journey with Technigo where I joined their 32-week fast-paced JavaScript Developer Bootcamp.", + "What I enjoy most about programming is the mix of problem solving, creativity, collaboration, and continuous learning. There's always a new concept to explore, a cleaner way to write something, or a new tool that changes how you think and work. And time after time, you surprise yourself by overcoming new challenges you once thought were impossible. My goal is to keep growing as a developer in a collaborative team where learning never stops, and where the focus is always on building things that truly make sense for users." + ], + "tag": "Nov 21th" + } + ] +} \ No newline at end of file diff --git a/src/data/projects.json b/src/data/projects.json index 7c426028..21a97b03 100644 --- a/src/data/projects.json +++ b/src/data/projects.json @@ -1,28 +1,61 @@ { "projects": [ { - "name": "Business site", - "image": "https://images.unsplash.com/photo-1557008075-7f2c5efa4cfd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2497&q=80", + "title": "Weather App", + "image": "/weather-app.png", + "description": "A TypeScript-based weather app built with SMHI’s Open Data REST API. It displays the current weather conditions and weather forecast for any locations in Sweden. The project focuses on API integration, clean UI, and structured TypeScript code.", "tags": [ - "HTML5", - "CSS3", - "JavaScript" + "JavaScript", + "TypeScript", + "REST API", + "CSS", + "HTML" ], - "netlify": "link", - "github": "link" + "netlify": "https://swe-weather-app.netlify.app/", + "github": "https://github.com/gabriellaberko/js-project-weather-app" }, { - "name": "Weather app", - "image": "https://images.unsplash.com/photo-1520792532857-293bd046307a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2370&q=80", + "title": "Accessible Quiz Game", + "image": "/quiz-game.png", + "description": "A quiz game built with TypeScript and Open Trivia Database’s REST API, styled with Tailwind CSS. Accessibility was a key focus and the app scored highly on Wave and Lighthouse tests, and supports full keyboard navigation. ", "tags": [ - "HTML5", - "CSS3", "JavaScript", "TypeScript", - "APIs" + "REST API", + "CSS", + "HTML", + "Tailwind" + ], + "netlify": "https://quizbonanza.netlify.app/", + "github": "https://github.com/gabriellaberko/js-project-accessibility" + }, + { + "title": "Happy Thoughts", + "image": "/happy-thoughts.png", + "description": "A small app where users can share and like each others' messages. Built with React, styled-components, and a REST API. Features real-time UI updates, dynamic timestamps, and automatic re-fetching to stay in sync with the backend.", + "tags": [ + "JavaScript", + "React", + "REST API", + "Styled components", + "CSS", + "HTML" + ], + "netlify": "https://happysharing.netlify.app/", + "github": "https://github.com/gabriellaberko/js-project-happy-thoughts/" + }, + { + "title": "Recipe Library", + "image": "/recipe-library.png", + "description": "This recipe library project is built upon Spoonacular’s REST API. It features search, sorting, and filtering, as well as functionality to save favorite recipes. Local storage is used to cache previously fetched recipes and the user’s favorite recipes.", + "tags": [ + "JavaScript", + "REST API", + "CSS", + "HTML" ], - "netlify": "link", - "github": "link" + "netlify": "https://js-recipe-library.netlify.app/", + "github": "https://github.com/gabriellaberko/js-project-recipe-library" } ] } \ No newline at end of file diff --git a/src/data/skills.json b/src/data/skills.json new file mode 100644 index 00000000..f19a5ae7 --- /dev/null +++ b/src/data/skills.json @@ -0,0 +1,42 @@ +{ + "skills": [ + { + "title": "Languages", + "skills": [ + "JavaScript (ES6+)", + "TypeScript", + "CSS3", + "HTML5", + "SQL" + ] + }, + { + "title": "Frontend", + "skills": [ + "React & Global State Management", + "API Integration", + "Responsive Web Design", + "Web Accessibility", + "Tailwind", + "Git" + ] + }, + { + "title": "Backend", + "skills": [ + "Node.js", + "Express.js", + "MongoDB" + ] + }, + { + "title": "Analytics", + "skills": [ + "Google Tag Manager", + "BigQuery", + "Google Analytics 4", + "A/B Testing" + ] + } + ] +} \ No newline at end of file diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 61010be6..00000000 --- a/src/index.css +++ /dev/null @@ -1,4 +0,0 @@ -body { - background: pink; - color: hotpink; -} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index ed109d76..5eb456c5 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,9 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' - import { App } from './App.jsx' -import './index.css' - createRoot(document.getElementById('root')).render( - , + ) diff --git a/src/style/GlobalStyle.jsx b/src/style/GlobalStyle.jsx new file mode 100644 index 00000000..3c74adc6 --- /dev/null +++ b/src/style/GlobalStyle.jsx @@ -0,0 +1,20 @@ +import { createGlobalStyle } from "styled-components"; + +export const GlobalStyle = createGlobalStyle` + + :root { + font-family: "Poppins", sans-serif; + background: ${(props) => props.theme.colors.main.bg}; + color: ${(props) => props.theme.colors.main.text}; + margin: 30px; + box-sizing: border-box; + } + + /* h1 { + color: ${(props) => props.theme.colors.main.accent} + } */ + + p { + margin: 0; + } +`; \ No newline at end of file diff --git a/src/style/Theme.styled.js b/src/style/Theme.styled.js new file mode 100644 index 00000000..a4bae3e5 --- /dev/null +++ b/src/style/Theme.styled.js @@ -0,0 +1,25 @@ +export const theme = { + colors: { + main: { + bg: "#FEFEFD", + text: "#121212", + accent: "#FD6F00", + outline: "rgba(253, 111, 0, 0.35)" + }, + primaryBtn: { + bg: "#FD6F00", + text: "#FFFFFF", + outline: "#FD6F00" + }, + secondaryBtn: { + bg: "transparent", + text: "#FD6F00", + outline: "#FD6F00" + } + }, + media: { + mobile: "(max-width: 768px)", + tablet: "(min-width: 769px)", + desktop: "(min-width: 1024px)" + } +} \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 8b0f57b9..e1ccb60c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,12 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' -// https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [ + react({ + babel: { + plugins: [['babel-plugin-styled-components', { displayName: true }]] + } + }) + ] +}) \ No newline at end of file