Added support for Cursor, Theme and Navigation providers

This commit is contained in:
KillerBossOriginal 2025-01-02 17:59:10 +01:00
parent e110345dea
commit 3cc1ee5cef
9 changed files with 240 additions and 1 deletions

View 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);
}

View 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>
);
}

View 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);
}

View 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
View file

@ -0,0 +1,5 @@
import Theme from "./types/theme";
export const defaultTheme: Theme = {
colorScheme: "light"
}

View file

@ -15,7 +15,9 @@
"ui",
"ui-kit"
],
"dependencies": {},
"dependencies": {
"gsap": "3.12.5"
},
"devDependencies": {
"typescript": "^5.5.3",
"@types/react": "^19.0.2",

View file

@ -0,0 +1,4 @@
export interface Coords {
x: number,
y: number
}

View file

@ -0,0 +1,5 @@
export enum Method {
CONTROLLER,
ARROW,
WASD
}

3
src/core/types/theme.ts Normal file
View file

@ -0,0 +1,3 @@
export default interface Theme {
colorScheme: "light" | "dark" | "auto"
}