diff --git a/README.md b/README.md index 200f4282..c4b3d2c7 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # Portfolio +- Netfly link: https://alex-codes-portfolio.netlify.app/ +- figma link: https://www.figma.com/design/OrBxZbZNOs23n7gznIQGOX/Alex-U_Portfolio-design?node-id=1078-2177&m=dev diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..81d9eaec --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +## to do / log +- [x] animations +- [x] fix gerarchies and type font +- [x] deploy \ No newline at end of file diff --git a/index.html b/index.html index 6676fb2d..d9ce1600 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,34 @@ - - - - - Portfolio - - -
- - - + + + + + + + + Alexander Übelhör + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json index 48911600..e2791e9b 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,17 @@ "preview": "vite preview" }, "dependencies": { + "framer-motion": "^12.9.4", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "styled-components": "^6.1.17" }, "devDependencies": { "@eslint/js": "^9.21.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.4.1", + "babel-plugin-styled-components": "^2.1.4", "eslint": "^9.21.0", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", diff --git a/public/assets/business-site.png b/public/assets/business-site.png new file mode 100644 index 00000000..48cea8bc Binary files /dev/null and b/public/assets/business-site.png differ diff --git a/public/assets/happy.png b/public/assets/happy.png new file mode 100644 index 00000000..749851aa Binary files /dev/null and b/public/assets/happy.png differ diff --git a/public/assets/movies.png b/public/assets/movies.png new file mode 100644 index 00000000..1723eb07 Binary files /dev/null and b/public/assets/movies.png differ diff --git a/public/assets/portfolio.png b/public/assets/portfolio.png new file mode 100644 index 00000000..59d6a5aa Binary files /dev/null and b/public/assets/portfolio.png differ diff --git a/public/assets/quiz-site.png b/public/assets/quiz-site.png new file mode 100644 index 00000000..99cb5ec3 Binary files /dev/null and b/public/assets/quiz-site.png differ diff --git a/public/assets/recipe-library.png b/public/assets/recipe-library.png new file mode 100644 index 00000000..9c6f2acb Binary files /dev/null and b/public/assets/recipe-library.png differ diff --git a/public/assets/todo-app.png b/public/assets/todo-app.png new file mode 100644 index 00000000..3669b652 Binary files /dev/null and b/public/assets/todo-app.png differ diff --git a/public/assets/weather-app.png b/public/assets/weather-app.png new file mode 100644 index 00000000..578bb702 Binary files /dev/null and b/public/assets/weather-app.png differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 00000000..f462cf1d --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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/pull_request_template.md b/pull_request_template.md index 4263c7e8..c957c3e0 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -1 +1,4 @@ -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. +- **Figma design link:** https://www.figma.com/design/OrBxZbZNOs23n7gznIQGOX/Alex-U_Portfolio-design?node-id=1078-906&t=ZSxtzeE0qFNKz3dm-1 +- **Netlify link:** + diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..337ed974 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,26 @@ +import React from "react"; +import { ThemeProvider } from "styled-components"; +import { GlobalStyle } from "./styles/GlobalStyle.jsx"; +import { theme } from "./styles/theme.js"; + +import ProjectsSection from "./components/section-project/ProjectsSection.jsx"; +import HeroSection from "./components/section-hero/HeroSection.jsx"; +import InfoSection from "./components/section-info/InfoSection.jsx"; +import Navbar from "./components/common/Navbar.jsx"; +import CompanionSubtitle from "./components/common/CompanionSubtitle.jsx"; + + + 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.

- - ) -} + + + + + + + + + + ); +}; \ No newline at end of file diff --git a/src/assets/fonts/.13375.otf b/src/assets/fonts/.13375.otf new file mode 100644 index 00000000..9e378f15 Binary files /dev/null and b/src/assets/fonts/.13375.otf differ diff --git a/src/assets/icons/Btn - github.svg b/src/assets/icons/Btn - github.svg new file mode 100644 index 00000000..274f953d --- /dev/null +++ b/src/assets/icons/Btn - github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/Btn - instagram.svg b/src/assets/icons/Btn - instagram.svg new file mode 100644 index 00000000..15192379 --- /dev/null +++ b/src/assets/icons/Btn - instagram.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/Btn - linkedin.svg b/src/assets/icons/Btn - linkedin.svg new file mode 100644 index 00000000..aadeb098 --- /dev/null +++ b/src/assets/icons/Btn - linkedin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/Btn - stackoverflow.svg b/src/assets/icons/Btn - stackoverflow.svg new file mode 100644 index 00000000..3fad985e --- /dev/null +++ b/src/assets/icons/Btn - stackoverflow.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/Btn - twitter.svg b/src/assets/icons/Btn - twitter.svg new file mode 100644 index 00000000..2283b8df --- /dev/null +++ b/src/assets/icons/Btn - twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/arrow_circle_down.svg b/src/assets/icons/arrow_circle_down.svg new file mode 100644 index 00000000..0b4ce9c7 --- /dev/null +++ b/src/assets/icons/arrow_circle_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/live-demo.svg b/src/assets/icons/live-demo.svg new file mode 100644 index 00000000..375d29ad --- /dev/null +++ b/src/assets/icons/live-demo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/view-code.svg b/src/assets/icons/view-code.svg new file mode 100644 index 00000000..43a3cf29 --- /dev/null +++ b/src/assets/icons/view-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/CompanionSubtitle.jsx b/src/components/common/CompanionSubtitle.jsx new file mode 100644 index 00000000..0659ec7d --- /dev/null +++ b/src/components/common/CompanionSubtitle.jsx @@ -0,0 +1,243 @@ +import React, { useEffect, useState, useRef } from "react"; +import styled from "styled-components"; +import { mediaQueries } from "../../styles/media"; + +const ToggleWrapper = styled.div` + position: fixed; + top: 16px; + left: 16px; + z-index: 2100; + font-size: 1.2rem; + font-family: inherit; + border-radius: 1.2em; + border: 2px solid #000000; + user-select: none; + pointer-events: auto; + display: flex; + align-items: center; + gap: 0.7em; +`; + +const SwitchLabel = styled.label` + display: flex; + align-items: center; + cursor: pointer; + gap: 0.5em; +`; + +const Switch = styled.span` + position: relative; + display: inline-block; + width: 38px; + height: 22px; +`; + +const SwitchInput = styled.input` + opacity: 0; + width: 0; + height: 0; + &:checked + span { + background: #00ff3c; + } + &:checked + span:before { + transform: translateX(16px); + background: #fff; + } +`; + +const Slider = styled.span` + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background: #ccc; + border-radius: 22px; + transition: background 0.2s; + &:before { + content: ""; + position: absolute; + height: 18px; + width: 18px; + left: 2px; + bottom: 2px; + background: #fff; + border-radius: 50%; + transition: transform 0.2s; + } +`; + +const OverlaySubtitle = styled.div` + position: fixed; + bottom: 40px; + left: 0; + width: 100vw; + z-index: 2000; + pointer-events: none; + display: flex; + justify-content: center; + + :hover { + cursor: pointer; + } +`; + +const SubtitleText = styled.span` + background: none; + line-height: 1.2; + color: #00ff3c; + font-size: 1.1rem; + font-weight: 200; + transition: opacity 0.5s; + text-align: center; + text-shadow: 0 0 10px rgba(0, 0, 0, 0.463); + -webkit-text-stroke: 0.5px #000; + pointer-events: auto; + + + ${mediaQueries.tablet} { + font-size: 2rem; + padding: 0.4em 2em; + border-radius: 2rem; + } + + ${mediaQueries.desktop} { + font-size: 3rem; + padding: 0.5em 3em; + border-radius: 2.5rem; + -webkit-text-stroke: 1px #000; + } +`; + +const heroText = [ + "This website uses subtitles.", + "Welcome to my Technigo Bootcamp portfolio.", + "Here I'm sharing what I have been learning.", + "And coding so far.", + "Scroll down to see..." +]; +const heroSubtitles = heroText; + +const projectsText = [ + "Here are some of the course projects.", + "Each one has a description, code, and live demo!", + "Enjoy ;)" +]; +const projectsSubtitles = projectsText.map(text => text.match(/[^.!?]+[.!?]+(\s|$)/g) || [text]).flat(); + +// Break infoText into shorter sentences for more frequent subtitle changes +const infoText = [ + "This is the info section.", + "It contains details about my journey, skills", + "and experiences.", + "Feel free to explore!", + "And to contact me.", + "I’m happy to share my design portfolio", + "and a more detailed CV upon request!" +]; +const infoSubtitles = infoText; + +const sectionMap = [ + { id: "hero-section", subtitles: heroSubtitles }, + { id: "projects-section", subtitles: projectsSubtitles }, + { id: "info-section", subtitles: infoSubtitles } +]; + +const CompanionSubtitle = () => { + const [showSubtitles, setShowSubtitles] = useState(true); + const [sectionIndex, setSectionIndex] = useState(0); + const prevSectionIndex = useRef(0); + const [subtitleIndex, setSubtitleIndex] = useState(0); + const [paused, setPaused] = useState(false); + const intervalRef = useRef(); + + // Detect which section is in view + useEffect(() => { + const handleScroll = () => { + const vh = window.innerHeight; + let active = 0; + sectionMap.forEach((section, idx) => { + const el = document.getElementById(section.id); + if (el) { + const rect = el.getBoundingClientRect(); + // Section is considered active if its top is above mid viewport and its bottom is below mid viewport + if (rect.top < vh / 2 && rect.bottom > vh / 2) { + active = idx; + } + } + }); + if (active !== prevSectionIndex.current) { + setSectionIndex(active); + setSubtitleIndex(0); // Only reset when section changes + prevSectionIndex.current = active; + } + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + handleScroll(); // Initial check + + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + // Animate through the current section's subtitles + useEffect(() => { + if (!showSubtitles || paused) return; + const currentSubtitles = sectionMap[sectionIndex].subtitles; + if (subtitleIndex === currentSubtitles.length - 1) return; + + intervalRef.current = setInterval(() => { + setSubtitleIndex((prev) => + prev < currentSubtitles.length - 1 ? prev + 1 : prev + ); + }, 2500); + + return () => clearInterval(intervalRef.current); + }, [showSubtitles, paused, sectionIndex, subtitleIndex]); + + const handleMouseEnter = () => setPaused(true); + const handleMouseLeave = () => setPaused(false); + + const handlePrev = () => { + setSubtitleIndex((prev) => (prev === 0 ? 0 : prev - 1)); + }; + + const currentSubtitles = sectionMap[sectionIndex].subtitles; + + return ( + <> +
showSubtitles && setShowSubtitles(false)} + /> + + + + { + setShowSubtitles((v) => !v); + setSubtitleIndex(0); + }} + aria-label="Toggle subtitles" + /> + + + + + {showSubtitles && ( + + + {currentSubtitles[subtitleIndex] || currentSubtitles[0]} + + + )} + + ); +}; + +export default CompanionSubtitle; \ No newline at end of file diff --git a/src/components/common/Navbar.jsx b/src/components/common/Navbar.jsx new file mode 100644 index 00000000..307f0f10 --- /dev/null +++ b/src/components/common/Navbar.jsx @@ -0,0 +1,68 @@ +import { useState, useEffect } from "react"; +import styled from "styled-components"; + +const NavbarWrapper = styled.nav` + position: fixed; + top: 0; + right: 0; + left: 0; + gap: 3rem; + display: flex; + justify-content: flex-end; + z-index: 1000; + height: 60px; + transform: ${({ $isVisible }) => ($isVisible ? "translateY(0)" : "translateY(-100%)")}; + transition: transform 0.3s ease-in-out; +`; + +const NavLink = styled.a` + text-decoration: none; + cursor: pointer; + font-size: ${({ theme }) => theme.fontSizes.navLink}; + &:hover { + text-decoration: underline; + } +`; + +const Navbar = () => { + const [isVisible, setIsVisible] = useState(true); + const [lastScrollY, setLastScrollY] = useState(0); + + const handleScroll = () => { + const currentScrollY = window.scrollY; + + if (currentScrollY > lastScrollY) { + // Scrolling down + setIsVisible(false); + } else { + // Scrolling up + setIsVisible(true); + } + + setLastScrollY(currentScrollY); + }; + + useEffect(() => { + window.addEventListener("scroll", handleScroll); + + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, [lastScrollY]); + + const scrollToInfo = () => { + const infoSection = document.getElementById("info-section"); + if (infoSection) { + infoSection.scrollIntoView({ behavior: "smooth" }); + } + }; + + return ( + + Info + Say Hi + + ); +}; + +export default Navbar; \ No newline at end of file diff --git a/src/components/common/SectionTitle.jsx b/src/components/common/SectionTitle.jsx new file mode 100644 index 00000000..7c22b75f --- /dev/null +++ b/src/components/common/SectionTitle.jsx @@ -0,0 +1,15 @@ +import styled from "styled-components"; +import { mediaQueries } from "../../styles/media"; + +const StyledTitle = styled.h2` + font-size: ${({ theme }) => theme.fontSizes.h2}; + margin-bottom: 1rem; + /* font-weight: bold; */ + text-decoration: underline; +`; + +const SectionTitle = ({ title }) => { + return {title}; +}; + +export default SectionTitle; \ No newline at end of file diff --git a/src/components/section-hero/HeroSection.jsx b/src/components/section-hero/HeroSection.jsx new file mode 100644 index 00000000..e0f95ec8 --- /dev/null +++ b/src/components/section-hero/HeroSection.jsx @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import infoData from '../../data/info.json'; +import { mediaQueries } from '../../styles/media'; + +// styled-compoenents +const SectionWrapper = styled.section` + display: flex; + justify-content: center; + align-items: center; + text-align: center; + height: 100vh; + + + ${mediaQueries.desktop} { + height: 80vh; + margin-bottom: 11rem; + } +`; + + +const HeroText = styled.h1` +hyphens: auto; +overflow-wrap: break-word; +word-wrap: break-word; + +${mediaQueries.desktop} { + max-width: 60%; +} +`; + +const HeroSection = () => { + + return ( + + {infoData.hero.content} + + ) +} +export default HeroSection; \ No newline at end of file diff --git a/src/components/section-info/About.jsx b/src/components/section-info/About.jsx new file mode 100644 index 00000000..91e7ede0 --- /dev/null +++ b/src/components/section-info/About.jsx @@ -0,0 +1,30 @@ +import styled from "styled-components"; +import infoData from "../../data/info.json"; +import SectionTitle from "../common/SectionTitle.jsx"; +import { mediaQueries } from "../../styles/media.js"; + +const AboutWrapper = styled.div` +display: flex; +flex-direction: column; + +`; + +const Content = styled.p` +width: 100%; +`; + + + +const About = () => { + return ( + + + "), + }} + /> + + ); +}; +export default About; \ No newline at end of file diff --git a/src/components/section-info/Clients.jsx b/src/components/section-info/Clients.jsx new file mode 100644 index 00000000..8709916a --- /dev/null +++ b/src/components/section-info/Clients.jsx @@ -0,0 +1,48 @@ +import React from "react"; +import styled from "styled-components"; +import infoData from "../../data/info.json"; +import SectionTitle from "../common/SectionTitle.jsx"; + + +const ClientsWrapper = styled.div` + display: flex; + flex-direction: column; + + /* ${mediaQueries.desktop} { + max-width: 60%; + } */ + +`; + +const ClientsList = styled.ul` + list-style: none; + padding: 0; + margin: 0; +`; +const ClientsIntro = styled.p` + font-size: ${({ theme }) => theme.fontSizes.body}; + color: ${({ theme }) => theme.colors.text}; + margin-bottom: 1rem; +`; + +const ClientItem = styled.li` + line-height: 1.6; +`; + +const Clients = () => { + const { title, content, names } = infoData.clients; + + return ( + + + {content} + + {names.map((name, idx) => ( + {name} + ))} + + + ); +}; + +export default Clients; \ No newline at end of file diff --git a/src/components/section-info/Contact.jsx b/src/components/section-info/Contact.jsx new file mode 100644 index 00000000..2b192d64 --- /dev/null +++ b/src/components/section-info/Contact.jsx @@ -0,0 +1,71 @@ +import React from "react"; +import styled from "styled-components"; +import infoData from "../../data/info.json"; +import SectionTitle from "../common/SectionTitle.jsx"; +import { mediaQueries } from "../../styles/media.js"; + +const ContactWrapper = styled.div` + display: flex; + flex-direction: column; + + ${mediaQueries.desktop} { + max-width: 80%; + } +`; + +const ContactText = styled.p` + color: #333333; + margin-bottom: 1.5rem; + +`; + +const ContactList = styled.ul` + list-style: none; + padding: 0; +`; + + +const Link = styled.a` + color: #333333; + text-decoration: none; + transition: color 0.3s ease; + + &:hover { + text-decoration: underline; + } + `; + +const ContactItem = styled.li` + color: #333333; + +`; + +const Contact = () => { + return ( + + + + {infoData.contact.text.split('\n').map((line, i) => ( + + {line} +
+
+ ))} +
+ + Email: {infoData.contact.email} + + + {infoData.contact.content.map((item, index) => ( + + + {item.name} + + + ))} + +
+ ); +}; + +export default Contact; \ No newline at end of file diff --git a/src/components/section-info/Friends.jsx b/src/components/section-info/Friends.jsx new file mode 100644 index 00000000..00120949 --- /dev/null +++ b/src/components/section-info/Friends.jsx @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import infoData from "../../data/info.json"; +import SectionTitle from "../common/SectionTitle"; +import { mediaQueries } from "../../styles/media"; + +const FriendsWrapper = styled.div``; +const Link = styled.a` + color: #333333; + text-decoration: none; + transition: color 0.3s ease; + + &:hover { + text-decoration: underline; + } + `; + +const Friends = () => { + return ( + + +
    + {infoData.friends.content.map((friend, index) => ( +
  • + + {friend.name} + +
  • + ))} +
+
+ ); +}; + +export default Friends; \ No newline at end of file diff --git a/src/components/section-info/InfoSection.jsx b/src/components/section-info/InfoSection.jsx new file mode 100644 index 00000000..dfc01955 --- /dev/null +++ b/src/components/section-info/InfoSection.jsx @@ -0,0 +1,88 @@ +import styled from "styled-components"; +import About from "./About.jsx"; +// import Services from "./Services.jsx"; +import Skills from "./SkillsSummery.jsx"; +import Studies from "./Studies.jsx"; +import Contact from "./Contact.jsx"; +import Friends from "./Friends.jsx"; +import Tech from "./Tech.jsx"; +import Clients from "./Clients.jsx"; +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; +import { mediaQueries } from "../../styles/media.js"; + + + +const SectionWrapper = styled.section` + display: flex; + flex-direction: column; + gap: 2.5rem; + + ${mediaQueries.tablet} { + gap: 3rem; + } + + ${mediaQueries.desktop} { + max-width: 70%; +} +`; + + + +const AnimatedSection = styled(motion.div)` +`; + +const InfoSection = () => { + const sectionVariants = { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + }; + + const AnimatedComponent = ({ children }) => { + const ref = useRef(null); + const isInView = useInView(ref); + + return ( + + {children} + + ); + }; + + return ( + + + + + {/* + + */} + + + + + + + + + + + + + + + + + + + + ); +}; + +export default InfoSection; \ No newline at end of file diff --git a/src/components/section-info/Services.jsx b/src/components/section-info/Services.jsx new file mode 100644 index 00000000..f01e24df --- /dev/null +++ b/src/components/section-info/Services.jsx @@ -0,0 +1,23 @@ + +import styled from "styled-components"; +import infoData from "../../data/info.json"; +import SectionTitle from "../common/SectionTitle.jsx"; + +const ServicesWrapper = styled.div``; +const ServicesList = styled.ul`` +const ServiceItem = styled.li`` + + +const Services = () => { + return ( + + + + {infoData.services.content.map((service, index) => ( + {service} + ))} + + + ) +} +export default Services; \ No newline at end of file diff --git a/src/components/section-info/SkillsSummery.jsx b/src/components/section-info/SkillsSummery.jsx new file mode 100644 index 00000000..2ced2544 --- /dev/null +++ b/src/components/section-info/SkillsSummery.jsx @@ -0,0 +1,49 @@ + +import styled from "styled-components"; +import infoData from "../../data/info.json"; +import SectionTitle from "../common/SectionTitle.jsx"; +import { mediaQueries } from "../../styles/media.js"; + +const SkillsWrapper = styled.div``; +const CategoryHeading = styled.h3` + display: inline-block; + color: #333333; + background-color: white; + text-decoration: underline; + +`; + +const SkillsList = styled.ul` + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 0.8rem; + + ${mediaQueries.tablet} { + flex-direction: row; + gap: 10rem; +}`; + + + +const Skills = () => { + return ( + + + + {Object.entries(infoData.skills.categories).map(([category, skills], index) => ( +
+ {category} +
    + {skills.map((skill, i) => ( +
  • {skill}
  • + ))} +
+
+ ))} +
+
+ ); +}; + +export default Skills; \ No newline at end of file diff --git a/src/components/section-info/Studies.jsx b/src/components/section-info/Studies.jsx new file mode 100644 index 00000000..9e092fc6 --- /dev/null +++ b/src/components/section-info/Studies.jsx @@ -0,0 +1,32 @@ +import styled from "styled-components"; +import infoData from "../../data/info.json"; +import SectionTitle from "../common/SectionTitle"; + +const EducationWrapper = styled.div``; +const EducationList = styled.ul``; +const EducationItem = styled.li` + color: ${({ theme }) => theme.colors.text}; + line-height: 1.6; + + & > span.school { + font-style: italic; + } +`; + +const Studies = () => { + const education = infoData.education?.content || []; + return ( + + + + {education.map((item, index) => ( + + {item.school} – {item.program} + + ))} + + + ); +}; + +export default Studies; \ No newline at end of file diff --git a/src/components/section-info/Tech.jsx b/src/components/section-info/Tech.jsx new file mode 100644 index 00000000..87bf7b13 --- /dev/null +++ b/src/components/section-info/Tech.jsx @@ -0,0 +1,46 @@ +import styled from 'styled-components'; +import infoData from '../../data/info.json'; +import SectionTitle from "../common/SectionTitle.jsx"; +import { mediaQueries } from '../../styles/media.js'; + +const TechWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: left; +`; + +const TechList = styled.ul` + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + + ${mediaQueries.desktop} { + gap: 1rem; +} +`; + +const TechItem = styled.li` + padding: 0.5rem 1rem; + border-radius: 25px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.211); + transition: transform 0.2s ease-in-out; + + &:hover { + transform: scale(1.05); + } +`; + +const Tech = () => { + return ( + + + + {infoData.tech.items.map((item, index) => ( + {item} + ))} + + + ); +}; + +export default Tech; \ No newline at end of file diff --git a/src/components/section-project/ProjectCard.jsx b/src/components/section-project/ProjectCard.jsx new file mode 100644 index 00000000..fa7d83ec --- /dev/null +++ b/src/components/section-project/ProjectCard.jsx @@ -0,0 +1,105 @@ +import styled from "styled-components"; +import { mediaQueries } from "../../styles/media"; +import Tag from "./Tags.jsx"; +import ProjectLink from "./ProjectLink.jsx"; +import LiveDemoIcon from "../../assets/icons/live-demo.svg"; +import ViewCodeIcon from "../../assets/icons/view-code.svg"; + +const CardWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; + + ${mediaQueries.desktop} { + gap: 5rem; + flex-direction: ${({ $reverse }) => ($reverse ? "row-reverse" : "row")}; + align-items: stretch; /* Ensures children fill the card height */ + min-height: 350px; /* Set a minimum height for alignment */ + } +`; + + +const InfoWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 32px; + + ${mediaQueries.desktop} { + width: 50%; + justify-content: center; + align-items: flex-start; + height: 100%; /* Fill card height */ + } +` + +const TextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + + ${mediaQueries.desktop} { + flex-direction: column; + gap: 1.5rem; + + } +`; + +const Image = styled.img` + width: 100%; + height: auto; + border-radius: 8px; + box-shadow: 0px 4px 54.6px -8px rgba(0, 0, 0, 0.25); + + ${mediaQueries.desktop} { + width: 50%; + height: 100%; /* Make image fill the card height */ + object-fit: cover; /* Crop image to fit if needed */ + } +`; + +const TagContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +`; + +const Title = styled.h3` + font-size: ${({ theme }) => theme.fontSizes.h3}; +`; + +const Description = styled.p` + font-size: ${({ theme }) => theme.fontSizes.body}; +`; + +const LinksContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; +`; + +const ProjectCard = ({ name, image, description, tags, netlify, github, reverse }) => { + return ( + + {image && {name}} + + + + {tags.map((tag, index) => ( + + ))} + + {name} + {description} + + + + } /> + } /> + + + + ); +}; + +export default ProjectCard; \ No newline at end of file diff --git a/src/components/section-project/ProjectLink.jsx b/src/components/section-project/ProjectLink.jsx new file mode 100644 index 00000000..df822a4f --- /dev/null +++ b/src/components/section-project/ProjectLink.jsx @@ -0,0 +1,56 @@ +import styled from "styled-components"; + + + +const ProjectButton = styled.button` + background-color:white; + align-items:center; + display: flex; + justify-content: space-between; + color: black; + border: solid 2px black; + border-radius: 12px; + padding-bottom: 0.5rem; + padding-top: 0.4rem; + padding-left: 1rem; + padding-right: 6rem; + cursor: pointer; + font-size: 1rem; + + &:hover { + background-color: black; + border: solid 2px black; + color: #FFFFFF; + + img { + filter: invert(1); + } + + } + + a { + display: inline-flex; + align-items:center; + gap: 1.5rem; + } + + img { + width: 30px; + height: 30px; + } + +`; + + + +const ProjectLink = ({ label, href, icon }) => { + return ( + + + {icon && icon}{label} + + + ); +}; + +export default ProjectLink; \ No newline at end of file diff --git a/src/components/section-project/ProjectsSection.jsx b/src/components/section-project/ProjectsSection.jsx new file mode 100644 index 00000000..9218f38b --- /dev/null +++ b/src/components/section-project/ProjectsSection.jsx @@ -0,0 +1,66 @@ +import styled from "styled-components"; +import projectsData from "../../data/projects.json"; +import ProjectCard from "./ProjectCard.jsx"; +import SectionTitle from "../common/SectionTitle.jsx"; +import { mediaQueries } from "../../styles/media.js"; +import { motion, useInView } from "framer-motion"; +import { useRef } from "react"; + +const SectionWrapper = styled.section` + display: flex; + flex-direction: column; + margin-bottom: 5rem; +`; + +const SelectedProjects = styled.div` + display: flex; + flex-direction: column; + gap: 4rem; + + ${mediaQueries.desktop} { + gap: 8rem; + } +`; + +const AnimatedProject = styled(motion.div)``; + +const ProjectsSection = () => { + const projectVariants = { + hidden: { opacity: 0, y: 50 }, + visible: { opacity: 1, y: 0 }, + }; + + const AnimatedComponent = ({ children }) => { + const ref = useRef(null); + const isInView = useInView(ref); + + return ( + + {children} + + ); + }; + + return ( + + + + + + {projectsData.projects.map((project, index) => ( + + + + ))} + + + ); +}; + +export default ProjectsSection; \ No newline at end of file diff --git a/src/components/section-project/Tags.jsx b/src/components/section-project/Tags.jsx new file mode 100644 index 00000000..09078010 --- /dev/null +++ b/src/components/section-project/Tags.jsx @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import { mediaQueries } from "../../styles/media.js"; + +const TagSpan = styled.span` +display: inline-block; +padding: 0.2rem 1rem; +border-radius: 25px; +box-shadow: 0 2px 4px rgba(0, 0, 0, 0.211); + transition: transform 0.2s ease-in-out; + + &:hover { + transform: scale(1.05); + } + +${mediaQueries.desktop} { +padding: 0.2rem 1.5rem; +} +`; + + + +const Tag = ({ text }) => { + return {text}; +}; + +export default Tag; \ No newline at end of file diff --git a/src/data/info.json b/src/data/info.json new file mode 100644 index 00000000..0c25eff5 --- /dev/null +++ b/src/data/info.json @@ -0,0 +1,169 @@ +{ + "skills": { + "title": "Skills", + "categories": { + "Code": [ + "HTML5", + "CSS3", + "JavaScript", + "ES6", + "React", + "Styled Components", + "GitHub", + "Node.js" + ], + "Tools": [ + "Adobe Suite", + "Blender", + "VSC", + "After Effect", + "DaVinci Resolve", + "Figma", + "Slack" + ], + "Creative & Workflow": [ + "Web Design and Development", + "Storytelling", + "Graphic Design", + "3D Animation", + "Motion Graphics", + "3D Modeling", + "Video Editing", + "Video creation", + "User interface design", + "Branding", + "Concept Development", + "Agile methodology", + "Mob programming", + "Pair programming", + "Teamwork" + ] + } + }, + "tech": { + "title": "Tech", + "items": [ + "HTML", + "CSS", + "Flexbox", + "JavaScript", + "ES6", + "React", + "Node.js", + "Web Accessibility", + "APIs", + "mob-programming", + "pair-programming", + "GitHub", + "Git" + ] + }, + "hero": { + "title": "Hero Section", + "content": "Alexander Übelhör is a designer learning to code—currently studying full-stack web development while freelancing across design and motion graphics." + }, + "about": { + "title": "About", + "content": "Currently, I'm expanding my skill set through the Technigo Web Development bootcamp, deepening my knowledge of front-end and full-stack development.\n\nMy journey began with Graphic Design, where I earned my BA at the Gerrit Rietveld Academy in Amsterdam. Subsequently, I started freelancing working across diverse formats: print design, digital layouts to motion graphics and short-form video while also working as an artist assistant.\n\nTools & technology has always been an area of interest and curiosity that I find strongly connected with the craft of design. The transition to web development feels natural—coding is another form of creative problem solving, and my design background can help me understand how users interact with interfaces. I'm looking forward to applying my newly acquired skills to the building of designs.\n\nRecently moved to Stockholm, I'm open to opportunities that let me combine my visual background with my growing development skills, whether as part of a collaborative team or on challenging solo projects. In addition to applied work, I continue a practice of personal and collaborative visual art and moving image work." + }, + "services": { + "title": "Services", + "content": [ + "Graphic Design", + "Web Development", + "Motion Graphics", + "3D Animation", + "Videography" + ] + }, + "education": { + "title": "Education", + "content": [ + { + "school": "Technigo JavaScript Bootcamp", + "program": "Fullstack Web development." + }, + { + "school": "Gerrit Rietveld Academie", + "program": "BA Art & Design, Graphic Design." + } + ] + }, + "contact": { + "title": "Contact", + "text": "I’m always open to new projects and collaborations. If you have something in mind—or just want to connect—feel free to reach out.\n\nI’m happy to share my graphic design portfolio and a more detailed CV upon request.", + "email": "a.ubelhor@gmail.com", + "content": [ + { + "name": "Are.na", + "link": "https://www.are.na/alexander-ubelhor/channels" + }, + { + "name": "Instagram", + "link": "https://www.instagram.com/alex91.jpg/" + }, + { + "name": "GitHub", + "link": "https://github.com/alex91-html" + }, + { + "name": "LinkedIn", + "link": "https://www.linkedin.com/in/aubelhor/" + } + ] + }, + "friends": { + "title": "Friends & Collaborators", + "content": [ + { + "name": "Jurgis Lietunovas", + "link": "https://jurgis.info/" + }, + { + "name": "Fabian Bergmark Näsman", + "link": "https://fabianbergmarknasman.com/?fbclid=PAZXh0bgNhZW0CMTEAAad6zNGzMPAU-W_ykPRKb53UgCW5Ih8JL9_R3lTH9XoljCN28cvyc2rAO7RGNg_aem_XaAjBoRSeyqNZp_euRzn7A" + }, + { + "name": "Lova Ranung", + "link": "https://lovaranung.com/" + }, + { + "name": "Matthew Lessner", + "link": "https://www.montelomax.com/" + }, + { + "name": "Davide Tisato", + "link": "https://www.swissfilms.ch/en/person/davide-tisato/2fcf80ba1c1f4e7c9f5e98b56c4ae1e9" + }, + { + "name": "Tisato_Sulzer Communication", + "link": "https://www.tisato-sulzer.com/" + } + ] + }, + "clients": { + "title": "Clients", + "content": "I have had the pleasure of working with a diverse range of clients, from local businesses to international organizations. Here are some of the clients I've collaborated with:", + "names": [ + "Avoidstreet (NL)", + "in_n_out_fragrances (NL)", + "Valextra (IT)", + "Tisato & Sulzer Communication Design (CH)", + "Studio fORMATS aND mECHANISMS (NL)", + "Studiox01 (IT)", + "Primer", + "Klarna", + "Voi (DK)", + "Amazon Music Italia (IT)", + "Stedelijk Museum (NL)", + "International Social Service (CH)", + "Ecran Mobile film production (CH)", + "Vendredi Films (FR)", + "European Documentary Network (NL)", + "filmmaker Davide Tisato (CH)", + "Artist Gabriel Lester (NL)", + "Artist Deniz Eroglu (NL)", + "Artist Golin (NL)" + ] + } +} \ No newline at end of file diff --git a/src/data/projects.json b/src/data/projects.json index 7c426028..af735dc5 100644 --- a/src/data/projects.json +++ b/src/data/projects.json @@ -1,19 +1,82 @@ { + "title": "Selected Projects", "projects": [ { - "name": "Business site", - "image": "https://images.unsplash.com/photo-1557008075-7f2c5efa4cfd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2497&q=80", + "name": "Todo App", + "image": "todo-app.png", + "description": "A Todo app built with React. It allows users to add, delete, and mark tasks as completed. The app uses local storage to persist data.", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "React.js", + "Zustand", + "Styled-components" + ], + "netlify": "https://alextodo-app.netlify.app/", + "github": "https://github.com/alex91-html/js-project-todo" + }, + { + "name": "Docu Tabloid", + "image": "movies.png", + "description": "A documentary movide search app that allows users to search for documentaries, view details and rankings. Built with React, it uses the TMDB API to fetch data.", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "React.js", + "React Router", + "APIs", + "Vite" + ], + "netlify": "https://alexmovieapp.netlify.app/", + "github": "https://github.com/alex91-html/js-project-movies" + }, + { + "name": "Happy Thoughts App", + "image": "happy.png", + "description": "An app that allows users to share happy thoughts. It features a responsive design, a form for submitting thoughts, and a list of all submitted thoughts. Made with React and Tailwind CSS, using useState, useEffect and API.", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "React.js", + "Tailwind CSS", + "APIs" + ], + "netlify": "https://happythoughtss.netlify.app/", + "github": "https://github.com/alex91-html/js-project-happy-thoughts" + }, + { + "name": "Portfolio Site", + "image": "portfolio.png", + "description": "This is the current portfolio site, built with React and styled Components. It features a responsive design, smooth scrolling, and a clean layout.", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "React.js", + "Styled Components" + ], + "netlify": "https://alex-codes-portfolio.netlify.app/", + "github": "https://github.com/alex91-html/js-project-portfolio" + }, + { + "name": "Business site project", + "image": "business-site.png", + "description": "This project is a web project created to learn the basics of responsive web design, CSS Grid and Flexbox layout, and DOM manipulation.", "tags": [ "HTML5", "CSS3", "JavaScript" ], - "netlify": "link", - "github": "link" + "netlify": "https://designshop.netlify.app/", + "github": "https://github.com/alex91-html/js-project-business-site?tab=readme-ov-file" }, { "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", + "image": "weather-app.png", + "description": "This project is a responsive weather app built with TypeScript, fetching data from the OpenWeather API. It shows the current weather, temperature, sunrise and sunset times, plus a 4-day forecast, all styled to match a selected design mockup.", "tags": [ "HTML5", "CSS3", @@ -21,8 +84,34 @@ "TypeScript", "APIs" ], - "netlify": "link", - "github": "link" + "netlify": "https://watherrr.netlify.app/", + "github": "https://github.com/alex91-html/js-project-weather-app?tab=readme-ov-file" + }, + { + "name": "Web Accessebility Quiz", + "image": "quiz-site.png", + "description": "A quiz about web accessibility where common accessibility guidelines were followed. The site is user-friendly and works for everyone, including those using screen readers, keyboards, and other assistive technologies.", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "WCAG" + ], + "netlify": "https://quiz-accessibility.netlify.app/", + "github": "https://github.com/alex91-html/js-project-accessibility/tree/main" + }, + { + "name": "Recipe Library", + "image": "recipe-library.png", + "description": "A functional recipe app that fetches recipes from an API and let users find recipes based on different filters and sorting options.", + "tags": [ + "HTML5", + "CSS3", + "JavaScript", + "APIs" + ], + "netlify": "https://jsrecipelibrary.netlify.app/", + "github": "https://github.com/alex91-html/js-project-recipe-library" } ] } \ 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..01150949 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -3,8 +3,6 @@ import { createRoot } from 'react-dom/client' import { App } from './App.jsx' -import './index.css' - createRoot(document.getElementById('root')).render( diff --git a/src/styles/GlobalStyle.jsx b/src/styles/GlobalStyle.jsx new file mode 100644 index 00000000..1b2650f4 --- /dev/null +++ b/src/styles/GlobalStyle.jsx @@ -0,0 +1,68 @@ + +import { createGlobalStyle } from "styled-components"; +import { mediaQueries } from "./media"; + +export const GlobalStyle = createGlobalStyle` + * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-optical-sizing: auto; + scroll-behavior: smooth; + text-decoration: none; + list-style: none; + font-family: ${({ theme }) => theme.fonts.body}; + font-weight: ${({ theme }) => theme.fonts.fontWeight}; + } + + body { + font-size: ${({ theme }) => theme.fontSizes.body}; + background: ${({ theme }) => theme.colors.background}; + color: ${({ theme }) => theme.colors.text}; + line-height: 1.5; + padding: clamp(1rem, 5vw, 5rem); + } + + + button { + transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; + cursor: pointer; + } + + + nav { + padding: clamp(1rem, 5vw, 2rem); + } + + + section:last-child { + + margin-bottom: 0; + padding-top: 0.5rem; + + ${mediaQueries.desktop} { + padding-top:2rem; + }} + + + + h1 { + font-size: ${({ theme }) => theme.fontSizes.h1}; + } + + h2 { + font-size: ${({ theme }) => theme.fontSizes.h2}; + } + + h3 { + font-size: ${({ theme }) => theme.fontSizes.h3}; + } + + a { + color: inherit; + text-decoration: none; + } +`; + +// NOTES: +// The global styles are applied to all elements in the application. \ No newline at end of file diff --git a/src/styles/media.js b/src/styles/media.js new file mode 100644 index 00000000..1b0e1e47 --- /dev/null +++ b/src/styles/media.js @@ -0,0 +1,15 @@ +const breakpoints = { + mobile: '320px', + tablet: '400px', + desktop: '1024px', +}; + +export const mediaQueries = { + mobile: `@media (max-width: ${breakpoints.mobile})`, + tablet: `@media (min-width: ${breakpoints.tablet})`, + desktop: `@media (min-width: ${breakpoints.desktop})`, +}; + + +// NOTES: +// media queries for different screen sizes \ No newline at end of file diff --git a/src/styles/theme.js b/src/styles/theme.js new file mode 100644 index 00000000..c7b09ff8 --- /dev/null +++ b/src/styles/theme.js @@ -0,0 +1,24 @@ +export const theme = { + colors: { + background: "#FFFFFF", + text: "#333333", + }, + fonts: { + body: "ocr-a-std, monospace, Arial, sans-serif", + fontWeight: 100, + fontStyle: "normal" + }, + fontSizes: { + body: "clamp(14px, 1.2vw, 1.5rem)", + h1: "clamp(1rem, 5vw, 1.5rem)", + h2: "clamp(1.2rem, 4vw, 2.5rem)", + h3: "clamp(1.2rem, 3vw, 2rem)", + navLink: "clamp(1rem, 2.5vw, 1.5rem)", + }, +}; + +// NOTES: + +// the theme.js file defines the theme for the application, including colors, fonts, and font sizes. + +// clamp() is used to create responsive font sizes that adjust based on the viewport width. \ No newline at end of file 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