diff --git a/README.md b/README.md index 200f4282..a662d142 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Portfolio +My custom link: https://leonekelund.dev/ diff --git a/index.html b/index.html index 6676fb2d..ba79cbd3 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,33 @@ - - - - - Portfolio - - -
- - - + + + + + + + + + + + + + + + + + + Portfolio + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index 48911600..cccf9d04 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ }, "dependencies": { "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-icons": "^5.5.0", + "styled-components": "^6.1.19" }, "devDependencies": { "@eslint/js": "^9.21.0", diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..38ddaf7a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,31 @@ +import HeaderSection from './sections/HeaderSection'; +import TechSection from './sections/TechSection'; +import ProjectsSection from './sections/ProjectsSection'; +// import ThoughtsSection from './sections/ThoughtsSection'; +import SkillsSection from './sections/SkillsSection'; +import ContactSection from './sections/ContactSection'; + +import GlobalStyle from './styles/GlobalStyle'; + 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/HeaderImage2.jpg b/src/assets/HeaderImage2.jpg new file mode 100644 index 00000000..350c27cd Binary files /dev/null and b/src/assets/HeaderImage2.jpg differ diff --git a/src/assets/HeaderImage3.jpg b/src/assets/HeaderImage3.jpg new file mode 100644 index 00000000..e196ebba Binary files /dev/null and b/src/assets/HeaderImage3.jpg differ diff --git a/src/assets/favicon.svg b/src/assets/favicon.svg new file mode 100644 index 00000000..d68c3cc8 --- /dev/null +++ b/src/assets/favicon.svg @@ -0,0 +1,21 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/src/assets/leonimage.jpg b/src/assets/leonimage.jpg new file mode 100644 index 00000000..66eb9f38 Binary files /dev/null and b/src/assets/leonimage.jpg differ diff --git a/src/assets/project1.jpg b/src/assets/project1.jpg new file mode 100644 index 00000000..69e5ee56 Binary files /dev/null and b/src/assets/project1.jpg differ diff --git a/src/assets/project2.jpg b/src/assets/project2.jpg new file mode 100644 index 00000000..cfd4f287 Binary files /dev/null and b/src/assets/project2.jpg differ diff --git a/src/assets/project3.jpg b/src/assets/project3.jpg new file mode 100644 index 00000000..43aa633c Binary files /dev/null and b/src/assets/project3.jpg differ diff --git a/src/components/ProjectButton.jsx b/src/components/ProjectButton.jsx new file mode 100644 index 00000000..95ebde01 --- /dev/null +++ b/src/components/ProjectButton.jsx @@ -0,0 +1,54 @@ +import styled from "styled-components"; + +const ProjectButton = ({ icon, text, link }) => { + return ( + + ); +}; + +export default ProjectButton; + +/* STYLING */ + +const Button = styled.a` + display: flex; + align-items: center; + gap: 16px; + + width: 250px; + height: 48px; + padding: 0 16px; + box-sizing: border-box; + + background: #000000; + color: #ffffff; + border-radius: 12px; + + text-decoration: none; + font-size: 0.875rem; /* 14px */ + font-weight: 500; + + transition: opacity 0.2s ease; + &:hover { + opacity: 0.8; + } +`; + +const IconWrapper = styled.span` + display: flex; + align-items: center; + justify-content: center; + + width: 24px; + height: 24px; + flex-shrink: 0; + + svg { + width: 20px; + height: 20px; + color: #ffffff; + } +`; diff --git a/src/components/ProjectCard.jsx b/src/components/ProjectCard.jsx new file mode 100644 index 00000000..157db2a1 --- /dev/null +++ b/src/components/ProjectCard.jsx @@ -0,0 +1,154 @@ + +import styled from "styled-components"; +import ProjectButton from "./ProjectButton"; +import { FaGlobe, FaGithub } from "react-icons/fa"; + +const ProjectCard = ({ + image, + tech, + title, + description, + liveLink, + codeLink, + reverse +}) => { + return ( + + + {title} + + + + + {tech.map((item) => ( + {item} + ))} + + +

{title}

+

{description}

+ + + } + text="Live demo" + link={liveLink} + /> + } + text="View Code" + link={codeLink} + /> + +
+
+ ); +}; + + +export default ProjectCard; + +/* STYLING */ + +const Card = styled.div` + display: flex; + align-items: center; + gap: 4.5rem; + margin: 4rem auto; + width: 100%; + max-width: 1000px; + + flex-direction: ${({ reverse }) => (reverse ? "row-reverse" : "row")}; + + /* IPAD */ + @media (max-width: 1024px) { + flex-direction: column; + align-items: flex-start; + gap: 1.75rem; + margin: 3rem auto; + padding: 0 1.5rem; + } + + /* MOBILE */ + @media (max-width: 600px) { + gap: 1.5rem; + margin: 2.5rem auto; + padding: 0 1.25rem; + } +`; + +const ImageWrapper = styled.div` + /* DESKTOP */ + flex: 0 0 479px; + height: 479px; + border-radius: 16px; + background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + + img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 16px; + display: block; + } + + /* IPAD: + @media (max-width: 1024px) { + flex: 0 0 auto; + width: 100%; + max-width: none; + height: auto; + + img { + width: 100%; + height: auto; + } + } + + /* MOBILE: + @media (max-width: 600px) { + width: 100%; + } +`; + +const Content = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 0.75rem; + text-align: left; + + /* IPAD: + @media (max-width: 1024px) { + width: 100%; + max-width: 650px; + } + /* MOBILE: + @media (max-width: 600px) { + max-width: none; + } +`; + +const TechList = styled.div` + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + + span { + border: 1px solid #000; + padding: 0.2rem 0.6rem; + font-size: 0.75rem; + border-radius: 4px; + } +`; + +const Buttons = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.75rem; +`; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 61010be6..e69de29b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,4 +0,0 @@ -body { - background: pink; - color: hotpink; -} \ No newline at end of file diff --git a/src/sections/ContactSection.jsx b/src/sections/ContactSection.jsx new file mode 100644 index 00000000..39be485f --- /dev/null +++ b/src/sections/ContactSection.jsx @@ -0,0 +1,88 @@ +import styled from "styled-components"; +import leonImage from "../assets/leonimage.jpg"; + +const ContactSection = () => { + return ( + + + Let’s Talk + + + + Leon Ekelund + 0709752924 + leongudmundssonekelund@gmail.com + + + + + + + + + + + ); +}; + +export default ContactSection; + +// STYLING + +const ContactWrapper = styled.section` + width: 100%; + background: #fff; + color: #000; + padding: 6rem 1.5rem; + display: flex; + justify-content: center; +`; + +const Content = styled.div` + text-align: center; +`; + +const Title = styled.h2` + font-size: clamp(2.4rem, 4vw, 3.2rem); + font-weight: 800; + margin-bottom: 2.5rem; +`; + +const Avatar = styled.img` + width: 90px; + height: 90px; + border-radius: 50%; + object-fit: cover; + margin: 0 auto; + display: block; + margin-bottom: 10px; +`; + + +const Name = styled.p` + font-weight: 700; + margin: 0; +`; + +const Info = styled.p` + margin: 0.3rem 0; + font-size: 0.95rem; +`; + +const Icons = styled.div` + display: flex; + justify-content: center; + gap: 1.2rem; + margin-top: 1.5rem; + + a { + text-decoration: none; + color: #000; + font-weight: 600; + font-size: 0.9rem; + } + + a:hover { + text-decoration: underline; + } +`; diff --git a/src/sections/HeaderSection.jsx b/src/sections/HeaderSection.jsx new file mode 100644 index 00000000..cefc68be --- /dev/null +++ b/src/sections/HeaderSection.jsx @@ -0,0 +1,149 @@ +import styled from "styled-components"; +import leonImage from "../assets/leonimage.jpg"; +import headerimage2 from "../assets/HeaderImage2.jpg"; +import headerimage3 from "../assets/HeaderImage3.jpg"; + +const HeaderSection = () => { + return ( + + + + + Hi there, I'm <Name>Leon Ekelund</Name> + + + Portrait of Leon Ekelund + Abstract background 1 + Abstract background 2 + + + + Creative Frontend Developer with a Background in EV charging and Music + + + + + + ); +}; + +export default HeaderSection; + +// STYLING + +const HeaderWrapper = styled.section` + background: #fff; + width: 100%; + padding-bottom: 5rem +`; + +const HeaderContent = styled.div` + max-width: 900px; + margin: 0 auto; + padding: 4rem 1.5rem 3rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 2rem; +`; + +const ImageWrapper = styled.div` + /* Desktop / default */ + width: 100%; + max-width: 358px; + aspect-ratio: 358 / 382; + position: relative; + margin: 0 auto; + margin-top: 100px; + + img { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: auto; + border-radius: 24px; + object-fit: cover; + transform: translate(-50%, -50%); + } + + /* center portrait (1st img = leonImage) */ + img:nth-child(1) { + z-index: 3; + transform: translate(-50%, -55%); + } + + /* left background card */ + img:nth-child(2) { + z-index: 1; + transform: translate(-68%, -50%) rotate(-8deg); + } + + /* right background card */ + img:nth-child(3) { + z-index: 2; + transform: translate(-32%, -50%) rotate(8deg); + } + + /* Regular small phones */ + @media (max-width: 480px) { + max-width: 280px; + + /* make the cards a bit smaller so they don't stick out */ + img { + width: 92%; + } + + img:nth-child(2) { + transform: translate(-62%, -50%) rotate(-6deg); + } + + img:nth-child(3) { + transform: translate(-38%, -50%) rotate(6deg); + } + } + + /* Extra tiny screens (iPhone 5, etc.) */ + @media (max-width: 360px) { + max-width: 230px; + + img { + width: 88%; + } + + img:nth-child(2) { + transform: translate(-60%, -50%) rotate(-4deg); + } + + img:nth-child(3) { + transform: translate(-40%, -50%) rotate(4deg); + } + } +`; + +const TextGroup = styled.div` + display: flex; + flex-direction: column; + gap: 0.75rem; +`; + +const Title = styled.h1` + font-size: 1.2rem; + font-weight: 400; + margin: 0; +`; + +const Name = styled.span` + display: block; + font-size: 3.2rem; + font-weight: 800; + margin-top: 0.5rem; +`; + +const Subtitle = styled.p` + font-size: 1rem; + margin-top: 2rem; + opacity: 0.8; + margin-top: 60px; +`; diff --git a/src/sections/ProjectsSection.jsx b/src/sections/ProjectsSection.jsx new file mode 100644 index 00000000..7102b6ed --- /dev/null +++ b/src/sections/ProjectsSection.jsx @@ -0,0 +1,68 @@ +import styled from "styled-components"; +import ProjectCard from "../components/ProjectCard"; +import projectImg1 from "../assets/project1.jpg"; +import projectImg2 from "../assets/project2.jpg"; +import projectImg3 from "../assets/project3.jpg"; + +const projects = [ + { + image: projectImg1, + tech: ["HTML5", "CSS3", "React", "Tailwind"], + title: "Pomodoro timer", + description: "A pomodoro timer with a clean UI.", + liveLink: "https://fatalistimer.netlify.app/", + codeLink: "https://github.com/LeonEkelund/fatalis-timer" + }, + { + image: projectImg2, + tech: ["HTML5", "CSS3", "Typescript", "Tailwind"], + title: "Quiz bonanza", + description: + "An accessible quiz that works fully without your mouse, using API.", + liveLink: "https://quizbonanza.netlify.app/", + codeLink: "https://github.com/gabriellaberko/js-project-accessibility" + }, + { + image: projectImg3, + tech: ["React", "Tailwind", "Lenis", "Framer Motion"], + title: "Band page", + description: + "A modern bandpage with animations and smoothscroll.", + liveLink: "https://band-page.netlify.app/", + codeLink: "https://github.com/LeonEkelund/band-page" + } +]; + +const ProjectsSection = () => { + return ( + +

Featured Projects

+ + {projects.map((project, index) => ( + + ))} +
+ ); +}; + +export default ProjectsSection; + +// STYLING + +const Wrapper = styled.section` + padding: 6rem 1.5rem; + max-width: 90%; + margin: 0 auto; + display: flex; +flex-direction: column; + + + h2 { + text-align: center; + font-size: 2.5rem; + } +`; diff --git a/src/sections/SkillsSection.jsx b/src/sections/SkillsSection.jsx new file mode 100644 index 00000000..4a0a9c22 --- /dev/null +++ b/src/sections/SkillsSection.jsx @@ -0,0 +1,161 @@ +import styled from "styled-components"; + +const skills = [ + { + title: "Code", + items: [ + "HTML5", + "CSS3", + "Javascript ES6", + "React", + "Typescript", + "Tailwind", + "GitHub", + "Electron", + ], + }, + { + title: "Toolbox", + items: [ + "Every popular DAW", + "Jira", + "Adobe Suite", + "Figma", + "Blender", + ], + }, + { + title: "Upcoming", + items: ["Node.js"], + }, + { + title: "More", + items: [], + }, +]; + +const SkillsSection = () => { + return ( + + + Skills + + + {skills.map((group) => ( + + {group.title} + + {group.items.map((item) => ( +
  • {item}
  • + ))} +
    +
    + ))} +
    +
    +
    + ); +}; + +export default SkillsSection; + +// STYLING + +const SkillsWrapper = styled.section` + width: 100%; + background: #000; + color: #fff; + padding: 6rem 1.5rem; + display: flex; + justify-content: center; +`; + +const SkillsInner = styled.div` + width: 100%; + max-width: 900px; + + @media (min-width: 768px) and (max-width: 1024px) { + max-width: 700px; + } + + @media (max-width: 767px) { + max-width: 100%; + } +`; + +const Title = styled.h2` + text-align: center; + font-size: clamp(2.4rem, 4vw, 3.2rem); + font-weight: 800; + margin: 0 0 3rem 0; +`; + +const Columns = styled.div` + display: flex; + gap: 3rem; + flex-wrap: wrap; + justify-content: flex-start; + + /* IPAD */ + @media (min-width: 768px) and (max-width: 1024px) { + flex-direction: column; + align-items: center; + justify-content: flex-start; + text-align: center; + gap: 2rem; + } + + /* MOBILE */ + @media (max-width: 767px) { + flex-direction: column; + align-items: flex-start; + text-align: left; + gap: 1.5rem; + } +`; + +const Column = styled.div` + min-width: 140px; + + /* IPAD */ + @media (min-width: 768px) and (max-width: 1024px) { + width: 260px; + } + + /* MOBILE */ + @media (max-width: 767px) { + width: 100%; + } +`; + + + +const CategoryLabel = styled.div` + display: flex; + padding: 2px 16px; + justify-content: center; + align-items: center; + + border-radius: 4px; + border: 1px solid #fff; + background: transparent; + font-size: 0.85rem; + margin-bottom: 0.75rem; + + /* MOBILE */ + @media (max-width: 767px) { + width: max-content; + min-width: 120px; + } +`; + +const List = styled.ul` + list-style: none; + padding: 0; + margin: 0; + + li { + font-size: 0.9rem; + line-height: 1.6; + } +`; diff --git a/src/sections/TechSection.jsx b/src/sections/TechSection.jsx new file mode 100644 index 00000000..ac06d1be --- /dev/null +++ b/src/sections/TechSection.jsx @@ -0,0 +1,47 @@ + +import styled from 'styled-components'; + +const TechSection = () => { + return ( + + + Tech + + HTML, CSS, Flexbox, JavaScript, ES6, JSX, React, React Hooks, Node.js, + Mongo DB, Web Accessibility, APIs, mob-programming, pair-programming, + GitHub. + + + + ); +}; + +export default TechSection; + +// STYLING + +const TechWrapper = styled.section` + width: 100%; + background: #000000; + color: #fff; + display: flex; + justify-content: center; + padding: 6rem 1.5rem; +`; + +const TechInner = styled.div` + max-width: 100%; + text-align: center; +`; + +const TechTitle = styled.h2` + font-size: clamp(2rem, 3vw, 3rem); + font-weight: 800; + margin: 0 0 1.5rem 0; +`; + +const TechText = styled.p` + font-size: 0.95rem; + line-height: 1.6; + margin: 0; +`; diff --git a/src/styles/GlobalStyle.js b/src/styles/GlobalStyle.js new file mode 100644 index 00000000..e1356bc2 --- /dev/null +++ b/src/styles/GlobalStyle.js @@ -0,0 +1,35 @@ +import { createGlobalStyle } from 'styled-components'; + +const GlobalStyle = createGlobalStyle` + *, *::before, *::after { + box-sizing: border-box; + } + + body { + margin: 0; + padding: 0; + font-family: 'Poppins', system-ui, -apple-system, sans-serif; + background: #f5f5f5; + color: #111; + line-height: 1.5; + overflow-x: hidden; + + + } + + img { + max-width: 100%; + display: block; + } + + a { + text-decoration: none; + color: inherit; + } + + button { + font-family: inherit; + } +`; + +export default GlobalStyle;