<repo_name>3103ski/react-sudoku<file_sep>/src/components/gameboard/Gameboard.jsx
import React, { useContext } from 'react';

import { GameContext } from '../../context/game';
import Block from '../block/Block.jsx';
import { NumberButtons } from '../../components/';
import * as style from './gameboard.module.scss';

export default function Gameboard() {
	const game = useContext(GameContext);

	const renderBlocks = () => {
		let blocks = [];
		for (let i = 0; i <= 8; i++) {
			blocks.push(game.cells.filter((cell) => cell.block === i));
		}
		return blocks.map((block, i) => <Block key={`block__${i}`} cells={block} />);
	};

	return (
		<div className={style.BoardContainer}>
			{renderBlocks()}
			{game.focusCell ? (
				<div className={style.CellButtons}>
					<NumberButtons isNotes={true} />
					<NumberButtons />
				</div>
			) : null}
		</div>
	);
}
<file_sep>/src/context/game.js
import React, { createContext, useReducer } from 'react';

import { updateObj } from '../util/util.js';
import { generateSudoku, returnBlock, returnRow, returnCol } from '../util/sudokuFunctions.js';

const initialState = {
	puzzleAnswers: generateSudoku(),
	difficulty: 'medium',
	cells: null,
	clues: null,
	focusCell: null,
	wrongAnswerCount: null,
	gameComplete: false,
};

initialState.cells = generateCells(initialState.puzzleAnswers);
initialState.clues = buildClueArray(initialState.difficulty);

//•••••••••••••••••••
// •• Game Utilities
//•••••••••••••••••••
function generateCells(puzzleAnswers) {
	return puzzleAnswers.map((cell, i) => ({
		correctAnswer: cell,
		notes: [],
		row: returnRow(i),
		col: returnCol(i),
		block: returnBlock(i),
		answer: null,
		cellIndex: i,
	}));
}

function buildClueArray(difficulty) {
	let [indexList, limit] = [[], null];

	switch (difficulty) {
		case 'easy':
			limit = 40;
			break;
		case 'hard':
			limit = 20;
			break;
		default:
			limit = 30;
			break;
	}

	while (indexList.length < limit) {
		let num = Math.floor(Math.random() * 80);
		if (!indexList.includes(num)) indexList.push(num);
	}

	return indexList;
}

//•••••••••••••••••••••••
// •• Build game reducer
//•••••••••••••••••••••••
const reducer = (
	state,
	{ type, clues, puzzleAnswers, wrongAnswerCount, difficulty, cells, focusCell }
) => {
	switch (type) {
		case 'UPDATE_CELL':
			return updateObj(state, { cells, focusCell: null });
		case 'SET_FOCUS_CELL':
			return updateObj(state, { focusCell });
		case 'CHECK_PUZZLE':
			return updateObj(state, { wrongAnswerCount });
		case 'SET_DIFFICULTY':
			return updateObj(state, { difficulty });
		case 'NEW_PUZZLE':
			return updateObj(state, {
				clues,
				puzzleAnswers,
				cells,
				focusCell: null,
				gameComplete: false,
			});
		case 'END_GAME':
			return updateObj(state, { gameComplete: true, cells, focusCell: null });
		default:
			return state;
	}
};

const GameContext = createContext(initialState);

const GameProvider = (props) => {
	// •• Game State
	const [state, dispatch] = useReducer(reducer, initialState);

	// •• Game Options
	const setDifficulty = (difficulty) => dispatch({ type: 'SET_DIFFICULTY', difficulty });
	//••••••••••••••••••
	// •• Puzzle & Game
	//••••••••••••••••••

	// Create new puzzle
	const newPuzzle = () => {
		const puzzleAnswers = generateSudoku();
		let [clues, cells] = [buildClueArray(state.difficulty), generateCells(puzzleAnswers)];
		return dispatch({ type: 'NEW_PUZZLE', clues, cells, puzzleAnswers });
	};

	// Check if the values entered by user are correct and provide wrong answer count
	const checkPuzzle = () => {
		let wrongAnswerCount = state.cells
			.filter((c) => !state.clues.includes(c.cellIndex))
			.filter((cell) => cell.answer !== cell.correctAnswer).length;

		dispatch({ type: 'CHECK_PUZZLE', wrongAnswerCount });

		if (wrongAnswerCount > 0)
			setTimeout(() => dispatch({ type: 'CHECK_PUZZLE', wrongAnswerCount: null }), 2000);
	};

	// End the game when the user clicks the end button
	const endGame = () =>
		dispatch({
			type: 'END_GAME',
			cells: state.cells.map((cell) => ({ ...cell, answer: cell.correctAnswer })),
		});

	//••••••••••
	// •• Cells
	//••••••••••

	// toggle or select focus cell's when clicking around the window or board
	const setFocusCell = (focusCell) => dispatch({ type: 'SET_FOCUS_CELL', focusCell });

	// update cell notes

	// Recieve an updated cell from cell component and update stored cells in state
	const updateCell = (newCell) => {
		dispatch({
			type: 'UPDATE_CELL',
			cells: state.cells.map((cell) => {
				if (newCell.answer) {
					if (
						cell.row === newCell.row ||
						cell.block === newCell.block ||
						cell.col === newCell.col
					)
						cell.notes = cell.notes.filter((n) => n !== newCell.answer);

					newCell.notes = [];
				}
				return cell.cellIndex === newCell.cellIndex ? newCell : cell;
			}),
		});
	};
	// TODO -

	return (
		<GameContext.Provider
			value={{
				puzzleAnswers: state.puzzleAnswers,
				cells: state.cells,
				clues: state.clues,
				difficulty: state.difficulty,
				flaggedBoxes: state.flaggedBoxes,
				focusCell: state.focusCell,
				wrongAnswerCount: state.wrongAnswerCount,
				gameComplete: state.gameComplete,
				setDifficulty,
				setFocusCell,
				updateCell,
				checkPuzzle,
				newPuzzle,
				endGame,
			}}
			{...props}
		/>
	);
};

export { GameContext, GameProvider };
<file_sep>/README.md
<!-- Todo -->

-   add hint option
-   refine styling
-   buttons don't show when clicking the first cell on the board
<file_sep>/src/components/numberButtons/NumberButtons.jsx
import React, { useContext } from 'react';

import { GameContext } from '../../context/game.js';

import { updateObj } from '../../util/util.js';
import * as style from './numberButtons.module.scss';

export default function NumberButtons({ isNotes = false }) {
	const game = useContext(GameContext);
	let cell = game.cells[game.focusCell];

	const handleButtonOnClick = (num) => {
		if (isNotes) {
			if (!cell.notes.includes(num)) {
				cell.notes.push(num);
			} else {
				cell.notes = cell.notes.filter((n) => {
					return n !== num;
				});
			}
		} else {
			if (cell.answer && num === cell.answer) {
				cell = updateObj(cell, { answer: null });
			} else {
				cell = updateObj(cell, { answer: num });
			}
		}

		return game.updateCell(cell);
	};

	const Btn = ({ num }) => (
		<div
			onClick={() => handleButtonOnClick(num)}
			data-fill={
				(cell.notes.includes(num) && isNotes) || (cell.answer === num && !isNotes) ? 1 : 0
			}
			className={style.NumberButton}>
			<span>{num}</span>
		</div>
	);

	const renderBtns = () =>
		new Array(9)
			.fill(null)
			.map((_, i) => <Btn num={i + 1} key={`${isNotes ? 'note-btn' : 'ans-btn'}-${i}`} />);

	return <div className={style.Container}>{renderBtns()}</div>;
}
<file_sep>/src/components/gameOptions/GameOptions.jsx
import React, { useContext, useEffect, useState } from 'react';

import { GameContext } from '../../context/game.js';
import { Button as Btn, DropMenu as Drop } from '../../components';
import * as style from './gameOptions.module.scss';

export default function GameOptions() {
	const game = useContext(GameContext);
	const [currDifficulty, setCurrentDifficulty] = useState(game.difficulty);

	const gameOptions = [{ value: 'easy' }, { value: 'medium' }, { value: 'hard' }];

	useEffect(() => {
		if (currDifficulty !== game.difficulty) {
			setCurrentDifficulty(game.difficulty);
			game.newPuzzle();
		}
	}, [currDifficulty, game, game.difficulty]);

	return (
		<div className={style.GameOptionsContainer}>
			<Btn text='Check Answers' callback={game.checkPuzzle} color='green' />
			<Btn text='New Puzzle' callback={game.newPuzzle} />
			<Btn text='Show Solution' callback={game.endGame} color='orange' />
			<Drop selected={game.difficulty} callback={game.setDifficulty} options={gameOptions} />
		</div>
	);
}
<file_sep>/src/components/UI/index.js
export { default as Button } from './Button/Button.jsx';
export { default as DropMenu } from './DropMenu/DropMenu.jsx';
<file_sep>/src/components/cell/Cell.jsx
import React, { useContext, useState, useEffect, useCallback } from 'react';

import { GameContext } from '../../context/game.js';
import { updateObj } from '../../util/util.js';

import * as style from './cell.module.scss';

export default function Cell({ cell }) {
	const game = useContext(GameContext);
	const isClue = game.clues.includes(cell.cellIndex);

	const [isEditing, setIsEditing] = useState(false);
	const [cellValue, setCellValue] = useState(null);
	const [userInput, setUserInput] = useState('');

	// •• Validate and handle user input before setting value of cell
	const handleInputOnChange = (e) => {
		if ((!isNaN(e.target.value) && userInput.length < 1) || e.target.value === '')
			setUserInput(e.target.value);
	};

	// •• If user input doesn't match stored value set the new value
	const handleInputSubmit = useCallback(() => {
		if (cellValue !== userInput) {
			setCellValue(userInput);
			game.updateCell(updateObj(cell, { answer: parseInt(userInput) }));
		}
	}, [cell, cellValue, game, userInput]);

	// •• Create function that fills cell answer
	const fillAnswer = useCallback(() => setCellValue(cell.correctAnswer), [cell.correctAnswer]);

	// •• Create a function that renders the notes assigned to the cell inside the block
	const renderNotes = (notes) => {
		const noteComponents = [];
		for (let i = 1; i <= 9; i++) {
			let note = (
				<Note
					key={`${cell.block}_${cell.row}_${cell.col}_cell-note_${i}`}
					num={i}
					cellIndex={cell.cellIndex}
				/>
			);
			noteComponents.push(note);
		}
		return noteComponents.map((n) => n);
	};

	// •• Handle enter key when cell is active input
	const handleOnEnterKey = (e) => (e.key === 'Enter' ? game.setFocusCell(null) : null);

	// •• Let game context know if a cell should be focused when clicked
	const handleCellClick = () =>
		!isClue && !game.gameComplete ? game.setFocusCell(cell.cellIndex) : game.setFocusCell(null);

	//••••••••
	// •• SideEffects
	//••••••••

	// •• Check if cell should show the answer as a clue
	useEffect(() => {
		if (isClue) fillAnswer();
	}, [fillAnswer, isClue]);

	// •• Fill answer if the game has been ended
	useEffect(() => {
		if (game.gameComplete === true && cell.answer !== cellValue) fillAnswer();
	}, [cell, cell.correctAnswer, cellValue, fillAnswer, game.gameComplete]);

	// •• Toggle input editing; set answer if new value is present
	useEffect(() => {
		if (!game.gameComplete) {
			if (game.focusCell === cell.cellIndex) {
				setIsEditing(true);
			} else if (game.focusCell !== cell.cellIndex) {
				setIsEditing(false);
				if (cell.answer !== userInput && !isClue) handleInputSubmit();
			}
		}
	}, [
		cellValue,
		isClue,
		userInput,
		handleInputSubmit,
		game.focusCell,
		game.gameComplete,
		cell.cellIndex,
		cell.answer,
	]);

	// •• Focus On Input When Visible
	useEffect(() => {
		let input = document.querySelector('input');
		if (isEditing && input) input.focus();
	}, [isEditing]);

	// •• Listen for updates to cell notes

	return (
		<div
			onClick={handleCellClick}
			className={style.CellContainer}
			data-show-correct={game.wrongAnswerCount ? 1 : 0}
			data-game-complete={game.gameComplete ? 1 : 0}
			data-is-correct={cell.answer === cell.correctAnswer ? 1 : 0}
			data-is-clue={isClue ? 1 : 0}>
			{isEditing && game.gameComplete === false ? (
				<input
					type='text'
					value={userInput}
					onKeyDown={handleOnEnterKey}
					onChange={handleInputOnChange}
				/>
			) : (
				<p>{isClue ? cell.correctAnswer : cell.answer ? cell.answer : null}</p>
			)}
			{!isClue && !isEditing && !cell.answer ? (
				<div className={style.CellNotes}>
					{renderNotes(game.cells[cell.cellIndex].notes)}
				</div>
			) : null}
		</div>
	);
}

function Note({ num, cellIndex }) {
	const [isVisible, setIsVisible] = useState(false);
	const game = useContext(GameContext);

	useEffect(() => {
		if (game.cells[cellIndex].notes.includes(num)) {
			setIsVisible(true);
		} else {
			setIsVisible(false);
		}
	}, [cellIndex, game.cells, num]);
	return (
		<div className={style.NoteBox}>
			{num ? <p className={style.NoteNum}>{isVisible ? num : null}</p> : null}
		</div>
	);
}
<file_sep>/src/components/index.js
export { default as Gameboard } from './gameboard/Gameboard.jsx';
export { default as Block } from './block/Block.jsx';
export { default as Cell } from './cell/Cell.jsx';
export { default as GameOptions } from './gameOptions/GameOptions.jsx';
export { default as NumberButtons } from './numberButtons/NumberButtons.jsx';

export * from './UI/';
<file_sep>/src/App.js
import React, { useContext } from 'react';

import * as style from './app.module.css';

import { GameContext } from './context/game.js';
import { Gameboard, GameOptions } from './components';

export default function App() {
	const game = useContext(GameContext);

	// Cancels focus on a cell if the user clicks outside of the board
	function TransparentCancelFocusBackdrop() {
		return <div onClick={() => game.setFocusCell(null)} className={style.ClearBG} />;
	}

	// Render game status when checked
	function RenderGameStatus() {
		return game.wrongAnswerCount && game.wrongAnswerCount > 0 ? (
			<h3>You have {game.wrongAnswerCount} wrong answers</h3>
		) : game.wrongAnswerCount === 0 ? (
			<h3>All Solved!</h3>
		) : null;
	}

	return (
		<>
			<GameOptions />
			<div className={style.AppContainer}>
				<Gameboard />
			</div>
			<RenderGameStatus />
			<TransparentCancelFocusBackdrop />
		</>
	);
}
<file_sep>/src/components/block/Block.jsx
import React from 'react';

import Cell from '../cell/Cell.jsx';

import * as style from './block.module.scss';

export default function Block({ cells }) {
	const renderCells = () => cells.map((cell, i) => <Cell key={`cell${i}`} cell={cell} />);

	return <div className={style.BlockContainer}>{renderCells()}</div>;
}
