diff --git a/README.md b/README.md index 200f4282..c626f28b 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ -# Portfolio +# Developer Portfolio – Jennifer Jansson + +This is my personal developer portfolio built with **React** and **styled-components**. +It showcases my projects, technical skills, articles, and ways to get in contact with me. +The design follows a provided Figma layout and the site is fully responsive. + +## Features + +- Hero section with introduction and portrait +- Tech overview +- Featured projects with links to GitHub and live demos +- Skills section (Code, Toolbox, Upcoming, More) +- My Words / thoughts about code +- Contact section with social links +- Accessible and responsive across all screen sizes from 320-1600px +- Skip link and focus states for improved accessibility + +## Tech Stack + +- React +- Vite +- JavaScript (ES6) +- styled-components +- JSON data for dynamic content + +## Getting Started + +```bash +npm install +npm run dev +``` diff --git a/index.html b/index.html index 6676fb2d..deb8818a 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,43 @@ - + - + - Portfolio + + + + + + + + + + + + + + Jennifer Jansson — Frontend Developer Portfolio
diff --git a/package.json b/package.json index 48911600..83c48e82 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.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/accessibility.png b/public/accessibility.png new file mode 100644 index 00000000..938fddd4 Binary files /dev/null and b/public/accessibility.png differ diff --git a/public/arts.png b/public/arts.png new file mode 100644 index 00000000..46b407d7 Binary files /dev/null and b/public/arts.png differ diff --git a/public/business.png b/public/business.png new file mode 100644 index 00000000..6b527446 Binary files /dev/null and b/public/business.png differ diff --git a/public/footer.png b/public/footer.png new file mode 100644 index 00000000..60656370 Binary files /dev/null and b/public/footer.png differ diff --git a/public/happy.png b/public/happy.png new file mode 100644 index 00000000..c5d039d7 Binary files /dev/null and b/public/happy.png differ diff --git a/public/jj-favicon.svg b/public/jj-favicon.svg new file mode 100644 index 00000000..51a85c6e --- /dev/null +++ b/public/jj-favicon.svg @@ -0,0 +1,6 @@ + + + + J + + diff --git a/public/mic.png b/public/mic.png new file mode 100644 index 00000000..f9d64048 Binary files /dev/null and b/public/mic.png differ diff --git a/public/pitch.png b/public/pitch.png new file mode 100644 index 00000000..335e6b21 Binary files /dev/null and b/public/pitch.png differ diff --git a/public/portrait.png b/public/portrait.png new file mode 100644 index 00000000..f7105039 Binary files /dev/null and b/public/portrait.png differ diff --git a/public/post2.png b/public/post2.png new file mode 100644 index 00000000..fcd38515 Binary files /dev/null and b/public/post2.png differ diff --git a/public/react-icon.png b/public/react-icon.png new file mode 100644 index 00000000..5e819ddd Binary files /dev/null and b/public/react-icon.png differ diff --git a/public/recipe.jpg b/public/recipe.jpg new file mode 100644 index 00000000..b8a21190 Binary files /dev/null and b/public/recipe.jpg differ diff --git a/public/weather.png b/public/weather.png new file mode 100644 index 00000000..a5edc9d5 Binary files /dev/null and b/public/weather.png differ diff --git a/public/web-accessibility.png b/public/web-accessibility.png new file mode 100644 index 00000000..e5578ead Binary files /dev/null and b/public/web-accessibility.png differ diff --git a/pull_request_template.md b/pull_request_template.md index 4263c7e8..626e7140 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1,5 @@ -Please include a link to your Figma design and a Netlify link. \ No newline at end of file +Please include a link to your Figma design and a Netlify link. + +https://jeffies-portfolio.netlify.app/ + +https://www.figma.com/design/QV6stSlcpaPdDl2fojewHc/Figma-designs-for-students--Copy-?node-id=1791-1386&m=dev diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..d5d26906 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,43 @@ +// src/App.jsx +import { GlobalStyle } from "./components/GlobalStyles"; +import { Hero } from "./sections/Hero"; +import { Tech } from "./sections/Tech"; +import { Projects } from "./sections/Projects"; +import { Skills } from "./sections/Skills"; +import { Journey } from "./sections/Journey"; +import { Contact } from "./sections/Contact"; +import styled from "styled-components"; + +const SkipLink = styled.a` + position: absolute; + left: -999px; + top: 16px; + padding: 8px 16px; + background: #000; + color: #fff; + border-radius: 8px; + z-index: 1000; + + &:focus-visible { + left: 16px; + } +`; + 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..0a2bcde2 --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,29 @@ +// src/components/Button.jsx +import styled from "styled-components"; + +// Styled button component for links +const Button = styled.a` + display: flex; + width: 303px; + height: 48px; + gap: 16px; + padding: 0 16px; + align-items: center; + border-radius: 12px; + text-decoration: none; + font-weight: 500; + color: #fff; + background: #000; + font-size: 18px; + cursor: pointer; + transition: background 0.2s, transform 0.1s; + + &:hover { + background: #fff; + color: #000; + outline: 2px solid #000; + } + +`; + +export default Button; diff --git a/src/components/Card.jsx b/src/components/Card.jsx new file mode 100644 index 00000000..562d7fc8 --- /dev/null +++ b/src/components/Card.jsx @@ -0,0 +1,21 @@ +import styled from "styled-components"; + +// (HTML, CSS, API…) +export const TagsRow = styled.div` +display: flex; +align-items: flex-start; +gap: 4px; +align-self: stretch; +`; + +// single tag +export const Tag = styled.span` +display: flex; +width: 142px; +padding: 2px 6px; +justify-content: center; +align-items: flex-start; +border-radius: 4px; +border: 1px solid #000; +background: #FFF; +`; \ No newline at end of file diff --git a/src/components/GlobalStyles.jsx b/src/components/GlobalStyles.jsx new file mode 100644 index 00000000..f1c30d26 --- /dev/null +++ b/src/components/GlobalStyles.jsx @@ -0,0 +1,89 @@ +// src/styles/GlobalStyle.jsx +import { createGlobalStyle } from "styled-components"; + +export const GlobalStyle = createGlobalStyle` + *, *::before, *::after { + box-sizing: border-box; + } + + html, body { + margin: 0; + padding: 0; + } + + html { + scroll-behavior: smooth; + } + + body { + min-height: 100vh; + font-family: "Poppins", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background-color: #ffffff; + color: #000000; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; + + } + + #root { + min-height: 100vh; + } + + img { + max-width: 100%; + height: auto; + display: block; + } + + a { + color: inherit; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + a:focus-visible, + button:focus-visible { + outline: 3px solid #e6229bff; + outline-offset: 4px; + } + + ul, ol { + margin: 0; + padding: 0; + list-style: none; + } + + button { + font-family: inherit; + border: none; + background: none; + cursor: pointer; + } + + h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: 700; + letter-spacing: -0.03em; + } + + h1 { + font-size: clamp(3rem, 6vw, 4.5rem); + } + + h2 { + font-size: clamp(2rem, 4vw, 3rem); + } + + h3 { + font-size: clamp(1.3rem, 2.4vw, 1.8rem); + } + + p { + margin: 0; + } + +`; diff --git a/src/components/Icons.jsx b/src/components/Icons.jsx new file mode 100644 index 00000000..57a4ec03 --- /dev/null +++ b/src/components/Icons.jsx @@ -0,0 +1,127 @@ + + //Icon for Live demo +export const LiveIcon = (props) => ( + +); + +// Icon for View code +export const CodeIcon = (props) => ( + +); + +export const SeeMoreIcon = (props) => ( + + +); +// Icons for Social Media +export const InstagramIcon = (props) => ( + +); + +export const StackOverflowIcon = (props) => ( + +); + +export const LinkedInIcon = (props) => ( + +); + +export const GitHubIcon = (props) => ( + +); diff --git a/src/components/SeeMoreButton.jsx b/src/components/SeeMoreButton.jsx new file mode 100644 index 00000000..266024ca --- /dev/null +++ b/src/components/SeeMoreButton.jsx @@ -0,0 +1,38 @@ +// src/components/SeeMoreButton.jsx +import styled from "styled-components"; +import { SeeMoreIcon } from "./Icons"; + +// Wrapper +const SeeMoreButtonBase = styled.button` + display: flex; + width: 303px; + height: 48px; + gap: 16px; + padding: 0 16px; + align-items: center; + border-radius: 12px; + text-decoration: none; + font-size: 18px; + font-weight: 500; + color: #000; + background: #fff; + cursor: pointer; + outline: 2px solid #000; + transition: background 0.2s, transform 0.1s; + + &:hover { + background: #000; + color: #fff; + } +`; + + +export const SeeMoreButton = ({ label, ...props }) => { + return ( + + + {label} {/* Screen reader only text */} + + ); + +}; diff --git a/src/data/media.js b/src/data/media.js new file mode 100644 index 00000000..3e206742 --- /dev/null +++ b/src/data/media.js @@ -0,0 +1,6 @@ +// src/styles/media.js +export const media = { + mobile: "(max-width: 767px)", + tablet: "(max-width: 1024px)", + desktop: "(min-width: 1025px)", +}; diff --git a/src/data/posts.json b/src/data/posts.json new file mode 100644 index 00000000..4dd47afb --- /dev/null +++ b/src/data/posts.json @@ -0,0 +1,46 @@ +[ + { + "id": "post1", + "title": "Pitch myself as a developer", + "excerpt": "Thoughts on how to to present myself in the tech industry.", + "link": "https://www.linkedin.com/posts/jennifer-jansson_i-veckan-har-det-handlat-om-n%C3%A5got-som-l%C3%A4tt-activity-7395085710417973248-1x9O?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB3H4eMBXAYupsRoNRJuXMD_IcGjMjaH20k", + "badge": "November 14th", + "image": { + "src": "/pitch.png", + "alt": "Pitch block graphic" + } + }, + { + "id": "post2", + "title": "Styled components", + "excerpt": "Learning a new way of styling and the advantages of styled-components compared to regular CSS.", + "link": "https://dev.to/pancompany/styled-components-why-you-should-or-should-not-use-it-23c", + "badge": "November 24th", + "image": { + "src": "/post2.png", + "alt": "Picture of a mixed color background and styled components word" + } + }, + { + "id": "post3", + "title": "React - getting comfortable", + "excerpt": "React went from a human response to a practical tool.", + "link": "https://www.simplilearn.com/tutorials/reactjs-tutorial/what-is-reactjs", + "badge": "November 18th", + "image": { + "src": "/react-icon.png", + "alt": "Picture of the react icon" + } + }, + { + "id": "post4", + "title": "Accessibility Guidelines", + "excerpt": "Implemented ARIA roles and learned how small changes can make a big impact for users.", + "link": "https://www.w3.org/WAI/standards-guidelines/wcag/", + "badge": "November 6th", + "image": { + "src": "/web-accessibility.png", + "alt": "web accessibility graphic" + } + } +] \ No newline at end of file diff --git a/src/data/projects.json b/src/data/projects.json index 7c426028..1fb00fd3 100644 --- a/src/data/projects.json +++ b/src/data/projects.json @@ -1,28 +1,87 @@ -{ - "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", - "tags": [ - "HTML5", - "CSS3", - "JavaScript" - ], - "netlify": "link", - "github": "link" - }, - { - "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", - "tags": [ - "HTML5", - "CSS3", - "JavaScript", - "TypeScript", - "APIs" - ], - "netlify": "link", - "github": "link" +[ + { + "id": "business-site", + "title": "Business site", + "summary": "A fictional responsive website built with HTML, CSS & JavaScript. Includes hero video, card-based layout, signup form, and interactive hamburger menu. No backend connection — focus on responsive UI and clean code.", + "tags": [ + "HTML", + "CSS", + "JavaScript" + ], + "live": "https://frontendproject2025.netlify.app/", + "code": "https://github.com/JeffieJansson/frontend-project", + "image": { + "src": "/business.png", + "alt": "picture of record studio website" } - ] -} \ No newline at end of file + }, + { + "id": "recipe", + "title": "Recipe library", + "summary": "An interactive recipe web app with possibility to search, filter and sort recipes by cuisine, diet, time and popularity using Spoonacular API. Data normalized and cached in localStorage for better performance and offline access", + "tags": [ + "HTML", + "CSS", + "JavaScript", + "Spoonacular API" + ], + "live": "https://library-recipe.netlify.app/", + "code": "https://github.com/JeffieJansson/recipe-library", + "image": { + "src": "/recipe.jpg", + "alt": "pictures of food recipes and filtering options" + } + }, + { + "id": "weather", + "title": "Weather app – SMHI API", + "summary": "A responsive weather app displaying current weather conditions and 5-day forecasts for multiple Swedish cities using the SMHI API. Built collaboratively using Git and TypeScript for safer data handling and cleaner code.", + "tags": [ + "HTML", + "CSS", + "TypeScript", + "SMHI API" + ], + "live": "https://project-weather-app-b2.netlify.app/", + "code": "https://github.com/marinalendt-png/js-project-weather-app", + "image": { + "src": "/weather.png", + "alt": "picture of weather app" + } + }, + { + "id": "accessibility", + "title": "Build a Quiz with Web Accessibility in mind", + "summary": "A responsive Quiz build with web accessibility in mind. The quiz tests users' knowledge of the team that built the website through multiple-choice questions. It provides instant feedback on answers and a final score at the end. As a second page reveals information about the team and answers to the quiz questions.", + "tags": [ + "HTML", + "CSS", + "JavaScript" + ], + "live": "https://accessibilitymixedgroup.netlify.app/", + "code": "https://github.com/irisdgz/js-project-accessibility", + "image": { + "src": "/accessibility.png", + "alt": "Picture of web accessibility quiz results" + } + }, + { + "id": "happy thoughts", + "title": "Happy Thoughts - Real-Time Validation & API Integration", + "summary": " is a Twitter-inspired React application where users share positive messages (5-140 characters) and like others' thoughts. The app features real-time character validation with visual feedback, spam-prevention on likes, and loading states for seamless UX. Built with React hooks (useState, useEffect), it demonstrates clean component architecture with separation of concerns: a centralized API layer, controlled form components with comprehensive validation, and optimistic UI updates. The fully responsive design (320px-1600px+) includes accessibility features (ARIA labels, live regions) and follows DRY principles with well-documented, error-free code.", + "tags": [ + "React", + "Javascript", + "CSS", + "API", + "Vite", + "react-timeago" + ], + "live": "https://jennifer-happy-thoughts.netlify.app/", + "code": "https://github.com/JeffieJansson/happy-thoughts", + "image": { + "src": "/happy.png", + "alt": "Picture of happy thoughts app" + } + } +] \ No newline at end of file diff --git a/src/data/skills.json b/src/data/skills.json new file mode 100644 index 00000000..802df95d --- /dev/null +++ b/src/data/skills.json @@ -0,0 +1,40 @@ +{ + "code": [ + "HTML", + "CSS", + "JavaScript", + "GitHub", + "API Integration", + "DOM Manipulation", + "localStorage", + "Responsive Design", + "Accessibility" + ], + "toolbox": [ + "VS Code", + "R-Studio", + "Postman", + "GitHub", + "Figma", + "BigQuery", + "Looker Studio", + "GTM", + "GTA", + "GSC", + "Figma" + ], + "upcoming": [ + "Node.js", + "MongoDB", + "TypeScript", + "Styled Components" + ], + "more": [ + "SEO Optimization", + "Data Visualization", + "Digital Marketing", + "Project Planning", + "UX Writing", + "Agile Methodology" + ] +} diff --git a/src/data/tech.json b/src/data/tech.json new file mode 100644 index 00000000..4b9de408 --- /dev/null +++ b/src/data/tech.json @@ -0,0 +1,19 @@ +{ + "tech": [ + "HTML", + "CSS", + "TypeScript", + "Web Accessibility", + "Flexbox", + "Javascript", + "ES6", + "JSX", + "React", + "React Hooks", + "Node.js", + "MongoDB", + "Mob-programming", + "Pair-programming", + "GitHub" + ] +} \ 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..15f32903 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' createRoot(document.getElementById('root')).render( diff --git a/src/sections/Contact.jsx b/src/sections/Contact.jsx new file mode 100644 index 00000000..07207967 --- /dev/null +++ b/src/sections/Contact.jsx @@ -0,0 +1,165 @@ +// src/sections/Contact.jsx +import styled from "styled-components"; +import { media } from "../data/media.js"; +import { + GitHubIcon, + InstagramIcon, + LinkedInIcon, + StackOverflowIcon, +} from "../components/Icons.jsx"; +import footerImage from "/footer.png"; + +// ---- STYLES ---- +const ContactSection = styled.section` + background: #000; + display: flex; + padding: 128px 0; + flex-direction: column; + align-items: center; + gap: 16px; + align-self: stretch; + width: 100%; + + @media ${media.tablet} { + padding: 96px 16px; + } + + @media ${media.mobile} { + padding: 64px 16px; + } +`; + +const ContactTitle = styled.h2` + color: #fff; + text-align: center; + font-size: 80px; + font-weight: 700; + line-height: normal; + + @media ${media.tablet} { + font-size: 56px; + } + + @media ${media.mobile} { + font-size: 40px; + } +`; + +const Avatar = styled.img.attrs({ loading: "lazy" })` + width: 164px; + height: 164px; + border-radius: 50%; + object-fit: cover; + display: block; + margin: 0 auto; + + @media ${media.mobile} { + width: 128px; + height: 128px; + } +`; + +const Info = styled.div` + text-align: center; + margin-top: 16px; + + p { + color: #fff; + font-size: 30px; + margin: 8px 0; + } + + a { + text-decoration: none; + color: #fff; + } + + @media ${media.tablet} { + p { + font-size: 24px; + } + } + + @media ${media.mobile} { + p { + font-size: 20px; + } + } +`; + +const SocialRow = styled.div` + display: flex; + align-items: flex-end; + gap: 32px; + margin-top: 24px; + + a { + color: inherit; + text-decoration: none; + } + + + @media ${media.mobile} { + gap: 16px; + flex-wrap: wrap; + justify-content: center; + } +`; + +// ---- COMPONENT ---- +export const Contact = () => ( + + Let’s Talk + + + +

Jennifer Jansson

+

+ +46 76 314 12 62 +

+

+ + jenniferjansson92@gmail.com + +

+
+ + + + + + + + + + + + + + + + + + +
+); diff --git a/src/sections/Hero.jsx b/src/sections/Hero.jsx new file mode 100644 index 00000000..1f1e9ebd --- /dev/null +++ b/src/sections/Hero.jsx @@ -0,0 +1,180 @@ +// src/sections/Hero.jsx +import styled from "styled-components"; +import PortraitImg from "/portrait.png"; +import Img2 from "/mic.png"; +import Img3 from "/arts.png"; +import { media } from "../data/media.js"; + +// ---- STYLES ---- +const IntroWrapper = styled.header` + display: flex; + padding: 128px 24px 64px 24px; + flex-direction: column; + align-items: center; + gap: 16px; + align-self: stretch; + + @media ${media.tablet} { + padding: 96px 16px 64px 16px; + } + + @media ${media.mobile} { + padding: 64px 16px 48px 16px; + gap: 24px; + } +`; + +const IntroText = styled.section` + max-width: 720px; + text-align: center; + + h3 { + color: #000; + font-size: 30px; + font-weight: 500; + line-height: normal; + margin: 0 0 8px; + } + + h1 { + color: #000; + font-size: 100px; + font-weight: 700; + line-height: normal; + margin: 0 0 24px; + } + + p { + color: #202020; + font-size: 18px; + font-weight: 400; + line-height: 32px; + margin: 0; + } + + @media ${media.tablet} { + h1 { + font-size: 56px; + } + + h1 br { + display: none; + } + + p { + font-size: 16px; + line-height: 28px; + } + } + + @media ${media.mobile} { + h3 { + font-size: 20px; + } + + h1 { + font-size: 40px; + } + + p { + font-size: 16px; + line-height: 26px; + } + } +`; + +const ImageBox = styled.div` + position: relative; + margin: 24px auto 32px; + width: 720px; + height: 400px; + + @media ${media.tablet} { + width: 520px; + height: 320px; + } + + @media ${media.mobile} { + width: 280px; + height: 230px; + } + + img { + position: absolute; + max-width: none; + border-radius: 16px; + object-fit: cover; + box-shadow: 0 18px 45px rgba(0, 0, 0, 0.25); + } + + img { + width: 340px; + height: 360px; + } + + @media ${media.tablet} { + img { + width: 240px; + height: 260px; + } + } + + @media ${media.mobile} { + img { + width: 170px; + height: 190px; + } + } + + /* left img */ + img:nth-child(1) { + left: 0; + top: 40px; + transform: rotate(-9deg); + z-index: 1; + } + + /* middle img */ + img:nth-child(2) { + left: 50%; + top: 18px; + transform: translateX(-50%); + z-index: 3; + } + + /* right img*/ + img:nth-child(3) { + right: 0; + top: 40px; + transform: rotate(9deg); + z-index: 2; + } +`; + +// ---- COMPONENT ---- +export const Hero = () => { + return ( + + +

Hi there, I'm

+

+ Jennifer
Jansson +

+ + Picture of woman doing martial arts + Portrait of Jennifer + Picture of microphone + +

Frontend Developer & Digital Analytics Specialst

+

+ I love bridging the gap between design, data, and technology. I have + enjoyed understanding user behavior and uncovering insights, but now I + have realized that I don't just want to analyze digital experiences — I + want to build them. Today, that passion drives me to create intuitive, + accessible and meaningful digital products that deliver real value for + real users. +

+
+
+ ); +}; diff --git a/src/sections/Journey.jsx b/src/sections/Journey.jsx new file mode 100644 index 00000000..b970b63a --- /dev/null +++ b/src/sections/Journey.jsx @@ -0,0 +1,175 @@ +// src/sections/Journey.jsx + +import { useState } from "react"; +import styled from "styled-components"; +import posts from "../data/posts.json"; +import { media } from "../data/media.js"; +import Button from "../components/Button"; +import { Tag } from "../components/Card"; +import { LiveIcon } from "../components/Icons"; +import { SeeMoreButton } from "../components/SeeMoreButton"; + +// ---- STYLES ---- +const JourneySection = styled.section` + background: #ffffff; + display: flex; + flex-direction: column; + align-items: center; + padding: 128px 0; + gap: 128px; +`; + +const JourneyTitle = styled.h2` + font-size: 80px; + font-weight: 500; + text-align: center; + + @media ${media.tablet} { + font-size: 56px; + } + + @media ${media.mobile} { + font-size: 40px; + } +`; + +const PostsList = styled.div` + display: flex; + flex-direction: column; + gap: 64px; + + + @media ${media.tablet} { + gap: 48px; + padding: 0 16px; + } + + @media ${media.mobile} { + gap: 40px; + padding: 0 16px; + } +`; + + + +const PostRow = styled.article` + display: flex; + align-items: center; + gap: 125px; + align-self: stretch; + + @media ${media.tablet} { + gap: 32px; + } + + @media ${media.mobile} { + flex-direction: column; + align-items: center; + gap: 24px; + } +`; + +const Thumb = styled.img.attrs({ loading: "lazy" })` + flex-shrink: 0; + border-radius: 8px; + object-fit: cover; + width: 480px; + height: 300px; + + @media ${media.tablet} { + width: 400px; + height: 250px; + } + + @media ${media.mobile} { + width: 100%; + max-width: 480px; + height: auto; + aspect-ratio: 16 / 10; + } +`; + + +const PostContent = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + max-width: 560px; + + @media ${media.tablet} { + max-width: unset; + } + + @media ${media.mobile} { + max-width: 100%; + } +`; + +const PostTitle = styled.h3` + font-size: 30px; + font-weight: 500; + + @media ${media.mobile} { + font-size: 24px; + } +`; + + +const PostExcerpt = styled.p` + color: #202020; + font-size: 18px; + font-weight: 400; + line-height: 32px; + + @media ${media.mobile} { + font-size: 16px; + line-height: 28px; + } +`; + +const ButtonRow = styled.div` + margin-top: 8px; +`; + +// ---- COMPONENT ---- +export const Journey = () => { + const [showAll, setShowAll] = useState(false); + const visiblePosts = showAll ? posts : posts.slice(0, 3); + const hasMorePosts = posts.length > 3; + return ( + + My Words + + {visiblePosts.map((post) => ( + + {post.image && } + + {post.badge && {post.badge}} + {post.title} + {post.excerpt} + {post.link && ( + + + + )} + + + ))} + + {hasMorePosts && ( + setShowAll((prev) => !prev)} + label={showAll ? "Show fewer articles" : "See more articles"} + /> + )} + + ); +}; \ No newline at end of file diff --git a/src/sections/Projects.jsx b/src/sections/Projects.jsx new file mode 100644 index 00000000..d11e8950 --- /dev/null +++ b/src/sections/Projects.jsx @@ -0,0 +1,235 @@ +// src/sections/Projects.jsx +import { useState } from "react"; +import styled from "styled-components"; +import { media } from "../data/media.js"; +import projects from "../data/projects.json"; +import Button from "../components/Button"; +import { TagsRow, Tag } from "../components/Card"; +import { LiveIcon, CodeIcon } from "../components/Icons"; +import { SeeMoreButton } from "../components/SeeMoreButton"; + +// ---- STYLES ---- +const ProjectsSection = styled.section` + background: #ffffff; + display: flex; + justify-content: center; + padding: 128px 0; + + @media ${media.tablet} { + padding: 96px 16px; + } + + @media ${media.mobile} { + padding: 64px 16px; + } +`; + +const ProjectsInner = styled.div` + width: 1184px; + max-width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 128px; +`; + +const ProjectsTitle = styled.h2` + font-size: 80px; + font-weight: 700; + text-align: center; + + @media ${media.tablet} { + font-size: 56px; + } + + @media ${media.mobile} { + font-size: 40px; + } +`; + +const ProjectsList = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 125px; + + @media ${media.tablet} { + gap: 80px; + } + + @media ${media.mobile} { + gap: 64px; + } +`; + +const ProjectRow = styled.article` + display: flex; + align-items: center; + gap: 64px; + align-self: stretch; + flex-direction: ${({ $reverse }) => ($reverse ? "row-reverse" : "row")}; + + @media ${media.tablet} { + flex-direction: column; + align-items: flex-start; + gap: 32px; + } + + @media ${media.mobile} { + align-items: center; + } +`; + +const ProjectThumb = styled.img.attrs({ loading: "lazy" })` + flex-shrink: 0; + border-radius: 12px; + object-fit: cover; + width: 479px; + height: 479px; + + @media ${media.tablet} { + width: 696px; + height: 479px; + margin-left: auto; + margin-right: auto; + } + + @media ${media.mobile} { + width: 100%; + max-width: 343px; + aspect-ratio: 343 / 300; + height: auto; + margin-left: auto; + margin-right: auto; + } +`; + + +const ProjectContent = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + max-width: 560px; + + @media ${media.tablet} { + max-width: 100%; + } +`; + +const ProjectTitle = styled.h3` + font-size: 30px; + font-weight: 600; + + @media ${media.mobile} { + font-size: 24px; + } +`; + +const ProjectSummary = styled.p` + font-size: 18px; + line-height: 1.6; + + @media ${media.mobile} { + font-size: 16px; + line-height: 1.5; + } +`; + +const ButtonsRow = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + align-self: stretch; + + @media ${media.mobile} { + align-items: stretch; + + a { + width: 100%; + display: flex; + justify-content: center; + } + } +`; + +// ---- COMPONENT ---- +export const Projects = () => { + const [showAll, setShowAll] = useState(false); + + const visibleProjects = showAll ? projects : projects.slice(0, 3); + const hasMoreProjects = projects.length > 3; + + return ( + + + Featured Projects + + + {visibleProjects.map((project, index) => ( + + {project.image && ( + + )} + + + {Array.isArray(project.tags) && project.tags.length > 0 && ( + + {project.tags.map((tag) => ( + {tag} + ))} + + )} + + {project.title} + + {project.summary && ( + {project.summary} + )} + + + {project.live && ( + + )} + + {project.code && ( + + )} + + + + ))} + + + {hasMoreProjects && ( + setShowAll((prev) => !prev)} + label={showAll ? "Show fewer projects" : "See more projects"} + /> + )} + + + ); +}; diff --git a/src/sections/Skills.jsx b/src/sections/Skills.jsx new file mode 100644 index 00000000..dae5d9a5 --- /dev/null +++ b/src/sections/Skills.jsx @@ -0,0 +1,176 @@ +// src/sections/Skills.jsx +import styled from "styled-components"; +import skillsData from "../data/skills.json"; +import { media } from "../data/media.js"; + +// ---- STYLES ---- +const SkillsSection = styled.section` + background: #000; + display: flex; + padding: 128px 0; + flex-direction: column; + align-items: center; + gap: 32px; + align-self: stretch; + + @media ${media.tablet} { + padding: 96px 16px; + } + + @media ${media.mobile} { + padding: 64px 16px; + } +`; + +const SkillsTitle = styled.h2` + color: #fff; + text-align: center; + font-size: 80px; + font-weight: 700; + line-height: normal; + + @media ${media.tablet} { + font-size: 56px; + } + + @media ${media.mobile} { + font-size: 40px; + } +`; + +const Columns = styled.div` + display: flex; + justify-content: center; + align-items: flex-start; + gap: 24px; + width: 100%; + max-width: 982px; + + @media ${media.tablet} { + flex-direction: column; + align-items: center; + gap: 32px; + } + + @media ${media.mobile} { + align-items: flex-start; + } +`; + +const Column = styled.div` + display: flex; + flex-direction: column; + color: #fff; + font-size: 18px; + font-weight: 400; + line-height: 32px; + + @media ${media.mobile} { + align-items: flex-start; + text-align: left; + } +`; + +const Tag = styled.div` + display: flex; + height: 28px; + padding: 2px 6px; + justify-content: center; + align-items: center; + align-self: stretch; + width: 177px; + + border-radius: 4px; + border: 1px solid #fff; + background: #000; + + @media ${media.mobile} { + width: 100%; + max-width: 220px; + } +`; + +const TagTitle = styled.span` + color: #fff; + font-size: 16px; + font-weight: 500; + line-height: normal; +`; + +const List = styled.ul` + display: flex; + flex-direction: column; + list-style: none; + padding: 0; + margin: 16px 0 0; +`; + +const Item = styled.li` + color: #fff; + font-size: 18px; + font-weight: 400; + line-height: 32px; + + @media ${media.tablet} { + text-align: center; + } + @media ${media.mobile} { + text-align: left; + } +`; + +// ---- COMPONENT ---- +export const Skills = () => { + const { code, toolbox, upcoming, more } = skillsData; + return ( + + Skills + + + + + Code + + + {code.map((item, index) => ( + {item} + ))} + + + + + + Toolbox + + + {toolbox.map((item, index) => ( + {item} + ))} + + + + + + Upcoming + + + {upcoming.map((item, index) => ( + {item} + ))} + + + + + + More + + + {more.map((item, index) => ( + {item} + ))} + + + + + ); +}; diff --git a/src/sections/Tech.jsx b/src/sections/Tech.jsx new file mode 100644 index 00000000..60c41986 --- /dev/null +++ b/src/sections/Tech.jsx @@ -0,0 +1,81 @@ +// src/sections/Tech.jsx (din nuvarande Journey.jsx med Tech-export) +import styled from "styled-components"; +import techData from "../data/tech.json"; +import { media } from "../data/media.js"; + +// ---- STYLES ---- +const Wrap = styled.section` + display: flex; + flex-direction: column; + align-items: center; + padding: 128px 0; + gap: 16px; + background: #000; + color: #fff; + text-align: center; + + @media ${media.tablet} { + padding: 96px 16px; + } + + @media ${media.mobile} { + padding: 64px 16px; + } +`; + +const Inner = styled.div` + max-width: 720px; + padding: 0 24px; + + h2 { + margin: 0 0 16px; + font-size: 80px; + font-weight: 700; + } + + p { + margin: 0; + font-size: 30px; + line-height: normal; + font-weight: 500; + } + + @media ${media.tablet} { + padding: 0 16px; + + h2 { + font-size: 60px; + } + + p { + font-size: 16px; + font-weight: 500; + line-height: normal; + } + } + + @media ${media.mobile} { + h2 { + font-size: 40px; + } + + p { + font-size: 16px; + font-weight: 500; + line-height: normal; + } + } +`; + +// ---- COMPONENT ---- +export const Tech = () => { + const techLine = techData.tech.join(", "); + return ( + + +

Tech

+

{techLine}

+
+
+ ); +}; diff --git a/vite.config.js b/vite.config.js index 8b0f57b9..315dcdd9 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,13 @@ +// vite.config.js 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