diff --git a/README.md b/README.md index 200f4282..7d59e238 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# Portfolio +# Oscars Portfolio + +# https://oscars-js-portfolio.netlify.app diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..c5e88e37 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,17 @@ +import js from "@eslint/js"; +import globals from "globals"; +import pluginReact from "eslint-plugin-react"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { + files: ["**/*.{js,mjs,cjs,jsx}"], + plugins: { js }, + extends: ["js/recommended", "stylelint-config-tailwindcss"], + }, + { + files: ["**/*.{js,mjs,cjs,jsx}"], + languageOptions: { globals: globals.browser }, + }, + pluginReact.configs.flat.recommended, +]); diff --git a/index.html b/index.html index 6676fb2d..375cbded 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,46 @@ - + - + + + - Portfolio + + + + + + + + + + Oscar's Portfolio + + + + + + +
- + + + diff --git a/package.json b/package.json index 48911600..8cd0337f 100644 --- a/package.json +++ b/package.json @@ -7,21 +7,46 @@ "dev": "vite", "build": "vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest" }, "dependencies": { + "@tailwindcss/postcss": "^4.1.4", + "@versoly/react-taos": "^0.1.0", + "aos": "^3.0.0-beta.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "styled-components": "^6.1.17", + "taos": "^1.0.5" }, "devDependencies": { - "@eslint/js": "^9.21.0", + "@babel/core": "^7.26.10", + "@babel/preset-env": "^7.26.9", + "@babel/preset-react": "^7.26.3", + "@eslint/js": "^9.25.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.21.0", + "@vitejs/plugin-react": "^4.4.0", + "autoprefixer": "^10.4.21", + "babel-jest": "^29.7.0", + "babel-plugin-styled-components": "^2.1.4", + "eslint": "^9.25.1", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^15.15.0", - "vite": "^6.2.0" + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jsdom": "^26.1.0", + "postcss": "^8.5.3", + "react-test-renderer": "^19.1.0", + "tailwindcss": "^3.4.17", + "tailwindcss-motion": "^1.1.0", + "vite": "^6.2.0", + "vitest": "^3.1.2" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..e5ce722e --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,8 @@ +import { TAOS } from "@versoly/react-taos"; + +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/article-acces.avif b/public/article-acces.avif new file mode 100644 index 00000000..7121f61b Binary files /dev/null and b/public/article-acces.avif differ diff --git a/public/article-api.avif b/public/article-api.avif new file mode 100644 index 00000000..41a458f1 Binary files /dev/null and b/public/article-api.avif differ diff --git a/public/article-first.avif b/public/article-first.avif new file mode 100644 index 00000000..ec8090b2 Binary files /dev/null and b/public/article-first.avif differ diff --git a/public/article-movie.avif b/public/article-movie.avif new file mode 100644 index 00000000..63a7ddeb Binary files /dev/null and b/public/article-movie.avif differ diff --git a/public/article-portfolio.avif b/public/article-portfolio.avif new file mode 100644 index 00000000..a6c56c8b Binary files /dev/null and b/public/article-portfolio.avif differ diff --git a/public/article-recipe.avif b/public/article-recipe.avif new file mode 100644 index 00000000..f4130bea Binary files /dev/null and b/public/article-recipe.avif differ diff --git a/public/article-todo.avif b/public/article-todo.avif new file mode 100644 index 00000000..1452423b Binary files /dev/null and b/public/article-todo.avif differ diff --git a/public/article-weather.avif b/public/article-weather.avif new file mode 100644 index 00000000..460c930b Binary files /dev/null and b/public/article-weather.avif differ diff --git a/public/happy_thoughts.avif b/public/happy_thoughts.avif new file mode 100644 index 00000000..6143313a Binary files /dev/null and b/public/happy_thoughts.avif differ diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 00000000..cecc781a --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/meta-og-img.avif b/public/meta-og-img.avif new file mode 100644 index 00000000..d8d126ef Binary files /dev/null and b/public/meta-og-img.avif differ 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..6f5a50fc 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. + +# www.figma.com/board/PymjF0tI8PYjLJ45Ud1Ai9/Water-🌊?node-id=0-1&p=f&t=fyWZXSjCKeiorHlN-0 + +# https://oscars-js-portfolio.netlify.app diff --git a/src/App.css b/src/App.css new file mode 100644 index 00000000..311f8e9b --- /dev/null +++ b/src/App.css @@ -0,0 +1,5 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/*This is required to activate tailwind*/ diff --git a/src/App.jsx b/src/App.jsx index a161d8d3..5fbed51a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,8 +1,28 @@ +import React from "react"; + +import { FeaturedProjects } from "./sections/FeaturedProjects"; +import { HiThere } from "./sections/HiThere"; +import { LetsTalk } from "./sections/LetsTalk"; +import { MyWords } from "./sections/MyWords"; +import { Skills } from "./sections/Skills"; +import { Tech } from "./sections/Tech"; +import { GlobalStyle } from "./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.

+ + + + + + + - ) -} + ); +}; + +//App is our main hub for all section and components. the "sandwich" + +//add tests +//add accessibility and aria-labels diff --git a/src/App.test.jsx b/src/App.test.jsx new file mode 100644 index 00000000..151a8853 --- /dev/null +++ b/src/App.test.jsx @@ -0,0 +1,19 @@ +import { render, screen } from "@testing-library/react"; +import { App } from "./App"; + +/* +describe("A truthy statement", () => { + it("should be equal to 2", () => { + expect(1 + 1).toEqual(2); + }); +}); +test +*/ + +describe("App", () => { + it("renders the App component", () => { + render(); + + screen.debug(); // prints out the jsx in the App component unto the command line + }); +}); diff --git a/src/Breakpoints.js b/src/Breakpoints.js new file mode 100644 index 00000000..7754ea6a --- /dev/null +++ b/src/Breakpoints.js @@ -0,0 +1,12 @@ +export const media = { + mobile: "(min-width: 768px)", + tablet: "(min-width: 1024px)", + smalldesktop: "(min-width: 1280px)", + desktop: "(min-width: 1536px)", +}; + +//sm = 640px +//mobile = 768px = md +//tablet = 1024px = lg +//smalldesktop = 1280px = xl +//desktop = 1536px = 2xl diff --git a/src/GlobalStyle.jsx b/src/GlobalStyle.jsx new file mode 100644 index 00000000..fdfd2614 --- /dev/null +++ b/src/GlobalStyle.jsx @@ -0,0 +1,66 @@ +import { createGlobalStyle } from "styled-components"; + +import { media } from "./Breakpoints"; + +export const GlobalStyle = createGlobalStyle` + * { + font-family: "Poppins", sans-serif; + font-style: normal; + line-height: normal; + color: #000000; + } + + :focus-visible { + outline: 4px solid yellow; + outline-offset: 2px; + border-radius: 2px; + } + + body { + margin: 0; + padding: 0; + } + + #root { + width: 100%; + } + + + h1 { + font-size: 52px; + font-weight: 700; + } + + h2 { + font-size: 48px; + font-weight: 700; + } + + h3 { + font-size: 24px; + font-weight: 500; + } + + li { + font-size: 16px; + font-weight: 400; + list-style-type: none; + } + + @media ${media.smalldesktop} { + h1 { + font-size: 100px; + font-weight: 700; + } + + h2 { + font-size: 80px; + font-weight: 700; + } + + h3 { + font-size: 30px; + font-weight: 500; + } + } +`; diff --git a/src/assets/Btn-instagram.svg b/src/assets/Btn-instagram.svg new file mode 100644 index 00000000..512ca9f9 --- /dev/null +++ b/src/assets/Btn-instagram.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/Btn-linkedin.svg b/src/assets/Btn-linkedin.svg new file mode 100644 index 00000000..7748cd68 --- /dev/null +++ b/src/assets/Btn-linkedin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/Btn-stackoverflow.svg b/src/assets/Btn-stackoverflow.svg new file mode 100644 index 00000000..912255d9 --- /dev/null +++ b/src/assets/Btn-stackoverflow.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/Ic-ArrowDown.svg b/src/assets/Ic-ArrowDown.svg new file mode 100644 index 00000000..5febcfa6 --- /dev/null +++ b/src/assets/Ic-ArrowDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/Ic-Github.svg b/src/assets/Ic-Github.svg new file mode 100644 index 00000000..cc43e6d8 --- /dev/null +++ b/src/assets/Ic-Github.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/Ic-Github_on-white.svg b/src/assets/Ic-Github_on-white.svg new file mode 100644 index 00000000..06c6fed1 --- /dev/null +++ b/src/assets/Ic-Github_on-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/Ic-Web.svg b/src/assets/Ic-Web.svg new file mode 100644 index 00000000..cecc781a --- /dev/null +++ b/src/assets/Ic-Web.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/Ic-Web_on-white.svg b/src/assets/Ic-Web_on-white.svg new file mode 100644 index 00000000..52b2fdf1 --- /dev/null +++ b/src/assets/Ic-Web_on-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/cropped_profile.avif b/src/assets/cropped_profile.avif new file mode 100644 index 00000000..e52f6795 Binary files /dev/null and b/src/assets/cropped_profile.avif differ diff --git a/src/data/mywords.json b/src/data/mywords.json new file mode 100644 index 00000000..fb6b6eae --- /dev/null +++ b/src/data/mywords.json @@ -0,0 +1,36 @@ +{ + "articles": [ + { + "title": "Placeholder", + "image": "https://images.unsplash.com/photo-1635745694780-767722ca4007?q=80&w=2071&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "desc": "Placeholder", + "date": "Placeholder", + "link": "Placeholder", + "id": "mw-1" + }, + { + "title": "Placeholder", + "image": "https://images.unsplash.com/photo-1635745694780-767722ca4007?q=80&w=2071&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "desc": "Placeholder", + "date": "Placeholder", + "link": "Placeholder", + "id": "mw-2" + }, + { + "title": "Placeholder", + "image": "https://images.unsplash.com/photo-1635745694780-767722ca4007?q=80&w=2071&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "desc": "Placeholder", + "date": "Placeholder", + "link": "Placeholder", + "id": "mw-3" + }, + { + "title": "Placeholder", + "image": "https://images.unsplash.com/photo-1635745694780-767722ca4007?q=80&w=2071&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + "desc": "Placeholder", + "date": "Placeholder", + "link": "Placeholder", + "id": "mw-4" + } + ] +} diff --git a/src/data/projects.json b/src/data/projects.json index 7c426028..8e560dcd 100644 --- a/src/data/projects.json +++ b/src/data/projects.json @@ -1,28 +1,93 @@ { "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": "Happy Thoughts", + "image": "/happy_thoughts.avif", + "desc": "Happy Thoughts is a full-stack web app where users can share and like short, uplifting messages. It features user authentication, real-time updates, and a clean, responsive interface styled with Tailwind CSS.", + "tags": ["HTML5", "CSS3", "JavaScript", "React.js", "API", "TailwindCSS"], + "netlify": "https://js-project-happy-thoughts.netlify.app", + "github": "https://github.com/osckli990/js-project-happy-thoughts", + "id": "p-1" }, { - "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", + "name": "API Project", + "image": "/article-api.avif", + "desc": "A RESTful API built with Express.js, allowing users to perform CRUD operations on a MongoDB database. It includes user authentication and validation using Mongoose. Connected to the Happy Thoughts project.", + "tags": ["Express.js", "MongoDB", "Mongoose", "JavaScript", "API"], + "netlify": "", + "github": "https://github.com/Technigo/js-project-api", + "id": "p-1_25" + }, + { + "name": "Todo webbapp", + "image": "/article-todo.avif", + "desc": "A simple todo application built with React, allowing users to add, remove, and edit tasks. A deepdive into global state management using Zustand, a small and fast state management library for react", + "tags": ["HTML5", "CSS3", "JavaScript", "React.js", "API", "Zustand"], + "netlify": "https://oscar-todo.netlify.app/", + "github": "https://github.com/osckli990/js-project-todo", + "id": "p-1_5" + }, + { + "name": "Portfolio", + "image": "/article-portfolio.avif", + "desc": "The portfolio you are currently looking at! Made through JSON, such as this text, and JavaScript and React to handle components and states. Monitored with a the testing framework Vitest", "tags": [ "HTML5", "CSS3", "JavaScript", - "TypeScript", - "APIs" + "React.js", + "JSON", + "Vitest", + "TailwindCSS" ], - "netlify": "link", - "github": "link" + "netlify": "https://oscars-js-portfolio.netlify.app", + "github": "https://github.com/osckli990/js-project-portfolio", + "id": "p-2" + }, + { + "name": "Recipe library", + "image": "/article-recipe.avif", + "desc": "Using Spoonacular's complex-search API to gather specific recipes, then using JavaScript to sort through different alternatives, independently and together. Inlcludes pagination/infinite scrolling (well, until the api's daily limit)", + "tags": ["HTML5", "CSS3", "JavaScript", "API"], + "netlify": "https://js-project-recipe-library.netlify.app", + "github": "https://github.com/osckli990/js-project-recipe-library", + "id": "p-3" + }, + { + "name": "Weather app", + "image": "/article-weather.avif", + "desc": "A mob-programming project and weather application that enables the user to retrieve weather information from any city, inlucing general location-tracking, through OpenWeather's API", + "tags": ["HTML5", "CSS3", "JavaScript", "TypeScript", "API"], + "netlify": "https://watherrr.netlify.app", + "github": "https://github.com/alex91-html/js-project-weather-app", + "id": "p-4" + }, + { + "name": "Accessibility site", + "image": "/article-acces.avif", + "desc": "Another project done in mob-programming. The site has a rudementary design but follows different guidelines to allow accessibility for different disabilities or conditions", + "tags": ["HTML5", "CSS3", "JavaScript", "WCAG", "W3C", "WAI"], + "netlify": "https://js-project-accessibility.netlify.app", + "github": "https://github.com/osckli990/js-project-accessibility", + "id": "p-5" + }, + { + "name": "Movie site", + "image": "/article-movie.avif", + "desc": "Multi-page movie site with a simple design, using React and API to fetch data from The Movie Database (TMDB). Includes a search function and pagination.", + "tags": ["HTML5", "CSS3", "JavaScript", "React.js", "API"], + "netlify": "https://js-movie-react-oscar.netlify.app", + "github": "https://github.com/osckli990/js-project-movies", + "id": "p-7" + }, + { + "name": "First project", + "image": "/article-first.avif", + "desc": "My first web project! A front-end project with an introduction to design, while utilizing a form with simple validation and site functions, like a dark-mode", + "tags": ["HTML5", "CSS3", "JavaScript"], + "netlify": "https://js-project-business-site.netlify.app", + "github": "https://github.com/osckli990/js-project-business-site", + "id": "p-8" } ] -} \ No newline at end of file +} diff --git a/src/data/skills.json b/src/data/skills.json new file mode 100644 index 00000000..d5cdadeb --- /dev/null +++ b/src/data/skills.json @@ -0,0 +1,37 @@ +{ + "skills": [ + { + "names": [ + "HTML5", + "CSS3", + "Javascript ES6", + "React", + "API's", + "Styled Components", + "Github" + ], + "title": "Code", + "id": "s-1" + }, + { + "names": ["Figma", "Slack"], + "title": "Toolbox", + "id": "s-2" + }, + { + "names": ["React Native"], + "title": "Upcoming", + "id": "s-3" + }, + { + "names": [ + "Agile Methodology", + "Development", + "Analysis", + "Problem solving" + ], + "title": "More", + "id": "s-4" + } + ] +} diff --git a/src/data/tech.json b/src/data/tech.json new file mode 100644 index 00000000..65675e1a --- /dev/null +++ b/src/data/tech.json @@ -0,0 +1,24 @@ +{ + "tech": [ + { + "names": ["HTML", "CSS", "Flexbox", "Web Accessibility"], + "title": "Basic", + "id": "t-1" + }, + { + "names": ["Javascript", "JSX", "Node.js", "Mongo DB", "API's"], + "title": "Backend", + "id": "t-2" + }, + { + "names": ["JavaScript", "React", "React Hooks"], + "title": "Frontend", + "id": "t-3" + }, + { + "names": ["Mob-programming", "Pair-programming", "Github", "Git"], + "title": "Methods", + "id": "t-4" + } + ] +} 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..f4d05e33 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,12 +1,18 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' +import React from "react"; -import { App } from './App.jsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; -import './index.css' +import { App } from "./App.jsx"; +// { } if exported directly in function +//else imporn App from... -createRoot(document.getElementById('root')).render( +import "./App.css"; + +createRoot(document.getElementById("root")).render( - , -) + +); + +//main is a recipe and should not be changed diff --git a/src/sections/DescComponent/Desc.jsx b/src/sections/DescComponent/Desc.jsx new file mode 100644 index 00000000..4d38c4b7 --- /dev/null +++ b/src/sections/DescComponent/Desc.jsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const Desc = ({ desc }) => { + return

{desc}

; +}; diff --git a/src/sections/FeaturedProjects.jsx b/src/sections/FeaturedProjects.jsx new file mode 100644 index 00000000..16c9cadb --- /dev/null +++ b/src/sections/FeaturedProjects.jsx @@ -0,0 +1,29 @@ +import React, { useState } from "react"; + +import { Title } from "./TitleComponents/Title"; +import { ProjectBox } from "./FeaturedProjectsComponents/ProjectBox"; +import { SeeMore } from "./SeeMoreComponent/SeeMore"; + +export const FeaturedProjects = () => { + let [articles, setArticles] = useState(4); + + const handleClick = () => { + setArticles(articles + 4); + }; + + return ( +
+ + <ProjectBox load={articles} /> + {articles === 4 && <SeeMore onClick={handleClick} />} + </section> + ); +}; + +// question ? do this : else this +//articles === 4 ? <seeMore/> : null + +//question && do this +//articles === 4 && <seeMore/> + +//ternary operator but tweaked when it should do nothing if false diff --git a/src/sections/FeaturedProjectsComponents/ProjectBox.jsx b/src/sections/FeaturedProjectsComponents/ProjectBox.jsx new file mode 100644 index 00000000..b993ffaf --- /dev/null +++ b/src/sections/FeaturedProjectsComponents/ProjectBox.jsx @@ -0,0 +1,43 @@ +import React, { useEffect } from "react"; + +import projects from "../../data/projects.json"; + +import { Image } from "./ProjectBoxComponents/Image"; +import { Tags } from "./ProjectBoxComponents/Tags"; +import { Desc } from "../DescComponent/Desc"; +import { Links } from "./ProjectBoxComponents/Links"; +import { Title } from "../TitleComponents/TitleH3"; + +import AOS from "aos"; +import "aos/dist/aos.css"; + +export const ProjectBox = ({ load }) => { + useEffect(() => { + AOS.init({ duration: 1000 }); + }, []); + console.log(load); + + return ( + <> + {projects.projects.slice(0, load).map((project, index) => ( + <section + key={project.id} + className="mb-[64px] xl:mb-[128px] xl:grid xl:grid-cols-2 xl:items-center xl:gap-[125px]" + data-aos="fade-up" + > + <Image index={index} url={project.image} /> + <section className=""> + <Tags tags={project.tags} /> + <article> + <Title title={project.name} /> + <Desc desc={project.desc} /> + </article> + <Links netlf={project.netlify} github={project.github} /> + </section> + </section> + ))} + </> + ); +}; + +//only show four items at first, then load four more. State? diff --git a/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Image.jsx b/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Image.jsx new file mode 100644 index 00000000..9a812121 --- /dev/null +++ b/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Image.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +export const Image = ({ index, url }) => { + const orderClass = index % 2 === 0 ? "xl:order-first" : "xl:order-last"; + + return ( + <figure className={`${orderClass} `}> + <img + src={url} + alt="Image of project" + aria-label="Image of project" + className="bg-cover bg-center bg-no-repeat sm:h-full rounded-[12px] xl:w-[749px] border-none" + /> + </figure> + ); +}; + +//object-fill self-stretch diff --git a/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Links.jsx b/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Links.jsx new file mode 100644 index 00000000..2c17c581 --- /dev/null +++ b/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Links.jsx @@ -0,0 +1,31 @@ +import React from "react"; + +import netlifyPic from "../../../assets/Ic-Web.svg"; +import githubPic from "../../../assets/Ic-Github.svg"; + +export const Links = ({ netlf, github }) => { + return ( + <section role="navigation"> + <div> + <a + href={netlf} + className="flex items-center bg-black text-white text-18 font-medium mb-[6px] py-0 px-[16px] rounded-[16px] h-[48px] w-[290px] lg:w-[303px] hover:underline" + > + <img src={netlifyPic} alt="" className="size-[30px] mr-[16px]" /> + Live demo + </a> + </div> + <div> + <a + href={github} + className="flex items-center bg-black text-white text-18 font-medium py-0 px-[16px] rounded-[16px] h-[48px] w-[290px] lg:w-[303px] hover:underline" + > + <img src={githubPic} alt="" className="size-[30px] mr-[16px]" /> + View code + </a> + </div> + </section> + ); +}; + +/*w-[303px] in bigger sizes */ diff --git a/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Tags.jsx b/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Tags.jsx new file mode 100644 index 00000000..d566dfa5 --- /dev/null +++ b/src/sections/FeaturedProjectsComponents/ProjectBoxComponents/Tags.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +export const Tags = ({ tags }) => { + return ( + <div> + <ul className="flex flex-wrap mt-[32px] sm:mt-[64px] mb-[16px] xl:mt-0"> + {tags.map((tag, index) => ( + <li + className="border-[2px] border-black px-[6px] py-[2px] mr-[4px] mb-[4px] rounded-[4px] basis-1" + key={index} + > + {tag} + </li> + ))} + </ul> + </div> + ); +}; diff --git a/src/sections/HiThere.jsx b/src/sections/HiThere.jsx new file mode 100644 index 00000000..6502fb8c --- /dev/null +++ b/src/sections/HiThere.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +import { Greeting } from "./HiThereComponents/Greeting"; +import { Introduction } from "./HiThereComponents/Introduction"; +import { Profile } from "./HiThereComponents/Profile"; +import { SecondGreeting } from "./HiThereComponents/SecondGreeting"; + +export const HiThere = () => { + return ( + <header + role="presentation" + aria-label="main presentation" + className="px-[16px] sm:px-[24px] lg:w-[782px] lg:mx-auto sm:text-center" + > + <Greeting /> + <Profile /> + <SecondGreeting /> + <Introduction /> + </header> + ); +}; diff --git a/src/sections/HiThereComponents/Greeting.jsx b/src/sections/HiThereComponents/Greeting.jsx new file mode 100644 index 00000000..81c5fdca --- /dev/null +++ b/src/sections/HiThereComponents/Greeting.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +export const Greeting = () => { + return ( + <div className="text-center" tabIndex="0"> + <h3 className="pt-[64px] xl:pt-[128px]">Hi there! I'm</h3> + <h1 className="pt-[16px]">Oscar Liljefors</h1> + <p className="pt-[16px] sm:hidden text-[24px] font-[500] xl:text-[30px]"> + Web developer with a background in gardening, healthcare, and + 3D-printing + </p> + </div> + ); +}; diff --git a/src/sections/HiThereComponents/Introduction.jsx b/src/sections/HiThereComponents/Introduction.jsx new file mode 100644 index 00000000..4984bde5 --- /dev/null +++ b/src/sections/HiThereComponents/Introduction.jsx @@ -0,0 +1,16 @@ +import React from "react"; + +export const Introduction = () => { + return ( + <p className="pb-[64px] xl:pb-[16px] text-16 font-normal" tabIndex="0"> + As a developer with a background in health and social care 💊, I have a + unique perspective on how technology can improve people's lives. I am + currently enrolled in a 32-week remote bootcamp at Technigo, where I am + learning JavaScript (ES6), TypeScript, React, HTML5, CSS, and server-side + programming with Node.js. I have also worked as a 3D technician at + Sculptur, where I designed furniture through CAD and launched tests on a + new filament. My goal is to combine my passion for technology and social + impact to create innovative solutions that make a difference. 🧠 + </p> + ); +}; diff --git a/src/sections/HiThereComponents/Profile.jsx b/src/sections/HiThereComponents/Profile.jsx new file mode 100644 index 00000000..aab1a94c --- /dev/null +++ b/src/sections/HiThereComponents/Profile.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +import profileImage from "../../assets/cropped_profile.avif"; + +export const Profile = () => { + return ( + <img + className="my-[16px] mx-auto min-h-[154px] size-2/3" + src={profileImage} + alt="Profile image" + aria-label="Profile image" + /> + ); +}; diff --git a/src/sections/HiThereComponents/SecondGreeting.jsx b/src/sections/HiThereComponents/SecondGreeting.jsx new file mode 100644 index 00000000..5226a898 --- /dev/null +++ b/src/sections/HiThereComponents/SecondGreeting.jsx @@ -0,0 +1,12 @@ +import React from "react"; + +export const SecondGreeting = () => { + return ( + <div className="text-center" tabIndex="0"> + <p className="pt-[16px] pb-[16px] hidden sm:block text-[24px] font-[500] xl:text-[30px]"> + Web developer with a background in gardening, healthcare, and + 3D-printing + </p> + </div> + ); +}; diff --git a/src/sections/LetsTalk.jsx b/src/sections/LetsTalk.jsx new file mode 100644 index 00000000..dc09b7e2 --- /dev/null +++ b/src/sections/LetsTalk.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +import { Title } from "./LetsTalkComponents/Title"; +import { Image } from "./LetsTalkComponents/Image"; +import { Info } from "./LetsTalkComponents/Info"; +import { Links } from "./LetsTalkComponents/Links"; + +export const LetsTalk = () => { + return ( + <section + aria-label="footer" + className="bg-black flex flex-col gap-y-[64px] py-[64px] px-[16px]" + > + <Title title="Let's Talk" className="mb-0" /> + <Image /> + <Info /> + <Links /> + </section> + ); +}; diff --git a/src/sections/LetsTalkComponents/Image.jsx b/src/sections/LetsTalkComponents/Image.jsx new file mode 100644 index 00000000..6aca7989 --- /dev/null +++ b/src/sections/LetsTalkComponents/Image.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +import profileImage from "../../assets/cropped_profile.avif"; + +export const Image = () => { + return ( + <img + src={profileImage} + alt="profile image" + aria-label="profile image" + className="mx-auto size-[164px]" + /> + ); +}; diff --git a/src/sections/LetsTalkComponents/Info.jsx b/src/sections/LetsTalkComponents/Info.jsx new file mode 100644 index 00000000..d7deeb9b --- /dev/null +++ b/src/sections/LetsTalkComponents/Info.jsx @@ -0,0 +1,17 @@ +import React from "react"; + +export const Info = () => { + return ( + <article className="mx-auto" tabIndex="0"> + <p className="text-[24px] text-white pb-[8px] sm:text-center"> + Oscar Liljefors + </p> + <p className="text-[24px] text-white pb-[8px] sm:text-center"> + +46 (0)70 366 33 71 + </p> + <p className="text-[24px] text-white sm:text-center"> + oscar.lf(at)live.se + </p> + </article> + ); +}; diff --git a/src/sections/LetsTalkComponents/Links.jsx b/src/sections/LetsTalkComponents/Links.jsx new file mode 100644 index 00000000..a4a9b1af --- /dev/null +++ b/src/sections/LetsTalkComponents/Links.jsx @@ -0,0 +1,29 @@ +import React from "react"; + +import LinkedInPic from "../../assets/Btn-linkedin.svg"; +import StackOverflowPic from "../../assets/Btn-stackoverflow.svg"; +import GithubPic from "../../assets/Ic-Github.svg"; + +export const Links = () => { + return ( + <div role="navigation" className="flex gap-[32px] mx-auto"> + <a href="www.linkedin.com/in/oscar-kling-liljefors-139474159"> + <img + src={LinkedInPic} + alt="Link to LinkedIn" + className="invert size-[32px]" + /> + </a> + <a href="https://github.com/osckli990"> + <img src={GithubPic} alt="Link to Github" className="size-[32px]" /> + </a> + <a href="https://stackoverflowteams.com/c/technigo/users/691/?tab=profile"> + <img + src={StackOverflowPic} + alt="Link to StackOverflow" + className="invert size-[32px]" + /> + </a> + </div> + ); +}; diff --git a/src/sections/LetsTalkComponents/Title.jsx b/src/sections/LetsTalkComponents/Title.jsx new file mode 100644 index 00000000..b8c7701e --- /dev/null +++ b/src/sections/LetsTalkComponents/Title.jsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const Title = ({ title }) => { + return <h2 className="text-center text-white">{title}</h2>; +}; diff --git a/src/sections/MyWords.jsx b/src/sections/MyWords.jsx new file mode 100644 index 00000000..1bce0be3 --- /dev/null +++ b/src/sections/MyWords.jsx @@ -0,0 +1,15 @@ +import React from "react"; + +import { Title } from "./TitleComponents/Title"; +import { ArticleBox } from "./MyWordsComponents/ArticleBox"; +import { SeeMore } from "./SeeMoreComponent/SeeMore"; + +export const MyWords = () => { + return ( + <section className="py-[64px] xl:py-[128px] px-[24px] xl:px-[128px] font-normal xl:flex xl:flex-col justify-center items-center"> + <Title title="My Words" /> + <ArticleBox /> + <SeeMore text="See more articles" /> + </section> + ); +}; diff --git a/src/sections/MyWordsComponents/ArticleBox.jsx b/src/sections/MyWordsComponents/ArticleBox.jsx new file mode 100644 index 00000000..1108332e --- /dev/null +++ b/src/sections/MyWordsComponents/ArticleBox.jsx @@ -0,0 +1,36 @@ +import React, { useEffect } from "react"; + +import mywords from "../../data/mywords.json"; + +import { Image } from "./ArticleBoxComponents/Image"; +import { Date } from "./ArticleBoxComponents/Date"; +import { Desc } from "../DescComponent/Desc"; +import { Link } from "./ArticleBoxComponents/Link"; +import { Title } from "../TitleComponents/TitleH3"; + +export const ArticleBox = () => { + useEffect(() => { + AOS.init({ duration: 1000 }); + }); + return ( + <> + {mywords.articles.slice(0, 4).map((article) => ( + <section + key={article.id} + className="mb-[64px] sm:grid sm:grid-cols-2 sm:items-center xl:items-start" + data-aos="fade-up" + > + <Image url={article.image} /> + <section className="sm:pl-[32px] xl:pl-0 h-full sm:flex sm:flex-col sm:justify-center"> + <Date date={article.date} /> + <article> + <Title title={article.title} /> + <Desc desc={article.desc} /> + </article> + <Link link={article.link} /> + </section> + </section> + ))} + </> + ); +}; diff --git a/src/sections/MyWordsComponents/ArticleBoxComponents/Date.jsx b/src/sections/MyWordsComponents/ArticleBoxComponents/Date.jsx new file mode 100644 index 00000000..3ee0a2fb --- /dev/null +++ b/src/sections/MyWordsComponents/ArticleBoxComponents/Date.jsx @@ -0,0 +1,11 @@ +import React from "react"; + +export const Date = ({ date }) => { + return ( + <div className="flex flex-wrap mt-[32px] sm:mt-0 mb-[16px]"> + <p className="border-[2px] border-black px-[6px] py-[2px] mr-[4px] mb-[4px] rounded-[4px] w-[124px] text-center"> + {date} + </p> + </div> + ); +}; diff --git a/src/sections/MyWordsComponents/ArticleBoxComponents/Image.jsx b/src/sections/MyWordsComponents/ArticleBoxComponents/Image.jsx new file mode 100644 index 00000000..e4e2d2d3 --- /dev/null +++ b/src/sections/MyWordsComponents/ArticleBoxComponents/Image.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +export const Image = ({ url }) => { + return ( + <figure className="xl:flex xl:justify-end xl:h-full xl:pr-[125px]"> + <img + src={url} + alt="Image of article" + aria-label="Image of article" + className=" bg-cover bg-center bg-no-repeat sm:h-full rounded-[12px] xl:w-[479px] " + /> + </figure> + ); +}; diff --git a/src/sections/MyWordsComponents/ArticleBoxComponents/Link.jsx b/src/sections/MyWordsComponents/ArticleBoxComponents/Link.jsx new file mode 100644 index 00000000..238ad1c9 --- /dev/null +++ b/src/sections/MyWordsComponents/ArticleBoxComponents/Link.jsx @@ -0,0 +1,19 @@ +import React from "react"; + +import linkPic from "../../../assets/Ic-Web.svg"; + +export const Link = ({ link }) => { + return ( + <div role="navigation"> + <a + href={link} + className="flex items-center bg-black text-white text-18 font-medium py-0 px-[16px] rounded-[16px] h-[48px] w-[270px] lg:w-[303px] hover:underline" + > + <img src={linkPic} alt="" className="size-[30px] mr-[16px]" /> + Read article + </a> + </div> + ); +}; + +/*w-[302px] when in larger screens*/ diff --git a/src/sections/SeeMoreComponent/SeeMore.jsx b/src/sections/SeeMoreComponent/SeeMore.jsx new file mode 100644 index 00000000..af530823 --- /dev/null +++ b/src/sections/SeeMoreComponent/SeeMore.jsx @@ -0,0 +1,55 @@ +import React from "react"; +import styled from "styled-components"; + +import seeMore from "../../assets/Ic-ArrowDown.svg"; +import { media } from "../../Breakpoints"; + +const StyledSeeMore = styled.button` + padding: 0px 16px; + background-color: white; + color: black; + border: 2px outset black; + text-decoration: none; + border-radius: 12px; + cursor: pointer; + margin: 0 auto; + transition: 0.3s linear; + display: flex; + font-weight: 500; + font-size: 18px; + height: 48px; + align-items: center; + + &:hover { + text-decoration: underline; + } + + @media ${media.smalldesktop} { + margin-top: 64px; + } +`; + +const StyledImg = styled.img` + margin-right: 16px; + height: 30px; + width: 30px; +`; + +export const SeeMore = ({ text = "See more projects", onClick }) => { + return ( + <StyledSeeMore + aria-label="button to more projects" + onClick={onClick} + data-aos="fade-up" + > + <StyledImg + src={seeMore} + alt="down-arrow icon" + aria-label="presentation" + /> + {text} + </StyledSeeMore> + ); +}; + +//a styled component. do i like them? i think not diff --git a/src/sections/Skills.jsx b/src/sections/Skills.jsx new file mode 100644 index 00000000..12185d9a --- /dev/null +++ b/src/sections/Skills.jsx @@ -0,0 +1,42 @@ +/* + +import React from "react"; + +import skills from "../data/skills.json"; + +import { SkillBox } from "./SkillsComponents/SkillBox"; +import { Title } from "./TitleInWhite"; + +export const Skills = () => { + return ( + <section className="bg-black flex flex-col gap-y-4 pt-14 pb-14"> + <Title title="Skills" /> + {skills.skills.map((category) => ( + <div className="text-white w-1/2 mx-auto text-16 font-normal"> + <SkillBox + key={category.id} + skill={category.names} + title={category.title} + /> + </div> + ))} + </section> + ); +}; +*/ +import React from "react"; + +import skills from "../data/skills.json"; + +import { CategorySection } from "./SkillsTechComponents/CategorySection"; +import { CategoryBox } from "./SkillsTechComponents/CategoryBox"; + +export const Skills = () => ( + <CategorySection + title="Skills" + data={skills.skills} + RenderBox={({ title, items }) => ( + <CategoryBox title={title} items={items} /> + )} + /> +); diff --git a/src/sections/SkillsTechComponents/CategoryBox.jsx b/src/sections/SkillsTechComponents/CategoryBox.jsx new file mode 100644 index 00000000..a8dd99d4 --- /dev/null +++ b/src/sections/SkillsTechComponents/CategoryBox.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +export const CategoryBox = ({ title, items }) => { + return ( + <> + <p className="text-white text-center border border-solid rounded-md block mb-[16px] h-[28px] w-[177px]"> + {title} + </p> + <ul> + {items.map((item, index) => ( + <li key={index} className="text-white text-center xl:text-left"> + {item} + </li> + ))} + </ul> + </> + ); +}; diff --git a/src/sections/SkillsTechComponents/CategorySection.jsx b/src/sections/SkillsTechComponents/CategorySection.jsx new file mode 100644 index 00000000..fc2ed714 --- /dev/null +++ b/src/sections/SkillsTechComponents/CategorySection.jsx @@ -0,0 +1,37 @@ +import React from "react"; + +import { Title } from "../TitleComponents/TitleInWhite"; + +import { useEffect } from "react"; + +//aos +import AOS from "aos"; +import "aos/dist/aos.css"; + +export const CategorySection = ({ title, data, RenderBox }) => { + useEffect(() => { + AOS.init({ duration: 1000 }); + }); + + return ( + <section + className="bg-black flex flex-col pt-[64px] xl:tp-[128px] pb-[64px] xl:pb-[128px] xl:items-center " + tabIndex="0" + > + <Title title={title} /> + <div className="flex flex-col xl:flex-row"> + {data.map((category) => ( + <div + key={category.id} + className="text-white mx-auto xl:ml-0 xl:mr-[16px] text-16 font-normal mb-[24px] xl:mb-0" + data-aos="fade-up" + > + <RenderBox items={category.names} title={category.title} /> + </div> + ))} + </div> + </section> + ); +}; + +//360 - 24 diff --git a/src/sections/Tech.jsx b/src/sections/Tech.jsx new file mode 100644 index 00000000..e55f5aa4 --- /dev/null +++ b/src/sections/Tech.jsx @@ -0,0 +1,42 @@ +/* +import React from "react"; + +import tech from "../data/tech.json"; + +import { TechBox } from "./TechComponents/TechBox"; +import { Title } from "./TitleInWhite"; + +export const Tech = () => { + return ( + <section className="bg-black flex flex-col pt-[64px] pb-[64px]"> + <Title title="Tech" /> + {tech.tech.map((category) => ( + <div className="text-white w-1/2 mx-auto text-16 font-normal mb-[24px]"> + <TechBox + key={category.id} + tech={category.names} + title={category.title} + /> + </div> + ))} + </section> + ); +}; +*/ + +import React from "react"; + +import tech from "../data/tech.json"; + +import { CategorySection } from "./SkillsTechComponents/CategorySection"; +import { CategoryBox } from "./SkillsTechComponents/CategoryBox"; + +export const Tech = () => ( + <CategorySection + title="Tech" + data={tech.tech} + RenderBox={({ title, items }) => ( + <CategoryBox title={title} items={items} /> + )} + /> +); diff --git a/src/sections/TitleComponents/Title.jsx b/src/sections/TitleComponents/Title.jsx new file mode 100644 index 00000000..0c04afa3 --- /dev/null +++ b/src/sections/TitleComponents/Title.jsx @@ -0,0 +1,18 @@ +import React, { useEffect } from "react"; + +import AOS from "aos"; +import "aos/dist/aos.css"; + +export const Title = ({ title }) => { + useEffect(() => { + AOS.init({ duration: 1000 }); + }); + + return ( + <h2 className="text-center pb-[64px] xl:pb-[128px] " data-aos="fade-up"> + {title} + </h2> + ); +}; + +//mb-3?? diff --git a/src/sections/TitleComponents/TitleH3.jsx b/src/sections/TitleComponents/TitleH3.jsx new file mode 100644 index 00000000..74f0f203 --- /dev/null +++ b/src/sections/TitleComponents/TitleH3.jsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const Title = ({ title }) => { + return <h3 className="mb-[16px]">{title}</h3>; +}; diff --git a/src/sections/TitleComponents/TitleInWhite.jsx b/src/sections/TitleComponents/TitleInWhite.jsx new file mode 100644 index 00000000..50e05835 --- /dev/null +++ b/src/sections/TitleComponents/TitleInWhite.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +import { useEffect } from "react"; + +import AOS from "aos"; +import "aos/dist/aos.css"; + +export const Title = ({ title }) => { + useEffect(() => { + AOS.init({ duration: 1000 }); + }); + + return ( + <h2 className="text-center text-white mb-[16px]" data-aos="fade-up"> + {title} + </h2> + ); +}; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..de475d87 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [require("tailwindcss-motion")], +}; diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 00000000..ee72373a --- /dev/null +++ b/test/setup.js @@ -0,0 +1,8 @@ +import { afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; + +// runs a clean after each test case (e.g. clearing jsdom) +afterEach(() => { + cleanup(); +}); diff --git a/vite.config.js b/vite.config.js index 8b0f57b9..1d7a34b2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,7 +1,20 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +// 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 }]], + }, + }), + ], + test: { + // 👋 add the line below to add jsdom to vite + environment: "jsdom", + + globals: true, + setupFiles: "./test/setup.js", // assuming the test folder is in the root of our project + }, +});