diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3b9de294..be2a6707 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react'; import { Montserrat } from 'next/font/google'; +import Cursor from '@/components/Cursor/Cursor'; import PageAnimatePresence from '@/components/HOC/PageAnimatePresence/PageAnimatePresence'; import Menu from '@/components/Menu/Menu'; @@ -30,6 +31,7 @@ const RootLayout = ({ {children} + ); diff --git a/src/components/Cursor/Cursor.stories.tsx b/src/components/Cursor/Cursor.stories.tsx new file mode 100644 index 00000000..974d915f --- /dev/null +++ b/src/components/Cursor/Cursor.stories.tsx @@ -0,0 +1,15 @@ +import Cursor from './Cursor'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + component: Cursor, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; diff --git a/src/components/Cursor/Cursor.test.tsx b/src/components/Cursor/Cursor.test.tsx new file mode 100644 index 00000000..f937dc68 --- /dev/null +++ b/src/components/Cursor/Cursor.test.tsx @@ -0,0 +1,44 @@ +import { screen, render, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +import Cursor from './Cursor'; + +import type { RenderResult } from '@testing-library/react'; + +describe('src/components/Cursor/Cursor.test.tsx', () => { + let renderResult: RenderResult; + + beforeEach(() => { + renderResult = render(); + }); + + afterEach(() => { + renderResult.unmount(); + }); + + it('should render the Cursor component without crashing', () => { + const cursorElement = screen.getByTestId('cursor'); + expect(cursorElement).toBeInTheDocument(); + }); + + it('should render the Cursor component with mouse movement', async () => { + const cursorElement = screen.getByTestId('cursor'); + const initialPosition = { + x: cursorElement.style.left, + y: cursorElement.style.top, + }; + + fireEvent.mouseMove(document, { + clientX: 100, + clinetY: 200, + }); + + await waitFor(() => { + expect(cursorElement.style.left).not.toBe(initialPosition.x); + }); + + await waitFor(() => { + expect(cursorElement.style.top).not.toBe(initialPosition.y); + }); + }); +}); diff --git a/src/components/Cursor/Cursor.tsx b/src/components/Cursor/Cursor.tsx new file mode 100644 index 00000000..4f96dcfd --- /dev/null +++ b/src/components/Cursor/Cursor.tsx @@ -0,0 +1,43 @@ +'use client'; + +import { useEffect, type FC } from 'react'; + +import { useMotionValue, useSpring, motion } from 'framer-motion'; + +const Cursor: FC = () => { + const cursorSize = 15; + + const mouse = { + x: useMotionValue(0), + y: useMotionValue(0), + }; + + const smoothOptions = { damping: 20, stiffness: 300, mass: 0.5 }; + const smoothMouse = { + x: useSpring(mouse.x, smoothOptions), + y: useSpring(mouse.y, smoothOptions), + }; + + const manageMouseMove = (e: MouseEvent) => { + const { clientX, clientY } = e; + mouse.x.set(clientX - cursorSize / 2); + mouse.y.set(clientY - cursorSize / 2); + }; + + useEffect(() => { + window.addEventListener('mousemove', manageMouseMove); + return () => { + window.removeEventListener('mousemove', manageMouseMove); + }; + }, []); + + return ( + + ); +}; + +export default Cursor;