├── public ├── robots.txt ├── favicon.ico ├── pdf │ └── brunodziecielski.pdf └── svg │ ├── framermotion.svg │ ├── socketio.svg │ ├── nextjs.svg │ ├── typescript.svg │ ├── graphql.svg │ ├── react.svg │ ├── tailwindcss.svg │ ├── recoiljs.svg │ ├── express.svg │ ├── nodejs.svg │ ├── webrtc.svg │ ├── mongodb.svg │ └── nestjs.svg ├── .eslintignore ├── common ├── fonts │ └── ArticulatCF-Bold.otf ├── hooks │ ├── useScrollY.ts │ ├── useWindowSize.ts │ └── useElementDimensions.ts ├── styles │ └── global.css ├── components │ └── ScrollOpacity.tsx └── lib │ └── checkIsMobile.ts ├── postcss.config.js ├── README.md ├── next.config.js ├── modules ├── customMouse │ ├── types │ │ └── mouse.types.ts │ ├── index.ts │ ├── store │ │ └── mouseStore.tsx │ ├── hooks │ │ └── useMouseVariant.ts │ └── components │ │ └── CircleMouse.tsx ├── projects │ ├── index.ts │ └── components │ │ ├── Project.tsx │ │ ├── Header.tsx │ │ └── ProjectsList.tsx ├── hero │ ├── animations │ │ ├── headerAnimation.ts │ │ └── scrollAnimation.ts │ ├── components │ │ ├── Header.tsx │ │ └── ScrollIndicator.tsx │ └── index.tsx ├── ballzone │ ├── types │ │ └── ball.types.ts │ ├── index.tsx │ ├── components │ │ ├── Header.tsx │ │ └── Game.tsx │ ├── hooks │ │ ├── useRenderBoard.ts │ │ ├── useSetupMovables.ts │ │ └── useGameLoop.ts │ └── helpers │ │ ├── renderMovables.ts │ │ ├── calculateBallPosition.ts │ │ ├── renderBoard.ts │ │ └── handleBallPhysics.ts ├── collabio │ ├── index.tsx │ ├── helpers │ │ ├── drawPath.tsx │ │ ├── drawLines.ts │ │ ├── calculateClientsPositions.ts │ │ └── handleDraw.ts │ └── components │ │ ├── Header.tsx │ │ ├── Windows.tsx │ │ └── SingleWindow.tsx ├── about │ ├── index.tsx │ └── components │ │ ├── Header.tsx │ │ ├── Intro.tsx │ │ └── Skills.tsx └── contact │ └── index.tsx ├── next-env.d.ts ├── tailwind.config.js ├── .gitignore ├── tsconfig.json ├── app ├── page.tsx └── layout.tsx ├── package.json └── .eslintrc /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriziu/portfolio/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | build 4 | .next 5 | 6 | tailwind.config.js 7 | postcss.config.js -------------------------------------------------------------------------------- /common/fonts/ArticulatCF-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriziu/portfolio/HEAD/common/fonts/ArticulatCF-Bold.otf -------------------------------------------------------------------------------- /public/pdf/brunodziecielski.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriziu/portfolio/HEAD/public/pdf/brunodziecielski.pdf -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My portfolio 2 | 3 | ## Created using 4 | 5 | - Next.JS 6 | - TailwindCSS 7 | - Framer-motion 8 | 9 | ## Demo 10 | 11 | https://brunodzi.dev 12 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | i18n: { 5 | locales: ['en'], 6 | defaultLocale: 'en', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /modules/customMouse/types/mouse.types.ts: -------------------------------------------------------------------------------- 1 | export enum MouseVariant { 2 | DEFAULT = 'default', 3 | TEXT = 'text', 4 | TECHNOLOGY = 'technology', 5 | DRAWING = 'drawing', 6 | GAME = 'game', 7 | } 8 | -------------------------------------------------------------------------------- /modules/projects/index.ts: -------------------------------------------------------------------------------- 1 | import ProjectsHeader from './components/Header'; 2 | import ProjectsList from './components/ProjectsList'; 3 | 4 | export { ProjectsHeader }; 5 | 6 | export default ProjectsList; 7 | -------------------------------------------------------------------------------- /modules/hero/animations/headerAnimation.ts: -------------------------------------------------------------------------------- 1 | export const wordAnimation = { 2 | hidden: { 3 | opacity: 0, 4 | y: 20, 5 | }, 6 | visible: { 7 | opacity: 1, 8 | y: 0, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /public/svg/framermotion.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/ballzone/types/ball.types.ts: -------------------------------------------------------------------------------- 1 | export type Ball = { 2 | position: { x: number; y: number }; 3 | velocityVector: { x: number; y: number }; 4 | }; 5 | 6 | export type Settings = { 7 | ballSize: number; 8 | goalWidth: number; 9 | boardSize: { width: number; height: number }; 10 | goalHeight: number; 11 | }; 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './app/**/*.{js,ts,jsx,tsx}', 4 | './common/**/*.{js,ts,jsx,tsx}', 5 | './modules/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: { 9 | fontSize: { 10 | extra: '6rem', 11 | }, 12 | }, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /modules/customMouse/index.ts: -------------------------------------------------------------------------------- 1 | import CircleMouse from './components/CircleMouse'; 2 | import { useMouseVariant } from './hooks/useMouseVariant'; 3 | import { useMouseStore } from './store/mouseStore'; 4 | import { MouseVariant } from './types/mouse.types'; 5 | 6 | export default CircleMouse; 7 | 8 | export { useMouseVariant, MouseVariant, useMouseStore }; 9 | -------------------------------------------------------------------------------- /modules/ballzone/index.tsx: -------------------------------------------------------------------------------- 1 | import Game from './components/Game'; 2 | import BallzoneHeader from './components/Header'; 3 | 4 | export default function Ballzone() { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /modules/hero/animations/scrollAnimation.ts: -------------------------------------------------------------------------------- 1 | import { Variants } from 'framer-motion'; 2 | 3 | export const itemVariant: Variants = { 4 | hidden: { 5 | opacity: 0, 6 | }, 7 | visible: { 8 | opacity: 1, 9 | transition: { 10 | repeat: Infinity, 11 | repeatType: 'reverse', 12 | duration: 1, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /modules/collabio/index.tsx: -------------------------------------------------------------------------------- 1 | import CollabioHeader from './components/Header'; 2 | import Windows from './components/Windows'; 3 | 4 | export default function Collabio() { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /common/hooks/useScrollY.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import { useScroll } from 'framer-motion'; 4 | 5 | export const useScrollY = () => { 6 | const [scrolled, setScrolled] = useState(0); 7 | 8 | const { scrollY } = useScroll(); 9 | 10 | useEffect(() => { 11 | const unsubscribe = scrollY.on('change', setScrolled); 12 | 13 | return unsubscribe; 14 | }, [scrollY]); 15 | 16 | return scrolled; 17 | }; 18 | -------------------------------------------------------------------------------- /modules/about/index.tsx: -------------------------------------------------------------------------------- 1 | import AboutHeader from './components/Header'; 2 | import Intro from './components/Intro'; 3 | import Skills from './components/Skills'; 4 | 5 | export default function AboutSkills() { 6 | return ( 7 |
8 | 9 |
10 | 11 | 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | .vscode 40 | -------------------------------------------------------------------------------- /modules/customMouse/store/mouseStore.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { create } from 'zustand'; 4 | 5 | import { MouseVariant } from '../types/mouse.types'; 6 | 7 | interface MouseStore { 8 | variant: MouseVariant; 9 | text: string; 10 | setText: (text: string) => void; 11 | setVariant: (variant: MouseVariant) => void; 12 | } 13 | 14 | export const useMouseStore = create((set) => ({ 15 | variant: MouseVariant.DEFAULT, 16 | text: '', 17 | setText: (text: string) => { 18 | set({ text }); 19 | }, 20 | setVariant: (variant: MouseVariant) => { 21 | set({ variant }); 22 | }, 23 | })); 24 | -------------------------------------------------------------------------------- /modules/collabio/helpers/drawPath.tsx: -------------------------------------------------------------------------------- 1 | export const drawPath = ( 2 | ctx: CanvasRenderingContext2D, 3 | path: { x: number; y: number }[] 4 | ) => { 5 | if (path.length === 0) return; 6 | 7 | ctx.strokeStyle = '#000'; 8 | ctx.lineCap = 'round'; 9 | ctx.lineJoin = 'round'; 10 | ctx.lineWidth = 2; 11 | 12 | ctx.beginPath(); 13 | if (path.length === 1) { 14 | ctx.arc(path[0].x, path[0].y, ctx.lineWidth / 4, 0, 2 * Math.PI); 15 | ctx.fill(); 16 | } else { 17 | ctx.moveTo(path[0].x, path[0].y); 18 | 19 | for (let i = 1; i < path.length; i += 1) ctx.lineTo(path[i].x, path[i].y); 20 | 21 | ctx.stroke(); 22 | } 23 | ctx.stroke(); 24 | ctx.closePath(); 25 | }; 26 | -------------------------------------------------------------------------------- /modules/ballzone/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import ScrollOpacity from '@/common/components/ScrollOpacity'; 2 | import { useMouseVariant } from '@/modules/customMouse'; 3 | 4 | export default function Header() { 5 | const { setMouseVariant } = useMouseVariant(); 6 | 7 | return ( 8 |
9 | 10 |

15 | simple games 16 |

17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /modules/collabio/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import ScrollOpacity from '@/common/components/ScrollOpacity'; 2 | import { useMouseVariant } from '@/modules/customMouse'; 3 | 4 | export default function Header() { 5 | const { setMouseVariant } = useMouseVariant(); 6 | 7 | return ( 8 |
9 | 10 |

15 | real-time tools 16 |

17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /modules/collabio/helpers/drawLines.ts: -------------------------------------------------------------------------------- 1 | export const drawLines = ( 2 | ctx: CanvasRenderingContext2D, 3 | { width, height }: { width: number; height: number } 4 | ) => { 5 | ctx.strokeStyle = '#ddd'; 6 | ctx.lineWidth = 1; 7 | ctx.lineCap = 'round'; 8 | ctx.lineJoin = 'round'; 9 | 10 | const lineFactor = width > 450 ? 10 : 7; 11 | 12 | for (let i = 0; i < width; i += lineFactor) { 13 | ctx.beginPath(); 14 | ctx.moveTo(i, 0); 15 | ctx.lineTo(i, height); 16 | ctx.stroke(); 17 | ctx.closePath(); 18 | } 19 | 20 | for (let i = 0; i < height; i += lineFactor) { 21 | ctx.beginPath(); 22 | ctx.moveTo(0, i); 23 | ctx.lineTo(width, i); 24 | ctx.stroke(); 25 | ctx.closePath(); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /common/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const getSize = () => { 4 | if (typeof window !== 'undefined') { 5 | return { 6 | width: window.innerWidth, 7 | height: window.innerHeight, 8 | }; 9 | } 10 | 11 | return { 12 | width: 0, 13 | height: 0, 14 | }; 15 | }; 16 | 17 | export const useWindowSize = () => { 18 | const [windowSize, setWindowSize] = useState(getSize); 19 | 20 | useEffect(() => { 21 | const handleResize = () => { 22 | setWindowSize(getSize()); 23 | }; 24 | 25 | window.addEventListener('resize', handleResize); 26 | 27 | return () => { 28 | window.removeEventListener('resize', handleResize); 29 | }; 30 | }, []); 31 | 32 | return windowSize; 33 | }; 34 | -------------------------------------------------------------------------------- /modules/customMouse/hooks/useMouseVariant.ts: -------------------------------------------------------------------------------- 1 | import { useMouseStore } from '../store/mouseStore'; 2 | import { MouseVariant } from '../types/mouse.types'; 3 | 4 | export const useMouseVariant = () => { 5 | const { setVariant, setText } = useMouseStore((store) => ({ 6 | setVariant: store.setVariant, 7 | setText: store.setText, 8 | })); 9 | 10 | const setMouseVariant = { 11 | default: () => setVariant(MouseVariant.DEFAULT), 12 | text: () => setVariant(MouseVariant.TEXT), 13 | technology: (newText: string) => { 14 | setVariant(MouseVariant.TECHNOLOGY); 15 | setText(newText); 16 | }, 17 | drawing: () => setVariant(MouseVariant.DRAWING), 18 | game: () => setVariant(MouseVariant.GAME), 19 | }; 20 | 21 | return { setMouseVariant }; 22 | }; 23 | -------------------------------------------------------------------------------- /common/hooks/useElementDimensions.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect, useState } from 'react'; 2 | 3 | export const useElementDimensions = (ref: RefObject) => { 4 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); 5 | 6 | useEffect(() => { 7 | const node = ref.current; 8 | if (!node) return; 9 | 10 | const resizeObserver = new ResizeObserver((entries) => { 11 | entries.forEach((entry) => { 12 | setDimensions({ 13 | width: entry.contentRect.width, 14 | height: entry.contentRect.height, 15 | }); 16 | }); 17 | }); 18 | 19 | resizeObserver.observe(node); 20 | 21 | return () => { 22 | resizeObserver.unobserve(node); 23 | }; 24 | }, [ref]); 25 | 26 | return dimensions; 27 | }; 28 | -------------------------------------------------------------------------------- /modules/ballzone/hooks/useRenderBoard.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { renderBoard } from '../helpers/renderBoard'; 4 | 5 | interface Props { 6 | width: number; 7 | height: number; 8 | lineWidth: number; 9 | goalHeight: number; 10 | playerSize: number; 11 | isSmall: boolean; 12 | } 13 | 14 | export const useRenderBoard = ( 15 | canvasRef: React.RefObject, 16 | { width, height, lineWidth, goalHeight, playerSize, isSmall }: Props 17 | ) => { 18 | useEffect(() => { 19 | if (!canvasRef.current) return; 20 | 21 | renderBoard(canvasRef.current, { 22 | boardSize: { width, height }, 23 | lineWidth, 24 | goalHeight, 25 | playerSize, 26 | isSmall, 27 | }); 28 | }, [width, height, lineWidth, playerSize, isSmall, goalHeight, canvasRef]); 29 | }; 30 | -------------------------------------------------------------------------------- /modules/about/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | 3 | import ScrollOpacity from '@/common/components/ScrollOpacity'; 4 | import { useMouseVariant } from '@/modules/customMouse'; 5 | 6 | export default function AboutHeader() { 7 | const { setMouseVariant } = useMouseVariant(); 8 | 9 | return ( 10 | 11 | 16 | I'm Bruno,
a Full stack{' '} 17 | Developer
that creates 18 | interactive
19 | web applications. 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /public/svg/socketio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "CommonJS", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "downlevelIteration": true, 22 | "baseUrl": ".", 23 | "paths": { 24 | "@/*": [ 25 | "./*" 26 | ] 27 | }, 28 | "plugins": [ 29 | { 30 | "name": "next" 31 | } 32 | ] 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | import { MotionConfig } from 'framer-motion'; 6 | 7 | import About from '@/modules/about'; 8 | import Ballzone from '@/modules/ballzone'; 9 | import Collabio from '@/modules/collabio'; 10 | import Contact from '@/modules/contact'; 11 | import CircleMouse from '@/modules/customMouse'; 12 | import Hero from '@/modules/hero'; 13 | import ProjectsList, { ProjectsHeader } from '@/modules/projects'; 14 | 15 | export default function HomePage() { 16 | useEffect(() => { 17 | const scrollTop = () => { 18 | window.scrollTo(0, 0); 19 | }; 20 | 21 | window.onunload = scrollTop; 22 | }, []); 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /modules/ballzone/hooks/useSetupMovables.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | export const useSetupMovables = ( 4 | movablesRef: React.RefObject, 5 | ball: React.MutableRefObject<{ position: { x: number; y: number } }>, 6 | playerPosition: React.MutableRefObject<{ x: number; y: number }>, 7 | { 8 | width, 9 | height, 10 | }: { 11 | width: number; 12 | height: number; 13 | } 14 | ) => { 15 | useEffect(() => { 16 | const canvas = movablesRef.current; 17 | 18 | if (canvas) { 19 | const dpi = window.devicePixelRatio; 20 | 21 | canvas.width = width * dpi; 22 | canvas.height = height * dpi; 23 | 24 | ball.current.position = { 25 | x: width / 2, 26 | y: height / 2, 27 | }; 28 | 29 | playerPosition.current = { 30 | x: width - 100, 31 | y: height / 2, 32 | }; 33 | 34 | const ctx = canvas.getContext('2d'); 35 | 36 | if (ctx) ctx.scale(dpi, dpi); 37 | } 38 | }, [width, height, movablesRef, ball, playerPosition]); 39 | }; 40 | -------------------------------------------------------------------------------- /modules/collabio/helpers/calculateClientsPositions.ts: -------------------------------------------------------------------------------- 1 | const STARTING_POSITIONS = { 2 | client1: { 3 | x: 100, 4 | y: 30, 5 | }, 6 | client2: { 7 | x: 60, 8 | y: 30, 9 | }, 10 | }; 11 | 12 | const client1 = ( 13 | progress: number, 14 | { width, height }: { width: number; height: number } 15 | ) => ({ 16 | x: (width - 130) * (progress / 100) + STARTING_POSITIONS.client1.x, 17 | y: (height - 75) * (progress / 100) + STARTING_POSITIONS.client1.y, 18 | }); 19 | 20 | const client2 = ( 21 | progress: number, 22 | { width, height }: { width: number; height: number } 23 | ) => ({ 24 | x: (width - 140) * (progress / 100) + STARTING_POSITIONS.client2.x, 25 | y: 26 | Math.sin(progress / 40) * 100 + 27 | (height - 160) / 2 + 28 | STARTING_POSITIONS.client2.y, 29 | }); 30 | 31 | export const calculateClientsPositions = ( 32 | progress: number, 33 | size: { width: number; height: number } 34 | ) => { 35 | return { 36 | client1: client1(Math.min(50, Math.max(0, progress - 50)) * 2, size), 37 | client2: client2(Math.min(50, progress) * 2, size), 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /public/svg/nextjs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/common/styles/global.css'; 2 | 3 | import { ReactNode } from 'react'; 4 | 5 | import { Analytics } from '@vercel/analytics/react'; 6 | import { Metadata } from 'next'; 7 | import localFont from 'next/font/local'; 8 | 9 | const ArticulatCF = localFont({ 10 | src: '../common/fonts/ArticulatCF-Bold.otf', 11 | }); 12 | 13 | export const metadata: Metadata = { 14 | title: 'Bruno Dzięcielski | Expert Full Stack Developer in Web Development', 15 | description: 16 | 'Discover the portfolio of Bruno Dzięcielski, a seasoned Full Stack Developer specializing in Web Development. Explore projects demonstrating expertise in React.js.', 17 | keywords: [ 18 | 'Bruno Dzięcielski', 19 | 'Full Stack Developer', 20 | 'Frontend Expert', 21 | 'React', 22 | 'React.js', 23 | 'Next.js', 24 | ], 25 | icons: '/favicon.ico', 26 | authors: { 27 | name: 'Bruno Dzięcielski', 28 | url: process.env.NEXT_PUBLIC_AUTHOR_LINKEDIN_URL, 29 | }, 30 | }; 31 | 32 | export default function Layout({ children }: { children: ReactNode }) { 33 | return ( 34 | 35 | {children} 36 | 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /common/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | overflow-x: hidden; 7 | 8 | @apply bg-black text-white; 9 | } 10 | 11 | span { 12 | display: inline-block; 13 | } 14 | 15 | @layer components { 16 | .header { 17 | @apply text-4xl sm:text-5xl md:text-6xl 2xl:text-8xl; 18 | } 19 | 20 | .primary-gradient { 21 | background-image: linear-gradient( 22 | -45deg, 23 | #ff7b00, 24 | #ee0979, 25 | #60a5fa, 26 | #4ade80 27 | ); 28 | 29 | background-position: 100% 50%; 30 | 31 | background-size: 400% 400%; 32 | 33 | transition: background-position 0.4s ease-in-out; 34 | } 35 | 36 | .hover-gradient { 37 | background-position: 0% 50%; 38 | } 39 | 40 | .text-gradient { 41 | @apply primary-gradient bg-clip-text text-transparent; 42 | } 43 | 44 | .sans { 45 | @apply font-sans; 46 | } 47 | 48 | .scale-btn { 49 | @apply transition-transform hover:scale-110 active:scale-95; 50 | } 51 | 52 | .project-btn { 53 | @apply scale-btn flex items-center justify-center gap-2 rounded-2xl bg-zinc-500 px-6 py-2 font-sans font-bold text-black; 54 | } 55 | } 56 | 57 | /* from-[#1f4037] to-[#99f2c8] */ 58 | /* from-[#ee0979] to-[#ff6a00] */ 59 | -------------------------------------------------------------------------------- /public/svg/typescript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | TypeScript logo 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /modules/hero/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | 3 | import { useMouseVariant } from '@/modules/customMouse'; 4 | 5 | import { wordAnimation } from '../animations/headerAnimation'; 6 | 7 | export default function Header() { 8 | const { setMouseVariant } = useMouseVariant(); 9 | 10 | return ( 11 | 21 | A{' '} 22 | passionate{' '} 23 | dev,{' '} 24 |
25 | making{' '} 26 |
27 | 28 | things 29 | {' '} 30 |
31 | the{' '} 32 | right{' '} 33 | way. 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brunodzi-portfolio", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@vercel/analytics": "^1.1.0", 12 | "clsx": "^2.0.0", 13 | "framer-motion": "^10.16.4", 14 | "next": "14.0.4", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0", 17 | "react-icons": "^4.11.0", 18 | "zustand": "^4.4.7" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "20.10.5", 22 | "@types/react": "18.2.45", 23 | "@typescript-eslint/eslint-plugin": "^5.59.9", 24 | "@typescript-eslint/parser": "^5.0.0", 25 | "autoprefixer": "10.4.16", 26 | "eslint": "^8.42.0", 27 | "eslint-config-airbnb": "19.0.4", 28 | "eslint-config-airbnb-typescript": "17.0.0", 29 | "eslint-config-next": "^14.0.3", 30 | "eslint-config-prettier": "^8.8.0", 31 | "eslint-plugin-import": "2.27.5", 32 | "eslint-plugin-jsx-a11y": "^6.7.1", 33 | "eslint-plugin-prettier": "4.2.1", 34 | "eslint-plugin-react": "^7.32.2", 35 | "eslint-plugin-react-hooks": "4.6.0", 36 | "eslint-plugin-tailwindcss": "3.6.2", 37 | "eslint-plugin-unused-imports": "2.0.0", 38 | "npm-run-all": "4.1.5", 39 | "postcss": "8.4.32", 40 | "prettier": "^2.8.8", 41 | "prettier-plugin-tailwindcss": "0.1.8", 42 | "tailwindcss": "3.4.0", 43 | "typescript": "5.3.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/projects/components/Project.tsx: -------------------------------------------------------------------------------- 1 | import { BsGithub, BsGlobe } from 'react-icons/bs'; 2 | 3 | interface Props { 4 | title: string; 5 | description: string; 6 | github?: string; 7 | demo?: string; 8 | scrollTo?: boolean; 9 | } 10 | 11 | export default function Project({ 12 | title, 13 | demo, 14 | description, 15 | github, 16 | scrollTo, 17 | }: Props) { 18 | return ( 19 |
23 |

{title}

24 |
25 |

26 | {description} 27 |

28 |
29 | {github && ( 30 | 36 | 37 | Github 38 | 39 | )} 40 | {demo && ( 41 | 47 | 48 | Demo 49 | 50 | )} 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /modules/hero/components/ScrollIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | 3 | import { itemVariant } from '../animations/scrollAnimation'; 4 | 5 | export default function ScrollIndicator() { 6 | return ( 7 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /common/components/ScrollOpacity.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, ReactNode } from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | import { useScrollY } from '../hooks/useScrollY'; 6 | import { useWindowSize } from '../hooks/useWindowSize'; 7 | 8 | interface ScrollOpacityProps { 9 | children: ReactNode; 10 | center?: boolean; 11 | setStartScroll?: (startScroll: number) => void; 12 | } 13 | 14 | export default function ScrollOpacity({ 15 | children, 16 | center = false, 17 | setStartScroll, 18 | }: ScrollOpacityProps) { 19 | const { height, width } = useWindowSize(); 20 | const scrollY = useScrollY(); 21 | 22 | const startScroll = useRef(0); 23 | 24 | const mobile = width < 768; 25 | let opacity = 0; 26 | 27 | if (startScroll.current) { 28 | opacity = Math.min(Math.max((scrollY - startScroll.current) / 300, 0), 1); 29 | } 30 | 31 | return ( 32 | { 35 | if (startScroll.current === 0) { 36 | startScroll.current = 37 | scrollY + (center ? height / 2 : 0) + (mobile ? 0 : 200); 38 | if (setStartScroll) setStartScroll(startScroll.current); 39 | } 40 | }} 41 | onViewportLeave={() => { 42 | if (scrollY <= startScroll.current) { 43 | startScroll.current = 0; 44 | if (setStartScroll) setStartScroll(startScroll.current); 45 | } 46 | }} 47 | > 48 | {children} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /modules/collabio/helpers/handleDraw.ts: -------------------------------------------------------------------------------- 1 | import { drawPath } from './drawPath'; 2 | 3 | const MAX_PATH_LENGTH = 250; 4 | 5 | let path: { x: number; y: number }[] = []; 6 | let tempImageData: ImageData | undefined; 7 | 8 | const drawCtx = (ctx: CanvasRenderingContext2D) => { 9 | if (tempImageData) { 10 | ctx.putImageData(tempImageData, 0, 0); 11 | } 12 | 13 | drawPath(ctx, path); 14 | }; 15 | 16 | const handleDraw = ( 17 | { clientX, clientY }: { clientX: number; clientY: number }, 18 | canvas: HTMLCanvasElement | null, 19 | secondCtx: CanvasRenderingContext2D | null 20 | ) => { 21 | if (canvas) { 22 | const ctx = canvas.getContext('2d'); 23 | 24 | if (ctx && secondCtx) { 25 | const { x, y } = canvas.getBoundingClientRect(); 26 | 27 | const mouseX = clientX - x; 28 | const mouseY = clientY - y; 29 | 30 | path.push({ x: mouseX, y: mouseY }); 31 | 32 | drawCtx(ctx); 33 | drawCtx(secondCtx); 34 | 35 | if (path.length > MAX_PATH_LENGTH) { 36 | path = path.slice(1); 37 | } 38 | } 39 | } 40 | }; 41 | 42 | const handleDrawEnd = ( 43 | callback: (path: { x: number; y: number }[]) => void 44 | ) => { 45 | callback(path); 46 | path = []; 47 | }; 48 | 49 | const handleDrawStart = (canvas: HTMLCanvasElement | null) => { 50 | if (canvas) { 51 | const ctx = canvas.getContext('2d'); 52 | 53 | if (ctx) { 54 | tempImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 55 | } 56 | } 57 | }; 58 | 59 | export { handleDraw, handleDrawEnd, handleDrawStart }; 60 | -------------------------------------------------------------------------------- /modules/projects/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | import ScrollOpacity from '@/common/components/ScrollOpacity'; 6 | import { useScrollY } from '@/common/hooks/useScrollY'; 7 | import { useWindowSize } from '@/common/hooks/useWindowSize'; 8 | import { useMouseVariant } from '@/modules/customMouse'; 9 | 10 | export default function Header() { 11 | const scrollY = useScrollY(); 12 | const { setMouseVariant } = useMouseVariant(); 13 | 14 | const { height } = useWindowSize(); 15 | 16 | const [startScroll, setStartScroll] = useState(0); 17 | 18 | const scale = Math.max((scrollY - startScroll) / 5000 + 0.2); 19 | 20 | let opacity = 1; 21 | 22 | if (scrollY > startScroll + height * 1.3) { 23 | opacity = 0.9 - (scrollY - (startScroll + height * 1.3)) / 400; 24 | } 25 | 26 | return ( 27 |
28 |
29 | 30 | 36 | and more. 37 | 38 | 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /modules/ballzone/helpers/renderMovables.ts: -------------------------------------------------------------------------------- 1 | const PLAYER_COLOR = '#3b82f6'; 2 | 3 | export const renderMovables = ( 4 | canvas: HTMLCanvasElement, 5 | ball: React.MutableRefObject<{ position: { x: number; y: number } }>, 6 | playerPosition: React.MutableRefObject<{ x: number; y: number }>, 7 | ballSize: number, 8 | playerSize: number 9 | ) => { 10 | const ctx = canvas.getContext('2d'); 11 | 12 | if (ctx) { 13 | ctx.clearRect(0, 0, canvas.width, canvas.height); 14 | 15 | renderBall({ 16 | ctx, 17 | position: ball.current.position, 18 | ballSize, 19 | }); 20 | 21 | renderPlayer({ 22 | ctx, 23 | position: playerPosition.current, 24 | playerSize, 25 | }); 26 | } 27 | }; 28 | 29 | const renderBall = ({ 30 | ctx, 31 | position, 32 | ballSize, 33 | }: { 34 | ctx: CanvasRenderingContext2D; 35 | position: { x: number; y: number }; 36 | ballSize: number; 37 | }) => { 38 | ctx.fillStyle = '#fff'; 39 | ctx.lineWidth = 2; 40 | ctx.strokeStyle = '#000'; 41 | ctx.beginPath(); 42 | ctx.arc(position.x, position.y, ballSize, 0, Math.PI * 2); 43 | ctx.fill(); 44 | ctx.stroke(); 45 | ctx.closePath(); 46 | }; 47 | 48 | const renderPlayer = ({ 49 | ctx, 50 | position, 51 | playerSize, 52 | }: { 53 | ctx: CanvasRenderingContext2D; 54 | position: { x: number; y: number }; 55 | playerSize: number; 56 | }) => { 57 | ctx.fillStyle = PLAYER_COLOR; 58 | ctx.beginPath(); 59 | ctx.arc(position.x, position.y, playerSize, 0, Math.PI * 2); 60 | ctx.fill(); 61 | ctx.stroke(); 62 | ctx.closePath(); 63 | }; 64 | -------------------------------------------------------------------------------- /modules/ballzone/helpers/calculateBallPosition.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from '../types/ball.types'; 2 | 3 | const LINE_WIDTH = 2; 4 | const OPPOSITE_MULTIPLIER = -0.85; 5 | 6 | export const calculateBallPosition = ( 7 | x: number, 8 | y: number, 9 | { goalWidth, ballSize, boardSize, goalHeight }: Settings 10 | ) => { 11 | const moveAreaSize = goalWidth * 2; 12 | let areaToBlock = moveAreaSize; 13 | 14 | if ( 15 | y < boardSize.height / 2 + goalHeight - ballSize && 16 | y > boardSize.height / 2 - goalHeight + ballSize 17 | ) { 18 | areaToBlock = 0; 19 | } 20 | 21 | const maxX = ballSize + areaToBlock + LINE_WIDTH; 22 | const minX = boardSize.width - ballSize - areaToBlock - LINE_WIDTH; 23 | 24 | const newX = Math.max(Math.min(x, minX), maxX); 25 | 26 | let newY = 0; 27 | let maxY = ballSize + moveAreaSize + LINE_WIDTH; 28 | let minY = boardSize.height - ballSize - moveAreaSize - LINE_WIDTH; 29 | if ( 30 | newX < moveAreaSize + ballSize - LINE_WIDTH || 31 | newX > boardSize.width - moveAreaSize - ballSize + LINE_WIDTH 32 | ) { 33 | maxY = boardSize.height / 2 - goalHeight + ballSize + LINE_WIDTH; 34 | minY = boardSize.height / 2 + goalHeight - ballSize - LINE_WIDTH; 35 | } 36 | 37 | newY = Math.max(Math.min(y, minY), maxY); 38 | 39 | const multiplier = { x: 1, y: 1 }; 40 | if (newX === maxX || newX === minX) multiplier.x = OPPOSITE_MULTIPLIER; 41 | if (newY === maxY || newY === minY) multiplier.y = OPPOSITE_MULTIPLIER; 42 | 43 | return [ 44 | { 45 | x: Math.round(newX * 10) / 10, 46 | y: Math.round(newY * 10) / 10, 47 | }, 48 | multiplier, 49 | ]; 50 | }; 51 | -------------------------------------------------------------------------------- /modules/hero/index.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | 3 | import Header from './components/Header'; 4 | import ScrollIndicator from './components/ScrollIndicator'; 5 | 6 | export default function Hero() { 7 | return ( 8 |
12 | 18 |
19 | 29 | 34 | Resume 35 | 36 | 46 |
47 | 48 | 49 |
50 |
51 |
52 |
53 | 54 | 55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /modules/ballzone/hooks/useGameLoop.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | import { handleBallPhysics } from '../helpers/handleBallPhysics'; 4 | import { renderMovables } from '../helpers/renderMovables'; 5 | import { Ball } from '../types/ball.types'; 6 | 7 | interface Props { 8 | ballSize: number; 9 | playerSize: number; 10 | goalHeight: number; 11 | } 12 | 13 | export const useGameLoop = ( 14 | movablesRef: React.RefObject, 15 | ball: React.MutableRefObject, 16 | playerPosition: React.MutableRefObject<{ x: number; y: number }>, 17 | { ballSize, playerSize, goalHeight }: Props 18 | ) => { 19 | const [isRunning, setIsRunning] = useState(false); 20 | const [scores, setScores] = useState<[number, number]>([0, 0]); 21 | 22 | const animationframeId = useRef(); 23 | 24 | useEffect(() => { 25 | const dpi = window.devicePixelRatio; 26 | const canvas = movablesRef.current; 27 | 28 | const loopFn = () => { 29 | if (!isRunning || !canvas) return; 30 | 31 | const newBall = handleBallPhysics( 32 | ball.current, 33 | playerPosition.current, 34 | setScores, 35 | { 36 | ballSize, 37 | goalWidth: playerSize, 38 | boardSize: { 39 | width: canvas.width / dpi, 40 | height: canvas.height / dpi, 41 | }, 42 | goalHeight, 43 | } 44 | ); 45 | ball.current = newBall; 46 | 47 | renderMovables(canvas, ball, playerPosition, ballSize, playerSize); 48 | 49 | animationframeId.current = requestAnimationFrame(loopFn); 50 | }; 51 | 52 | animationframeId.current = requestAnimationFrame(loopFn); 53 | 54 | return () => { 55 | if (animationframeId.current !== undefined) { 56 | cancelAnimationFrame(animationframeId.current); 57 | } 58 | }; 59 | }, [ 60 | ballSize, 61 | playerSize, 62 | goalHeight, 63 | isRunning, 64 | movablesRef, 65 | ball, 66 | playerPosition, 67 | setScores, 68 | ]); 69 | 70 | return { 71 | isRunning, 72 | setIsRunning, 73 | scores, 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /modules/about/components/Intro.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, Fragment } from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | import { useScrollY } from '@/common/hooks/useScrollY'; 6 | import { useWindowSize } from '@/common/hooks/useWindowSize'; 7 | import { useMouseVariant } from '@/modules/customMouse'; 8 | 9 | const TEXT = 'Using the latest technologies, I develop things like...'; 10 | const TEXT_ARR = TEXT.split(' '); 11 | const PROGRESS_PER_WORD = 100 / TEXT_ARR.length; 12 | 13 | export default function Intro() { 14 | const { setMouseVariant } = useMouseVariant(); 15 | const scrollY = useScrollY(); 16 | const { height } = useWindowSize(); 17 | 18 | const startScroll = useRef(0); 19 | 20 | const progress = calculateProgress(startScroll.current, scrollY, height); 21 | 22 | return ( 23 | { 28 | if (startScroll.current === 0) { 29 | startScroll.current = scrollY + height / 4; 30 | } 31 | }} 32 | onViewportLeave={() => { 33 | if (scrollY <= startScroll.current) { 34 | startScroll.current = 0; 35 | } 36 | }} 37 | > 38 | {TEXT_ARR.map((char, index) => { 39 | return ( 40 | 41 | = PROGRESS_PER_WORD * index + 1 44 | ? { y: 0, opacity: 1 } 45 | : { y: 20, opacity: 0 } 46 | } 47 | transition={{ duration: 0.3 }} 48 | > 49 | {char} 50 | {' '} 51 | 52 | ); 53 | })} 54 | 55 | ); 56 | } 57 | 58 | const calculateProgress = ( 59 | startScroll: number, 60 | scrollY: number, 61 | height: number 62 | ) => 63 | startScroll && 64 | (Math.round( 65 | Math.min(100, Math.max(0, ((scrollY - startScroll) / (height / 3)) * 100)) 66 | ) * 67 | 100) / 68 | 100; 69 | -------------------------------------------------------------------------------- /common/lib/checkIsMobile.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export const checkIsMobile = () => { 4 | if (typeof window === 'undefined') { 5 | return false; 6 | } 7 | 8 | let check = false; 9 | (function (a) { 10 | if ( 11 | /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test( 12 | a 13 | ) || 14 | /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( 15 | a.substr(0, 4) 16 | ) 17 | ) 18 | check = true; 19 | })(navigator.userAgent || navigator.vendor || (window as any).opera); 20 | return check; 21 | }; 22 | -------------------------------------------------------------------------------- /modules/collabio/components/Windows.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | import { useScrollY } from '@/common/hooks/useScrollY'; 6 | import { useWindowSize } from '@/common/hooks/useWindowSize'; 7 | 8 | import SingleWindow from './SingleWindow'; 9 | 10 | export default function Windows({ 11 | windowLength = 1, 12 | }: { 13 | windowLength?: number; 14 | }) { 15 | const { height } = useWindowSize(); 16 | const scrollY = useScrollY(); 17 | 18 | const [client1Ctx, setClient1Ctx] = useState(); 19 | const [client2Ctx, setClient2Ctx] = useState(); 20 | 21 | const [userMoves, setUserMoves] = useState<{ x: number; y: number }[][]>([]); 22 | 23 | const startScroll = useRef(0); 24 | 25 | const progress = calculateProgress( 26 | startScroll.current, 27 | scrollY, 28 | height, 29 | windowLength 30 | ); 31 | 32 | return ( 33 | { 36 | if (startScroll.current === 0) { 37 | startScroll.current = scrollY + height; 38 | } 39 | }} 40 | onViewportLeave={() => { 41 | if (scrollY <= startScroll.current) { 42 | startScroll.current = 0; 43 | } 44 | }} 45 | > 46 |

47 | (try to draw something on the screen) 48 |

49 |
50 | setUserMoves([...userMoves, move])} 54 | setCtx={setClient1Ctx} 55 | oppositeCtx={client2Ctx} 56 | /> 57 | setUserMoves([...userMoves, move])} 62 | setCtx={setClient2Ctx} 63 | oppositeCtx={client1Ctx} 64 | /> 65 |
66 |
67 | ); 68 | } 69 | 70 | const calculateProgress = ( 71 | startScroll: number, 72 | scrollY: number, 73 | height: number, 74 | windowLength: number 75 | ) => 76 | startScroll && 77 | Math.round( 78 | Math.min( 79 | 100, 80 | Math.max( 81 | 0, 82 | ((scrollY - startScroll) / (windowLength * height - 100)) * 100 83 | ) 84 | ) * 100 85 | ) / 100; 86 | -------------------------------------------------------------------------------- /modules/customMouse/components/CircleMouse.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | 5 | import { motion, useSpring } from 'framer-motion'; 6 | 7 | import { checkIsMobile } from '@/common/lib/checkIsMobile'; 8 | 9 | import { useMouseStore } from '../store/mouseStore'; 10 | import { MouseVariant } from '../types/mouse.types'; 11 | 12 | const TRANSITION = { type: 'spring', stiffness: 2000, damping: 100 }; 13 | const OFFSETS = { 14 | [MouseVariant.TEXT]: { x: -75, y: -75 }, 15 | [MouseVariant.TECHNOLOGY]: { x: -75, y: -125 }, 16 | }; 17 | const VARIANTS = { 18 | [MouseVariant.TEXT]: { 19 | height: 150, 20 | width: 150, 21 | }, 22 | [MouseVariant.TECHNOLOGY]: { 23 | height: 150, 24 | width: 150, 25 | backgroundImage: 26 | 'linear-gradient(rgba(255,255,255, 1), rgba(255,255,255, 1))', 27 | }, 28 | }; 29 | const HIDDEN_MOUSE_VARIANTS = [MouseVariant.DEFAULT, MouseVariant.TECHNOLOGY]; 30 | 31 | export default function CircleMouse() { 32 | const { mouseVariant, text } = useMouseStore((store) => ({ 33 | mouseVariant: store.variant, 34 | text: store.text, 35 | })); 36 | 37 | const [isTouchDevice, setIstouchDevice] = useState(false); 38 | 39 | const mouse = { 40 | x: useSpring(-100, TRANSITION), 41 | y: useSpring(-100, TRANSITION), 42 | }; 43 | 44 | useEffect(() => { 45 | if (HIDDEN_MOUSE_VARIANTS.includes(mouseVariant)) { 46 | document.body.style.cursor = 'default'; 47 | } else { 48 | document.body.style.cursor = 'none'; 49 | } 50 | }, [mouseVariant]); 51 | 52 | useEffect(() => { 53 | if (checkIsMobile()) setIstouchDevice(true); 54 | 55 | const updateMousePosition = (e: MouseEvent) => { 56 | const currentOffset = OFFSETS[mouseVariant as keyof typeof OFFSETS] ?? { 57 | x: 0, 58 | y: 0, 59 | }; 60 | 61 | mouse.x.set(e.clientX + currentOffset.x); 62 | mouse.y.set(e.clientY + currentOffset.y); 63 | }; 64 | 65 | document.addEventListener('mousemove', updateMousePosition); 66 | 67 | return () => document.removeEventListener('mousemove', updateMousePosition); 68 | }, [mouseVariant, mouse.x, mouse.y]); 69 | 70 | if (isTouchDevice) return null; 71 | 72 | return ( 73 | 80 | {mouseVariant === MouseVariant.TECHNOLOGY && text} 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /modules/about/components/Skills.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import Image, { StaticImageData } from 'next/image'; 3 | 4 | import { useMouseVariant } from '@/modules/customMouse'; 5 | import expressSVG from '@/public/svg/express.svg'; 6 | import framerMotionSVG from '@/public/svg/framermotion.svg'; 7 | import graphQLSVG from '@/public/svg/graphql.svg'; 8 | import mongoDBSVG from '@/public/svg/mongodb.svg'; 9 | import nestJSVG from '@/public/svg/nestjs.svg'; 10 | import nextSVG from '@/public/svg/nextjs.svg'; 11 | import nodeSVG from '@/public/svg/nodejs.svg'; 12 | import reactSVG from '@/public/svg/react.svg'; 13 | import recoilSVG from '@/public/svg/recoiljs.svg'; 14 | import socketIoSVG from '@/public/svg/socketio.svg'; 15 | import tailwindSVG from '@/public/svg/tailwindcss.svg'; 16 | import typescriptSVG from '@/public/svg/typescript.svg'; 17 | import webRTCSVG from '@/public/svg/webrtc.svg'; 18 | 19 | interface SkillBadgeProps { 20 | svg: StaticImageData; 21 | name: string; 22 | className?: string; 23 | } 24 | 25 | function SkillBadge({ svg, name, className }: SkillBadgeProps) { 26 | const { setMouseVariant } = useMouseVariant(); 27 | 28 | return ( 29 | 37 | {name} setMouseVariant.technology(name)} 43 | onMouseLeave={setMouseVariant.default} 44 | /> 45 | 46 | ); 47 | } 48 | 49 | export default function Skills() { 50 | return ( 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 67 | 68 | 69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /public/svg/graphql.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 72 | -------------------------------------------------------------------------------- /public/svg/react.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5_logos -------------------------------------------------------------------------------- /modules/contact/index.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { BsChevronUp } from 'react-icons/bs'; 3 | import { FaLinkedin, FaGithub } from 'react-icons/fa'; 4 | 5 | import ScrollOpacity from '@/common/components/ScrollOpacity'; 6 | 7 | import { useMouseVariant } from '../customMouse'; 8 | 9 | const Contact = () => { 10 | const { setMouseVariant } = useMouseVariant(); 11 | 12 | return ( 13 |
14 |
15 | 16 |
17 |

23 | Let's work together. 24 |

25 | 26 |

27 | dziecielskibruno@gmail.com 28 |

29 | 33 | Contact me 34 | 35 |
36 |
37 | 38 | window.scrollTo({ top: 0, behavior: 'smooth' })} 41 | animate={{ y: [0, 10, 0] }} 42 | transition={{ duration: 1.5, repeat: Infinity }} 43 | > 44 | 45 | Back to top 46 | 47 | 48 |
49 |
50 |

© {getCopyrightYear()} Bruno Dzięcielski

51 | 58 | 59 | 60 | 61 | 68 | 69 | 70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default Contact; 78 | 79 | const getCopyrightYear = () => { 80 | const date = new Date(); 81 | const year = date.getFullYear(); 82 | 83 | return year; 84 | }; 85 | -------------------------------------------------------------------------------- /public/svg/tailwindcss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Configuration for JavaScript files 3 | "extends": [ 4 | "airbnb-base", 5 | "next/core-web-vitals", 6 | "plugin:prettier/recommended" 7 | ], 8 | "root": true, 9 | "rules": { 10 | "prettier/prettier": [ 11 | "error", 12 | { 13 | "singleQuote": true, 14 | "semi": true 15 | } 16 | ] 17 | }, 18 | "overrides": [ 19 | // Configuration for TypeScript files 20 | { 21 | "files": ["**/*.ts", "**/*.tsx"], 22 | "plugins": ["@typescript-eslint", "unused-imports", "tailwindcss"], 23 | "extends": [ 24 | "plugin:tailwindcss/recommended", 25 | "airbnb-typescript", 26 | "next/core-web-vitals", 27 | "plugin:prettier/recommended" 28 | ], 29 | "parserOptions": { 30 | "project": "./tsconfig.json" 31 | }, 32 | "rules": { 33 | "no-param-reassign": "off", 34 | "consistent-return": "off", 35 | "@typescript-eslint/no-use-before-define": "off", 36 | "prettier/prettier": [ 37 | "error", 38 | { 39 | "singleQuote": true, 40 | "endOfLine": "auto", 41 | "semi": true 42 | } 43 | ], 44 | "react/destructuring-assignment": "off", // Vscode doesn't support automatically destructuring, it's a pain to add a new variable 45 | "jsx-a11y/anchor-is-valid": "off", // Next.js use his own internal link system 46 | "react/require-default-props": "off", // Allow non-defined react props as undefined 47 | "react/jsx-props-no-spreading": "off", // _app.tsx uses spread operator and also, react-hook-form 48 | "@next/next/no-img-element": "off", // We currently not using next/image because it isn't supported with SSG mode, 49 | "@next/next/no-head-element": "off", // Next.js 13, 50 | "import/order": [ 51 | "error", 52 | { 53 | "groups": ["builtin", "external", "internal"], 54 | "pathGroups": [ 55 | { 56 | "pattern": "react", 57 | "group": "external", 58 | "position": "before" 59 | } 60 | ], 61 | "pathGroupsExcludedImportTypes": ["react"], 62 | "newlines-between": "always", 63 | "alphabetize": { 64 | "order": "asc", 65 | "caseInsensitive": true 66 | } 67 | } 68 | ], 69 | "@typescript-eslint/comma-dangle": "off", // Avoid conflict rule between Eslint and Prettier 70 | "import/prefer-default-export": "off", // Named export is easier to refactor automatically 71 | "class-methods-use-this": "off", // _document.tsx use render method without `this` keyword 72 | // "tailwindcss/classnames-order": [ 73 | // { 74 | // "officialSorting": true 75 | // } 76 | // ], // Follow the same ordering as the official plugin `prettier-plugin-tailwindcss` 77 | "@typescript-eslint/no-unused-vars": "off", 78 | "unused-imports/no-unused-imports": "error", 79 | "unused-imports/no-unused-vars": [ 80 | "error", 81 | { "argsIgnorePattern": "^_" } 82 | ] 83 | } 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /modules/projects/components/ProjectsList.tsx: -------------------------------------------------------------------------------- 1 | import ScrollOpacity from '@/common/components/ScrollOpacity'; 2 | 3 | import Project from './Project'; 4 | 5 | export default function ProjectsList() { 6 | return ( 7 | <> 8 |
9 |
13 | 14 |
15 | 22 | 30 | 36 | 41 | 47 | 53 |
54 | 55 |

56 | For more projects checkout my{' '} 57 | 63 | github 64 | 65 | . 66 |

67 |
68 |
69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /public/svg/recoiljs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /modules/ballzone/helpers/renderBoard.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | export const STRIPS_COLOR = { 4 | light: '#5A9D61', 5 | dark: '#54925A', 6 | }; 7 | 8 | interface RenderBoardProps { 9 | boardSize: { width: number; height: number }; 10 | playerSize: number; 11 | lineWidth: number; 12 | goalHeight: number; 13 | isSmall: boolean; 14 | } 15 | 16 | export const renderBoard = ( 17 | canvas: HTMLCanvasElement, 18 | { 19 | boardSize: { width, height }, 20 | playerSize, 21 | lineWidth, 22 | goalHeight, 23 | isSmall, 24 | }: RenderBoardProps 25 | ) => { 26 | const dpi = window.devicePixelRatio; 27 | 28 | canvas.width = width * dpi; 29 | canvas.height = height * dpi; 30 | 31 | const ctx = canvas.getContext('2d'); 32 | 33 | if (ctx) { 34 | ctx.scale(dpi, dpi); 35 | ctx.lineCap = 'round'; 36 | ctx.lineJoin = 'round'; 37 | const moveAreaSize = playerSize * 2; 38 | 39 | const props = { 40 | ctx, 41 | height, 42 | width, 43 | moveAreaSize, 44 | lineWidth, 45 | goalHeight, 46 | isSmall, 47 | }; 48 | 49 | renderGrass(props); 50 | renderLines(props); 51 | renderGoals(props); 52 | removeWhiteLines(props); 53 | } 54 | }; 55 | 56 | interface RenderComponentProps { 57 | ctx: CanvasRenderingContext2D; 58 | height: number; 59 | width: number; 60 | goalHeight: number; 61 | moveAreaSize: number; 62 | lineWidth: number; 63 | isSmall: boolean; 64 | } 65 | 66 | const renderGrass = ({ 67 | ctx, 68 | height, 69 | width, 70 | moveAreaSize, 71 | }: RenderComponentProps) => { 72 | ctx.strokeStyle = STRIPS_COLOR.dark; 73 | const lineCount = 11; 74 | let bgLineWidth = (width - moveAreaSize * 2) / lineCount; 75 | const howManyLines = width / bgLineWidth; 76 | if (howManyLines % 2 === 0) 77 | bgLineWidth = Math.ceil(width / (howManyLines + 1)); 78 | 79 | let darker = true; 80 | for (let i = 0; i < width; i += bgLineWidth) { 81 | if (darker) ctx.fillStyle = STRIPS_COLOR.dark; 82 | else ctx.fillStyle = STRIPS_COLOR.light; 83 | 84 | ctx.fillRect( 85 | i + moveAreaSize, 86 | moveAreaSize, 87 | bgLineWidth, 88 | height - moveAreaSize * 2 89 | ); 90 | 91 | darker = !darker; 92 | } 93 | }; 94 | 95 | const renderLines = ({ 96 | ctx, 97 | height, 98 | width, 99 | moveAreaSize, 100 | lineWidth, 101 | isSmall, 102 | }: RenderComponentProps) => { 103 | ctx.strokeStyle = '#fff'; 104 | ctx.lineWidth = lineWidth; 105 | ctx.beginPath(); 106 | ctx.arc(width / 2, height / 2, isSmall ? 35 : 80, 0, Math.PI * 2); 107 | ctx.stroke(); 108 | 109 | ctx.moveTo(width / 2, moveAreaSize); 110 | ctx.lineTo(width / 2, height - moveAreaSize); 111 | ctx.stroke(); 112 | ctx.closePath(); 113 | 114 | ctx.strokeRect( 115 | moveAreaSize, 116 | moveAreaSize, 117 | width - moveAreaSize * 2, 118 | height - moveAreaSize * 2 119 | ); 120 | }; 121 | 122 | const renderGoals = ({ 123 | ctx, 124 | height, 125 | width, 126 | moveAreaSize, 127 | goalHeight, 128 | }: RenderComponentProps) => { 129 | ctx.beginPath(); 130 | 131 | // LEFT GOAL 132 | ctx.moveTo(1, height / 2); 133 | ctx.lineTo(1, height / 2 - goalHeight); 134 | ctx.lineTo(moveAreaSize, height / 2 - goalHeight); 135 | 136 | ctx.moveTo(1, height / 2); 137 | ctx.lineTo(1, height / 2 + goalHeight); 138 | ctx.lineTo(moveAreaSize, height / 2 + goalHeight); 139 | 140 | // RIGHT GOAL 141 | ctx.moveTo(width - 1, height / 2); 142 | ctx.lineTo(width - 1, height / 2 - goalHeight); 143 | ctx.lineTo(width - moveAreaSize, height / 2 - goalHeight); 144 | 145 | ctx.moveTo(width - 1, height / 2); 146 | ctx.lineTo(width - 1, height / 2 + goalHeight); 147 | ctx.lineTo(width - moveAreaSize, height / 2 + goalHeight); 148 | 149 | ctx.stroke(); 150 | ctx.closePath(); 151 | }; 152 | 153 | const removeWhiteLines = ({ 154 | ctx, 155 | height, 156 | width, 157 | moveAreaSize, 158 | lineWidth, 159 | goalHeight, 160 | }: RenderComponentProps) => { 161 | ctx.beginPath(); 162 | ctx.lineWidth = lineWidth + 1; 163 | ctx.strokeStyle = '#5A9D61'; 164 | ctx.lineCap = 'square'; 165 | 166 | ctx.moveTo(moveAreaSize, height / 2 - goalHeight + lineWidth); 167 | ctx.lineTo(moveAreaSize, height / 2 + goalHeight - lineWidth); 168 | 169 | ctx.moveTo(width - moveAreaSize, height / 2 - goalHeight + lineWidth); 170 | ctx.lineTo(width - moveAreaSize, height / 2 + goalHeight - lineWidth); 171 | 172 | ctx.stroke(); 173 | 174 | ctx.closePath(); 175 | }; 176 | -------------------------------------------------------------------------------- /modules/ballzone/helpers/handleBallPhysics.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import { Dispatch, SetStateAction } from 'react'; 3 | 4 | import { calculateBallPosition } from './calculateBallPosition'; 5 | import { Ball, Settings } from '../types/ball.types'; 6 | 7 | const BALL_MOVEMENT_CONFIG = { 8 | MAX_SPEED: 5, 9 | ACCELERATION: 0.35, 10 | DECELERATION: 0.12, 11 | SMALL_DECELERATION: 0.015, 12 | }; 13 | 14 | type Vector = { 15 | x: number; 16 | y: number; 17 | }; 18 | 19 | export const handleBallPhysics = ( 20 | ball: Ball, 21 | playerPosition: Vector, 22 | setScores: Dispatch>, 23 | settings: Settings 24 | ) => { 25 | let newBall = { ...ball }; 26 | 27 | newBall.velocityVector = applyDeceleration(newBall.velocityVector); 28 | 29 | const velocityUpdate = updateVelocity( 30 | newBall.velocityVector, 31 | newBall.position, 32 | settings 33 | ); 34 | newBall.velocityVector = velocityUpdate.velocityVector; 35 | newBall.position = velocityUpdate.position; 36 | 37 | newBall = checkForCollision(newBall, playerPosition, settings); 38 | 39 | updateScores(newBall.position, setScores, settings); 40 | 41 | newBall = { 42 | ...newBall, 43 | ...resetBallIfGoal(newBall.position, settings), 44 | }; 45 | 46 | return newBall; 47 | }; 48 | 49 | const applyDeceleration = (velocityVector: Vector) => { 50 | const decelerate = ( 51 | velocity: number, 52 | threshold: number, 53 | largeDecel: number, 54 | smallDecel: number 55 | ): number => { 56 | const deceleration = 57 | Math.abs(velocity) > threshold ? largeDecel : smallDecel; 58 | return velocity > 0 59 | ? Math.max(velocity - deceleration, 0) 60 | : Math.min(velocity + deceleration, 0); 61 | }; 62 | 63 | return { 64 | x: decelerate( 65 | velocityVector.x, 66 | 4, 67 | BALL_MOVEMENT_CONFIG.DECELERATION, 68 | BALL_MOVEMENT_CONFIG.SMALL_DECELERATION 69 | ), 70 | y: decelerate( 71 | velocityVector.y, 72 | 3, 73 | BALL_MOVEMENT_CONFIG.DECELERATION, 74 | BALL_MOVEMENT_CONFIG.SMALL_DECELERATION 75 | ), 76 | }; 77 | }; 78 | 79 | const updateVelocity = ( 80 | velocityVector: Vector, 81 | position: Vector, 82 | settings: Settings 83 | ) => { 84 | const [newPosition, multiplier] = calculateBallPosition( 85 | position.x + velocityVector.x, 86 | position.y + velocityVector.y, 87 | settings 88 | ); 89 | 90 | return { 91 | velocityVector: { 92 | x: velocityVector.x * multiplier.x, 93 | y: velocityVector.y * multiplier.y, 94 | }, 95 | position: newPosition, 96 | }; 97 | }; 98 | 99 | const checkForCollision = ( 100 | ball: Ball, 101 | playerPosition: Vector, 102 | settings: Settings 103 | ): Ball => { 104 | const { goalWidth, ballSize } = settings; 105 | const COLLISION_DISTANCE = goalWidth + ballSize; 106 | const distanceX = ball.position.x - playerPosition.x; 107 | const distanceY = ball.position.y - playerPosition.y; 108 | const length = Math.sqrt(distanceX ** 2 + distanceY ** 2) || 1; 109 | 110 | if (length < COLLISION_DISTANCE + 1) { 111 | const unitX = distanceX / length; 112 | const unitY = distanceY / length; 113 | 114 | const impactStrengthX = 4; 115 | const impactStrengthY = 4; 116 | const dampingFactor = 10; 117 | 118 | const vX = 119 | (ball.velocityVector.x - unitX * impactStrengthX) / dampingFactor; 120 | const vY = 121 | (ball.velocityVector.y - unitY * impactStrengthY) / dampingFactor; 122 | 123 | ball.velocityVector.x -= vX; 124 | ball.velocityVector.y -= vY; 125 | 126 | const [newPosition] = calculateBallPosition( 127 | playerPosition.x + (COLLISION_DISTANCE + 1) * unitX, 128 | playerPosition.y + (COLLISION_DISTANCE + 1) * unitY, 129 | settings 130 | ); 131 | 132 | ball.position = newPosition; 133 | } 134 | 135 | return ball; 136 | }; 137 | 138 | const updateScores = ( 139 | position: Vector, 140 | setScores: Dispatch>, 141 | settings: Settings 142 | ) => { 143 | const { goalWidth, boardSize } = settings; 144 | 145 | if (position.x < goalWidth) { 146 | setScores((scores) => [scores[0], scores[1] + 1]); 147 | } 148 | 149 | if (position.x > boardSize.width - goalWidth) { 150 | setScores((scores) => [scores[0] + 1, scores[1]]); 151 | } 152 | }; 153 | 154 | const resetBallIfGoal = (position: Vector, settings: Settings) => { 155 | const { goalWidth, boardSize } = settings; 156 | if (position.x < goalWidth || position.x > boardSize.width - goalWidth) { 157 | return { 158 | position: { 159 | x: boardSize.width / 2, 160 | y: boardSize.height / 2, 161 | }, 162 | velocityVector: { 163 | x: 0, 164 | y: 0, 165 | }, 166 | }; 167 | } 168 | 169 | return {}; 170 | }; 171 | -------------------------------------------------------------------------------- /public/svg/express.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/svg/nodejs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/ballzone/components/Game.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef, useState } from 'react'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | import { useElementDimensions } from '@/common/hooks/useElementDimensions'; 6 | import { checkIsMobile } from '@/common/lib/checkIsMobile'; 7 | import { useMouseVariant } from '@/modules/customMouse'; 8 | 9 | import { STRIPS_COLOR } from '../helpers/renderBoard'; 10 | import { useGameLoop } from '../hooks/useGameLoop'; 11 | import { useRenderBoard } from '../hooks/useRenderBoard'; 12 | import { useSetupMovables } from '../hooks/useSetupMovables'; 13 | import { Ball } from '../types/ball.types'; 14 | 15 | export default function Game() { 16 | const { setMouseVariant } = useMouseVariant(); 17 | 18 | const playerPosition = useRef({ x: 0, y: 0 }); 19 | const ball = useRef({ 20 | position: { x: 0, y: 0 }, 21 | velocityVector: { x: 0, y: 0 }, 22 | }); 23 | 24 | const windowRef = useRef(null); 25 | const canvasRef = useRef(null); 26 | const movablesRef = useRef(null); 27 | 28 | //! Workaround for server hydration 29 | const [isMobile, setIsMobile] = useState(false); 30 | useEffect(() => { 31 | setIsMobile(checkIsMobile()); 32 | }, []); 33 | 34 | const { width, height } = useElementDimensions(windowRef); 35 | 36 | const { isSmall, lineWidth, playerSize, ballSize, goalHeight } = 37 | useMemo(() => { 38 | const tempIsSmall = width < 700; 39 | const tempLineWidth = tempIsSmall ? 2 : 4; 40 | const tempPlayerSize = tempIsSmall ? 12 : 18; 41 | const tempBallSize = tempIsSmall ? 9 : 14; 42 | const tempGoalHeight = height / 5; 43 | 44 | return { 45 | isSmall: tempIsSmall, 46 | lineWidth: tempLineWidth, 47 | playerSize: tempPlayerSize, 48 | ballSize: tempBallSize, 49 | goalHeight: tempGoalHeight, 50 | }; 51 | }, [width, height]); 52 | 53 | useRenderBoard(canvasRef, { 54 | width, 55 | height, 56 | lineWidth, 57 | playerSize, 58 | goalHeight, 59 | isSmall, 60 | }); 61 | 62 | useSetupMovables(movablesRef, ball, playerPosition, { width, height }); 63 | 64 | const { setIsRunning, scores } = useGameLoop( 65 | movablesRef, 66 | ball, 67 | playerPosition, 68 | { 69 | playerSize, 70 | ballSize, 71 | goalHeight, 72 | } 73 | ); 74 | 75 | const handleUserMove = ({ x, y }: { x: number; y: number }) => { 76 | const node = windowRef.current; 77 | 78 | if (node) { 79 | const rect = node.getBoundingClientRect(); 80 | 81 | playerPosition.current = { 82 | x: x - rect.left, 83 | y: y - rect.top, 84 | }; 85 | } 86 | }; 87 | 88 | return ( 89 |
90 |

91 | {isMobile 92 | ? '(try to move ball with your finger)' 93 | : '(try to move ball with your mouse)'} 94 |

95 | setIsRunning(true)} 98 | onViewportLeave={() => setIsRunning(false)} 99 | > 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | 108 | 114 | ballzone.herokuapp.com/room 115 | 116 |
117 | 118 |
119 |

{scores[0]}

120 |

{scores[1]}

121 |
122 | 123 |
handleUserMove({ x: e.clientX, y: e.clientY })} 130 | onTouchMove={(e) => 131 | handleUserMove({ 132 | x: e.touches[0].clientX, 133 | y: e.touches[0].clientY, 134 | }) 135 | } 136 | > 137 | 144 | 145 | 150 |
151 |
152 | 153 |
154 | ); 155 | } 156 | -------------------------------------------------------------------------------- /modules/collabio/components/SingleWindow.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | import clsx from 'clsx'; 4 | import { motion, useMotionValue } from 'framer-motion'; 5 | import { BsCursorFill } from 'react-icons/bs'; 6 | 7 | import { useElementDimensions } from '@/common/hooks/useElementDimensions'; 8 | import { useMouseVariant } from '@/modules/customMouse'; 9 | 10 | import { calculateClientsPositions } from '../helpers/calculateClientsPositions'; 11 | import { drawLines } from '../helpers/drawLines'; 12 | import { drawPath } from '../helpers/drawPath'; 13 | import { 14 | handleDraw, 15 | handleDrawEnd, 16 | handleDrawStart, 17 | } from '../helpers/handleDraw'; 18 | 19 | interface SingleWindowProps { 20 | isSecond?: boolean; 21 | progress: number; 22 | userMoves: { x: number; y: number }[][]; 23 | addUserMove: (userMoves: { x: number; y: number }[]) => void; 24 | setCtx: (ctx: CanvasRenderingContext2D) => void; 25 | oppositeCtx: CanvasRenderingContext2D | undefined; 26 | } 27 | 28 | export default function SingleWindow({ 29 | isSecond = false, 30 | progress, 31 | userMoves, 32 | addUserMove, 33 | setCtx, 34 | oppositeCtx, 35 | }: SingleWindowProps) { 36 | const { setMouseVariant } = useMouseVariant(); 37 | 38 | const [isDrawing, setIsDrawing] = useState(false); 39 | 40 | const windowRef = useRef(null); 41 | const canvasRef = useRef(null); 42 | 43 | const mouse = { 44 | x: useMotionValue(0), 45 | y: useMotionValue(0), 46 | }; 47 | 48 | const { width, height } = useElementDimensions(windowRef); 49 | 50 | useEffect(() => { 51 | const ctx = canvasRef.current?.getContext('2d'); 52 | if (ctx) setCtx(ctx); 53 | }, [setCtx]); 54 | 55 | useEffect(() => { 56 | const canvas = canvasRef.current; 57 | 58 | if (canvas) { 59 | const dpi = window.devicePixelRatio; 60 | 61 | canvas.width = width * dpi; 62 | canvas.height = height * dpi; 63 | 64 | const ctx = canvas.getContext('2d'); 65 | 66 | if (ctx) { 67 | ctx.scale(dpi, dpi); 68 | 69 | drawLines(ctx, { width, height }); 70 | 71 | const moves1: { x: number; y: number }[] = []; 72 | const moves2: { x: number; y: number }[] = []; 73 | 74 | for (let i = 0.5; i <= progress; i += 0.5) { 75 | const { client1, client2 } = calculateClientsPositions(i, { 76 | width, 77 | height, 78 | }); 79 | 80 | if (i >= 50) moves1.push(client1); 81 | if (i <= 50) moves2.push(client2); 82 | } 83 | 84 | ctx.strokeStyle = '#000'; 85 | ctx.lineWidth = 2; 86 | 87 | drawPath(ctx, moves1); 88 | drawPath(ctx, moves2); 89 | 90 | userMoves.forEach((moves) => drawPath(ctx, moves)); 91 | } 92 | } 93 | }, [width, height, progress, userMoves]); 94 | 95 | const { client1, client2 } = calculateClientsPositions(progress, { 96 | width, 97 | height, 98 | }); 99 | 100 | return ( 101 |
102 |
103 |
104 |
105 |
106 | 119 | 120 |
{ 126 | const rect = windowRef.current?.getBoundingClientRect(); 127 | if (rect) { 128 | const x = e.clientX - rect.left; 129 | const y = e.clientY - rect.top; 130 | 131 | mouse.x.set(x); 132 | mouse.y.set(y); 133 | } 134 | 135 | if (isDrawing && oppositeCtx) { 136 | handleDraw(e, canvasRef.current, oppositeCtx); 137 | } 138 | }} 139 | onMouseDown={(e) => { 140 | setIsDrawing(true); 141 | handleDrawStart(canvasRef.current); 142 | if (oppositeCtx) { 143 | handleDraw(e, canvasRef.current, oppositeCtx); 144 | } 145 | }} 146 | onMouseUp={() => { 147 | setIsDrawing(false); 148 | handleDrawEnd(addUserMove); 149 | }} 150 | onTouchStart={(e) => { 151 | setIsDrawing(true); 152 | handleDrawStart(canvasRef.current); 153 | if (oppositeCtx) { 154 | handleDraw(e.touches[0], canvasRef.current, oppositeCtx); 155 | } 156 | }} 157 | onTouchMove={(e) => { 158 | if (isDrawing && oppositeCtx) { 159 | handleDraw(e.touches[0], canvasRef.current, oppositeCtx); 160 | } 161 | }} 162 | onTouchEnd={() => { 163 | setIsDrawing(false); 164 | handleDrawEnd(addUserMove); 165 | }} 166 | > 167 | 171 | 172 | 173 | 174 | 179 |
180 | 184 | 190 | 191 | 192 | 196 | 202 | 203 |
204 |
205 | 206 |
212 | Client {isSecond ? '2' : '1'} 213 |
214 |
215 | ); 216 | } 217 | -------------------------------------------------------------------------------- /public/svg/webrtc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | image/svg+xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /public/svg/mongodb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/svg/nestjs.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 22 | 23 | 25 | 26 | 28 | image/svg+xml 29 | 31 | 32 | 33 | 34 | 35 | 38 | 42 | 43 | 44 | --------------------------------------------------------------------------------