diff --git a/README.md b/README.md index 200f4282..91875967 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # Portfolio + +Link: https://mimmi-eriksson.netlify.app/ diff --git a/index.html b/index.html index 6676fb2d..8580ee68 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,59 @@ - - - - - Portfolio - - -
- - - + + + + + + + + + + + + + + Portfolio + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index 48911600..f4edef7e 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,15 @@ }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "styled-components": "^6.1.17" }, "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.4.1", + "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/assets/accessibility-quiz.png b/public/assets/accessibility-quiz.png new file mode 100644 index 00000000..3c9b7197 Binary files /dev/null and b/public/assets/accessibility-quiz.png differ diff --git a/public/assets/arrow-small.svg b/public/assets/arrow-small.svg new file mode 100644 index 00000000..85a85622 --- /dev/null +++ b/public/assets/arrow-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/arrow.svg b/public/assets/arrow.svg new file mode 100644 index 00000000..fd7e9aa3 --- /dev/null +++ b/public/assets/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/article-icon.svg b/public/assets/article-icon.svg new file mode 100644 index 00000000..bae6fc9a --- /dev/null +++ b/public/assets/article-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/business-site.png b/public/assets/business-site.png new file mode 100644 index 00000000..cd42773b Binary files /dev/null and b/public/assets/business-site.png differ diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico new file mode 100644 index 00000000..6334eaa4 Binary files /dev/null and b/public/assets/favicon.ico differ diff --git a/public/assets/github-btn.svg b/public/assets/github-btn.svg new file mode 100644 index 00000000..f013194a --- /dev/null +++ b/public/assets/github-btn.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/github-icon.svg b/public/assets/github-icon.svg new file mode 100644 index 00000000..43a3cf29 --- /dev/null +++ b/public/assets/github-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/globe-icon.svg b/public/assets/globe-icon.svg new file mode 100644 index 00000000..375d29ad --- /dev/null +++ b/public/assets/globe-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/happy-thoughts.png b/public/assets/happy-thoughts.png new file mode 100644 index 00000000..ca74dd94 Binary files /dev/null and b/public/assets/happy-thoughts.png differ diff --git a/public/assets/linkedin-btn.svg b/public/assets/linkedin-btn.svg new file mode 100644 index 00000000..1a52769d --- /dev/null +++ b/public/assets/linkedin-btn.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/assets/movies.png b/public/assets/movies.png new file mode 100644 index 00000000..2308c116 Binary files /dev/null and b/public/assets/movies.png differ diff --git a/public/assets/portfolio.png b/public/assets/portfolio.png new file mode 100644 index 00000000..7d769a8f Binary files /dev/null and b/public/assets/portfolio.png differ diff --git a/public/assets/profile-picture.jpg b/public/assets/profile-picture.jpg new file mode 100644 index 00000000..e9162d6b Binary files /dev/null and b/public/assets/profile-picture.jpg differ diff --git a/public/assets/recipe-app.png b/public/assets/recipe-app.png new file mode 100644 index 00000000..b4661743 Binary files /dev/null and b/public/assets/recipe-app.png differ diff --git a/public/assets/todo.png b/public/assets/todo.png new file mode 100644 index 00000000..ded077cd Binary files /dev/null and b/public/assets/todo.png differ diff --git a/public/assets/weather-app.png b/public/assets/weather-app.png new file mode 100644 index 00000000..4899168c Binary files /dev/null and b/public/assets/weather-app.png differ diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..bcb20beb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,38 @@ -export const App = () => { +import SkipLink from "./components/SkipLink" +import Header from "./sections/Header" +import IntroSection from "./sections/IntroSection" +import TechSection from "./sections/TechSection" +import ProjectsSection from "./sections/ProjectsSection" +import SkillsSection from "./sections/SkillsSection" +import ArticlesSection from "./sections/ArticlesSection" +import ContactSection from "./sections/ContactSection" + +import tech from "./data/tech.json" +import projects from "./data/projects.json" +import skills from "./data/skills.json" +import articles from "./data/articles.json" + +import GlobalStyle from "./styles/GlobalStyle" +import { ThemeProvider } from "styled-components" +import theme from "./styles/theme.js" + +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.

+ + + +
+ + + + + + ) } + +export default App diff --git a/src/components/ArrowLink.jsx b/src/components/ArrowLink.jsx new file mode 100644 index 00000000..0e7bbdb7 --- /dev/null +++ b/src/components/ArrowLink.jsx @@ -0,0 +1,42 @@ +import styled from "styled-components" + +const Link = styled.a` + display: block; + text-align: center; + + &:focus-visible { + outline: 2px solid; + outline-offset: 5px; + border-radius: 2px; + } + + &:focus-visible>img { + animation: scrollAnimation 1s ease-out infinite alternate; + } +` +const Arrow = styled.img` + filter: ${({ $mode }) => $mode === "dark" ? "brightness(0) invert(1)" : "none"}; + position: relative; + top: 0; + + &:hover { + animation: scrollAnimation 1s ease-out infinite alternate; + } + + @keyframes scrollAnimation { + 0% {top: 0;} + 25% {top: 0;} + 75% {top: 16px;} + 100% {top: 16px;} + } +` + +const ArrowLink = ({ link, mode }) => { + return ( + + + + ) +} + +export default ArrowLink diff --git a/src/components/ArticleCard.jsx b/src/components/ArticleCard.jsx new file mode 100644 index 00000000..062964e3 --- /dev/null +++ b/src/components/ArticleCard.jsx @@ -0,0 +1,56 @@ +import ButtonLink from "./ButtonLink" +import CardHeading from "../typhography/CardHeading.jsx" +import BodyText from "../typhography/BodyText.jsx" +import Tag from "./Tag" +import styled from "styled-components" +import media from "../styles/media.js" + + +const Card = styled.article` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.medium}; + align-items: flex-start; + + @media ${media.tablet} { + gap: 2rem; + } +` + +const ArticleImage = styled.img` + height: 200px; + width: 100%; + object-fit: cover; + box-shadow: 2px 2px 5px rgba(31, 0, 124, 0.25); + + @media ${media.tablet} { + height: 340px; + } + @media ${media.desktop} { + height: 340px; + } +` +const TitleWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.xSmall}; + align-items: flex-start; +` + +const ArticleCard = ({ article }) => { + return ( + + + +
    + +
+ +
+ + +
+ ) +} + +export default ArticleCard \ No newline at end of file diff --git a/src/components/ButtonLink.jsx b/src/components/ButtonLink.jsx new file mode 100644 index 00000000..82ae95cc --- /dev/null +++ b/src/components/ButtonLink.jsx @@ -0,0 +1,48 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledButton = styled.a` + background-color: ${({ theme }) => theme.colors.ternary}; + color: ${({ theme }) => theme.colors.neutral}; + border-radius: 40px; + display: flex; + align-items: center; + gap: 7px; + padding-right: 16px; + font-size: 1em; + font-weight: 500; + + &:hover { + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; + } + + &:hover>img { + filter: invert(1); + } + + &:focus-visible { + outline: 2px solid ${({ theme }) => theme.colors.primary}; + outline-offset: 5px; + } + + @media ${media.desktop} { + font-size: 1.125em; + } +` + +const ButtonIcon = styled.img` + height: 32px; + width: 32px; + margin: 8px; +` + +const ButtonLink = ({ text, link, icon }) => { + return ( + + {text} + + ) +} + +export default ButtonLink \ No newline at end of file diff --git a/src/components/ButtonSocial.jsx b/src/components/ButtonSocial.jsx new file mode 100644 index 00000000..cffbfa6f --- /dev/null +++ b/src/components/ButtonSocial.jsx @@ -0,0 +1,37 @@ +import styled from "styled-components" + +const Link = styled.a` + &:focus-visible { + outline: 2px solid; + outline-offset: 5px; + border-radius: 2px; + } +` + +const ButtonIcon = styled.img` + width: 32px; + height: 32px; + filter: invert(1); + + &:hover { + filter: invert(0.3) + } + + &:focus-visible { + outline: 2px solid; + outline-offset: 5px; + border-radius: 2px; + } +` + +const ButtonSocial = ({ link, icon, text }) => { + return ( +
  • + + + +
  • + ) +} + +export default ButtonSocial \ No newline at end of file diff --git a/src/components/ProjectCard.jsx b/src/components/ProjectCard.jsx new file mode 100644 index 00000000..74cbb690 --- /dev/null +++ b/src/components/ProjectCard.jsx @@ -0,0 +1,99 @@ +import ButtonLink from "./ButtonLink.jsx" +import Tag from "./Tag.jsx" +import CardHeading from "../typhography/CardHeading.jsx" +import BodyText from "../typhography/BodyText.jsx" +import styled from "styled-components" +import media from "../styles/media.js" + +const Card = styled.article` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.medium}; + + @media ${media.tablet} { + flex-direction: row; + gap: 37px; + } + + @media ${media.desktop} { + flex-direction: row; + gap: ${({ theme }) => theme.spacings.small}; + } +` + +const CardImage = styled.img` + height: 200px; + width: 100%; + object-fit: cover; + box-shadow: 2px 2px 5px rgba(31, 0, 124, 0.25); + + @media ${media.tablet} { + width: 200px; + height: 280px; + } + + @media ${media.desktop} { + width: 408px; + height: 280px; + } +` + +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.medium}; + justify-content: space-between; +` + +const InfoWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.small}; +` + +const TagsWrapper = styled.ul` + display: flex; + flex-wrap: wrap; + gap: 4px; +` + +const ButtonWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: ${({ theme }) => theme.spacings.small}; + + @media ${media.tablet} { + flex-direction: row; + gap: 2rem; + } + @media ${media.desktop} { + flex-direction: row; + gap: 2rem; + } +` + +const ProjectCard = ({ project }) => { + return ( + + + + + + + + {project.tags.map(tag => { + return + })} + + + + + + + + + ) +} + +export default ProjectCard diff --git a/src/components/SkillsCard.jsx b/src/components/SkillsCard.jsx new file mode 100644 index 00000000..cdfe43cb --- /dev/null +++ b/src/components/SkillsCard.jsx @@ -0,0 +1,34 @@ +import BodyText from "../typhography/BodyText.jsx" +import CardHeading from "../typhography/CardHeading.jsx" +import styled from "styled-components" + +const CardWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +` +const SkillsWrapper = styled.ul` + display: flex; + flex-direction: column; + text-align: center; +` + +const SkillsCard = ({ skillGroup }) => { + return ( + + + + {skillGroup.skills.map(skill => { + return ( +
  • + +
  • + ) + })} +
    +
    + ) +} + +export default SkillsCard \ No newline at end of file diff --git a/src/components/SkipLink.jsx b/src/components/SkipLink.jsx new file mode 100644 index 00000000..c7fbbcbd --- /dev/null +++ b/src/components/SkipLink.jsx @@ -0,0 +1,35 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledLink = styled.a` + position: absolute; + top: -50px; + left: 10px; + text-decoration: none; + padding: 10px; + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; + z-index: 100; + transition: top 0.3s; + font-size: 1em; + font-weight: 500; + + &:focus-visible { + top: 10px; + outline: 2px solid ${({ theme }) => theme.colors.primary}; + outline-offset: 5px; + border-radius: 2px; + } + + @media ${media.desktop} { + font-size: 1.125em; + } +` + +const SkipLink = () => { + return ( + Skip to main content + ) +} + +export default SkipLink \ No newline at end of file diff --git a/src/components/Tag.jsx b/src/components/Tag.jsx new file mode 100644 index 00000000..977b7ff6 --- /dev/null +++ b/src/components/Tag.jsx @@ -0,0 +1,20 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledTag = styled.li` + background-color: ${({ theme }) => theme.colors.neutral}; + color: ${({ theme }) => theme.colors.secondary}; + padding: 2px 6px; + font-size: 1em; + font-weight: 500; + + @media ${media.desktop} { + font-size: 1.125em; + } +` + +const Tag = ({ tag }) => { + return {tag} +} + +export default Tag \ No newline at end of file diff --git a/src/data/articles.json b/src/data/articles.json new file mode 100644 index 00000000..41e848be --- /dev/null +++ b/src/data/articles.json @@ -0,0 +1,22 @@ +{ + "articles": [ + { + "id": 1, + "title": "Article title", + "image": "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?q=80&w=2069&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "altText": "code on a screen", + "date": "March 2025", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "link": "#" + }, + { + "id": 2, + "title": "Article title", + "image": "https://images.unsplash.com/photo-1515879218367-8466d910aaa4?q=80&w=2069&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "altText": "code on a screen", + "date": "April 2025", + "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "link": "#" + } + ] +} \ No newline at end of file diff --git a/src/data/projects.json b/src/data/projects.json index 7c426028..6527041d 100644 --- a/src/data/projects.json +++ b/src/data/projects.json @@ -1,28 +1,135 @@ { "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", + "name": "First project", + "id": 1, + "image": "../assets/business-site.png", + "altText": "Business Site Thumbnail", "tags": [ "HTML5", "CSS3", "JavaScript" ], - "netlify": "link", - "github": "link" + "netlify": "https://starlight-unicorn-rentals.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-business-site", + "description": "My first web project created to learn the basics of responsive web design, CSS Grid and Flexbox layout, and DOM manipulation." }, { - "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", + "name": "Recipe App", + "id": 2, + "image": "../assets/recipe-app.png", + "altText": "Recipe App Thumbnail", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "API" + ], + "netlify": "https://mimmis-recipe-library.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-recipe-library", + "description": "A functional recipe app that fetches recipes from an API and let users find recipes based on different filters and sorting options." + }, + { + "name": "Weather App", + "id": 3, + "image": "../assets/weather-app.png", + "altText": "Weather App Thumbnail", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "TypeScript", + "API" + ], + "netlify": "https://rainchecker.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-weather-app", + "description": "A working weather app that fetches data from a weather API to display today's weather and a 5-day forecast. Includes a search bar where the user can search for a specific city." + }, + { + "name": "Web Accessibility Quiz", + "id": 4, + "image": "../assets/accessibility-quiz.png", + "altText": "Web Accessibility Quiz Thumbnail", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "WCAG" + ], + "netlify": "https://web-accessibility-quiz.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-accessibility", + "description": "A quiz about web accessibility where common accessibility guidelines were followed. The site is user-friendly and works for everyone, including those using screen readers, keyboards, and other assistive technologies." + }, + { + "name": "Portfolio", + "id": 5, + "image": "../assets/portfolio.png", + "altText": "Portfolio Thumbnail", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "React", + "Styled Components" + ], + "netlify": "https://mimmi-eriksson.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-portfolio", + "description": "The website you are looking at right now. Built in React and styled using the CSS library Styled Components." + }, + { + "name": "Happy Thoughts", + "id": 6, + "image": "../assets/happy-thoughts.png", + "altText": "Happy Thoughts Thumbnail", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "React", + "React hooks", + "TailwindCSS", + "API" + ], + "netlify": "https://think-happy.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-happy-thoughts", + "description": "A small messaging app built in React to learn useState and useEffect hooks. The app fetches and posts messages to an API. Styled using tailwindCSS." + }, + { + "name": "Movie App", + "id": 7, + "image": "../assets/movies.png", + "altText": "Movie App Thumbnail", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "React", + "React hooks", + "React Router", + "TailwindCSS", + "API" + ], + "netlify": "https://movieblaster.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-movies", + "description": "A dynamic multi-page application built in React and using React Router. The app displays a page with a list movies fetched from an external API, and then links to a movie detail page when when a movie is clicked." + }, + { + "name": "To Do App", + "id": 8, + "image": "../assets/todo.png", + "altText": "To Do App Thumbnail", "tags": [ "HTML5", "CSS3", "JavaScript", "TypeScript", - "APIs" + "React", + "TailwindCSS", + "Zustand" ], - "netlify": "link", - "github": "link" + "netlify": "https://task-completed.netlify.app/", + "github": "https://github.com/mimmi-eriksson/js-project-todo", + "description": "A to do app build in React using Zustand for state management. Users are able to add and remove tasks, list tasks, and toggle whether a task is done or not." } ] } \ No newline at end of file diff --git a/src/data/skills.json b/src/data/skills.json new file mode 100644 index 00000000..d0c167b2 --- /dev/null +++ b/src/data/skills.json @@ -0,0 +1,42 @@ +{ + "skills": [ + { + "title": "Code", + "skills": [ + "HTML5", + "CSS3", + "Javascript ES6", + "Typescript", + "React", + "Python" + ] + }, + { + "title": "Toolbox", + "skills": [ + "Figma", + "Slack", + "GitHub", + "Git", + "Postman" + ] + }, + { + "title": "Upcoming", + "skills": [ + "Node.js", + "Mongo DB" + ] + }, + { + "title": "Other", + "skills": [ + "Agile methodology", + "Project management", + "Teamwork", + "Analysis", + "Problem solving" + ] + } + ] +} \ No newline at end of file diff --git a/src/data/tech.json b/src/data/tech.json new file mode 100644 index 00000000..74bd0c16 --- /dev/null +++ b/src/data/tech.json @@ -0,0 +1,23 @@ +{ + "tech": [ + "HTML", + "CSS", + "Flexbox", + "JavaScript ES6", + "TypeScript", + "JSX", + "React", + "React Hooks", + "Styled Components", + "TailwindCSS", + "Node.js", + "Mongo DB", + "Python", + "APIs", + "Web Accessibility", + "Mob-programming", + "Pair-programming", + "Github", + "Git" + ] +} \ No newline at end of file diff --git a/src/hooks/useInView.jsx b/src/hooks/useInView.jsx new file mode 100644 index 00000000..dd01beee --- /dev/null +++ b/src/hooks/useInView.jsx @@ -0,0 +1,22 @@ +// use to detect when an element enters the viewport (i.e., when it becomes visible as you scroll) + +import { useEffect, useState, useRef } from 'react'; + +const useInView = (options) => { + const ref = useRef(null); + const [isInView, setIsInView] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => setIsInView(entry.isIntersecting), + options + ); + + if (ref.current) observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, options]); + + return [ref, isInView]; +} + +export default useInView \ 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..f9756ceb 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,9 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { App } from './App.jsx' - -import './index.css' +import App from './App' createRoot(document.getElementById('root')).render( diff --git a/src/sections/ArticlesSection.jsx b/src/sections/ArticlesSection.jsx new file mode 100644 index 00000000..070f26ea --- /dev/null +++ b/src/sections/ArticlesSection.jsx @@ -0,0 +1,68 @@ +import FadeInSection from "./FadeInSection" +import ArticleCard from "../components/ArticleCard" +import SectionHeading from "../typhography/SectionHeading" +import styled from "styled-components" +import media from "../styles/media.js" + +const SectionWrapper = styled.div` + min-height: 100vh; + padding: ${({ theme }) => theme.spacings.xLarge} ${({ theme }) => theme.spacings.small}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +` +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: ${({ theme }) => theme.spacings.large}; + + @media ${media.tablet} { + padding-inline: 2rem; + align-items: flex-start; + } + @media ${media.desktop} { + padding-inline: 2rem; + align-items: flex-start; + } +` + +const ArticlesWrapper = styled.div` + max-width: 958px; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.large}; + justify-content: center; + + @media ${media.desktop} { + flex-direction: row; + align-self: center; + } +` + +const ArticlesSection = ({ articles }) => { + // show latest article first + const sortedArticles = articles.sort((a, b) => (b.id - a.id)) + return ( + + + + + + {sortedArticles.map(article => { + return ( + + ) + })} + + + + + ) +} + +export default ArticlesSection \ No newline at end of file diff --git a/src/sections/ContactSection.jsx b/src/sections/ContactSection.jsx new file mode 100644 index 00000000..7e69f821 --- /dev/null +++ b/src/sections/ContactSection.jsx @@ -0,0 +1,97 @@ +import FadeInSection from "./FadeInSection" +import ButtonSocial from "../components/ButtonSocial" +import SectionHeading from "../typhography/SectionHeading" +import SmallHeading from "../typhography/SmallHeading" +import styled from "styled-components" +import media from "../styles/media.js" + +const SectionWrapper = styled.div` + min-height: 100vh; + /* background-color: ${({ theme }) => theme.colors.primary}; */ + /* color: ${({ theme }) => theme.colors.secondary}; */ + color: ${({ theme }) => theme.colors.primary}; + padding: ${({ theme }) => theme.spacings.xLarge} ${({ theme }) => theme.spacings.small}; + display: flex; + flex-direction: column; + justify-content: center; + gap: ${({ theme }) => theme.spacings.large}; +` + +const ContentWrapper = styled.div` + color: black; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.large}; + align-items: center; +` + +const ProfileImage = styled.img` + width: 164px; + height: 164px; + object-fit: cover; + clip-path: circle(50%); +` + +const InfoWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.xSmall}; + align-items: center; +` + +const Link = styled.a` + + &:hover { + color: #c0c0c0; + } + + &:focus-visible { + outline: 2px solid; + outline-offset: 5px; + border-radius: 2px; + } + + @media ${media.mobile} { + > * { + font-size: 1.25em; + } + } + @media (max-width: 330px) { + > * { + font-size: 1em; + } + } +` +const ButtonsWrapper = styled.ul` + display: flex; + align-items: center; + gap: 2rem; +` + +const ContactSection = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default ContactSection \ No newline at end of file diff --git a/src/sections/FadeInSection.jsx b/src/sections/FadeInSection.jsx new file mode 100644 index 00000000..a9059a29 --- /dev/null +++ b/src/sections/FadeInSection.jsx @@ -0,0 +1,27 @@ +import styled, { css } from "styled-components" +import useInView from '../hooks/useInView'; + +const Section = styled.section` + opacity: 0; + transform: translateY(2rem); + transition: opacity 1s ease-out, transform 1s ease-out; + + ${(props) => + props.$isVisible && + css` + opacity: 1; + transform: translateY(0); + `} +` + +const FadeInSection = ({ name, children }) => { + const [ref, isInView] = useInView({ threshold: 0.1 }); + + return ( +
    + {children} +
    + ); +}; + +export default FadeInSection diff --git a/src/sections/Footer.jsx b/src/sections/Footer.jsx new file mode 100644 index 00000000..51b66fad --- /dev/null +++ b/src/sections/Footer.jsx @@ -0,0 +1,32 @@ +import ButtonSocial from "../components/ButtonSocial" + +const Footer = () => { + return ( +
    +
    +
    +

    © {new Date().getFullYear()} Mimmi Eriksson

    +
      +
    • +
    • +
    +
    + +
    +
    + ) +} + +export default Footer \ No newline at end of file diff --git a/src/sections/Header.jsx b/src/sections/Header.jsx new file mode 100644 index 00000000..284ca9d3 --- /dev/null +++ b/src/sections/Header.jsx @@ -0,0 +1,71 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const HeaderContent = styled.div` + color: ${({ theme }) => theme.colors.primary}; + padding: ${({ theme }) => theme.spacings.small}; + font-size: 1em; + font-weight: 400; + + @media ${media.mobile} { + display: none; + } + @media ${media.tablet} { + display: block; + } + @media ${media.desktop} { + font-size: 1.125em; + } +` +const NavContent = styled.ul` + display: flex; + justify-content: flex-end; + gap: 2rem; +` + +const NavLink = styled.a` + text-underline-offset: ${({ theme }) => theme.spacings.xSmall}; + + &:hover { + text-decoration: underline; + } + + &:focus-visible { + outline: 2px solid ${({ theme }) => theme.colors.primary}; + outline-offset: 5px; + border-radius: 2px; + } +` + +const Header = () => { + return ( +
    + + + +
    + ) +} + +export default Header \ No newline at end of file diff --git a/src/sections/IntroSection.jsx b/src/sections/IntroSection.jsx new file mode 100644 index 00000000..3ecca442 --- /dev/null +++ b/src/sections/IntroSection.jsx @@ -0,0 +1,119 @@ +import MainHeading from "../typhography/MainHeading" +import SmallHeading from "../typhography/SmallHeading" +import BodyText from "../typhography/BodyText" +import ArrowLink from "../components/ArrowLink" +import styled from "styled-components" +import media from "../styles/media.js" + +const SectionWrapper = styled.div` + min-height: 100vh; + padding: ${({ theme }) => theme.spacings.large} ${({ theme }) => theme.spacings.medium}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +` +const ContentWrapper = styled.div` + max-width: 700px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: ${({ theme }) => theme.spacings.medium}; + margin-bottom: ${({ theme }) => theme.spacings.large}; + + @media ${media.tablet} { + gap: ${({ theme }) => theme.spacings.large}; + } + @media ${media.desktop} { + max-width: 1200px; + flex-direction: row; + align-items: center; + } + @media (${media.desktopXL}) { + gap: 6.25rem; + margin-bottom: 8rem; + } +` +const InfoWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2rem; +` + +const ImageWrapper = styled.div` + width: 100%; + max-width: 300px; + position: relative; + align-self: center; + + @media ${media.tablet} { + max-width: 400px; + } + @media ${media.desktop} { + max-width: 400px; + } + @media ${media.desktopXL} { + max-width: 500px; + width: 500px; + } +` +const ImageBorder = styled.div` + aspect-ratio: 1 / 1; + background-color: ${({ theme }) => theme.colors.primary}; + clip-path: circle(50%); + + @media ${media.desktop} { + width: 400px; + } + @media ${media.desktopXL} { + width: 500px; + } +` +const ProfileImage = styled.img` + position: absolute; + top: 5px; + left: 5px; + width: calc(100% - 10px); + aspect-ratio: 1 / 1; + max-width: 290px; + object-fit: cover; + clip-path: circle(50%); + + @media ${media.tablet} { + max-width: 390px; + } + @media ${media.desktop} { + max-width: 390px; + } + @media ${media.desktopXL} { + max-width: 490px; + width: 490px; + } +` + +const IntroSection = () => { + return ( +
    + + + +
    + + +
    + +
    + + + + +
    + +
    +
    + ) +} + +export default IntroSection \ No newline at end of file diff --git a/src/sections/ProjectsSection.jsx b/src/sections/ProjectsSection.jsx new file mode 100644 index 00000000..ef817e6e --- /dev/null +++ b/src/sections/ProjectsSection.jsx @@ -0,0 +1,53 @@ +import FadeInSection from "./FadeInSection" +import ProjectCard from "../components/ProjectCard" +import SectionHeading from "../typhography/SectionHeading" +import styled from "styled-components" +import media from "../styles/media.js" + +const SectionWrapper = styled.div` + min-height: 100vh; + padding: ${({ theme }) => theme.spacings.xLarge} ${({ theme }) => theme.spacings.small}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + @media ${media.tablet} { + padding-inline: 2rem; + } +` + +const ContentWrapper = styled.div` + max-width: 958px; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: ${({ theme }) => theme.spacings.large}; + + @media ${media.desktopXL} { + gap: ${({ theme }) => theme.spacings.xLarge}; + } +` + +const ProjectsSection = ({ projects }) => { + // show latest project first + const sortedProjects = projects.sort((a, b) => (b.id - a.id)) + return ( + + + + + {sortedProjects.map(project => { + return ( + + ) + })} + + + + ) +} + +export default ProjectsSection \ No newline at end of file diff --git a/src/sections/SkillsSection.jsx b/src/sections/SkillsSection.jsx new file mode 100644 index 00000000..d76ccb22 --- /dev/null +++ b/src/sections/SkillsSection.jsx @@ -0,0 +1,71 @@ +import FadeInSection from "./FadeInSection" +import SkillsCard from "../components/SkillsCard" +import SectionHeading from "../typhography/SectionHeading" +import styled from "styled-components" +import media from "../styles/media.js" + +const SectionWrapper = styled.div` + min-height: 100vh; + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; + padding: ${({ theme }) => theme.spacings.large} 0; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacings.large}; + + @media ${media.desktop} { + padding: 15.5rem 16.5rem; + } +` + +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + @media ${media.desktop} { + flex-direction: row; + align-items: stretch; + } +` + +const StyledSkillCard = styled.div` + padding: ${({ theme }) => theme.spacings.medium} 0; + width: 200px; + + &:not(:last-of-type) { + border-bottom: 2px solid ${({ theme }) => theme.colors.secondary}; + } + + @media ${media.desktop} { + width: 100%; + padding: 0 ${({ theme }) => theme.spacings.medium}; + + &:not(:last-of-type) { + border-bottom: none; + border-right: 2px solid ${({ theme }) => theme.colors.secondary}; + } + } +` + +const SkillsSection = ({ skills }) => { + return ( + + + + + {skills.map(skillGroup => { + return ( + + + + ) + })} + + + + ) +} + +export default SkillsSection \ No newline at end of file diff --git a/src/sections/TechSection.jsx b/src/sections/TechSection.jsx new file mode 100644 index 00000000..89eb76e5 --- /dev/null +++ b/src/sections/TechSection.jsx @@ -0,0 +1,56 @@ +import FadeInSection from "./FadeInSection" +import SectionHeading from "../typhography/SectionHeading" +import SmallHeading from "../typhography/SmallHeading" +import ArrowLink from "../components/ArrowLink" +import styled from "styled-components" +import media from "../styles/media.js" + +const SectionWrapper = styled.div` + min-height: 100vh; + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; + padding-inline: ${({ theme }) => theme.spacings.small}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10rem; +` +const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4.5rem; + + @media ${media.tablet} { + padding-inline: 2rem; + gap: ${({ theme }) => theme.spacings.large}; + text-align: center; + } + @media ${media.desktop} { + padding-inline: 10rem; + flex-direction: row; + gap: 7.75rem; + text-align: left; + } + @media ${media.desktopXL} { + padding-inline: 17rem; + } +` + +const TechSection = (tech) => { + return ( + + + + + + + {/* */} + + + ) +} + +export default TechSection \ No newline at end of file diff --git a/src/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx new file mode 100644 index 00000000..106bd496 --- /dev/null +++ b/src/styles/GlobalStyle.jsx @@ -0,0 +1,32 @@ +import { createGlobalStyle } from "styled-components" + +const GlobalStyle = createGlobalStyle` + * { + box-sizing: border-box; + padding: 0; + margin: 0; + } + + html { + scroll-behavior: smooth; + } + + body { + font-family: "Poppins", sans-serif; + } + + h1, h2 { + font-family: "Urbanist", "Poppins", sans-serif; + } + + a { + text-decoration: none; + color: inherit; + } + + ul { + list-style: none; + } +` + +export default GlobalStyle \ No newline at end of file diff --git a/src/styles/media.js b/src/styles/media.js new file mode 100644 index 00000000..2f9ee1ee --- /dev/null +++ b/src/styles/media.js @@ -0,0 +1,8 @@ +const media = { + mobile: "(max-width: 767px)", + tablet: "(min-width: 768px) and (max-width: 1024px)", + desktop: "(min-width: 1025px)", + desktopXL: "(min-width: 1240px)" +} + +export default media \ No newline at end of file diff --git a/src/styles/theme.js b/src/styles/theme.js new file mode 100644 index 00000000..abbce869 --- /dev/null +++ b/src/styles/theme.js @@ -0,0 +1,17 @@ +const theme = { + colors: { + primary: "#1F007C", + secondary: "#FFF", + ternary: "#F5F5F5", + neutral: "#000" + }, + spacings: { + xSmall: "0.5rem", + small: "1rem", + medium: "1.5rem", + large: "4rem", + xLarge: "8rem" + } +} + +export default theme \ No newline at end of file diff --git a/src/typhography/BodyText.jsx b/src/typhography/BodyText.jsx new file mode 100644 index 00000000..162321c8 --- /dev/null +++ b/src/typhography/BodyText.jsx @@ -0,0 +1,20 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledText = styled.p` + font-size: 1em; + font-weight: 400; + line-height: ${({ $lineHeight }) => $lineHeight === "double" ? "200%" : "normal"}; + + @media ${media.desktop} { + font-size: 1.125em; + } +` + +const BodyText = ({ text, lineHeight }) => { + return ( + {text} + ) +} + +export default BodyText \ No newline at end of file diff --git a/src/typhography/CardHeading.jsx b/src/typhography/CardHeading.jsx new file mode 100644 index 00000000..5b3472c3 --- /dev/null +++ b/src/typhography/CardHeading.jsx @@ -0,0 +1,17 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledHeading = styled.h3` + font-size: 1.5em; + font-weight: 500; + + @media ${media.desktop} { + font-size: 1.875em; + } +` + +const CardHeading = ({ title }) => { + return {title} +} + +export default CardHeading \ No newline at end of file diff --git a/src/typhography/MainHeading.jsx b/src/typhography/MainHeading.jsx new file mode 100644 index 00000000..da91e7c4 --- /dev/null +++ b/src/typhography/MainHeading.jsx @@ -0,0 +1,24 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledHeading = styled.h1` + font-size: 3.75em; + font-weight: 700; + line-height: 100%; + + @media ${media.tablet} { + font-size: 5em; + line-height: 125%; + } + @media ${media.desktop} { + font-size: 6.25em; + } +` + +const MainHeading = ({ text }) => { + return ( + {text} + ) +} + +export default MainHeading \ No newline at end of file diff --git a/src/typhography/SectionHeading.jsx b/src/typhography/SectionHeading.jsx new file mode 100644 index 00000000..f78b62de --- /dev/null +++ b/src/typhography/SectionHeading.jsx @@ -0,0 +1,19 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledHeading = styled.h2` + font-size: 3.75em; + font-weight: 700; + line-height: 110%; + text-align: center; + + @media ${media.desktop} { + font-size: 5em; + } +` + +const SectionHeading = ({ title }) => { + return {title} +} + +export default SectionHeading \ No newline at end of file diff --git a/src/typhography/SmallHeading.jsx b/src/typhography/SmallHeading.jsx new file mode 100644 index 00000000..e19693fa --- /dev/null +++ b/src/typhography/SmallHeading.jsx @@ -0,0 +1,20 @@ +import styled from "styled-components" +import media from "../styles/media.js" + +const StyledHeading = styled.h3` + font-size: 1.5em; + font-weight: 500; + line-height: normal; + + @media ${media.desktop} { + font-size: 1.875em; + } +` + +const SmallHeading = ({ text }) => { + return ( + {text} + ) +} + +export default SmallHeading \ 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