diff --git a/app/components/ColorContextChangerContainer.tsx b/app/components/ColorContextChangerContainer.tsx new file mode 100644 index 0000000..9d1b943 --- /dev/null +++ b/app/components/ColorContextChangerContainer.tsx @@ -0,0 +1,54 @@ +import { motion, useInView } from 'framer-motion'; +import { debounce } from 'lodash-es'; +import * as React from 'react'; + +import { ColorContext } from '~/context/ColorContext'; +import type { ColorThemeContextEnum } from '~/context/types'; + +export interface ColorChangeContainerProps { + children: React.ReactNode; + className?: string; + colorContext: ColorThemeContextEnum; + tag?: string; +} + +export function ColorContextChangerContainer({ + colorContext, + className, + tag, + children, +}: ColorChangeContainerProps): React.ReactNode { + const ref = React.useRef(null); + + const { setColorContext } = React.useContext(ColorContext); + + const isInView = useInView(ref, { once: false, margin: '-500px' }); + + const debouncedColorContextHandler = debounce( + (debounceColorContext: ColorThemeContextEnum) => { + setColorContext(debounceColorContext); + }, + 100, + ); + + React.useEffect(() => { + if (isInView) { + debouncedColorContextHandler(colorContext); + } + }, [isInView, colorContext, debouncedColorContextHandler]); + + const CurrentTag = tag || 'div'; + // @ts-expect-error Element mismatch between motion + const ColorContextChanger = motion[CurrentTag]; + + return ( + + {children} + + ); +} diff --git a/app/components/StaggerSplitText.tsx b/app/components/StaggerSplitText.tsx new file mode 100644 index 0000000..98f692d --- /dev/null +++ b/app/components/StaggerSplitText.tsx @@ -0,0 +1,39 @@ +import { motion } from 'framer-motion'; +import * as React from 'react'; + +export function StaggerSplitText(props: { text: string }): React.ReactNode { + const { text } = props; + + return ( + + {text.split('').map((individualLetter, individualLetterIndex) => ( + ({ + opacity: 1, + y: 0, + transition: { + duration: 0.6, + delay: currentIndex * 0.04, + }, + }), + }} + > + {individualLetter} + + ))} + + ); +} diff --git a/app/context/ColorContext.tsx b/app/context/ColorContext.tsx new file mode 100644 index 0000000..382d003 --- /dev/null +++ b/app/context/ColorContext.tsx @@ -0,0 +1,63 @@ +import { motion } from 'framer-motion'; +import { get } from 'lodash-es'; +import React, { createContext, useState } from 'react'; + +import { ColorThemeContextEnum } from '~/context/types'; +import type { ColorContextState } from '~/context/types'; +import { useColorThemeResolver } from '~/hooks/useColorThemeResolver'; + +export const ColorContext = createContext({ + colorContext: ColorThemeContextEnum.DEFAULT, + setColorContext: (colorContext: ColorThemeContextEnum): void => { + /** + * as we need a default action to handle the param + */ + // eslint-disable-next-line no-console + console.debug(colorContext); + }, +}); + +export function BodyHTMLTagColorProvider({ + children, +}: { + children: React.ReactNode; +}): React.ReactNode { + const { resolveColorTheme, colorThemeMap, colorTheme } = + useColorThemeResolver(); + + const [colors, setColors] = useState( + get(colorThemeMap, [ColorThemeContextEnum.DEFAULT]), + ); + + const setColorsHandler = ( + selectedColorContext: ColorThemeContextEnum, + ): void => { + const { background, foreground } = resolveColorTheme(selectedColorContext); + + setColors({ background, foreground }); + }; + + const providerValue = React.useMemo( + () => ({ + colorContext: colorTheme, + setColorContext: setColorsHandler, + }), + [colorTheme], + ); + + return ( + + + {children} + + + ); +} diff --git a/app/context/types.ts b/app/context/types.ts new file mode 100644 index 0000000..d14dad7 --- /dev/null +++ b/app/context/types.ts @@ -0,0 +1,53 @@ +export type ColorContextState = { + background: string; + foreground: string; +}; + +export enum BaseColorsEnum { + WHITE = 'WHITE', + BLACK = 'BLACK', +} + +export enum PrimaryColorsEnum { + RED = 'RED', + BLUE = 'BLUE', + YELLOW = 'YELLOW', +} + +export enum SecondaryColorsEnum { + GREEN = 'GREEN', + ORANGE = 'ORANGE', + PURPLE = 'PURPLE', +} + +type PrimaryColorsObject = { + [K in keyof typeof PrimaryColorsEnum]: string; +}; + +type SecondaryColorsObject = { + [K in keyof typeof SecondaryColorsEnum]: string; +}; + +type BaseColorsObject = { + [K in keyof typeof BaseColorsEnum]: string; +}; + +export type CombinedColorsObject = PrimaryColorsObject & + SecondaryColorsObject & + BaseColorsObject; + +export enum ColorThemeContextEnum { + BLUE = PrimaryColorsEnum.BLUE, + DEFAULT = BaseColorsEnum.BLACK, + WHITE = BaseColorsEnum.WHITE, + RED = PrimaryColorsEnum.RED, + YELLOW = PrimaryColorsEnum.YELLOW, + + GREEN = SecondaryColorsEnum.GREEN, + ORANGE = SecondaryColorsEnum.ORANGE, + PURPLE = SecondaryColorsEnum.PURPLE, +} + +export type ColorThemeContextMap = { + [K in ColorThemeContextEnum]: ColorContextState; +}; diff --git a/app/hooks/useColorThemeResolver.ts b/app/hooks/useColorThemeResolver.ts new file mode 100644 index 0000000..b3a2031 --- /dev/null +++ b/app/hooks/useColorThemeResolver.ts @@ -0,0 +1,91 @@ +import { get } from 'lodash-es'; +import { useState } from 'react'; + +import { + BaseColorsEnum, + ColorThemeContextEnum, + PrimaryColorsEnum, + SecondaryColorsEnum, +} from '~/context/types'; +import type { + ColorContextState, + ColorThemeContextMap, + CombinedColorsObject, +} from '~/context/types'; + +export type UseColorThemeResolver = () => { + colorTheme: ColorThemeContextEnum; + colorThemeMap: ColorThemeContextMap; + resolveColorTheme: (theme: ColorThemeContextEnum) => ColorContextState; +}; + +export const useColorThemeResolver: UseColorThemeResolver = () => { + const [colorTheme, setColorTheme] = useState( + ColorThemeContextEnum.DEFAULT, + ); + + const colors: { + [K in keyof CombinedColorsObject]: string; + } = { + [BaseColorsEnum.BLACK]: 'Black', + [BaseColorsEnum.WHITE]: 'GhostWhite', + [PrimaryColorsEnum.BLUE]: 'DarkBlue', + [PrimaryColorsEnum.RED]: 'Crimson', + [PrimaryColorsEnum.YELLOW]: 'Khaki', + [SecondaryColorsEnum.GREEN]: 'MediumSeaGreen', + [SecondaryColorsEnum.ORANGE]: 'DarkOrange', + [SecondaryColorsEnum.PURPLE]: 'Indigo', + }; + const colorThemeMap: ColorThemeContextMap = { + [ColorThemeContextEnum.DEFAULT]: { + background: get(colors, [BaseColorsEnum.BLACK]), + foreground: get(colors, [PrimaryColorsEnum.RED]), + }, + [ColorThemeContextEnum.WHITE]: { + background: get(colors, [BaseColorsEnum.WHITE]), + foreground: get(colors, [BaseColorsEnum.BLACK]), + }, + [ColorThemeContextEnum.RED]: { + background: get(colors, [PrimaryColorsEnum.RED]), + foreground: get(colors, [SecondaryColorsEnum.GREEN]), + }, + [ColorThemeContextEnum.GREEN]: { + background: get(colors, [SecondaryColorsEnum.GREEN]), + foreground: get(colors, [PrimaryColorsEnum.RED]), + }, + [ColorThemeContextEnum.BLUE]: { + background: get(colors, [PrimaryColorsEnum.BLUE]), + foreground: get(colors, [SecondaryColorsEnum.ORANGE]), + }, + [ColorThemeContextEnum.ORANGE]: { + background: get(colors, [SecondaryColorsEnum.ORANGE]), + foreground: get(colors, [PrimaryColorsEnum.BLUE]), + }, + [ColorThemeContextEnum.PURPLE]: { + background: get(colors, [SecondaryColorsEnum.PURPLE]), + foreground: get(colors, [PrimaryColorsEnum.YELLOW]), + }, + [ColorThemeContextEnum.YELLOW]: { + background: get(colors, [PrimaryColorsEnum.YELLOW]), + foreground: get(colors, [SecondaryColorsEnum.PURPLE]), + }, + }; + + const resolveColorTheme = ( + selectedColorContext: ColorThemeContextEnum, + ): ColorContextState => { + setColorTheme(selectedColorContext); + + return get( + colorThemeMap, + [selectedColorContext], + get(colorThemeMap, [ColorThemeContextEnum.DEFAULT]), + ); + }; + + return { + resolveColorTheme, + colorThemeMap, + colorTheme, + }; +}; diff --git a/app/lib/HorizontalScrollText.tsx b/app/lib/HorizontalScrollText.tsx index 6ded13d..ee59e1b 100644 --- a/app/lib/HorizontalScrollText.tsx +++ b/app/lib/HorizontalScrollText.tsx @@ -80,7 +80,7 @@ export function HorizontalScrollText({ )} style={{ lineHeight: '1.0', - color: idx % 3 === 0 ? '#344054' : '#e0e6e6', + color: idx % 3 === 0 ? undefined : '#e0e6e6', }} > {children} diff --git a/app/root.tsx b/app/root.tsx index 3f0dc0c..57eef1c 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -20,6 +20,7 @@ import { } from '@remix-run/react'; import React from 'react'; +import { BodyHTMLTagColorProvider } from '~/context/ColorContext'; import '~/tailwind.css'; export const links: LinksFunction = () => [ @@ -49,14 +50,11 @@ export function Layout({ Kurocado Studio - + {children} - + ); } diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 8f5b856..07f2dd6 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -13,6 +13,8 @@ import type { MetaFunction } from '@remix-run/node'; import React from 'react'; +import { ColorContextChangerContainer } from '~/components/ColorContextChangerContainer'; +import { ColorThemeContextEnum } from '~/context/types'; import { Intro } from '~/views/Intro'; export const meta: MetaFunction = () => { @@ -34,5 +36,57 @@ export const meta: MetaFunction = () => { }; export default function Index(): React.ReactNode { - return ; + return ( + <> + + + Some Children + + + + Some Children + + + + Some Children + + + + Some Children + + + + Some Children + + + + Some Children + + + + Some Children + + + ); } diff --git a/app/views/Intro.tsx b/app/views/Intro.tsx index ed14e02..c9c2e0d 100644 --- a/app/views/Intro.tsx +++ b/app/views/Intro.tsx @@ -1,15 +1,19 @@ import React from 'react'; +import { ColorContextChangerContainer } from '~/components/ColorContextChangerContainer'; +import { StaggerSplitText } from '~/components/StaggerSplitText'; +import { ColorThemeContextEnum } from '~/context/types'; import { HorizontalScrollText } from '~/lib/HorizontalScrollText'; export function Intro(): React.ReactNode { return ( - <> -
-
- Kurocado - Studio -
- + + Kurocado + Studio + + ); }