diff --git a/package.json b/package.json index 5457cb1..de4befa 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ }, "dependencies": { "@mui/material": "^6.4.8", + "@reduxjs/toolkit": "^2.6.1", "devicon": "^2.16.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-redux": "^9.2.0", "react-router": "^7.4.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63bcd1e..018aeb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@mui/material': specifier: ^6.4.8 version: 6.4.8(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.12)(react@19.0.0))(@types/react@19.0.12)(react@19.0.0))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@reduxjs/toolkit': + specifier: ^2.6.1 + version: 2.6.1(react-redux@9.2.0(@types/react@19.0.12)(react@19.0.0)(redux@5.0.1))(react@19.0.0) devicon: specifier: ^2.16.0 version: 2.16.0 @@ -20,6 +23,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-redux: + specifier: ^9.2.0 + version: 9.2.0(@types/react@19.0.12)(react@19.0.0)(redux@5.0.1) react-router: specifier: ^7.4.0 version: 7.4.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -531,6 +537,17 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@reduxjs/toolkit@2.6.1': + resolution: {integrity: sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rollup/rollup-android-arm-eabi@4.36.0': resolution: {integrity: sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==} cpu: [arm] @@ -675,6 +692,9 @@ packages: '@types/react@19.0.12': resolution: {integrity: sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.27.0': resolution: {integrity: sha512-4henw4zkePi5p252c8ncBLzLce52SEUz2Ebj8faDnuUXz2UuHEONYcJ+G0oaCF+bYCWVZtrGzq3FD7YXetmnSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1158,6 +1178,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1480,6 +1503,18 @@ packages: react-is@19.0.0: resolution: {integrity: sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -1504,6 +1539,14 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -1515,6 +1558,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1706,6 +1752,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vite@6.2.2: resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2231,6 +2282,16 @@ snapshots: '@popperjs/core@2.11.8': {} + '@reduxjs/toolkit@2.6.1(react-redux@9.2.0(@types/react@19.0.12)(react@19.0.0)(redux@5.0.1))(react@19.0.0)': + dependencies: + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.0.0 + react-redux: 9.2.0(@types/react@19.0.12)(react@19.0.0)(redux@5.0.1) + '@rollup/rollup-android-arm-eabi@4.36.0': optional: true @@ -2348,6 +2409,8 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.27.0(@typescript-eslint/parser@8.27.0(eslint@9.23.0)(typescript@5.8.2))(eslint@9.23.0)(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3031,6 +3094,8 @@ snapshots: ignore@5.3.2: {} + immer@10.1.1: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3357,6 +3422,15 @@ snapshots: react-is@19.0.0: {} + react-redux@9.2.0(@types/react@19.0.12)(react@19.0.0)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.0.0 + use-sync-external-store: 1.5.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.12 + redux: 5.0.1 + react-refresh@0.14.2: {} react-router@7.4.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0): @@ -3380,6 +3454,12 @@ snapshots: react@19.0.0: {} + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -3402,6 +3482,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve@1.22.10: @@ -3668,6 +3750,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.5.0(react@19.0.0): + dependencies: + react: 19.0.0 + vite@6.2.2(@types/node@22.13.11): dependencies: esbuild: 0.25.1 diff --git a/src/components/game/Game.tsx b/src/components/game/Game.tsx index bc1b08b..39a8317 100644 --- a/src/components/game/Game.tsx +++ b/src/components/game/Game.tsx @@ -1,61 +1,32 @@ -import { useEffect, useState } from 'react' import '@/styles/game/index.css' -import type { Item, OwnedItems } from '@/types' +import { useEffect } from 'react' import { Grid2 as Grid, Card, CardContent, CardHeader } from '@mui/material' -import { items } from '@/constants/items' import { Score, Gitcoin } from '@/components/game/core' import { Skills } from '@/components/game/skills' import { Store } from '@/components/game/store' +import { loop } from '@/modules/game' -export function Game() { - const [lines, setLines] = useState(0) - const [linesPerMillisecond, setLinesPerMillisecond] = useState(0) +import { useDispatch } from 'react-redux' - const [ownedItems, setOwnedItems] = useState({}) +export function Game() { + const dispatch = useDispatch() useEffect(() => { const interval = setInterval(() => { - setLines(prev => prev + linesPerMillisecond) + dispatch(loop()) }, 100) - return () => clearInterval(interval) - }, [linesPerMillisecond]) - - useEffect(() => { - let count = 0 - Object.keys(ownedItems).forEach((name) => { - const item = items.find(element => element.name === name) - - if (item != null) { - count += item.linesPerMillisecond * ownedItems[name] - } - }) - - setLinesPerMillisecond(count) - }, [ownedItems]) - - const handleClick = () => { - setLines(lines + 1) - } - - const handleBuy = (item: Item) => { - setLines(lines - item.price) - setOwnedItems({ - ...ownedItems, - [item.name]: (ownedItems[item.name] || 0) + 1, - }) - } + return () => clearInterval(interval) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) return ( <> - - + + @@ -63,7 +34,7 @@ export function Game() { - + @@ -71,7 +42,7 @@ export function Game() { - + diff --git a/src/components/game/core/Gitcoin.tsx b/src/components/game/core/Gitcoin.tsx index 397d212..d84223d 100644 --- a/src/components/game/core/Gitcoin.tsx +++ b/src/components/game/core/Gitcoin.tsx @@ -1,13 +1,14 @@ import '@/styles/game/core/gitcoin.css' import githubIcon from '@/assets/github.svg' +import { click } from '@/modules/game' +import { useDispatch } from 'react-redux' -type Props = { - onClick: () => void -} +export function Gitcoin() { + const dispatch = useDispatch() + const handleClick = () => dispatch(click()) -export function Gitcoin({ onClick }: Props) { return ( - ) diff --git a/src/components/game/core/Score.tsx b/src/components/game/core/Score.tsx index 26d85ec..53d47df 100644 --- a/src/components/game/core/Score.tsx +++ b/src/components/game/core/Score.tsx @@ -1,16 +1,17 @@ -type Props = { - lines: number - linesPerSecond: number -} +import { useSelector } from 'react-redux' +import { RootState } from '@/store' + +export const Score = () => { + const lines = useSelector((state: RootState) => state.game.lines) + const linesPerMillisecond = useSelector((state: RootState) => state.game.linesPerMillisecond) -export function Score({ lines, linesPerSecond }: Props) { return ( <>

{Math.ceil(lines)} lines

- per second: {Math.ceil(linesPerSecond * 10)} + per second: {Math.ceil(Math.ceil(linesPerMillisecond * 10) * 10)} ) diff --git a/src/components/game/skills/Skills.tsx b/src/components/game/skills/Skills.tsx index 353b6d1..d1b8258 100644 --- a/src/components/game/skills/Skills.tsx +++ b/src/components/game/skills/Skills.tsx @@ -1,16 +1,15 @@ +import { RootState } from '@/store' +import { useSelector } from 'react-redux' import { Section } from './Section' -import { OwnedItems } from '@/types' -type Props = { - skills: OwnedItems -} +export const Skills = () => { + const skills = useSelector((state: RootState) => state.game.skills) -export const Skills = ({ skills }: Props) => { return ( <> - {Object.keys(skills).map((name, key) => ( + {Object.keys(skills).map(name => (
diff --git a/src/components/game/store/Store.tsx b/src/components/game/store/Store.tsx index 2da3bf5..ef3a122 100644 --- a/src/components/game/store/Store.tsx +++ b/src/components/game/store/Store.tsx @@ -2,13 +2,15 @@ import { Item as ItemType } from '@/types' import { Item } from './Item.tsx' import { items } from '@/constants/items.ts' import { Grid2 as Grid } from '@mui/material' +import { buyItem } from '@/modules/game.ts' +import { RootState } from '@/store.ts' +import { useSelector, useDispatch } from 'react-redux' -type Props = { - lines: number - onBuy: (item: ItemType) => void -} +export function Store() { + const lines = useSelector((state: RootState) => state.game.lines) + const dispatch = useDispatch() + const handleBuy = (item: ItemType) => dispatch(buyItem(item)) -export function Store({ lines, onBuy }: Props) { return ( {items.map((item, key) => ( @@ -16,7 +18,7 @@ export function Store({ lines, onBuy }: Props) { key={key} item={item} lines={lines} - onBuy={onBuy} + onBuy={handleBuy} /> ))} diff --git a/src/main.tsx b/src/main.tsx index ebd1922..12d66f6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,13 +4,17 @@ import App from './App' import { CssBaseline } from '@mui/material' import { ThemeProvider } from '@mui/material/styles' import theme from '@/styles/theme' +import { Provider } from 'react-redux' +import store from './store' const root = createRoot(document.getElementById('root') as HTMLElement) root.render( - - + + + + , ) diff --git a/src/modules/game.ts b/src/modules/game.ts new file mode 100644 index 0000000..9eeeb47 --- /dev/null +++ b/src/modules/game.ts @@ -0,0 +1,45 @@ +import type { OwnedItems, Item } from '@/types' +import { PayloadAction, createSlice } from '@reduxjs/toolkit' + +// Initial state +type GameState = { + lines: number + linesPerMillisecond: number + skills: OwnedItems +} + +const INITIAL_STATE: GameState = { + lines: 0, + linesPerMillisecond: 0, + skills: {}, +} + +const game = createSlice({ + name: 'game', + initialState: INITIAL_STATE, + reducers: { + click: (state) => { + state.lines += 1 + }, + buyItem: (state, action: PayloadAction) => { + const { name, price, linesPerMillisecond: itemLinesPerMillisecond } = action.payload + + return { + ...state, + lines: state.lines - price, + linesPerMillisecond: state.linesPerMillisecond + itemLinesPerMillisecond, + skills: { + ...state.skills, + [name]: (state.skills[name] || 0) + 1, + }, + } + }, + loop: (state) => { + state.lines += state.linesPerMillisecond + }, + }, +}) + +export const { click, buyItem, loop } = game.actions + +export default game.reducer diff --git a/src/modules/index.ts b/src/modules/index.ts new file mode 100644 index 0000000..c9984cd --- /dev/null +++ b/src/modules/index.ts @@ -0,0 +1,6 @@ +import { combineReducers } from '@reduxjs/toolkit' +import game from './game' + +export const rootReducer = combineReducers({ + game, +}) diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..afb5a85 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,14 @@ +import { configureStore } from '@reduxjs/toolkit' +import { rootReducer } from './modules' + +function createStore() { + return configureStore({ + reducer: rootReducer, + }) +} + +const store = createStore() + +export type RootState = ReturnType + +export default store