Added support for Cursor, Theme and Navigation providers
This commit is contained in:
parent
e110345dea
commit
3cc1ee5cef
9 changed files with 240 additions and 1 deletions
src/core
126
src/core/components/Cursor.tsx
Normal file
126
src/core/components/Cursor.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
"use client";
|
||||
import React, {createContext, ReactNode, useContext, useEffect, useRef, useState} from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
|
||||
const CursorContext = createContext<{
|
||||
isCursorHidden: boolean;
|
||||
isCursorEnabled: boolean;
|
||||
isCursorInViewport: boolean;
|
||||
toggleCursor: () => void;
|
||||
toggleCursorEnabled: () => void;
|
||||
} | null>(null);
|
||||
|
||||
export interface CursorProviderProps {
|
||||
hidden: boolean;
|
||||
exists: boolean;
|
||||
children: ReactNode;
|
||||
cursor?: ReactNode;
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function CursorProvider({ hidden, exists, children, cursor, enabled }: CursorProviderProps) {
|
||||
if (!enabled) return <>{children}</>
|
||||
const [isCursorHidden, setIsCursorHidden] = useState<boolean>(hidden);
|
||||
const [doesCursorExists, setDoesCursorExists] = useState(exists);
|
||||
const [isCursorInViewport, setIsCursorInViewport] = useState(false);
|
||||
|
||||
const toggleCursor = () => setIsCursorHidden(!isCursorHidden);
|
||||
const toggleCursorEnabled = () => {
|
||||
setDoesCursorExists(!doesCursorExists);
|
||||
localStorage.setItem('cursorEnabled', JSON.stringify(!doesCursorExists));
|
||||
}
|
||||
|
||||
const cursorRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const moveCursor = (e: MouseEvent) => {
|
||||
const { clientX: x, clientY: y } = e;
|
||||
|
||||
setIsCursorInViewport((x > 0 && x < window.innerWidth) && (y > 0 && y < window.innerHeight));
|
||||
|
||||
gsap.to(cursorRef.current, {
|
||||
x: x - 16,
|
||||
y: y - 16,
|
||||
duration: 0.1,
|
||||
ease: "power3.out",
|
||||
});
|
||||
|
||||
const savedCursorState = localStorage.getItem('cursorEnabled');
|
||||
if (savedCursorState !== null) {
|
||||
setDoesCursorExists(JSON.parse(savedCursorState));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointer = (event: PointerEvent) => {
|
||||
switch (event.pointerType) {
|
||||
case "mouse":
|
||||
setDoesCursorExists(true);
|
||||
break;
|
||||
case "pen":
|
||||
setDoesCursorExists(false);
|
||||
break;
|
||||
case "touch":
|
||||
setDoesCursorExists(false);
|
||||
break;
|
||||
default:
|
||||
setDoesCursorExists(false);
|
||||
console.log(`pointerType ${event.pointerType} is not supported`);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"pointerdown",
|
||||
handlePointer,
|
||||
false,
|
||||
);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsCursorInViewport(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsCursorInViewport(false);
|
||||
};
|
||||
|
||||
window.addEventListener('mouseenter', handleMouseEnter);
|
||||
window.addEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
window.addEventListener('mousemove', moveCursor);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('mouseenter', handleMouseEnter);
|
||||
window.removeEventListener('mouseleave', handleMouseLeave);
|
||||
|
||||
window.removeEventListener('mousemove', moveCursor);
|
||||
|
||||
window.removeEventListener('pointerdown', handlePointer, false)
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const htmlElement = document.documentElement;
|
||||
if (doesCursorExists) {
|
||||
htmlElement.classList.add('custom-cursor-enabled-env');
|
||||
} else {
|
||||
htmlElement.classList.remove('custom-cursor-enabled-env');
|
||||
}
|
||||
}, [doesCursorExists]);
|
||||
|
||||
return (
|
||||
<CursorContext.Provider value={{isCursorHidden, isCursorEnabled: doesCursorExists, isCursorInViewport, toggleCursor, toggleCursorEnabled}}>
|
||||
{doesCursorExists ?
|
||||
cursor ??
|
||||
<div
|
||||
ref={cursorRef}
|
||||
className={`pointer-events-none fixed top-0 left-0 ${isCursorHidden || !doesCursorExists || !isCursorInViewport ? 'invisible' : ''} w-10 h-10 bg-white rounded-full z-50 mix-blend-difference`}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
{children}
|
||||
</CursorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCursor() {
|
||||
return useContext(CursorContext);
|
||||
}
|
26
src/core/components/DynamicProvider.tsx
Normal file
26
src/core/components/DynamicProvider.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
"use client";
|
||||
import {CursorProvider, CursorProviderProps} from "./Cursor";
|
||||
import {ThemeProvider} from "./Theme";
|
||||
import Theme from "../types/theme";
|
||||
import {defaultTheme} from "../index";
|
||||
import {NavigationProvider, NavigationProviderProps} from "./Navigation";
|
||||
|
||||
interface DynamicProviderProps {
|
||||
cursor?: CursorProviderProps,
|
||||
theme?: Theme,
|
||||
children: React.ReactNode,
|
||||
selector?: NavigationProviderProps
|
||||
}
|
||||
|
||||
export function DynamicProvider({ children, cursor, theme, selector }: DynamicProviderProps) {
|
||||
const actualTheme = theme || defaultTheme
|
||||
return (
|
||||
<ThemeProvider defaultTheme={actualTheme}>
|
||||
<CursorProvider hidden={cursor?.hidden || true} exists={cursor?.exists || false} cursor={cursor?.cursor}>
|
||||
<NavigationProvider defaultCoords={selector?.defaultCoords}>
|
||||
{children}
|
||||
</NavigationProvider>
|
||||
</CursorProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
29
src/core/components/Navigation.tsx
Normal file
29
src/core/components/Navigation.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
import React, {createContext, ReactNode, useContext, useState} from 'react';
|
||||
import {Coords} from "../types/navigation/coords";
|
||||
import {Method} from "../types/navigation/method";
|
||||
|
||||
export const NavigationContext = createContext<{
|
||||
coords: Coords,
|
||||
setCoords: (coords: Coords) => void
|
||||
} | null>(null);
|
||||
|
||||
export interface NavigationProviderProps {
|
||||
children: ReactNode;
|
||||
defaultCoords?: Coords;
|
||||
methods?: Method[]
|
||||
}
|
||||
|
||||
export function NavigationProvider({ children, defaultCoords }: NavigationProviderProps) {
|
||||
const [coords, setCoords] = useState<Coords>(defaultCoords || {x: 0, y: 0});
|
||||
|
||||
return (
|
||||
<NavigationContext.Provider value={{coords, setCoords}}>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useNavigation() {
|
||||
return useContext(NavigationContext);
|
||||
}
|
39
src/core/components/Theme.tsx
Normal file
39
src/core/components/Theme.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
"use client";
|
||||
import React, {createContext, ReactNode, useContext, useState} from 'react';
|
||||
import Theme from "../types/theme";
|
||||
|
||||
export const themeContext = createContext<{
|
||||
theme: Theme,
|
||||
setTheme: (theme: Theme) => void
|
||||
} | null>(null);
|
||||
|
||||
export const colorSchemeContext = createContext<{
|
||||
colorScheme: "dark" | "light" | "auto",
|
||||
setColorScheme: (scheme: "dark" | "light" | "auto") => void
|
||||
} | null>(null)
|
||||
|
||||
export interface ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
defaultTheme: Theme;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children, defaultTheme }: ThemeProviderProps) {
|
||||
const [colorScheme, setColorScheme] = useState<"dark" | "light" | "auto">("dark");
|
||||
const [theme, setTheme] = useState<Theme>(defaultTheme);
|
||||
|
||||
return (
|
||||
<themeContext.Provider value={{theme, setTheme}}>
|
||||
<colorSchemeContext.Provider value={{colorScheme, setColorScheme}}>
|
||||
{children}
|
||||
</colorSchemeContext.Provider>
|
||||
</themeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
return useContext(themeContext);
|
||||
}
|
||||
|
||||
export function useColorScheme() {
|
||||
return useContext(colorSchemeContext);
|
||||
}
|
5
src/core/index.ts
Normal file
5
src/core/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Theme from "./types/theme";
|
||||
|
||||
export const defaultTheme: Theme = {
|
||||
colorScheme: "light"
|
||||
}
|
|
@ -15,7 +15,9 @@
|
|||
"ui",
|
||||
"ui-kit"
|
||||
],
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"gsap": "3.12.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
"@types/react": "^19.0.2",
|
||||
|
|
4
src/core/types/navigation/coords.ts
Normal file
4
src/core/types/navigation/coords.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface Coords {
|
||||
x: number,
|
||||
y: number
|
||||
}
|
5
src/core/types/navigation/method.ts
Normal file
5
src/core/types/navigation/method.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum Method {
|
||||
CONTROLLER,
|
||||
ARROW,
|
||||
WASD
|
||||
}
|
3
src/core/types/theme.ts
Normal file
3
src/core/types/theme.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default interface Theme {
|
||||
colorScheme: "light" | "dark" | "auto"
|
||||
}
|
Loading…
Add table
Reference in a new issue