Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d791e4ffcc | |||
| fe38d6af18 | |||
| 09cc4cb09b | |||
| 660f59530a | |||
| 8e241cc6e9 | |||
| 11a432a2c1 | |||
| 0619ff0693 | |||
| 6b263f340e | |||
| 8ff494c616 | |||
| 98251fcf78 | |||
| 2ce2aa3d23 | |||
| 44c68e68e9 | |||
| ec35f4c954 | |||
| 6a6c24ab20 | |||
| 055975162a | |||
| bae2d2b680 | |||
| 69d4b4f59e | |||
| 1d4d70c26c | |||
| a75027d8fa | |||
| f32c26bb7d | |||
| 1668da060a | |||
| 019b1ad4e7 | |||
| 85f23f5288 | |||
| 51fb14784d | |||
| 4a4372a36d | |||
|
|
21f20a93a3 | ||
|
|
b34b33215b | ||
|
|
368dce10e7 |
@ -7,12 +7,12 @@ jobs:
|
|||||||
runs-on: all
|
runs-on: all
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
- name: Declare variables
|
- name: Declare variables
|
||||||
run: |
|
run: |
|
||||||
echo "sha_short=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV"
|
echo "sha_short=$(git rev-parse --short "$GITHUB_SHA")" >> "$GITHUB_ENV"
|
||||||
echo "run_name=$(echo "$GITHUB_REPOSITORY" | sed "s/\//_/")" >> "$GITHUB_ENV"
|
echo "run_name=$(echo "$GITHUB_REPOSITORY" | sed "s/\//_/")" >> "$GITHUB_ENV"
|
||||||
echo "domain_name="www.mariia.art"" >> "$GITHUB_ENV"
|
echo "domain_name="www.msweb.io"" >> "$GITHUB_ENV"
|
||||||
echo "docker_port="80"" >> "$GITHUB_ENV"
|
echo "docker_port="80"" >> "$GITHUB_ENV"
|
||||||
- name: Build docker
|
- name: Build docker
|
||||||
run: |
|
run: |
|
||||||
@ -21,8 +21,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker container rm -f ${{ env.run_name }} || true
|
docker container rm -f ${{ env.run_name }} || true
|
||||||
docker run -d --network=web --restart unless-stopped --name ${{ env.run_name }} ${{ gitea.repository }}:${{ env.sha_short }}
|
docker run -d --network=web --restart unless-stopped --name ${{ env.run_name }} ${{ gitea.repository }}:${{ env.sha_short }}
|
||||||
/usr/local/bin/caddycontrol -host ${{ env.domain_name }} -dial ${{ env.run_name }}:${{ env.docker_port }}
|
/usr/bin/caddyconf --add --url ${{ env.domain_name }} --name ${{ env.run_name }} --port ${{ env.docker_port }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
Dockerfile
@ -1,14 +1,12 @@
|
|||||||
FROM node:17.1.0-alpine3.12 as builder
|
FROM node:20-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json ./
|
COPY package*.json ./
|
||||||
COPY package-lock.json ./
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY ./ ./
|
COPY . .
|
||||||
RUN NODE_ENV=production npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
FROM caddy:2-alpine
|
FROM caddy:2-alpine
|
||||||
WORKDIR /usr/share/caddy
|
WORKDIR /usr/share/caddy
|
||||||
COPY Caddyfile /etc/caddy/Caddyfile
|
COPY Caddyfile /etc/caddy/Caddyfile
|
||||||
COPY --from=builder ./app/dist .
|
COPY --from=builder /app/dist .
|
||||||
|
|
||||||
|
|||||||
30
index.html
@ -2,27 +2,43 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=5.0"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Mariia Shabelnik - Frontend Engineer portfolio showcasing web development projects and skills"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#f0f0f3" />
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22>
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22>, <text y=%22.9em%22 font-size=%2290%22>
|
||||||
<text y=%22.9em%22 font-size=%2290%22>
|
🫰
|
||||||
👋
|
|
||||||
</text>
|
</text>
|
||||||
</svg>"
|
</svg>"
|
||||||
/>
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<!-- nav font -->
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<!-- body font -->
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Jura:wght@300..700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<!-- headline-title font -->
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=Tektur:wght@400;500;600;700;800;900&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Sora:wght@100..800&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<!-- logo font -->
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Sixtyfour&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
4696
package-lock.json
generated
@ -9,11 +9,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.9.7",
|
||||||
"hamburger-react": "^2.5.0",
|
"hamburger-react": "^2.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"react-image-gallery": "^1.3.0",
|
"react-image-gallery": "^1.3.0",
|
||||||
|
"react-intersection-observer": "^9.16.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-scroll-motion": "^0.3.0",
|
"react-scroll-motion": "^0.3.0",
|
||||||
"recoil": "^0.7.7"
|
"recoil": "^0.7.7"
|
||||||
@ -21,10 +23,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react": "^2.0.0",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
"postcss": "^8.4.29",
|
"postcss": "^8.4.29",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"vite": "^3.0.0"
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/images/datatjej-it-placeholder.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
public/images/datatjej-placeholder.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
public/img/ProfileMe.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
1
public/img/icons8-linkedin.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#0288D1" d="M42,37c0,2.762-2.238,5-5,5H11c-2.761,0-5-2.238-5-5V11c0-2.762,2.239-5,5-5h26c2.762,0,5,2.238,5,5V37z"/><path fill="#FFF" d="M12 19H17V36H12zM14.485 17h-.028C12.965 17 12 15.888 12 14.499 12 13.08 12.995 12 14.514 12c1.521 0 2.458 1.08 2.486 2.499C17 15.887 16.035 17 14.485 17zM36 36h-5v-9.099c0-2.198-1.225-3.698-3.192-3.698-1.501 0-2.313 1.012-2.707 1.99C24.957 25.543 25 26.511 25 27v9h-5V19h5v2.616C25.721 20.5 26.85 19 29.738 19c3.578 0 6.261 2.25 6.261 7.274L36 36 36 36z"/></svg>
|
||||||
|
After Width: | Height: | Size: 598 B |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 375 KiB |
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 641 KiB After Width: | Height: | Size: 139 KiB |
BIN
public/img/mockup_net0.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 584 KiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 546 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 234 KiB |
BIN
public/img/oreo-logo.png
Normal file
|
After Width: | Height: | Size: 980 B |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.4 KiB |
11
src/App.jsx
@ -1,20 +1,18 @@
|
|||||||
//import "./App.css";
|
//import "./App.css";
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
import Home from "./pages/Home";
|
|
||||||
import Experience from "./pages/Experience";
|
|
||||||
import Contact from "./pages/Contact";
|
|
||||||
import Footer from "./components/Footer";
|
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
import ExperianceDetail from "./pages/ExperienceDetail";
|
import ExperianceDetail from "./pages/ExperienceDetail";
|
||||||
import FullPage from "./pages/FullPage";
|
import FullPage from "./pages/FullPage";
|
||||||
import { RecoilRoot } from "recoil";
|
import { RecoilRoot } from "recoil";
|
||||||
import ScrollToAnchor from "./components/ScrollToAnchor";
|
import ScrollToAnchor from "./components/ScrollToAnchor";
|
||||||
|
import AnimatedBackground from "./components/AnimatedBackground";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="text-white">
|
<div className="text-gray-600">
|
||||||
|
<AnimatedBackground />
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<main className=" min-h-screen">
|
<main className="min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
<ScrollToAnchor />
|
<ScrollToAnchor />
|
||||||
<Routes>
|
<Routes>
|
||||||
@ -23,7 +21,6 @@ function App() {
|
|||||||
<Route path="/contact" element={<Contact />} />*/}
|
<Route path="/contact" element={<Contact />} />*/}
|
||||||
<Route path="/experience/:id" element={<ExperianceDetail />} />
|
<Route path="/experience/:id" element={<ExperianceDetail />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
97
src/components/AnimatedBackground.jsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
const AnimatedBackground = () => {
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
let animationFrameId;
|
||||||
|
let lastScrollY = 0;
|
||||||
|
|
||||||
|
// Set canvas size
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
canvas.width = window.innerWidth;
|
||||||
|
canvas.height = window.innerHeight;
|
||||||
|
};
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
|
||||||
|
// Create dots
|
||||||
|
const dots = [];
|
||||||
|
const dotCount = 80; // Reduced from 100 for smoother performance
|
||||||
|
for (let i = 0; i < dotCount; i++) {
|
||||||
|
dots.push({
|
||||||
|
x: Math.random() * canvas.width,
|
||||||
|
y: Math.random() * canvas.height,
|
||||||
|
size: Math.random() * 2 + 1,
|
||||||
|
speed: Math.random() * 0.3 + 0.1, // Reduced speed for smoother movement
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
const animate = () => {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw dots
|
||||||
|
ctx.fillStyle = 'rgba(150, 150, 150, 0.3)'; // Increased from 0.2
|
||||||
|
dots.forEach(dot => {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(dot.x, dot.y, dot.size * 1.2, 0, Math.PI * 2); // Increased size
|
||||||
|
ctx.fill();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Draw connections
|
||||||
|
ctx.strokeStyle = 'rgba(150, 150, 150, 0.15)'; // Increased from 0.1
|
||||||
|
ctx.lineWidth = 1; // Increased from 0.8
|
||||||
|
dots.forEach((dot1, i) => {
|
||||||
|
dots.slice(i + 1).forEach(dot2 => {
|
||||||
|
const dx = dot1.x - dot2.x;
|
||||||
|
const dy = dot1.y - dot2.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (distance < 180) { // Increased connection distance
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(dot1.x, dot1.y);
|
||||||
|
ctx.lineTo(dot2.x, dot2.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle scroll
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const scrollDiff = scrollY - lastScrollY;
|
||||||
|
|
||||||
|
dots.forEach(dot => {
|
||||||
|
dot.y += scrollDiff * dot.speed * 0.15; // Reduced movement speed
|
||||||
|
if (dot.y > canvas.height) dot.y = 0;
|
||||||
|
if (dot.y < 0) dot.y = canvas.height;
|
||||||
|
});
|
||||||
|
|
||||||
|
lastScrollY = scrollY;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', resizeCanvas);
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className="fixed top-0 left-0 w-full h-full pointer-events-none -z-10"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnimatedBackground;
|
||||||
@ -1,10 +1,35 @@
|
|||||||
|
import { FaLinkedin, FaEnvelope } from "react-icons/fa";
|
||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
const fullYear = new Date().getFullYear();
|
const fullYear = new Date().getFullYear();
|
||||||
|
const email = "mariia.shabelnik@gmail.com"; // Replace with your email
|
||||||
|
const linkedinUrl = "https://www.linkedin.com/in/mariia-shabelnik/"; // Replace with your LinkedIn URL
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="container mx-auto my-10 px-6 sticky top-[95vh]">
|
<footer className="container mx-auto my-10 px-6 sticky top-[95vh]">
|
||||||
<div className=" text-center text-sm text-white/50 ">
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<a
|
||||||
|
href={`mailto:${email}`}
|
||||||
|
className="text-gray-500 hover:text-highlight transition-colors duration-300"
|
||||||
|
aria-label="Send email to Mariia"
|
||||||
|
>
|
||||||
|
<FaEnvelope className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={linkedinUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-gray-500 hover:text-highlight transition-colors duration-300"
|
||||||
|
aria-label="Visit Mariia's LinkedIn profile"
|
||||||
|
>
|
||||||
|
<FaLinkedin className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-sm text-gray-500">
|
||||||
© {fullYear} Mariia Shabelnik, all rights reserved
|
© {fullYear} Mariia Shabelnik, all rights reserved
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,17 +13,17 @@ function Header() {
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const menu = [
|
const menu = [
|
||||||
{ link: "/#about", title: ".me()" },
|
{ link: "/#about", title: ".about( )" },
|
||||||
{ link: "/#experience", title: ".experience()" },
|
{ link: "/#experience", title: ".projects( )" },
|
||||||
{ link: "/#contact", title: ".contact()" },
|
{ link: "/#contact", title: ".contact( )" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const menuUI = menu.map((item) => {
|
const menuUI = menu.map((item) => {
|
||||||
let className = "drop-shadow-light hover:drop-shadow-doublelight";
|
let className = "drop-shadow-doublelight hover:drop-shadow-active";
|
||||||
if (location.pathname === item.link) {
|
if (`/${location.hash}` === item.link) {
|
||||||
className += " text-white";
|
className += " text-white";
|
||||||
} else {
|
} else {
|
||||||
className += " text-white/40";
|
className += " text-gray-500";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li className="my-10 text-xl md:my-0 md:text-base" key={item.link}>
|
<li className="my-10 text-xl md:my-0 md:text-base" key={item.link}>
|
||||||
@ -33,6 +33,9 @@ function Header() {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
to={item.link}
|
to={item.link}
|
||||||
|
aria-current={
|
||||||
|
location.hash === item.link.slice(2) ? "page" : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
@ -40,10 +43,19 @@ function Header() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerClasses = ["sticky", "top-0", "z-40", "backdrop-blur-md", "h-16"];
|
const headerClasses = [
|
||||||
|
"sticky",
|
||||||
|
"top-0",
|
||||||
|
"z-40",
|
||||||
|
"backdrop-blur-sm",
|
||||||
|
"h-16",
|
||||||
|
"border-b-[1px]",
|
||||||
|
"border-white",
|
||||||
|
"rounded-b-[1rem]",
|
||||||
|
];
|
||||||
const logoClasses = ["hover:drop-shadow-light"];
|
const logoClasses = ["hover:drop-shadow-light"];
|
||||||
const overlayMenu = [
|
const overlayMenu = [
|
||||||
"bg-black/90",
|
"bg-white/90",
|
||||||
"z-50",
|
"z-50",
|
||||||
"fixed",
|
"fixed",
|
||||||
"left-0",
|
"left-0",
|
||||||
@ -54,37 +66,64 @@ function Header() {
|
|||||||
"p-2",
|
"p-2",
|
||||||
];
|
];
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
headerClasses.push("bg-black/90");
|
headerClasses.push("bg-white/70");
|
||||||
logoClasses.push("drop-shadow-yellow");
|
logoClasses.push("drop-shadow-gray");
|
||||||
} else {
|
} else {
|
||||||
headerClasses.push("bg-bgColor/90");
|
headerClasses.push("bg-bgColor/70");
|
||||||
logoClasses.push("drop-shadow-doublelight");
|
logoClasses.push("drop-shadow-active");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className={overlayMenu.join(" ")}>
|
<nav
|
||||||
<div className=" h-[calc(100vh-20rem)] flex flex-col justify-center">
|
className={overlayMenu.join(" ")}
|
||||||
<ul>{menuUI}</ul>
|
aria-label="Mobile navigation menu"
|
||||||
</div>
|
role="navigation"
|
||||||
|
>
|
||||||
|
<div className="h-[calc(100vh-20rem)] flex flex-col justify-center">
|
||||||
|
<ul role="menu" aria-label="Mobile menu items">
|
||||||
|
{menuUI}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
)}
|
)}
|
||||||
<header className={headerClasses.join(" ")}>
|
<header className={headerClasses.join(" ")}>
|
||||||
<div className="container mx-auto h-full">
|
<div className="container mx-auto h-full max-w-7xl">
|
||||||
<nav className="flex items-center h-full px-2">
|
<nav
|
||||||
<div className="flex-none text-3xl md:text-4xl font-black font-headline ">
|
className="flex items-center h-full px-10"
|
||||||
<Link className={logoClasses.join(" ")} to="/#start">
|
aria-label="Main navigation"
|
||||||
MS.
|
role="navigation"
|
||||||
|
>
|
||||||
|
<div className="flex-none text-3xl md:text-4xl font-logo text-[#6d6d6d]">
|
||||||
|
<Link
|
||||||
|
className={logoClasses.join(" ")}
|
||||||
|
to="/#start"
|
||||||
|
aria-label="Home"
|
||||||
|
>
|
||||||
|
MS
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grow"></div>
|
<div className="grow"></div>
|
||||||
<div className="flex-none hidden md:block ">
|
<div className="flex-none hidden md:block">
|
||||||
<ul className="flex flex-row gap-4 text-lg">{menuUI}</ul>
|
<ul
|
||||||
|
className="flex flex-row gap-4 text-lg"
|
||||||
|
role="menu"
|
||||||
|
aria-label="Main menu items"
|
||||||
|
>
|
||||||
|
{menuUI}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none block md:hidden">
|
<div className="flex-none block md:hidden">
|
||||||
<Hamburger toggled={isOpen} toggle={setOpen} duration={0.9} />
|
<Hamburger
|
||||||
|
toggled={isOpen}
|
||||||
|
toggle={setOpen}
|
||||||
|
duration={0.9}
|
||||||
|
aria-label="Toggle mobile menu"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
20
src/components/LeftNav.jsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { GoChevronLeft } from "react-icons/go";
|
||||||
|
|
||||||
|
const LeftNav = React.memo(({ disabled, onClick }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-gallery-icon image-gallery-left-nav text-2xl md:text-4xl text-highlight"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label="Previous Slide"
|
||||||
|
>
|
||||||
|
<GoChevronLeft />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
LeftNav.displayName = "LeftNav";
|
||||||
|
|
||||||
|
export default LeftNav;
|
||||||
25
src/components/PlayPause.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
// import { bool, func } from "prop-types";
|
||||||
|
// import SVG from "src/components/SVG";
|
||||||
|
|
||||||
|
const PlayPause = React.memo(({ isPlaying, onClick }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-gallery-icon image-gallery-play-button"
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label="Play or Pause Slideshow"
|
||||||
|
>
|
||||||
|
<SVG strokeWidth={2} icon={isPlaying ? "pause" : "play"} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
PlayPause.displayName = "PlayPause";
|
||||||
|
|
||||||
|
// PlayPause.propTypes = {
|
||||||
|
// isPlaying: bool.isRequired,
|
||||||
|
// onClick: func.isRequired,
|
||||||
|
// };
|
||||||
|
|
||||||
|
export default PlayPause;
|
||||||
19
src/components/RightNav.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { GoChevronRight } from "react-icons/go";
|
||||||
|
const RightNav = React.memo(({ disabled, onClick }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-gallery-icon image-gallery-right-nav text-2xl md:text-4xl text-highlight"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
aria-label="Next Slide"
|
||||||
|
>
|
||||||
|
<GoChevronRight />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
RightNav.displayName = "RightNav";
|
||||||
|
|
||||||
|
export default RightNav;
|
||||||
@ -9,10 +9,17 @@ function ScrollToAnchor() {
|
|||||||
// https://jasonwatmore.com/react-router-v6-listen-to-location-route-change-without-history-listen
|
// https://jasonwatmore.com/react-router-v6-listen-to-location-route-change-without-history-listen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.hash) {
|
if (location.hash) {
|
||||||
lastHash.current = location.hash.slice(1); // safe hash for further use after navigation
|
lastHash.current = location.hash.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastHash.current && document.getElementById(lastHash.current)) {
|
if (lastHash.current === "contact") {
|
||||||
|
// Scroll to bottom of the page
|
||||||
|
window.scrollTo({
|
||||||
|
top: document.documentElement.scrollHeight,
|
||||||
|
behavior: "smooth"
|
||||||
|
});
|
||||||
|
lastHash.current = "";
|
||||||
|
} else if (lastHash.current && document.getElementById(lastHash.current)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document
|
document
|
||||||
.getElementById(lastHash.current)
|
.getElementById(lastHash.current)
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
function Tags({ listOfTags }) {
|
function Tags({ listOfTags }) {
|
||||||
const tagList = listOfTags.map((itemTag, keyTag) => {
|
const tagList = listOfTags.map((itemTag, keyTag) => {
|
||||||
return (
|
return (
|
||||||
<div className={`bg-gray-800 py-1 px-2 rounded-xl `} key={keyTag}>
|
<div
|
||||||
|
className="bg-gray-500 py-1 px-2 rounded-xl shadow-md hover:shadow-lg transition-shadow duration-200 hover:bg-gray-600"
|
||||||
|
key={keyTag}
|
||||||
|
>
|
||||||
{itemTag}
|
{itemTag}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div className="text-tags flex flex-wrap gap-2 font-tags text-highlight ">
|
<div className="text-tags flex flex-wrap gap-2 font-tags text-white">
|
||||||
{tagList}
|
{tagList}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
body {
|
body {
|
||||||
background-color: #031417;
|
background-color: #f0f0f3;
|
||||||
}
|
}
|
||||||
::selection {
|
::selection {
|
||||||
background: #7d9c00b1;
|
background: #caebfa;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +1,115 @@
|
|||||||
import { useRecoilValue } from "recoil";
|
import { useRecoilValue } from "recoil";
|
||||||
import Tags from "../components/Tags";
|
import Tags from "../components/Tags";
|
||||||
import { skillsAtom } from "../store";
|
import { skillsAtom } from "../store";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
function About() {
|
function About() {
|
||||||
const skills = useRecoilValue(skillsAtom);
|
const skills = useRecoilValue(skillsAtom);
|
||||||
console.log(skills);
|
const [ref, inView] = useInView({
|
||||||
|
triggerOnce: true,
|
||||||
|
threshold: 0.1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "easeOut",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<section className="relative" aria-labelledby="about-heading">
|
||||||
<div id="about" className=" absolute -top-16 "></div>
|
<div id="about" className="absolute"></div>
|
||||||
<h2 className="mb-2 text-subTPhone md:text-subT font-headline">
|
<div className="min-h-screen flex items-center">
|
||||||
About<span className=" text-highlight">.</span>
|
<motion.div
|
||||||
</h2>
|
ref={ref}
|
||||||
<div className="flex flex-col md:flex-row">
|
variants={containerVariants}
|
||||||
<div className="basis-2/3 text-base">
|
initial="hidden"
|
||||||
|
animate={inView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<motion.h2
|
||||||
|
variants={itemVariants}
|
||||||
|
id="about-heading"
|
||||||
|
className="mb-6 text-subTPhone md:text-subT font-headline"
|
||||||
|
>
|
||||||
|
About me<span className="text-highlight">.</span>
|
||||||
|
</motion.h2>
|
||||||
|
<motion.div variants={itemVariants} className="flex flex-col md:flex-row gap-6">
|
||||||
|
<motion.article variants={itemVariants} className="basis-6/12 text-base font-body">
|
||||||
<p>
|
<p>
|
||||||
As a prospective Frontend Developer, I seek a challenging career
|
"I'm a Stockholm-based developer passionate about building
|
||||||
|
innovative, secure digital experiences with a focus on both
|
||||||
|
frontend and backend technologies. With expertise in JavaScript,
|
||||||
|
React, TypeScript, and a keen eye for web design, I create
|
||||||
|
solutions that combine functionality with creativity. I use
|
||||||
|
Node.js for backend development and SQL for database management,
|
||||||
|
ensuring my applications are both robust and scalable. I also
|
||||||
|
have a deep passion for 3D design, which adds a unique,
|
||||||
|
immersive dimension to my work. I thrive in collaborative
|
||||||
|
environments that encourage continuous learning and value
|
||||||
|
creative problem-solving. Let's collaborate to push the
|
||||||
|
boundaries of digital innovation!"
|
||||||
|
{/* As a prospective Frontend Developer, I seek a challenging career
|
||||||
opportunity in the IT industry, where I can collaborate with a
|
opportunity in the IT industry, where I can collaborate with a
|
||||||
dynamic team, continually learn, and foster innovation. My
|
dynamic team, continually learn, and foster innovation. My
|
||||||
dedication lies in transforming design concepts into user-friendly
|
dedication lies in transforming design concepts into
|
||||||
experiences and achieving organizational goals through creativity
|
user-friendly experiences and achieving organizational goals
|
||||||
and teamwork. I aspire to engage in work that allows the utilization
|
through creativity and teamwork. I aspire to engage in work that
|
||||||
of technical and creative skills to contribute effectively to the
|
allows the utilization of technical and creative skills to
|
||||||
growth of an organization. If you think you've got an opening that I
|
contribute effectively to the growth of an organization. If you
|
||||||
might like, let's connect 🔗
|
think you've got an opening that I might like, let's connect 🔗 */}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.article>
|
||||||
<div className=" basis-1/3">
|
<motion.figure variants={itemVariants} className="border">
|
||||||
<h4 className="mb-2 text-subTMini">Languages:</h4>
|
<img
|
||||||
|
className="rounded-3xl"
|
||||||
|
src="/img/ProfileMe.jpg"
|
||||||
|
alt="Mariia Shabelnik's profile photo"
|
||||||
|
/>
|
||||||
|
</motion.figure>
|
||||||
|
<motion.aside variants={itemVariants} className="basis-1/3" aria-labelledby="skills-heading">
|
||||||
|
<h2 id="skills-heading" className="sr-only">
|
||||||
|
Skills and Technologies
|
||||||
|
</h2>
|
||||||
|
<section aria-labelledby="languages-heading">
|
||||||
|
<h3 id="languages-heading" className="mb-2 text-subTMini">
|
||||||
|
Languages:
|
||||||
|
</h3>
|
||||||
<Tags listOfTags={skills.languages} />
|
<Tags listOfTags={skills.languages} />
|
||||||
<h4 className="my-2 text-subTMini">Frameworks:</h4>{" "}
|
</section>
|
||||||
|
<section aria-labelledby="frameworks-heading">
|
||||||
|
<h3 id="frameworks-heading" className="my-2 text-subTMini">
|
||||||
|
Frameworks:
|
||||||
|
</h3>
|
||||||
<Tags listOfTags={skills.frameworks} />
|
<Tags listOfTags={skills.frameworks} />
|
||||||
<h4 className="my-2 text-subTMini">Tools I use:</h4>{" "}
|
</section>
|
||||||
|
<section aria-labelledby="tools-heading">
|
||||||
|
<h3 id="tools-heading" className="my-2 text-subTMini">
|
||||||
|
Tools I use:
|
||||||
|
</h3>
|
||||||
<Tags listOfTags={skills.tools} />
|
<Tags listOfTags={skills.tools} />
|
||||||
|
</section>
|
||||||
|
</motion.aside>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,8 @@ function Contact() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div id="contact" className=" absolute -top-16 "></div>
|
<div id="contact" className=" absolute -top-16 "></div>
|
||||||
<h1>Welcome to C#ontact</h1>
|
<h1>Contact me</h1>
|
||||||
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,62 +2,105 @@ import { Link } from "react-router-dom";
|
|||||||
import { useRecoilValue } from "recoil";
|
import { useRecoilValue } from "recoil";
|
||||||
import { projectsAtom } from "../store";
|
import { projectsAtom } from "../store";
|
||||||
import Tags from "../components/Tags";
|
import Tags from "../components/Tags";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
function Experiance() {
|
function Experiance() {
|
||||||
const experianceList = useRecoilValue(projectsAtom);
|
const experianceList = useRecoilValue(projectsAtom);
|
||||||
|
|
||||||
|
const [ref, inView] = useInView({
|
||||||
|
triggerOnce: true,
|
||||||
|
threshold: 0.1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: "easeOut",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const experianceListUI = experianceList.map((item, key) => {
|
const experianceListUI = experianceList.map((item, key) => {
|
||||||
const position = key % 2;
|
const even = key % 2;
|
||||||
|
|
||||||
const imgClasses = ["basis-1/3", "hidden", "md:block"];
|
|
||||||
|
|
||||||
if (position === 1) {
|
|
||||||
imgClasses.push("md:order-last");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col md:flex-row gap-4 mb-12" key={item.id}>
|
<motion.article
|
||||||
<div className={imgClasses.join(" ")}>
|
ref={ref}
|
||||||
<img className="rounded-md" src={item.img[0]} />
|
variants={itemVariants}
|
||||||
</div>
|
className={`flex-1 gap-4 mb-12 bg-[#f0f0f3] border shadow-box ${
|
||||||
<div className="basis-2/3 flex flex-col">
|
(even === 0 && "rounded-tl-3xl rounded-br-3xl") ||
|
||||||
<div>
|
"rounded-tr-3xl rounded-bl-3xl"
|
||||||
<h2 className="text-subTMiniPhone md:text-subTMini mb-4 ">
|
}`}
|
||||||
|
key={item.id}
|
||||||
|
>
|
||||||
|
<div className={`flex pt-4 md:pt-14 flex-col `}>
|
||||||
|
<div className="px-4 mb-4">
|
||||||
|
<h3 className="text-subTMiniPhone md:text-subTMini font-ht">
|
||||||
{item.title}
|
{item.title}
|
||||||
</h2>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<figure className="basis-5/12">
|
||||||
<img
|
<img
|
||||||
className="block md:hidden mb-4 rounded-md"
|
className="w-full"
|
||||||
src={item.img[0]}
|
src={item.img[0]}
|
||||||
|
alt={`Preview of ${item.title} project`}
|
||||||
/>
|
/>
|
||||||
|
</figure>
|
||||||
|
<div className="grow"></div>
|
||||||
|
<div className="flex flex-col px-4 pt-4 pb-8 basis-3/12">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Tags listOfTags={item.tags} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base mb-6 line-clamp-3 md:line-clamp-6">
|
|
||||||
|
<div className="text-base mb-6">
|
||||||
<p>{item.info}</p>
|
<p>{item.info}</p>
|
||||||
</div>
|
</div>
|
||||||
<Tags listOfTags={item.tags} />
|
|
||||||
<div className="grow"></div>
|
|
||||||
<div className=" text-right">
|
|
||||||
<Link
|
|
||||||
className=" drop-shadow-doublelight hover:drop-shadow-light "
|
|
||||||
to={`/experience/${item.id}`}
|
|
||||||
>
|
|
||||||
read more
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</motion.article>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<section className="relative" aria-labelledby="projects-heading">
|
||||||
<div id="experience" className=" absolute -top-16 "></div>
|
<div
|
||||||
<h2 className=" text-subTPhone md:text-subT font-headline">
|
id="experience"
|
||||||
Experience<span className=" text-highlight">.</span>
|
className="absolute -top-16"
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<h2
|
||||||
|
id="projects-heading"
|
||||||
|
className="text-subTPhone md:text-subT font-headline mb-6"
|
||||||
|
>
|
||||||
|
Projects and experience<span className="text-highlight">.</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="py-4">{experianceListUI}</div>
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={inView ? "visible" : "hidden"}
|
||||||
|
className="py-4 grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||||
|
>
|
||||||
|
{experianceListUI}
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { projectsAtom } from "../store";
|
|||||||
import ImageGallery from "react-image-gallery";
|
import ImageGallery from "react-image-gallery";
|
||||||
import Tags from "../components/Tags";
|
import Tags from "../components/Tags";
|
||||||
import { IoChevronBackOutline as BackButton } from "react-icons/io5";
|
import { IoChevronBackOutline as BackButton } from "react-icons/io5";
|
||||||
|
import LeftNav from "../components/LeftNav";
|
||||||
|
import RightNav from "../components/RightNav";
|
||||||
|
|
||||||
function ExperianceDetail() {
|
function ExperianceDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@ -48,16 +50,16 @@ function ExperianceDetail() {
|
|||||||
</button>
|
</button>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h1 className=" text-subTPhone md:text-subT">
|
<h1 className="text-subTPhone md:text-subT font-headline">
|
||||||
Project: {myProject.title}
|
Project: {myProject.title}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 mt-2 mb-6">
|
<div className="flex-1 mt-2 mb-6 min-h-[80px]">
|
||||||
<Tags listOfTags={myProject.tags} />
|
<Tags listOfTags={myProject.tags} />
|
||||||
</div>{" "}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex flex-col md:flex-row gap-8">
|
||||||
|
<div className="w-full md:w-1/2">
|
||||||
<ImageGallery
|
<ImageGallery
|
||||||
showThumbnails={false}
|
showThumbnails={false}
|
||||||
showBullets={true}
|
showBullets={true}
|
||||||
@ -65,11 +67,20 @@ function ExperianceDetail() {
|
|||||||
slideDuration={1000}
|
slideDuration={1000}
|
||||||
slideInterval={4000}
|
slideInterval={4000}
|
||||||
items={projectImg}
|
items={projectImg}
|
||||||
|
renderLeftNav={(onClick, disabled) => (
|
||||||
|
<LeftNav onClick={onClick} disabled={disabled} />
|
||||||
|
)}
|
||||||
|
renderRightNav={(onClick, disabled) => (
|
||||||
|
<RightNav onClick={onClick} disabled={disabled} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-base">
|
<div className="w-full md:w-1/2 flex flex-col min-h-[300px]">
|
||||||
{myProject.info}
|
<div className="flex-1">
|
||||||
<div className=" drop-shadow-doublelight hover:drop-shadow-light my-2 text-end">
|
<h2 className="text-xl font-semibold mb-4">{myProject.subtitle}</h2>
|
||||||
|
<p className="text-base leading-relaxed">{myProject.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="drop-shadow-doublelight hover:drop-shadow-light my-2 text-end">
|
||||||
{linkUI}
|
{linkUI}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import About from "./About";
|
import About from "./About";
|
||||||
import Contact from "./Contact";
|
import Footer from "../components/Footer";
|
||||||
import Experiance from "./Experience";
|
import Experiance from "./Experience";
|
||||||
import Start from "./Start";
|
import Start from "./Start";
|
||||||
|
|
||||||
function FullPage() {
|
function FullPage() {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-2">
|
<div className="container mx-auto px-10 max-w-6xl">
|
||||||
<Start />
|
<Start />
|
||||||
<About />
|
<About />
|
||||||
<Experiance />
|
<Experiance />
|
||||||
<Contact />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
import {
|
|
||||||
Animator,
|
|
||||||
ScrollContainer,
|
|
||||||
ScrollPage,
|
|
||||||
batch,
|
|
||||||
Fade,
|
|
||||||
FadeIn,
|
|
||||||
FadeOut,
|
|
||||||
Move,
|
|
||||||
MoveIn,
|
|
||||||
MoveOut,
|
|
||||||
Sticky,
|
|
||||||
StickyIn,
|
|
||||||
StickyOut,
|
|
||||||
Zoom,
|
|
||||||
ZoomIn,
|
|
||||||
ZoomOut,
|
|
||||||
} from "react-scroll-motion";
|
|
||||||
|
|
||||||
function Home() {
|
|
||||||
const ZoomInScrollOut = batch(StickyIn(), FadeIn(), ZoomIn());
|
|
||||||
const FadeUp = batch(Fade(), Move(), Sticky());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollContainer>
|
|
||||||
<ScrollPage>
|
|
||||||
<Animator animation={batch(Fade(), Sticky(), MoveOut(0, 200))}>
|
|
||||||
<span>Hello, my name is Mariia 😀</span>
|
|
||||||
</Animator>
|
|
||||||
<span style={{ fontSize: "40px" }}>
|
|
||||||
<Animator animation={MoveIn(-1000, 0)}>Hello Guys 👋🏻</Animator>
|
|
||||||
</span>
|
|
||||||
</ScrollPage>
|
|
||||||
|
|
||||||
<ScrollPage>
|
|
||||||
<Animator animation={ZoomInScrollOut}>
|
|
||||||
<span style={{ fontSize: "40px" }}>I'm Frontend developer ✨</span>
|
|
||||||
</Animator>
|
|
||||||
</ScrollPage>
|
|
||||||
|
|
||||||
<ScrollPage>
|
|
||||||
<Animator animation={FadeUp}>
|
|
||||||
<span style={{ fontSize: "40px" }}>I'm Webbdesigner ⛅️</span>
|
|
||||||
</Animator>
|
|
||||||
</ScrollPage>
|
|
||||||
|
|
||||||
<ScrollPage>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: "40px" }}>
|
|
||||||
<Animator animation={MoveIn(1000, 0)}>Nice to meet you 🙋🏻♀️</Animator>
|
|
||||||
- I'm Dante Chun -
|
|
||||||
<Animator animation={MoveOut(1000, 0)}>Good bye ✋🏻</Animator>
|
|
||||||
<Animator animation={MoveOut(-1000, 0)}>See you 💛</Animator>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</ScrollPage>
|
|
||||||
|
|
||||||
<ScrollPage>
|
|
||||||
<Animator animation={batch(Fade(), Sticky())}>
|
|
||||||
<span style={{ fontSize: "40px" }}>Done</span>
|
|
||||||
<br />
|
|
||||||
<span style={{ fontSize: "30px" }}>
|
|
||||||
There's FadeAnimation, MoveAnimation, StickyAnimation, ZoomAnimation
|
|
||||||
</span>
|
|
||||||
</Animator>
|
|
||||||
</ScrollPage>
|
|
||||||
</ScrollContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home;
|
|
||||||
@ -1,23 +1,110 @@
|
|||||||
|
import { motion, useAnimation } from "framer-motion";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
function Start() {
|
function Start() {
|
||||||
|
const greeting = "Hello, my name is Maria.";
|
||||||
|
const greetingArray = greeting.split("");
|
||||||
|
const role = "I'm a Frontend Engineer.";
|
||||||
|
const roleArray = role.split("");
|
||||||
|
|
||||||
|
const controls = useAnimation();
|
||||||
|
const contentControls = useAnimation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Start the second animation after the first line completes
|
||||||
|
const timer1 = setTimeout(() => {
|
||||||
|
controls.start("visible");
|
||||||
|
}, greetingArray.length * 100 + 500); // Total time for first line + 500ms delay
|
||||||
|
|
||||||
|
// Start the content animation after the second line completes
|
||||||
|
const timer2 = setTimeout(() => {
|
||||||
|
contentControls.start("visible");
|
||||||
|
}, (greetingArray.length + roleArray.length) * 100 + 1000); // Total time for both lines + 1000ms delay
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer1);
|
||||||
|
clearTimeout(timer2);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div id="start" className="absolute -top-16 "></div>
|
<div id="start" className="absolute -top-16 "></div>
|
||||||
|
|
||||||
<div>
|
<div className=" min-h-screen flex items-center">
|
||||||
<h1 className="text-titlePhone md:text-title md:leading-tight mb-2 font-headline">
|
<div className="">
|
||||||
Hello, my name is Maria<span className=" text-highlight">.</span>
|
<h1 className="text-titlePhone md:text-title md:leading-tight mb-2 font-ht">
|
||||||
|
{greetingArray.map((letter, index) => (
|
||||||
|
<motion.span
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.1,
|
||||||
|
delay: index * 0.1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{letter}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className="text-subTPhone md:text-subT font-headline">
|
<h2 className="text-subTPhone md:text-subT font-ht">
|
||||||
I'm a<span className=" text-highlight"> Frontend Engineer</span>
|
{roleArray.map((letter, index) => (
|
||||||
|
<motion.span
|
||||||
|
key={index}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={controls}
|
||||||
|
variants={{
|
||||||
|
visible: { opacity: 1 }
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.1,
|
||||||
|
delay: index * 0.1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{letter}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-base">
|
<motion.div
|
||||||
In my recent journey as a junior frontend engineer, I've gained
|
initial={{ opacity: 0 }}
|
||||||
valuable experience through involvement in various projects within
|
animate={contentControls}
|
||||||
startup environments. Passionate about art and design, I've developed
|
variants={{
|
||||||
my skills beyond coding. Let's connect and explore opportunities
|
visible: { opacity: 1 }
|
||||||
together! By the way, you might catch a glimpse of my black mini
|
}}
|
||||||
poodle, Oreo, who's an integral part of my creative space.
|
transition={{
|
||||||
|
duration: 0.8,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-base md:w-1/2 w-full text-left my-4 font-body">
|
||||||
|
I'm a Stockholm-based web developer with startup experience,
|
||||||
|
building things with JavaScript, React, and Node.js. I'm also into
|
||||||
|
3D design and art, and my mini poodle,{" "}
|
||||||
|
<span className="font-black">Oreo</span>{" "}
|
||||||
|
<img
|
||||||
|
src="/img/oreo-logo.png"
|
||||||
|
alt="oreo_logo"
|
||||||
|
className="inline h-"
|
||||||
|
/>{" "}
|
||||||
|
, is always in my creative space. Let's chat!
|
||||||
</p>
|
</p>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/in/mariia-shabelnik/"
|
||||||
|
target="_blank"
|
||||||
|
className="flex items-center gap-2 hover:text-highlight transition-colors duration-300"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/img/icons8-linkedin.svg"
|
||||||
|
alt="LinkedIn icon"
|
||||||
|
className="inline h-6"
|
||||||
|
/>
|
||||||
|
LinkedIn
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
40
src/store.js
@ -4,8 +4,10 @@ const experianceList = [
|
|||||||
{
|
{
|
||||||
id: "46bf76ae-8915-4e5d-ae92-4151be80e75a",
|
id: "46bf76ae-8915-4e5d-ae92-4151be80e75a",
|
||||||
title: "Netzero web",
|
title: "Netzero web",
|
||||||
info: "Discover the intersection of sustainability and technology in my collaboration with Net0. As the creative force behind the UI/UX design, I meticulously crafted a seamless and visually appealing user experience. Bringing this vision to life, I also spearheaded the frontend development, using React to construct an intuitive and efficient system for Net0. Immerse yourself in the synergy of eco-conscious practices and cutting-edge technology as we navigate towards a greener future, one React component at a time.",
|
subtitle: "Lead UI/UX Designer & Frontend Developer",
|
||||||
|
info: "As the lead UI/UX designer and frontend developer for Net0's sustainability platform, I spearheaded the creation of an intuitive system that seamlessly integrates eco-conscious practices with modern technology. Built with React and TypeScript, the platform features a comprehensive dashboard for tracking carbon emissions, implementing sustainable practices, and generating detailed reports. I collaborated closely with the backend team to ensure smooth integration with the Node.js and PostgreSQL infrastructure, while maintaining a strong focus on user experience and accessibility throughout the development process.",
|
||||||
img: ["/img/net0_0.png", "/img/net0_1.png", "/img/net0_2.png"],
|
img: ["/img/net0_0.png", "/img/net0_1.png", "/img/net0_2.png"],
|
||||||
|
previewImg: "/img/mockup_net0.png",
|
||||||
tags: [
|
tags: [
|
||||||
"React",
|
"React",
|
||||||
"Vite",
|
"Vite",
|
||||||
@ -19,18 +21,47 @@ const experianceList = [
|
|||||||
"Javascript",
|
"Javascript",
|
||||||
],
|
],
|
||||||
link: "https://www.net0.se",
|
link: "https://www.net0.se",
|
||||||
|
sortOrder: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "55515a25-deb1-451c-bc7d-006d293f54aa",
|
id: "55515a25-deb1-451c-bc7d-006d293f54aa",
|
||||||
title: "Lets fly",
|
title: "Lets fly",
|
||||||
info: "Letsfly is a website aimed at providing a platform for aviation-related services and information. Built on Next.js, a popular React framework, the website leverages its server-side rendering (SSR) capability to enhance performance and user experience. Tailwind was used as the CSS framework to quickly and efficiently create an attractive and responsive design. WordPress, a well-known Content Management System (CMS), was employed as the content management system to administer and publish content on the website. The reason for choosing WordPress was a client request; he was familiar with this system and wanted to be able to modify the content himself. GraphQL was selected as the API layer to enable efficient data management and flexible data queries between the frontend and backend.",
|
subtitle: "Full Stack Developer",
|
||||||
|
info: "Developed a comprehensive aviation services platform using Next.js and WordPress, focusing on creating a seamless experience for both service providers and clients. Implemented GraphQL for efficient data management and real-time updates, while utilizing Tailwind CSS for responsive and modern design. The platform features a sophisticated booking system, real-time availability tracking, and integrated payment processing. I worked closely with the client to ensure the WordPress integration met their specific requirements while maintaining high performance and security standards throughout the development process.",
|
||||||
img: ["/img/letsfly_1.png", "/img/letsfly_2.png", "/img/letsfly_3.png"],
|
img: ["/img/letsfly_1.png", "/img/letsfly_2.png", "/img/letsfly_3.png"],
|
||||||
tags: ["React", "NextJS", "Wordpress", "GraphQL", "MySQL", "Tailwind"],
|
tags: ["React", "NextJS", "Wordpress", "GraphQL", "MySQL", "Tailwind"],
|
||||||
link: "https://preview.letsfly.app/",
|
link: "https://preview.letsfly.app/",
|
||||||
|
previewImg: "/img/mockup_net0.png",
|
||||||
|
sortOrder: 3
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "DataTjej",
|
||||||
|
subtitle: "Board Member & Web Developer",
|
||||||
|
info: "As a board member of DataTjej, I'm actively involved in shaping the future of this non-profit organization that supports women and non-binary individuals in tech. Currently leading the development of a new website using Next.js and Tailwind CSS, focusing on creating a modern, accessible platform. The project includes automating connections to various solutions like our podcast and event management systems, streamlining our digital presence and member engagement.",
|
||||||
|
img: ["/images/datatjej-placeholder.jpg"],
|
||||||
|
previewImg: "/images/datatjej-placeholder.jpg",
|
||||||
|
tags: ["Board Member", "Next.js", "Tailwind CSS", "Prisma","TypeScript", "Web Development", "Automation"],
|
||||||
|
link: "https://datatjej.se",
|
||||||
|
sortOrder: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "DataTjej",
|
||||||
|
subtitle: "IT Support & User Management",
|
||||||
|
info: "As part of the DataTjej board, I manage user access and IT support for our growing community of over 6000 members. I handle user permissions, group management, and ensure secure access to our various platforms. This includes setting up and maintaining appropriate access levels for different user groups, troubleshooting technical issues, and implementing security best practices. I also work on automating user management processes and maintaining our IT infrastructure using Google Cloud services to support our community's needs.",
|
||||||
|
img: ["/images/datatjej-it-placeholder.jpg"],
|
||||||
|
previewImg: "/images/datatjej-it-placeholder.jpg",
|
||||||
|
tags: ["User Management", "IT Support", "Access Control", "Security", "Automation", "Community Management", "Google Cloud"],
|
||||||
|
link: "https://datatjej.se",
|
||||||
|
sortOrder: 4
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const projectsAtom = atom({ key: "projects", default: experianceList });
|
// Sort the list by sortOrder
|
||||||
|
const sortedExperianceList = [...experianceList].sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
|
|
||||||
|
export const projectsAtom = atom({ key: "projects", default: sortedExperianceList });
|
||||||
export const skillsAtom = atom({
|
export const skillsAtom = atom({
|
||||||
key: "skills",
|
key: "skills",
|
||||||
default: {
|
default: {
|
||||||
@ -54,10 +85,11 @@ export const skillsAtom = atom({
|
|||||||
"MySQL",
|
"MySQL",
|
||||||
"Docker",
|
"Docker",
|
||||||
"Visual Studio Code",
|
"Visual Studio Code",
|
||||||
"Postman API",
|
"Rapid API",
|
||||||
"TablePlus",
|
"TablePlus",
|
||||||
"Trello",
|
"Trello",
|
||||||
"Figma",
|
"Figma",
|
||||||
|
"Blender",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,9 +3,14 @@ export default {
|
|||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Ubuntu Mono", "system-ui"],
|
//logo font
|
||||||
headline: ["Tektur", "system-ui"],
|
logo: ["Sixtyfour", "system-ui"],
|
||||||
tags: ["Ubuntu Mono", "system-ui"],
|
//headline-title text
|
||||||
|
ht: ["Sora", "system-ui"],
|
||||||
|
//body text
|
||||||
|
body: ["Jura", "system-ui"],
|
||||||
|
//nav
|
||||||
|
tags: ["Raleway", "system-ui"],
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
title: ["5rem", { lineHeight: "5rem", fontWeight: "900" }],
|
title: ["5rem", { lineHeight: "5rem", fontWeight: "900" }],
|
||||||
@ -16,7 +21,7 @@ export default {
|
|||||||
subTMiniPhone: ["1.2rem", { lineHeight: "1.2rem", fontWeight: "800" }],
|
subTMiniPhone: ["1.2rem", { lineHeight: "1.2rem", fontWeight: "800" }],
|
||||||
tags: ["0.8rem", { fontWeight: "400" }],
|
tags: ["0.8rem", { fontWeight: "400" }],
|
||||||
sm: "0.8rem",
|
sm: "0.8rem",
|
||||||
base: "1rem",
|
base: "1.2rem",
|
||||||
xl: "1.25rem",
|
xl: "1.25rem",
|
||||||
"2xl": "1.563rem",
|
"2xl": "1.563rem",
|
||||||
"3xl": "1.953rem",
|
"3xl": "1.953rem",
|
||||||
@ -25,20 +30,24 @@ export default {
|
|||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
neon: "#ccff00",
|
neon: "#caebfa",
|
||||||
bgColor: "#031417",
|
bgColor: "#F0F0F3",
|
||||||
highlight: "#ccff00",
|
highlight: "#D2D2D2",
|
||||||
},
|
},
|
||||||
|
//20px 20px 60px #bebebe, -20px -20px 60px #ffffff;
|
||||||
|
boxShadow: {
|
||||||
|
box: "8px 8px 16px #cacaca,-8px -8px 16px #f6f6f6",
|
||||||
|
// box: "rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;",
|
||||||
|
},
|
||||||
|
|
||||||
dropShadow: {
|
dropShadow: {
|
||||||
light: "0 0 5px theme('colors.indigo.800')",
|
light: "0 0 5px theme('colors.sky.100')",
|
||||||
yellow: "0 0 9px theme('colors.lime.400')",
|
gray: "0 0 9px #8c92a8",
|
||||||
doublelight: [
|
doublelight: ["0 0 5px #96989a", "0 0 15px #a0d9fa"],
|
||||||
"0 0 5px theme('colors.lime.400')",
|
//doublelight:
|
||||||
"0 0 15px theme('colors.yellow.400')",
|
|
||||||
],
|
|
||||||
active: [
|
active: [
|
||||||
"0 0 5px theme('colors.lime.400')",
|
"0 0 5px theme('colors.sky.300')",
|
||||||
"0 0 15px theme('colors.indigo.950')",
|
"0 0 15px theme('colors.sky.100')",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||