diff --git a/README.md b/README.md index 200f4282..874c7980 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -# Portfolio +# Sandra Hagevall — Portfolio + +## 🚀 Live Demo +https://sandrahagevall.netlify.app/ \ No newline at end of file diff --git a/index.html b/index.html index 6676fb2d..f04f6061 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,30 @@ - - - - - Portfolio - - -
- - - + + + + + + + Sandra Hagevall - Portfolio + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index 48911600..f610fd10 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,17 @@ "preview": "vite preview" }, "dependencies": { + "framer-motion": "^12.23.24", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "styled-components": "^6.1.19" }, "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/gitcontact.svg b/public/gitcontact.svg new file mode 100644 index 00000000..d129e59f --- /dev/null +++ b/public/gitcontact.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/github.svg b/public/github.svg new file mode 100644 index 00000000..89f5ddb4 --- /dev/null +++ b/public/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/arrow.svg b/public/images/arrow.svg new file mode 100644 index 00000000..85a85622 --- /dev/null +++ b/public/images/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/business2.png b/public/images/business2.png new file mode 100644 index 00000000..c003bb90 Binary files /dev/null and b/public/images/business2.png differ diff --git a/public/images/careerchange.jpg b/public/images/careerchange.jpg new file mode 100644 index 00000000..60092e27 Binary files /dev/null and b/public/images/careerchange.jpg differ diff --git a/public/images/contactimg.png b/public/images/contactimg.png new file mode 100644 index 00000000..b6f921cc Binary files /dev/null and b/public/images/contactimg.png differ diff --git a/public/images/eventfinder2.png b/public/images/eventfinder2.png new file mode 100644 index 00000000..6e70db64 Binary files /dev/null and b/public/images/eventfinder2.png differ diff --git a/public/images/favicon.png b/public/images/favicon.png new file mode 100644 index 00000000..5c170ac7 Binary files /dev/null and b/public/images/favicon.png differ diff --git a/public/images/footerimg.jpg b/public/images/footerimg.jpg new file mode 100644 index 00000000..cc25cadb Binary files /dev/null and b/public/images/footerimg.jpg differ diff --git a/public/images/frontend.jpg b/public/images/frontend.jpg new file mode 100644 index 00000000..f3c53126 Binary files /dev/null and b/public/images/frontend.jpg differ diff --git a/public/images/kugghjul.png b/public/images/kugghjul.png new file mode 100644 index 00000000..8c6dc754 Binary files /dev/null and b/public/images/kugghjul.png differ diff --git a/public/images/linkedin.svg b/public/images/linkedin.svg new file mode 100644 index 00000000..4d0ed354 --- /dev/null +++ b/public/images/linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/logistik.jpg b/public/images/logistik.jpg new file mode 100644 index 00000000..3c8c4f74 Binary files /dev/null and b/public/images/logistik.jpg differ diff --git a/public/images/placeholder.png b/public/images/placeholder.png new file mode 100644 index 00000000..8f6a6670 Binary files /dev/null and b/public/images/placeholder.png differ diff --git a/public/images/recipe2.png b/public/images/recipe2.png new file mode 100644 index 00000000..4f488c68 Binary files /dev/null and b/public/images/recipe2.png differ diff --git a/public/images/topimg.png b/public/images/topimg.png new file mode 100644 index 00000000..85657474 Binary files /dev/null and b/public/images/topimg.png differ diff --git a/public/images/watherapp2.png b/public/images/watherapp2.png new file mode 100644 index 00000000..6c345a3c Binary files /dev/null and b/public/images/watherapp2.png differ diff --git a/public/instagram.svg b/public/instagram.svg new file mode 100644 index 00000000..0a90f980 --- /dev/null +++ b/public/instagram.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/link.svg b/public/link.svg new file mode 100644 index 00000000..e752b728 --- /dev/null +++ b/public/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..010b530c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,34 @@ +import { Hero } from "./components/Hero" +import { Tech } from "./components/Tech" +import { FeaturedProjects } from "./components/FeaturedProjects" +import { Skills } from "./components/Skills" +import { MyWords } from "./components/MyWords" +import { Contact } from "./components/Contact" +import { GlobalStyle } from "./components/GlobalStyle.jsx" +import { theme } from "./components/theme.js" +import { ThemeProvider } from "styled-components" + + 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.

+ + + Skip to main content + +
+ + + + + + + + + + +
+
) } diff --git a/src/components/Button.jsx b/src/components/Button.jsx new file mode 100644 index 00000000..7fa7595b --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,75 @@ +import styled from "styled-components" + +export const StyledButton = styled.button` + display: flex; + align-items: center; + width: 303px; + height: 48px; + padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`}; + gap: ${({ theme }) => theme.spacing.md}; + font-family: inherit; + font-weight: 500; + font-size: 18px; + cursor: pointer; + border-radius: 12px; + + background: ${({ theme, $variant }) => + $variant === "secondary" ? "transparent" : theme.colors.primary}; + + color: ${({ theme, $variant }) => + $variant === "secondary" ? theme.colors.primary : theme.colors.secondary}; + border: ${({ theme, $variant }) => + $variant === "secondary" ? `1px solid ${theme.colors.primary}` : "none"}; + + transition: 0.2s ease-in-out; + + /* Smooth transition for hover and focus ring */ + transition: opacity 0.18s ease-in-out, box-shadow 0.12s ease-in-out; + + &:hover { + opacity: 0.85; + } + + &:focus { + outline: none; + } + + /* Use focus-visible so mouse users aren't shown the ring; adapt ring by variant */ + &:focus-visible { + outline: none; + box-shadow: ${({ $variant, theme }) => + $variant === "secondary" + ? "0 0 0 3px rgba(0,0,0,0.85)" + : `0 0 0 3px ${theme.colors.accent}`}; + outline-offset: 2px; + } +` + +const Icon = styled.img` + width: 2rem; + height: 2rem; + object-fit: contain; +` + +export const Button = ({ icon, children, variant = "primary", as: asProp, href, target, rel, ariaLabel, ...rest }) => { + // Render as an anchor when 'as="a"' is passed or when 'href' exists. + return (asProp === "a" || href) ? ( + + {icon && } + {children} + + ) : ( + + {icon && } + {children} + + ) +} \ No newline at end of file diff --git a/src/components/Contact/Contact.jsx b/src/components/Contact/Contact.jsx new file mode 100644 index 00000000..a5f8ca29 --- /dev/null +++ b/src/components/Contact/Contact.jsx @@ -0,0 +1,37 @@ +import { Heading } from '../Heading' +import { ContactInfo } from './ContactInfo.jsx' +import { SectionContainer } from '../SectionContainer' +import { IconButton } from '../IconButton' +import styled from 'styled-components' + +export const ContactWrapper = styled.section` + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; +` +export const SocialWrapper = styled.div` + display: flex; + gap: 1.5rem; + margin-top: 2rem; + + justify-content: center; /* ← centrera ikonerna */ +` + +export const Contact = () => { + return ( + + + Let's Talk + + + + + + + + ) +} \ No newline at end of file diff --git a/src/components/Contact/ContactInfo.jsx b/src/components/Contact/ContactInfo.jsx new file mode 100644 index 00000000..18544b04 --- /dev/null +++ b/src/components/Contact/ContactInfo.jsx @@ -0,0 +1,66 @@ +import styled from "styled-components" + +export const ContactInfoWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + ` + +export const ContactImage = styled.img` + width: 120px; + height: 120px; + border-radius: 50%; + object-fit: cover; + + + @media ${({ theme }) => theme.breakpoints.desktop} { + width: 150px; + height: 150px; +} +` + +export const TextContainer = styled.div` + width: 100%; + text-align: left; + + p { + margin: 0.25rem 0; + padding: 0 12px; +} + +.contact-link { + text-decoration: none; + color: ${({ theme }) => theme.colors.secondary}; + transition: 0.2s ease; + } + + .contact-link:hover { + text-decoration: underline; + opacity: 0.7; + } + + @media ${({ theme }) => theme.breakpoints.tablet} { + text-align: center; +} +` + + +export const ContactInfo = ({ name, phone, email, imageSrc }) => { + return ( + + + + + +

{name}

+

+ {phone} +

+

+ {email} +

+
+
+ ) +} \ No newline at end of file diff --git a/src/components/Contact/index.js b/src/components/Contact/index.js new file mode 100644 index 00000000..1a3a8bda --- /dev/null +++ b/src/components/Contact/index.js @@ -0,0 +1 @@ +export { Contact } from "./Contact" \ No newline at end of file diff --git a/src/components/FeaturedProjects/FeaturedProjects.jsx b/src/components/FeaturedProjects/FeaturedProjects.jsx new file mode 100644 index 00000000..d6a76171 --- /dev/null +++ b/src/components/FeaturedProjects/FeaturedProjects.jsx @@ -0,0 +1,32 @@ +import { ProjectCard } from "./ProjectCard.jsx" +import projectsData from "../../data/projects.json" +import { Heading } from "../Heading" +import { Button } from "../Button" +import { SectionContainer } from "../SectionContainer" +import { FeaturedWrapper, FeaturedInner } from "./FeaturedProjects.styled.js" + +export const FeaturedProjects = () => { + return ( + + + + Featured Projects + + {projectsData.projects.map((project) => ( + + ))} + + + + + ) +} diff --git a/src/components/FeaturedProjects/FeaturedProjects.styled.js b/src/components/FeaturedProjects/FeaturedProjects.styled.js new file mode 100644 index 00000000..b9c4e761 --- /dev/null +++ b/src/components/FeaturedProjects/FeaturedProjects.styled.js @@ -0,0 +1,15 @@ +import styled from "styled-components" + +export const FeaturedWrapper = styled.section` +background-color: ${({ theme }) => theme.colors.secondary}; + color: ${({ theme }) => theme.colors.primary}; +` +export const FeaturedInner = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 4rem; + width: 100%; + max-width: 1600px; + margin: 0 auto; +` \ No newline at end of file diff --git a/src/components/FeaturedProjects/ProjectCard.jsx b/src/components/FeaturedProjects/ProjectCard.jsx new file mode 100644 index 00000000..7d574074 --- /dev/null +++ b/src/components/FeaturedProjects/ProjectCard.jsx @@ -0,0 +1,41 @@ +import { motion } from "framer-motion" +import { useInView } from "framer-motion" +import { useRef } from "react" +import { Button } from "../Button" +import { Card, ImageWrapper, Content, Tags, Buttons } from "./ProjectCard.styled" + +export const ProjectCard = ({ title, description, tags, image, liveUrl, codeUrl, position }) => { + const ref = useRef(null) + const inView = useInView(ref, { once: true }) + return ( + + + + + {title} + + + + + {tags && tags.map((tag, i) => ( + {tag} + ))} + + +

{title}

+

{description}

+ + + + + +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/FeaturedProjects/ProjectCard.styled.js b/src/components/FeaturedProjects/ProjectCard.styled.js new file mode 100644 index 00000000..338bcf4f --- /dev/null +++ b/src/components/FeaturedProjects/ProjectCard.styled.js @@ -0,0 +1,118 @@ +import styled from "styled-components" + + +export const Card = styled.article` + width: 100%; + max-width: 1600px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 4rem; + align-items: center; + + @media ${({ theme }) => theme.breakpoints.desktop} { + flex-direction: ${({ $position }) => + $position === "left" ? "row" : "row-reverse"}; + gap: 6rem; + align-items: flex-start; + margin-bottom: 6rem; + } +` + +export const ImageWrapper = styled.div` + display: flex; + justify-content: center; + flex: 1.5; + + img { + width: 100%; + height: auto; + max-width: 480px; + display: block; + } + + @media ${({ theme }) => theme.breakpoints.tablet} { + img { + max-width: 650px; + } + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + img { + max-width: 750px; + } + } +` + +export const Content = styled.div` + display: flex; + flex-direction: column; + text-align: left; + max-width: 100%; + flex: 1; + + h3 { + font-size: 1.5rem; + font-weight: 500; + margin: 1rem 0; /* liten marginal ovan & under */ + } + + p { + margin: 0 0 1rem 0; /* liten marginal under description */ + line-height: 1.5; + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + max-width: 550px; + } + + h3 { + font-size: 1.8rem; + margin: 1.5 0; /* liten marginal ovan & under */ + } +`; + +export const Tags = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + gap: 0.2rem; + white-space: nowrap; + + span { + background: ${({ theme }) => theme.colors.secondary}; + color: ${({ theme }) => theme.colors.primary}; + padding: 0.15rem 0.35rem; + border: 1px solid ${({ theme }) => theme.colors.primary}; + border-radius: 4px; + font-size: 0.6rem; + } + + @media ${({ theme }) => theme.breakpoints.tablet} { + span { + font-size: 0.75rem; + padding: 0.25rem 0.55rem; + } + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + text-align: center; + span { + font-size: 1rem; + padding: 0.4rem 0.7rem; + } + } +` + +export const Buttons = styled.div` + display: flex; + flex-direction: column; + gap: 0.4rem; + flex-wrap: wrap; + justify-content: center; + margin-top: 1rem; + + @media ${({ theme }) => theme.breakpoints.desktop} { + justify-content: flex-start; + } +` \ No newline at end of file diff --git a/src/components/FeaturedProjects/index.js b/src/components/FeaturedProjects/index.js new file mode 100644 index 00000000..dd0bb04c --- /dev/null +++ b/src/components/FeaturedProjects/index.js @@ -0,0 +1 @@ +export { FeaturedProjects } from "./FeaturedProjects" \ No newline at end of file diff --git a/src/components/GlobalStyle.jsx b/src/components/GlobalStyle.jsx new file mode 100644 index 00000000..2f505773 --- /dev/null +++ b/src/components/GlobalStyle.jsx @@ -0,0 +1,41 @@ +import { createGlobalStyle } from "styled-components" + +export const GlobalStyle = createGlobalStyle` + *, *::before, *::after { + box-sizing: border-box; + } + + body { + margin: 0; + padding: 0; + font-family: 'Poppins', sans-serif; + } + + a { + text-decoration: none; + color: inherit; + } + + /* Skip link - hidden until focused */ + .skip-link { + position: absolute; + left: -999px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; + } + + .skip-link:focus, + .skip-link:focus-visible { + left: 16px; + top: 16px; + width: auto; + height: auto; + padding: 8px 12px; + background: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; + z-index: 9999; + border-radius: 4px; + } +` \ No newline at end of file diff --git a/src/components/Heading.jsx b/src/components/Heading.jsx new file mode 100644 index 00000000..32f11650 --- /dev/null +++ b/src/components/Heading.jsx @@ -0,0 +1,16 @@ +import styled from "styled-components" + +const StyledHeading = styled.h2` + font-size: 3.75rem; + font-weight: 700; + text-align: center; + margin-bottom: ${({ theme }) => theme.spacing.lg}; + +@media ${({ theme }) => theme.breakpoints.desktop} { + font-size: 5rem; +} +` + +export const Heading = ({ children }) => { + return {children} +} \ No newline at end of file diff --git a/src/components/Hero/Hero.jsx b/src/components/Hero/Hero.jsx new file mode 100644 index 00000000..2891546b --- /dev/null +++ b/src/components/Hero/Hero.jsx @@ -0,0 +1,26 @@ +import { SectionContainer } from "../SectionContainer" +import { HeroWrapper, HeroImages, HeroImage, HeroIntro, HeroSubheading, HeroTitle, HeroBody } from "./Hero.styled.js" + +export const Hero = () => { + return ( +
+ + + Hi there, I'm + Sandra Hagevall + + + + + Analytical Frontend Developer with a Background in Industrial Engineering and IT Strategy + + I am a Frontend Developer skilled at creating logical, structured, and accessible applications. I excel at solving complex problems and delivering efficient solutions. My analytical skills and drive to produce precise, high-quality code make me a valuable asset in any project. + + + +
+ ) +} \ No newline at end of file diff --git a/src/components/Hero/Hero.styled.js b/src/components/Hero/Hero.styled.js new file mode 100644 index 00000000..4f04bf21 --- /dev/null +++ b/src/components/Hero/Hero.styled.js @@ -0,0 +1,139 @@ +import styled from "styled-components" + +export const HeroWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + margin: 0 auto; + width: 100%; +` + +export const HeroTitle = styled.h1` + font-size: 3.25rem; + font-weight: 700; + order: 2; + margin: 0; + +@media ${({ theme }) => theme.breakpoints.desktop} { + font-size: 5rem; +} +` + +export const HeroIntro = styled.p` + font-size: 1.5rem; + font-weight: 500; + order: 1; + +@media ${({ theme }) => theme.breakpoints.desktop} { + font-size: 1.8rem; +} +` + +export const HeroSubheading = styled.p` + font-size: 1.25rem; + font-weight: 500; + max-width: 800px; + order: 3; + +@media ${({ theme }) => theme.breakpoints.tablet} { + font-size: 1.5rem; + order: 4; + } + +@media ${({ theme }) => theme.breakpoints.desktop} { + font-size: 1.8rem; +} +` + +export const HeroBody = styled.p` + font-size: 1rem; + max-width: 800px; + text-align: center; + order: 5; + margin: 0; + +@media ${({ theme }) => theme.breakpoints.tablet} { + text-align: center; +} + +@media ${({ theme }) => theme.breakpoints.desktop} { + font-size: 1.2rem; +} +` + +export const HeroImages = styled.div` + position: relative; + width: 290px; + height: 200px; + margin: 2rem auto; + order: 4; + + @media ${({ theme }) => theme.breakpoints.tablet} { + width: 490px; + height: 260px; + order: 3; + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + width: 530px; + height: 280px; + } +` + +export const HeroImage = styled.div` + position: absolute; + width: 140px; + height: 160px; + border-radius: 16px; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); + + &.hero-img-left { + background-image: url('/images/logistik.jpg'); + left: 0px; + top: 20px; + transform: rotate(-5deg); + z-index: 1; + } + + &.hero-img-main { + background-image: url('/images/topimg.png'); + left: 50%; + top: 8px; + transform: translateX(-50%); + z-index: 2; + } + + &.hero-img-right { + background-image: url('/images/frontend.jpg'); + right: 0px; + top: 20px; + transform: rotate(5deg); + z-index: 1; + } + + @media ${({ theme }) => theme.breakpoints.tablet} { + width: 230px; + height: 260px; + + &.hero-img-left { + top: 18px; + } + &.hero-img-main { + top: 0px; + } + &.hero-img-right { + top: 18px; + } + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + width: 250px; + height: 280px; + } +` + diff --git a/src/components/Hero/index.js b/src/components/Hero/index.js new file mode 100644 index 00000000..f89f7ea1 --- /dev/null +++ b/src/components/Hero/index.js @@ -0,0 +1 @@ +export { Hero } from "./Hero" \ No newline at end of file diff --git a/src/components/IconButton.jsx b/src/components/IconButton.jsx new file mode 100644 index 00000000..ce1efdb3 --- /dev/null +++ b/src/components/IconButton.jsx @@ -0,0 +1,32 @@ +import styled from "styled-components" + +const StyledIconButton = styled.a` + width: 44px; + height: 44px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background-color: ${({ theme }) => theme.colors.primary}; + cursor: pointer; + transition: 0.2s ease-in-out; + + &:hover { + opacity: 0.85; + } + + img { + width: 24px; + height: 24px; + object-fit: contain; + filter: brightness(0) invert(1); + } +` + +export const IconButton = ({ icon, url, label }) => { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/MyWords/BlogPostCard.jsx b/src/components/MyWords/BlogPostCard.jsx new file mode 100644 index 00000000..2405ab1f --- /dev/null +++ b/src/components/MyWords/BlogPostCard.jsx @@ -0,0 +1,36 @@ +import { motion } from "framer-motion" +import { useInView } from "framer-motion" +import { useRef } from "react" +import { Button } from "../Button" +import { Card, ImageContainer, Content } from "./BlogPostCard.styled.js" + +export const BlogPostCard = ({ title, date, image, description, link }) => { + const ref = useRef(null) + const inView = useInView(ref, { once: true }) + + return ( + + + + {title} + + + +

{date}

+

{title}

+

{description}

+ {link ? ( + + ) : ( + + )} +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/MyWords/BlogPostCard.styled.js b/src/components/MyWords/BlogPostCard.styled.js new file mode 100644 index 00000000..8b644a0d --- /dev/null +++ b/src/components/MyWords/BlogPostCard.styled.js @@ -0,0 +1,86 @@ +import styled from "styled-components"; + +export const Card = styled.article` + width: 100%; + max-width: 900px; + margin: 0 auto 2rem; + + @media ${({ theme }) => theme.breakpoints.tablet} { + max-width: 900px; + display: grid; + grid-template-columns: 1fr 1fr; + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + grid-template-columns: 50% 50%; + column-gap: ${({ theme }) => theme.spacing.xl}; + } +` + +export const ImageContainer = styled.div` + img { + width: 100%; + border-radius: 12px; + display: block; + } + + @media ${({ theme }) => theme.breakpoints.tablet} { + img { + width: 260px; + height: 300px; + object-fit: cover; + } + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + img { + width: 100%; + height: auto; + aspect-ratio: 3 / 2; + object-fit: cover; + } + } +` + +export const Content = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + + @media ${({ theme }) => theme.breakpoints.tablet} { + min-height: 280px; + } + + .date { + font-size: 0.9rem; + border: 1px solid #000; + padding: 2px 8px; + border-radius: 4px; + margin: 12px 0; + display: inline-block; + max-width: 150px; + text-align: center; + } + + h3 { + font-size: 1.8rem; + font-weight: 500; + margin: 0 0 12px 0; + } + + p { + margin-bottom: 0.6rem; + } + + a, button { + margin-bottom: 1.2rem; + align-self: center; + } + + @media ${({ theme }) => theme.breakpoints.tablet} { + a, button { + align-self: flex-start; + } + } +` \ No newline at end of file diff --git a/src/components/MyWords/MyWords.jsx b/src/components/MyWords/MyWords.jsx new file mode 100644 index 00000000..882f85af --- /dev/null +++ b/src/components/MyWords/MyWords.jsx @@ -0,0 +1,53 @@ +import postsData from '../../data/posts.json' +import { BlogPostCard } from './BlogPostCard' +import { Heading } from '../Heading' +import { Button } from '../Button' +import { SectionContainer } from '../SectionContainer' +import styled from 'styled-components' + +export const ContentWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + margin: 0 auto; + margin-top: 5rem; + + @media ${({ theme }) => theme.breakpoints.tablet} { + max-width: 650px; + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + max-width: 1000px; + } +` + +export const MoreArticles = styled.div` + display: flex; + justify-content: center; + margin-top: 3rem; +` + +export const MyWords = () => { + return ( +
+ + My Words + + {postsData.posts.map((post) => ( + + ))} + + + + + +
+ ) +} \ No newline at end of file diff --git a/src/components/MyWords/index.js b/src/components/MyWords/index.js new file mode 100644 index 00000000..5d3c5aa5 --- /dev/null +++ b/src/components/MyWords/index.js @@ -0,0 +1 @@ +export { MyWords } from "./MyWords" \ No newline at end of file diff --git a/src/components/SectionContainer.jsx b/src/components/SectionContainer.jsx new file mode 100644 index 00000000..2cc10c76 --- /dev/null +++ b/src/components/SectionContainer.jsx @@ -0,0 +1,5 @@ +import { StyledSectionContainer } from "./SectionContainer.styled" + +export const SectionContainer = ({ children }) => { + return {children} +} \ No newline at end of file diff --git a/src/components/SectionContainer.styled.js b/src/components/SectionContainer.styled.js new file mode 100644 index 00000000..e178433b --- /dev/null +++ b/src/components/SectionContainer.styled.js @@ -0,0 +1,15 @@ +import styled from 'styled-components' + +export const StyledSectionContainer = styled.div` + width: 100%; + margin: 0 auto; + padding: 24px 16px; + + @media ${({ theme }) => theme.breakpoints.tablet} { + padding: 42px 26px; + } + + @media ${({ theme }) => theme.breakpoints.desktop} { + padding: 30px 32px; + } +` \ No newline at end of file diff --git a/src/components/Skills/Skills.jsx b/src/components/Skills/Skills.jsx new file mode 100644 index 00000000..706f2e06 --- /dev/null +++ b/src/components/Skills/Skills.jsx @@ -0,0 +1,53 @@ +import { SkillsGroup } from './SkillsGroup.jsx' +import skillsData from '../../data/skills.json' +import { Heading } from '../Heading' +import { SectionContainer } from '../SectionContainer' +import styled from 'styled-components' + +export const SkillsWrapper = styled.section` + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; + padding: 0 0 5rem; + width: 100%; +` + +export const GroupsContainer = styled.div` + display: grid; + grid-template-columns: 1fr; + text-align: left; + +@media ${({ theme }) => theme.breakpoints.tablet} { + max-width: 600px; + margin: 0 auto; +} + +@media ${({ theme }) => theme.breakpoints.desktop} { + max-width: 1000px; + grid-template-columns: repeat(4, 1fr); + gap: 0; + text-align: left; + + div { + margin: 0 auto; + } +} +` + +export const Skills = () => { + return ( + + + Skills + + {skillsData.skills.map((skills) => + + )} + + + + ) +} \ No newline at end of file diff --git a/src/components/Skills/SkillsGroup.jsx b/src/components/Skills/SkillsGroup.jsx new file mode 100644 index 00000000..fa384e17 --- /dev/null +++ b/src/components/Skills/SkillsGroup.jsx @@ -0,0 +1,51 @@ +import styled from "styled-components" + +export const SkillGroupWrapper = styled.div` + text-align: left; + padding: 0 8px; + + h3 { + font-size: 1rem; + border: 1px solid #fff; + padding: 4px 18px; + display: inline-block; + margin-bottom: 12px; + border-radius: 4px; + font-weight: 500; + min-width: 150px; + text-align: center; + } + + ul { + list-style: none; + padding: 0; + margin: 0; + } + + li { + font-size: 14px; + margin-bottom: 6px; + } + + @media ${({ theme }) => theme.breakpoints.tablet} { + text-align: center; + } + + + @media ${({ theme }) => theme.breakpoints.desktop} { + text-align: left; + } +` + +export const SkillsGroup = ({ group, items }) => { + return ( + +

{group}

+ +
+ ) +} \ No newline at end of file diff --git a/src/components/Skills/index.js b/src/components/Skills/index.js new file mode 100644 index 00000000..6d8ea1f3 --- /dev/null +++ b/src/components/Skills/index.js @@ -0,0 +1 @@ +export { Skills } from "./Skills" \ No newline at end of file diff --git a/src/components/Tech/Tech.jsx b/src/components/Tech/Tech.jsx new file mode 100644 index 00000000..20e28bd8 --- /dev/null +++ b/src/components/Tech/Tech.jsx @@ -0,0 +1,48 @@ +import { Heading } from "../Heading" +import { SectionContainer } from "../SectionContainer" +import styled from "styled-components" + + +export const TechWrapper = styled.section` + background-color: ${({ theme }) => theme.colors.primary}; + color: ${({ theme }) => theme.colors.secondary}; + padding: 0 0 5rem; + width: 100%; +` + +export const TechInner = styled.div` + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +` + +export const TechBody = styled.p` + font-size: 1rem; + max-width: 350px; + text-align: center; + margin: 0 auto; + +@media ${({ theme }) => theme.breakpoints.tablet} { + max-width: 500px; +} + +@media ${({ theme }) => theme.breakpoints.desktop} { + font-size: 1.2rem; + max-width: 850px; +} +` + + +export const Tech = () => { + return ( + + + + Tech + HTML, CSS, Flexbox, JavaScript ES6, TypeScript, JSX, React, React Hooks, Node.js, Mongo DB, Web Accessibility, APIs, mob-programming, pair-programming, GitHub. + + + + ) +} \ No newline at end of file diff --git a/src/components/Tech/index.js b/src/components/Tech/index.js new file mode 100644 index 00000000..6a29ec4b --- /dev/null +++ b/src/components/Tech/index.js @@ -0,0 +1 @@ +export { Tech } from "./Tech" \ No newline at end of file diff --git a/src/components/theme.js b/src/components/theme.js new file mode 100644 index 00000000..ada67ff1 --- /dev/null +++ b/src/components/theme.js @@ -0,0 +1,18 @@ +export const theme = { + colors: { + primary: "#000000", + secondary: "#FFFFFF", + accent: "rgba(37,99,235,0.95)", + }, + spacing: { + xs: "0.25rem", + sm: "0.5rem", + md: "1rem", + lg: "2rem", + xl: "4rem", + }, + breakpoints: { + tablet: '(min-width: 768px)', + desktop: '(min-width: 1024px)', + } +} \ No newline at end of file diff --git a/src/data/posts.json b/src/data/posts.json new file mode 100644 index 00000000..330a31cd --- /dev/null +++ b/src/data/posts.json @@ -0,0 +1,25 @@ +{ + "posts": [ + { + "name": "Career change: My journey into tech", + "date": "Nov 12th", + "description": "After studying at university for four and a half years, I had no plans to return to school. I was done with that chapter of my life.", + "image": "./images/careerchange.jpg", + "link": "" + }, + { + "name": "Placeholder 1", + "date": "Nov 15th", + "description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptatibus quos deleniti quidem ea quod laborum commodi cupiditate ex odio soluta sit sequi cumque nobis tempora veniam recusandae necessitatibus, enim at!", + "image": "./images/kugghjul.png", + "link": "https://www.linkedin.com/feed/update/urn:li:activity:7395424129769893888/" + }, + { + "name": "Placeholder 2", + "date": "Nov 15th", + "description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptatibus quos deleniti quidem ea quod laborum commodi cupiditate ex odio soluta sit sequi cumque nobis tempora veniam recusandae necessitatibus, enim at!", + "image": "./images/placeholder.png", + "link": "https://www.linkedin.com/feed/update/urn:li:activity:7395424129769893888/" + } + ] +} \ No newline at end of file diff --git a/src/data/projects.json b/src/data/projects.json index 7c426028..22ab7bc0 100644 --- a/src/data/projects.json +++ b/src/data/projects.json @@ -2,18 +2,35 @@ "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", + "description": "This is a project for a fictional company, built with responsive design using Flexbox and Grid, featuring a mobile menu and contact form. It was designed to practice clean structure, interactivity, and enhance the user experience.", + "image": "images/business2.png", "tags": [ "HTML5", "CSS3", "JavaScript" ], - "netlify": "link", - "github": "link" + "position": "left", + "netlify": "https://skinexpert.netlify.app/", + "github": "https://github.com/sandrahagevall/project-skinexpert" + }, + { + "name": "Recipe library", + "description": "This is a web app built using the Spoonacular API, designed to practice fetching specific data and using local storage to enhance the user experience, with JavaScript animations for added interactivity.", + "image": "images/recipe2.png", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "APIs" + ], + "position": "right", + "netlify": "https://recipelibrary-app.netlify.app/", + "github": "https://github.com/sandrahagevall/js-project-recipe-library" }, { "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", + "description": "This weather app is a group project built using the SMHI API and TypeScript. It displays accurate weather data for different locations, includes dynamic icons, and was designed to practice API integration, asynchronous data handling, and collaborative development.", + "image": "images/watherapp2.png", "tags": [ "HTML5", "CSS3", @@ -21,8 +38,22 @@ "TypeScript", "APIs" ], - "netlify": "link", - "github": "link" + "position": "left", + "netlify": "https://swe-weather-app.netlify.app/", + "github": "https://github.com/sandrahagevall/js-project-weather-app" + }, + { + "name": "Accessibility site", + "description": "This fictional event site is a group project, created with accessbility in focus. It includes a modal with trapped focus, a high-contrast mode toggle, and text customization settings. The project was designed to practice building accessible and user-friendly interfaces while implementing interactive features that enhance usability for all users.", + "image": "images/eventfinder2.png", + "tags": [ + "HTML5", + "CSS3", + "JavaScript" + ], + "position": "right", + "netlify": "https://eventacc.netlify.app/", + "github": "https://github.com/sandrahagevall/js-project-accessibility" } ] } \ No newline at end of file diff --git a/src/data/skills.json b/src/data/skills.json new file mode 100644 index 00000000..ee6272b1 --- /dev/null +++ b/src/data/skills.json @@ -0,0 +1,43 @@ +{ + "skills": [ + { + "group": "Code", + "items": [ + "HTML5", + "CSS3", + "JavaScript", + "TypeScript", + "React", + "JSON" + ] + }, + { + "group": "Toolbox", + "items": [ + "VS Code", + "Figma", + "Chrome DevTools", + "Netlify", + "Slack" + ] + }, + { + "group": "Upcoming", + "items": [ + "Node.js", + "MongoDB" + ] + }, + { + "group": "More", + "items": [ + "Strategy", + "Process design", + "Responsive design", + "Accessibility", + "Agile methodology", + "Project management" + ] + } + ] +} \ 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..7c70722a 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client' import { App } from './App.jsx' -import './index.css' createRoot(document.getElementById('root')).render( diff --git a/vite.config.js b/vite.config.js index 8b0f57b9..d133d633 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,5 +3,11 @@ 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 }]] + } + }) + ] })