diff --git a/README.md b/README.md index 200f4282..9a64832b 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# Portfolio +Agnes Sjösten – Portfolio + +A personal portfolio built with React and Styled Components, showcasing my projects, skills, and background as a creative developer. + +Tech stack + +React + +Styled Components + +JavaScript (ES6) + +Vite + +Run locally +npm install +npm run dev diff --git a/index.html b/index.html index 6676fb2d..6b0a20b9 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,55 @@ - + - - Portfolio + Agnes Portfolio + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/package.json b/package.json index 48911600..4837d413 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "components": "^0.1.0", + "lucide-react": "^0.554.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "styled": "^1.0.0", + "styled-components": "^6.1.0" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/public/AgnesSjosten.jpg b/public/AgnesSjosten.jpg new file mode 100644 index 00000000..4fdad201 Binary files /dev/null and b/public/AgnesSjosten.jpg differ diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 00000000..1ba15791 Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 00000000..4fc5505d Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 00000000..c0a1ff85 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 00000000..517d466f Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 00000000..abce529d Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..04ed6ddc Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/og-image.jpg b/public/og-image.jpg new file mode 100644 index 00000000..44ca1d21 Binary files /dev/null and b/public/og-image.jpg differ diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 00000000..45dc8a20 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..edf4277a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,38 @@ +import Hero from "./components/Hero"; +import Tech from "./components/Tech"; +import Projects from "./components/Projects"; +import SkillsData from "./components/SkillsData"; +import MyTexts from "./components/MyTexts"; +import LetsTalk from "./components/LetsTalk"; +import { GlobalStyle } from "./components/GlobalStyle"; + +import styled from "styled-components"; + +/* Main wrapper for the entire page layout */ +const PageWrapper = styled.div` + min-height: 100vh; /* sidan minst lika hög som skärmen */ + display: flex; + flex-direction: column; +`; + +/* Content area that grows above the footer */ +const Content = styled.main` + flex: 1; /* innehållet tar allt utrymme ovanför footern */ +`; + +/* Root application component assembling all sections */ 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/assets/API2.png b/src/assets/API2.png new file mode 100644 index 00000000..8a3c34a3 Binary files /dev/null and b/src/assets/API2.png differ diff --git a/src/assets/APIPic.png b/src/assets/APIPic.png new file mode 100644 index 00000000..f4c1ed75 Binary files /dev/null and b/src/assets/APIPic.png differ diff --git a/src/assets/Agnes.jpg b/src/assets/Agnes.jpg new file mode 100644 index 00000000..44ca1d21 Binary files /dev/null and b/src/assets/Agnes.jpg differ diff --git a/src/assets/Agnes2.jpg b/src/assets/Agnes2.jpg new file mode 100644 index 00000000..d0c2d150 Binary files /dev/null and b/src/assets/Agnes2.jpg differ diff --git a/src/assets/BusinessSite.png b/src/assets/BusinessSite.png new file mode 100644 index 00000000..885d385c Binary files /dev/null and b/src/assets/BusinessSite.png differ diff --git a/src/assets/CoctailApp.png b/src/assets/CoctailApp.png new file mode 100644 index 00000000..264d5092 Binary files /dev/null and b/src/assets/CoctailApp.png differ diff --git a/src/assets/Code.png b/src/assets/Code.png new file mode 100644 index 00000000..7eb1f90f Binary files /dev/null and b/src/assets/Code.png differ diff --git a/src/assets/Coffee.jpg b/src/assets/Coffee.jpg new file mode 100644 index 00000000..27137aa5 Binary files /dev/null and b/src/assets/Coffee.jpg differ diff --git a/src/assets/Coffee2.png b/src/assets/Coffee2.png new file mode 100644 index 00000000..fc38682d Binary files /dev/null and b/src/assets/Coffee2.png differ diff --git a/src/assets/Happy.png b/src/assets/Happy.png new file mode 100644 index 00000000..84c49f57 Binary files /dev/null and b/src/assets/Happy.png differ diff --git a/src/assets/HayStack.png b/src/assets/HayStack.png new file mode 100644 index 00000000..f591bae2 Binary files /dev/null and b/src/assets/HayStack.png differ diff --git a/src/assets/MovieApp.png b/src/assets/MovieApp.png new file mode 100644 index 00000000..21b0706c Binary files /dev/null and b/src/assets/MovieApp.png differ diff --git a/src/assets/ReadingRoom.png b/src/assets/ReadingRoom.png new file mode 100644 index 00000000..2cc524ad Binary files /dev/null and b/src/assets/ReadingRoom.png differ diff --git a/src/assets/RecipeApp.png b/src/assets/RecipeApp.png new file mode 100644 index 00000000..19c70b46 Binary files /dev/null and b/src/assets/RecipeApp.png differ diff --git a/src/assets/RecipeLibrary.png b/src/assets/RecipeLibrary.png new file mode 100644 index 00000000..d8b55b4a Binary files /dev/null and b/src/assets/RecipeLibrary.png differ diff --git a/src/assets/RiddleRush.png b/src/assets/RiddleRush.png new file mode 100644 index 00000000..fca32c2c Binary files /dev/null and b/src/assets/RiddleRush.png differ diff --git "a/src/assets/Sk\303\244rmbild 2025-11-27 085359.png" "b/src/assets/Sk\303\244rmbild 2025-11-27 085359.png" new file mode 100644 index 00000000..4d34176b Binary files /dev/null and "b/src/assets/Sk\303\244rmbild 2025-11-27 085359.png" differ diff --git a/src/assets/Sun.png b/src/assets/Sun.png new file mode 100644 index 00000000..9fc73615 Binary files /dev/null and b/src/assets/Sun.png differ diff --git a/src/assets/SunRise.png b/src/assets/SunRise.png new file mode 100644 index 00000000..f3087a45 Binary files /dev/null and b/src/assets/SunRise.png differ diff --git a/src/assets/WeatherApp.png b/src/assets/WeatherApp.png new file mode 100644 index 00000000..67db8e56 Binary files /dev/null and b/src/assets/WeatherApp.png differ diff --git a/src/assets/icons/ArrowDown.svg b/src/assets/icons/ArrowDown.svg new file mode 100644 index 00000000..aff0efdb --- /dev/null +++ b/src/assets/icons/ArrowDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/Facebook.svg b/src/assets/icons/Facebook.svg new file mode 100644 index 00000000..3650c1ac --- /dev/null +++ b/src/assets/icons/Facebook.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/github.svg b/src/assets/icons/github.svg new file mode 100644 index 00000000..80cbbebf --- /dev/null +++ b/src/assets/icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/linkedin.svg b/src/assets/icons/linkedin.svg new file mode 100644 index 00000000..0fbcc94f --- /dev/null +++ b/src/assets/icons/linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/web.svg b/src/assets/icons/web.svg new file mode 100644 index 00000000..e09ab576 --- /dev/null +++ b/src/assets/icons/web.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/sunflower.jpg b/src/assets/sunflower.jpg new file mode 100644 index 00000000..817b2a76 Binary files /dev/null and b/src/assets/sunflower.jpg differ diff --git a/src/components/GlobalStyle.jsx b/src/components/GlobalStyle.jsx new file mode 100644 index 00000000..df801069 --- /dev/null +++ b/src/components/GlobalStyle.jsx @@ -0,0 +1,86 @@ +import { createGlobalStyle } from "styled-components"; + +/* + Basic global reset: + - Removes default margins and padding + - Sets border-box sizing for all elements + - Defines base font, colors, and line-height + - Prevents horizontal overflow + - Makes images responsive +*/ + +export const GlobalStyle = createGlobalStyle` + *, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + html, body { + width: 100%; + margin: 0; + padding: 0; + } + + body { + font-family: "Inter", sans-serif; + color: #000; + background: #fff; + overflow-x: hidden; + line-height: 1.6; + } + + img { + max-width: 100%; + display: block; + } + + /* ----------------------------- */ + /* MOBILE-FIRST*/ + /* ----------------------------- */ + + + h1 { + font-size: 40px; /* Mobil */ + font-weight:bold; + font-family: "Inter", sans-serif; + } + + h2 { + font-size: 38px; + font-family: "Inter", sans-serif; + + } + + h3 { + font-size: 20px; + font-family: "Inter", sans-serif; + } + + p { + font-size: 16px; +font-family: "Inter", sans-serif; + margin: 0 auto; + max-width: 750px; + } + + /* ----------------------------- */ + /* TABLET (≥ 768px) */ + /* ----------------------------- */ + @media (min-width: 768px) { + h1 { font-size: 70px; letter-spacing: -0.5px; } + h2 { font-size: 60px; letter-spacing: -0.9px; font-weight: 700; } + h3 { font-size: 30px; letter-spacing: -0.3px; font-weight: 500;} + p { font-size: 20px; letter-spacing: -0.1px;} + } + + /* ----------------------------- */ + /* DESKTOP (≥ 1024px) */ + /* ----------------------------- */ + @media (min-width: 1024px) { + h1 { font-size: 100px; letter-spacing: -0.5px; } + h2 { font-size: 80px; letter-spacing: -0.9px; font-weight: 700; } + h3 { font-size: 30px; letter-spacing: -0.3px; font-weight: 500;} + p { font-size: 18px; letter-spacing: -0.1px;} + } +`; diff --git a/src/components/Hero.jsx b/src/components/Hero.jsx new file mode 100644 index 00000000..efdc96f4 --- /dev/null +++ b/src/components/Hero.jsx @@ -0,0 +1,196 @@ +import styled from "styled-components"; +import agnesImg from "../assets/Agnes.jpg"; +import CoffeeImg from "../assets/Coffee.jpg"; +import sunflowerImg from "../assets/sunflower.jpg"; + +/* + Main wrapper for the hero section. + Uses flex layout so child elements can be reordered on mobile. +*/ +const HeroSection = styled.section` + padding: 40px 16px; + text-align: center; + background-color: #ffffff; + color: black; + display: flex; + flex-direction: column; + align-items: center; +`; + +/* Intro heading shown above the name */ +const IntroTitle = styled.h3` + margin: 0 0 8px; + line-height: 1.2; + + @media (max-width: 767px) { + order: 1; + } +`; + +/* Main name/title element */ +const Name = styled.h1` + margin: 8px 0 16px; + font-weight: 800; + text-align: center; + line-height: 1.1; + + @media (max-width: 767px) { + order: 2; + } +`; + +/* Subtitle/tagline describing the person */ +const Tagline = styled.h3` + max-width: 750px; + margin: 0 auto 16px; + padding: 0; + text-align: center; + + @media (min-width: 768px) { + text-align: center; + padding-top: 20px; + padding-left: 30px; + padding-right: 30px; + } + + @media (max-width: 767px) { + order: 3; + } +`; + +/* Main descriptive paragraph */ +const Text = styled.p` + text-align: left; + max-width: 750px; + margin: 0 auto; + padding-left: 10px; + padding-right: 10px; + + @media (min-width: 768px) { + text-align: center; + padding-left: 70px; + padding-right: 70px; + } + + /* Mobile: shown after the images */ + @media (max-width: 767px) { + order: 5; + } +`; + +/* Wrapper holding the three overlapping hero images */ +const HeroImages = styled.div` + position: relative; + width: 100%; + max-width: 650px; + height: 350px; + margin: 0 auto 70px; + + @media (max-width: 767px) { + max-width: 320px; + height: 200px; + margin-bottom: 40px; + order: 4; + } +`; + +/* Base style for all side images */ +const SideImage = styled.img` + position: absolute; + width: 300px; + height: 350px; + object-fit: cover; + border-radius: 16px; + + @media (max-width: 767px) { + width: 150px; + height: 180px; + } +`; + +/* Left image with rotation effect */ +const LeftImage = styled(SideImage)` + left: 0; + top: 50px; + transform: rotate(-8deg); + + @media (max-width: 767px) { + top: 20px; + } +`; + +/* Right image with rotation effect */ +const RightImage = styled(SideImage)` + right: 0; + top: 50px; + transform: rotate(8deg); + + @media (max-width: 767px) { + top: 20px; + } +`; + +/* Wrapper for the centered main portrait image */ +const CenterImageWrapper = styled.div` + position: absolute; + left: 65%; + top: 10px; + transform: translateX(-85%); + width: 300px; + height: 350px; + border-radius: 24px; + background-color: #000; + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + overflow: hidden; + + @media (max-width: 767px) { + width: 170px; + height: 200px; + top: 0; + } +`; + +/* Portrait image inside the center frame */ +const CenterImage = styled.img` + width: 290px; + height: 300px; + border-radius: 50%; + object-fit: cover; + + @media (max-width: 767px) { + width: 160px; + height: 160px; + } +`; + +// Hero component displaying the intro text, portrait images, and a brief description. +export default function Hero() { + return ( + + Hi there, I´m + Agnes Sjösten + + + + + + + + + + + Agnes is a creative developer with a background in social work. + + + + After exploring the world and working with people, she became a social + worker driven by empathy and a desire to make a difference. Today, she + combines that understanding with her passion for design and technology + to create meaningful, user-centered digital solutions. + + + ); +} diff --git a/src/components/LetsTalk.jsx b/src/components/LetsTalk.jsx new file mode 100644 index 00000000..f5f3dcb6 --- /dev/null +++ b/src/components/LetsTalk.jsx @@ -0,0 +1,136 @@ +import agnes2Img from "../assets/Agnes2.jpg"; +import linkedinIcon from "../assets/icons/linkedin.svg"; +import githubIcon from "../assets/icons/github.svg"; +import facebookIcon from "../assets/icons/Facebook.svg"; +import styled from "styled-components"; + +/* Title at the top of the contact section */ +const Talktitle = styled.h2` + margin: 60px 0px 10px; + padding-top: 30px; +`; + +/* Circular portrait image */ +const AgnesImage = styled.img` + width: 180px; + height: 180px; + border-radius: 50%; + object-fit: cover; +`; + +/* Main container for the contact section */ +const Contact = styled.section` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 30px; + background-color: black; + color: white; + padding: 0 0 80px 0; + width: 100%; + margin-top: 80px; +`; + +/* List of contact details */ +const ContactList = styled.ul` + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px 0 10px 0; + margin-top: 10px; +`; + +/* Each contact line (name, phone, email) */ +const Li = styled.li` + font-size: 20px; + font-weight: 550; + line-height: 1.6; + text-align: center; + + a { + color: inherit; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + @media (min-width: 768px) { + font-size: 30px; /* tablet */ + } + + @media (min-width: 1024px) { + font-size: 30px; /* desktop */ + } +`; + +/* Social media icon images */ +const IconImage = styled.img` + width: 35px; + height: 35px; +`; + +/* Row displaying social media icons horizontally */ +const IconRow = styled.div` + display: flex; + gap: 20px; /* avstånd mellan ikonerna */ + margin-top: 20px; + justify-content: center; +`; + +/* + Contact / Let's Talk section. + Shows a title, portrait image, contact details, + and a row of social media icons. +*/ +export default function LetsTalk() { + return ( + + Let’s Talk + + + +
  • Agnes Sjösten
  • +
  • + +46 (0)70 5 95 55 48 +
  • +
  • + + agnes_sjosten@hotmail.com + +
  • +
    + + + + + + + + + + + + +
    + ); +} diff --git a/src/components/LinkButton.jsx b/src/components/LinkButton.jsx new file mode 100644 index 00000000..bc8dd120 --- /dev/null +++ b/src/components/LinkButton.jsx @@ -0,0 +1,28 @@ +// Reusable link-style button component +// Styled as a flexible, full-width button with icon + text support. +// src/components/LinkButton.jsx +import styled from "styled-components"; + +export const LinkButton = styled.a` + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 15px; + + padding: 10px 20px; + border-radius: 12px; + font-size: 18px; + text-decoration: none; + + width: 100%; + max-width: 300px; + + background-color: #000000; + color: #ffffff; + border: none; + + cursor: pointer; + &:hover { + transform: scale(1.03); + } +`; diff --git a/src/components/MyTexts.jsx b/src/components/MyTexts.jsx new file mode 100644 index 00000000..8f5d9031 --- /dev/null +++ b/src/components/MyTexts.jsx @@ -0,0 +1,58 @@ +// Renders a section containing a list of article previews ("My Words"). +// Each MyWords component displays a date tag, title, short description, +// image, and a link to the full article. +import MyWords from "./MyWords"; +import APIPic from "../assets/API2.png"; +import CodeImg from "../assets/Code.png"; +import SunRise from "../assets/Sun.png"; +import styled from "styled-components"; + +/* Wrapper for the entire My Words section */ +const MyWordSection = styled.section` + background-color: white; +`; + +/* Section title styling */ +const MyWordTitle = styled.h2` + color: black; + text-align: center; + padding-bottom: 20px; + padding-top: 40px; + + @media (min-width: 768px) { + padding: 70px; + } +`; + +export default function MyTexts() { + return ( + + My Words + + + + + + ); +} diff --git a/src/components/MyWords.jsx b/src/components/MyWords.jsx new file mode 100644 index 00000000..ee26a3f6 --- /dev/null +++ b/src/components/MyWords.jsx @@ -0,0 +1,162 @@ +// Component that displays a single "My Words" article preview. +// Includes an image, date tag, title, short description, and a link button. + +import styled from "styled-components"; +import webIcon from "../assets/icons/web.svg"; +import { LinkButton } from "./LinkButton"; +import { Tag } from "./tag"; + +/* Wrapper for the entire article preview block */ +const MyWordWrapper = styled.article` + display: flex; + flex-direction: column; + gap: 10px; + background-color: #ffffff; + color: #000000; + padding: 20px 30px; + max-width: 1200px; + margin: 0 auto; + p { + color: black; + } + + @media (min-width: 1024px) { + display: flex; + flex-direction: row; + align-items: flex-start; + background-color: white; + padding-left: 150px; + padding-right: 70px; + padding-bottom: 70px; + gap: 90px; + } + + @media (min-width: 768px) { + display: flex; + flex-direction: row; + align-items: flex-start; + background-color: white; + padding-left: 60px; + padding-right: 30px; + padding-bottom: 20px; + gap: 20px; + } +`; + +/* Container for the article image */ +const ImageWord = styled.div` + flex: 1; + display: flex; + justify-content: center; + + @media (min-width: 768px) { + justify-content: flex-start; + } +`; + +/* The article image itself */ +const ProjectImage = styled.img` + width: 90%; + height: 300px; + object-fit: cover; + border-radius: 12px; + display: block; + + @media (min-width: 768px) { + width: 100%; + height: 420px; + } + @media (min-width: 1024px) { + max-height: 300px; + } +`; + +/* Column containing the text content and buttons */ +const ContentCol = styled.div` + flex: 1; + display: flex; + flex-direction: column; + text-align: left; + + > span { + margin-bottom: 20px; + } + + > h3 { + margin-bottom: 16px; + } + + > p { + margin-bottom: 10px; + line-height: 1.6; + } + + @media (min-width: 768px) { + padding-right: 80px; + margin-bottom: 0px; + + > span { + margin-bottom: 0; + } + } + @media (min-width: 1024px) { + > span { + margin-bottom: 10px; + } + p { + margin-bottom: 35px; + } + } +`; + +/* Wrapper for the link button */ +const ButtonsWrapper = styled.div` + margin-top: 15px; + align-items: flex-start; + + @media (min-width: 768px) { + margin-top: 0px; + } +`; + +/* Icon used inside the link button */ +const IconImage = styled.img` + width: 25px; + height: 25px; +`; + +/* Label next to the icon in the button */ +const ButtonLabel = styled.span``; + +/* Main component rendering a single article preview */ +export default function MyWords({ + title, + description, + imageSrc, + imageAlt, + tags, + articleUrl, +}) { + return ( + + + + + + {tags} + +

    {title}

    +

    {description}

    + + {articleUrl && ( + + + + Read article + + + )} +
    +
    + ); +} diff --git a/src/components/Projects.jsx b/src/components/Projects.jsx new file mode 100644 index 00000000..5602a2ce --- /dev/null +++ b/src/components/Projects.jsx @@ -0,0 +1,195 @@ +// Component rendering a list of featured projects. +// Shows the first 4 projects by default and expands to show all when the user clicks the button. + +import ProjectsCard from "./ProjectsCard"; +import BusinessSiteImg from "../assets/Coffee2.png"; +import WeatherAppImg from "../assets/WeatherApp.png"; +import RecipeLibraryImg from "../assets/RecipeLibrary.png"; +import ReadingRoomImg from "../assets/ReadingRoom.png"; +import MovieAppImg from "../assets/MovieApp.png"; +import RiddleRushImg from "../assets/RiddleRush.png"; +import CoctailAppImg from "../assets/CoctailApp.png"; +import HayStackImg from "../assets/HayStack.png"; +import styled from "styled-components"; +import { useState } from "react"; +import ArrowDownIcon from "../assets/icons/ArrowDown.svg"; +import Happy from "../assets/Happy.png"; + +/* Wrapper section for the entire projects area */ +const ProjectsSection = styled.section` + background-color: white; +`; + +/* Title styling for the projects section */ +const ProjectsTitle = styled.h2` + color: black; + text-align: center; + padding: 20px 0 10px; + + @media (min-width: 768px) { + padding: 80px 0 40px; + } +`; + +/* Container for the "See more" button */ +const ButtonWrapper = styled.div` + text-align: center; + margin-top: 10px; + padding-bottom: 60px; +`; + +/* Button used to toggle showing more or fewer projects */ +const MoreButton = styled.button` + background: #ffffff; + color: #000000; + padding: 7px 10px; + border-radius: 10px; + border: 3px solid black; + font-size: 18px; + font-weight: 500; + cursor: pointer; + width: 280px; + + display: inline-flex; + align-items: center; + justify-content: flex-start; + + &:hover { + transform: scale(1.03); + } + @media (min-width: 768px) { + width: 300px; + } +`; + +/* Arrow icon that rotates depending on expand/collapse state */ +const IconImage = styled.img` + width: 35px; + height: 35px; + margin-right: 15px; + transform: ${({ open }) => (open ? "rotate(180deg)" : "rotate(0deg)")}; +`; + +/* Array containing all project data */ +const allProjects = [ + { + title: "Business site", + description: + "This is a project I built during Technigo’s Web Development Bootcamp. The goal was to create a responsive business website using HTML, CSS, and JavaScript, with a focus on design, usability, and structure.", + imageSrc: BusinessSiteImg, + imageAlt: "Screenshot of a coffee themed business site", + tags: ["HTML5", "CSS3", "JavaScript"], + codeUrl: "https://github.com/AgnesSj01/js-project-business-site", + liveUrl: "https://legacy-coffee.netlify.app/", + }, + { + title: "Weather app", + description: + "This was our first mob-programming project during Technigo’s Web Development Bootcamp, where we collaboratively built a weather application using the SMHI Weather API. Together, we explored how to fetch and display real-time weather data, structure our JavaScript logic as a team, and design a clean, user-friendly interface.", + imageSrc: WeatherAppImg, + imageAlt: "Screenshot of a WeatherApp", + tags: ["HTML5", "CSS3", "JavaScript", "TypeScript"], + liveUrl: "https://weather-project-lar.netlify.app/", + codeUrl: "https://github.com/AgnesSj01/js-project-weather-app", + reverse: true, + }, + { + title: "Recipe Library", + description: + "This project was created as part of Technigo’s Web Development Bootcamp, during the module focused on APIs, JSON, fetch() and Promises. The goal was to replace static mock data with real recipe data from the Spoonacular API and build a dynamic, responsive recipe library.", + imageSrc: RecipeLibraryImg, + imageAlt: "Screenshot of a Recipe Library website", + tags: ["HTML5", "CSS3", "JavaScript", "API"], + liveUrl: "https://inspiring-sundae-2d353c.netlify.app/", + codeUrl: "https://github.com/AgnesSj01/js-project-recipe-library", + }, + { + title: "Reading Room", + description: + "This was an accessibility-focused group project built through mob coding, where we collaborated using branches and merges in Git. We created a multi-page website with semantic HTML, keyboard navigation, ARIA support, and strong color contrast.", + imageSrc: ReadingRoomImg, + imageAlt: "Screenshot of a ReadingRoom website", + tags: ["HTML5", "CSS3", "JavaScript"], + liveUrl: "https://the-reading-room-accesibility-project.netlify.app/", + codeUrl: "https://github.com/AgnesSj01/js-project-accessibility", + reverse: true, + }, + { + title: "Movie app – prototype / visual design", + description: + "As part of the course Grafiska användargränssnitt (Graphical User Interfaces) within the Digital Service Development program at Luleå University of Technology, my group and I designed a movie app aimed at making it easier for users to choose what to watch.", + imageSrc: MovieAppImg, + imageAlt: "Screenshots of pictures from MovieApp prototype", + tags: ["Figma Design"], + liveUrl: + "https://www.figma.com/proto/DKP6X7bdCzr15DLYFb9ARf/Movie-App?node-id=238-1679&starting-point-node-id=301%3A786&t=mgHEJgOWMcnCentK-1&show-proto-sidebar=1", + }, + { + title: "Game app – prototype / visual design", + description: + "As part of the course Grafiska användargränssnitt (Graphical User Interfaces) within the Digital Service Development program at Luleå University of Technology, I designed a graphical user interface in Figma for an interactive quiz game.", + imageSrc: RiddleRushImg, + imageAlt: "Screenshots of pictures from RiddleRush prototype", + tags: ["Figma Design"], + liveUrl: + "https://www.figma.com/proto/6mTiPD05LYmwh27nDq3KrS/Riddle-Rush?node-id=599-2551&starting-point-node-id=599%3A2551&t=dkTUenL4cQhc9C0j-1", + reverse: true, + }, + { + title: "Cocktail app – visual design", + description: + "As part of the course Interaktion och Mobilitet in the Digital Service Development program at Luleå University of Technology, I created a mobile-first cocktail app built in Flutter and Dart.", + imageSrc: CoctailAppImg, + imageAlt: "Screenshots from CoctailApp", + tags: ["Flutter", " Dart"], + codeUrl: "https://github.com/AgnesSj01/flutter_recept_app", + }, + { + title: "Hay Stack – prototype / visual design", + description: + "As part of the course Interaktion och Mobilitet in the Digital Service Development program at Luleå University of Technology, my group and I worked on a real-world design brief in collaboration with Haystack.", + imageSrc: HayStackImg, + imageAlt: "Screenshots of pictures from the game app HayStack prototype", + tags: ["Figma Design"], + liveUrl: + "https://www.figma.com/proto/tvyHQWabUvjfh6CvKQDXa9/Haystack-Hi-Fi-prototyp?node-id=340-6476&t=mjeqyGeD1oi3qzn6-1&scaling=scale-down&content-scaling=fixed&page-id=1%3A11&starting-point-node-id=340%3A6476&show-proto-sidebar=1", + reverse: true, + }, + { + title: "Happy Thoughts", + description: + "I developed a React-based “Happy Thoughts” app that communicates with a public API. The project focused on component lifecycle, the useEffect hook, state management, and form handling. Users can submit new thoughts, view an updated list in real time, and like posts. I implemented both POST requests and UI updates based on API responses. The main focus was understanding how React applications integrate with external APIs.", + imageSrc: Happy, + imageAlt: "Screenshot of a coffee themed business site", + tags: ["React", "JavaScript", "API Integration"], + codeUrl: + "https://github.com/AgnesSj01/js-project-happy-thoughts/blob/main/README.md", + liveUrl: "https://agnes-happythoughtsproject.netlify.app/", + }, +]; + +export default function Projects() { + // Controls whether all projects should be shown or only the first four + const [showAll, setShowAll] = useState(false); + + // Either returns all projects or the first four + const visibleProjects = showAll ? allProjects : allProjects.slice(0, 4); + + return ( + + Featured Projects + + {visibleProjects.map((project) => ( + + ))} + {allProjects.length > 4 && ( + + setShowAll((prev) => !prev)}> + + {showAll ? "See fewer projects" : "See more projects"} + + + )} + + ); +} diff --git a/src/components/ProjectsCard.jsx b/src/components/ProjectsCard.jsx new file mode 100644 index 00000000..9d759f60 --- /dev/null +++ b/src/components/ProjectsCard.jsx @@ -0,0 +1,145 @@ +// src/components/ProjectsCard.jsx +import styled from "styled-components"; +import { LinkButton } from "./LinkButton"; +import webIcon from "../assets/icons/web.svg"; +import githubIcon from "../assets/icons/github.svg"; +import { Tag } from "./tag"; + +/* Wrapper for the entire project card. + Mobile/Tablet = column layout. + Desktop = row or reversed row depending on "reverse" prop. */ +const ProjectCardWrapper = styled.article` + display: flex; + flex-direction: column; + background-color: #ffffff; + color: #000000; + padding: 30px 30px; + max-width: 1500px; + margin: 0 auto; + + @media (min-width: 1024px) { + flex-direction: ${({ reverse }) => (reverse ? "row-reverse" : "row")}; + gap: 20px; + padding: 80px 120px; + } +`; + +/* Column containing the project image */ +const ImageCol = styled.div` + flex: 1; +`; + +/* Project preview image */ +const ProjectImage = styled.img` + width: 100%; + max-height: 400px; + height: auto; + object-fit: contain; + display: block; +`; + +/* Column containing title, tags, text and buttons */ +const ContentCol = styled.div` + flex: 1; + display: flex; + flex-direction: column; + text-align: left; + gap: 10px; + + @media (min-width: 1024px) { + padding-left: 60px; + padding-right: 60px; + gap: 10px; + } +`; + +/* Row displaying all tech tags */ +const TagsRow = styled.div` + display: flex; + flex-wrap: wrap; + gap: 2px; + margin-bottom: 8px; + margin-top: 20px; + + @media (min-width: 1024px) { + margin-top: 0; + } + @media (min-width: 768px) { + gap: 8px; + } +`; + +/* Project description text */ +const Description = styled.p` + margin: 0; + max-width: 100%; + line-height: 1.4; + color: #000; +`; + +/* Wrapper for the action buttons (live demo / code) */ +const ButtonsWrapper = styled.div` + margin-top: 15px; + display: flex; + flex-direction: column; + gap: 5px; + align-items: flex-start; + + @media (min-width: 1024px) { + margin-top: 24px; + } +`; + +/* Icon inside each link button */ +const IconImage = styled.img` + width: 25px; + height: 25px; +`; + +const ButtonLabel = styled.span``; + +/* Main component for rendering a single project card */ +export default function ProjectsCard({ + title, + description, + imageSrc, + imageAlt, + tags, + liveUrl, + codeUrl, + reverse, +}) { + return ( + + + + + + + + {tags.map((tag) => ( + {tag} + ))} + + +

    {title}

    + {description} + + + {liveUrl && ( + + + Live demo + + )} + {codeUrl && ( + + + View Code + + )} + +
    +
    + ); +} diff --git a/src/components/SkillCategory.jsx b/src/components/SkillCategory.jsx new file mode 100644 index 00000000..533cab21 --- /dev/null +++ b/src/components/SkillCategory.jsx @@ -0,0 +1,76 @@ +import styled from "styled-components"; + +/* Wrapper for a single skill category: + Contains the category label + list of items. + Alignment changes depending on screen size. */ +const TagRow = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + align-items: flex-start; + + @media (min-width: 768px) and (max-width: 1023px) { + align-items: center; + } + + @media (min-width: 1024px) { + align-items: flex-start; + max-width: 200px; + } +`; + +/* Label/title for the skill category */ +const SkillTag = styled.h3` + display: flex; + justify-content: center; + width: 150px; + padding: 2px; + border-radius: 5px; + border: 1px solid #fff; + font-size: 16px; + font-weight: 600; + align-items: flex-start; + text-align: left; +`; + +/* List of items inside each skill category */ +const ItemsList = styled.ul` + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 3px; + text-align: left; + align-items: flex-start; + + @media (min-width: 768px) and (max-width: 1023px) { + text-align: center; + align-items: center; + } + + @media (min-width: 1024px) { + text-align: left; + align-items: flex-start; + } +`; + +/* Single skill item inside the list */ +const Item = styled.li` + font-size: 18px; +`; + +/* Component displaying a skill category with label + items */ +export default function SkillCategory({ label, items }) { + return ( + + {label} + + {items.map((item) => ( + {item} + ))} + + + ); +} diff --git a/src/components/SkillsData.jsx b/src/components/SkillsData.jsx new file mode 100644 index 00000000..aecc0591 --- /dev/null +++ b/src/components/SkillsData.jsx @@ -0,0 +1,85 @@ +import styled from "styled-components"; +import SkillCategory from "./SkillCategory"; + +/* Wrapper for the entire skills section */ +const SkillsSection = styled.section` + background-color: #000; + color: #fff; + padding: 60px 16px 80px; +`; + +/* Section heading */ +const SkillTitle = styled.h2` + margin-bottom: 40px; + text-align: center; +`; + +/* Layout wrapper for all skill categories. + Mobile/Tablet = single column + Desktop = multi-column row with wrapping */ +const CategoryRow = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; + + align-items: flex-start; + + @media (min-width: 768px) and (max-width: 1023px) { + align-items: center; + } + + @media (min-width: 1024px) { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + gap: 10px; + max-width: 1000px; + margin: 0 auto; + } +`; + +/* Component rendering all skill categories */ +export default function SkillsData() { + return ( + + Skills + + + + + + + + ); +} diff --git a/src/components/Tech.jsx b/src/components/Tech.jsx new file mode 100644 index 00000000..844a9543 --- /dev/null +++ b/src/components/Tech.jsx @@ -0,0 +1,44 @@ +import styled from "styled-components"; + +/* Section displaying a short list of technical skills */ +const TechSection = styled.section` + padding: 20px; + text-align: center; + background-color: black; + p { + text-align: center; + max-width: 700px; + margin: 0 auto; + letter-spacing: 0.4px; + line-height: 1.6; + } + @media (min-width: 768px) { + padding: 90px; + } +`; + +/* Heading for the tech section */ +const TechTitle = styled.h2` + color: white; + margin: 10px 0px 20px; +`; + +/* Paragraph containing the list of technologies */ +const Description = styled.p` + color: white; + padding-bottom: 20px; +`; + +/* Component rendering the Tech section */ +export default function Tech() { + return ( + + Tech + + HTML, CSS, Flexbox, JavaScript, ES6, JSX, React, React Hooks, Node.js, + Mongo DB, Web Accessibility, APIs, mob-programming, pair-programming, + GitHub. + + + ); +} diff --git a/src/components/tag.jsx b/src/components/tag.jsx new file mode 100644 index 00000000..c648f184 --- /dev/null +++ b/src/components/tag.jsx @@ -0,0 +1,19 @@ +import styled from "styled-components"; + +/* Reusable tag style used in both the Projects section and My Words section */ +export const Tag = styled.span` + width: 60px; + text-align: center; + font-size: 11px; + font-weight: 500; + border: 2px solid black; + border-radius: 5px; + display: inline-flex; + align-items: center; + justify-content: center; + + @media (min-width: 768px) { + width: 130px; + font-size: 15px; + } +`; diff --git a/src/data/index.css b/src/data/index.css new file mode 100644 index 00000000..e69de29b 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..f433f809 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; -import { App } from './App.jsx' +import { App } from "./App.jsx"; -import './index.css' - -createRoot(document.getElementById('root')).render( +createRoot(document.getElementById("root")).render( - , -) + +);