Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 44 additions & 9 deletions src/components/GameLevel/Level.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import {
getClashingQueensPreference,
getShowInstructionsPreference,
isLevelCompleted,
markLevelAsCompleted,
saveLevelAsCompleted,
setShowClockPreference,
setAutoPlaceXsPreference,
setClashingQueensPreference,
setShowInstructionsPreference,
saveLevelAsNotCompleted,
getStoredLevel,
} from "../../utils/localStorage";
import getNavigationLevels from "@/utils/getNavigationLevels";
import { useTheme } from "next-themes";
Expand Down Expand Up @@ -56,17 +58,37 @@ const Level = ({ id, level }) => {
const history = useRef([]);
const isVisible = useVisibility();
const [timerRunning, setTimerRunning] = useState(false);
const [completed, setCompleted] = useState(false);
const [boardLoaded, setBoardLoaded] = useState(false);

const { previousLevel, nextLevel, previousDisabled, nextDisabled } =
getNavigationLevels(id, level);

const boardSize = levelSize;
const colorRegions = levels[level].colorRegions;

const completed = isLevelCompleted(Number(id));
useEffect(() => {
const levelSaved = getStoredLevel(Number(id));
setCompleted(isLevelCompleted(Number(id)));
if (!levelSaved) {
setBoardLoaded(true);
return;
}
if (levelSaved.completed) {
setHasWon(true);
}
if (levelSaved.time) {
setTimer(levelSaved.time);
}
if (levelSaved.board) {
setBoard(levelSaved.board);
setBoardLoaded(true);
}
}, []);

// Handle click on square
const handleSquareClick = (row, col) => {
if (hasWon) return;
// Initialize newBoard as a copy of the current board
const newBoard = structuredClone(board);

Expand All @@ -89,7 +111,6 @@ const Level = ({ id, level }) => {
setTimeout(() => setShowWinningScreen(true), 0);
}
setHasWon(true);
markLevelAsCompleted(Number(id));
} else {
setHasWon(false);
setShowWinningScreen(false);
Expand All @@ -110,6 +131,7 @@ const Level = ({ id, level }) => {
};

const handleDrag = (squares) => {
if (hasWon) return;
const newBoard = structuredClone(board);
for (const [row, col] of squares) {
if (newBoard[row][col] !== "Q") {
Expand Down Expand Up @@ -332,6 +354,14 @@ const Level = ({ id, level }) => {
setBoard(newBoard);
};

const handleResetButtonClick = () => {
setBoard(createEmptyBoard(levelSize));
setHasWon(false);
setShowWinningScreen(false);
history.current = []
setTimer(0);
}

const PreviousLevelButton = ({ children, className }) => {
return (
<Link
Expand Down Expand Up @@ -388,6 +418,15 @@ const Level = ({ id, level }) => {
setTimerRunning(true)
}
}, [isVisible, hasWon])

useEffect(() => {
if (!boardLoaded) return;
if (hasWon) {
saveLevelAsCompleted(Number(id), timer, board);
} else {
saveLevelAsNotCompleted(Number(id), timer, board);
}
}, [timer, board, boardLoaded]);

return (
<div key={id} className="flex flex-col justify-center items-center pt-4">
Expand Down Expand Up @@ -427,12 +466,7 @@ const Level = ({ id, level }) => {
/>
)}
<button
onClick={() => {
setBoard(createEmptyBoard(levelSize));
setHasWon(false);
setShowWinningScreen(false);
history.current = [];
}}
onClick={handleResetButtonClick}
className="border border-slate-500 rounded-full p-2 mr-2"
>
<ResetIcon size="18" />
Expand All @@ -456,6 +490,7 @@ const Level = ({ id, level }) => {
run={timerRunning}
onTimeUpdate={handleTimeUpdate}
showTimer={showClock}
initialTimer={timer}
/>
</div>

Expand Down
6 changes: 5 additions & 1 deletion src/components/GameLevel/components/Timer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import formatDuration from "@/utils/formatDuration";
const ONE_HOUR_IN_SECONDS = 3600;
const TEN_HOURS_IN_SECONDS = 36000;

const Timer = ({ run, onTimeUpdate, showTimer, className = "" }) => {
const Timer = ({ run, onTimeUpdate, showTimer, initialTimer, className = "" }) => {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
Expand All @@ -27,6 +27,10 @@ const Timer = ({ run, onTimeUpdate, showTimer, className = "" }) => {
onTimeUpdate(seconds);
}, [seconds]);

useEffect(() => {
setSeconds(initialTimer || 0);
}, [initialTimer]);

if (!showTimer) {
return <></>;
}
Expand Down
10 changes: 6 additions & 4 deletions src/components/LevelSelection/components/LevelButton.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React from "react";
import React, { useMemo } from "react";
import { Link } from "react-router-dom";
import Queen from "../../../components/Queen";
import { isLevelCompleted } from "../../../utils/localStorage";
import { isLevelCompleted, isLevelInProgress } from "../../../utils/localStorage";

const LevelButton = ({ level, disabled }) => {
const completed = isLevelCompleted(level);
const completed = useMemo(() => isLevelCompleted(level), [level]);
const inProgress = useMemo(() => isLevelInProgress(level), [level]);

return (
<Link to={`/level/${level}`} key={level}>
<button
className={`relative rounded p-2 w-full text-white bg-[#F96C51] ${
className={`relative rounded p-2 w-full text-white ${
disabled ? "opacity-75" : ""
}`}
style={{ backgroundColor: inProgress ? "red" : "#F96C51" }}
>
{level}
{completed && (
Expand Down
118 changes: 98 additions & 20 deletions src/utils/localStorage.js
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@samimsu I've placed all the game-saving logic in this file to avoid altering the project structure too much. However, I believe it would be better to use a class and implement the desired storage solution. This way, switching between storage options (like from localStorage to a database, for example) would be easier.

Original file line number Diff line number Diff line change
@@ -1,59 +1,137 @@
export const markLevelAsCompleted = (levelNumber) => {
let completedLevels =
JSON.parse(localStorage.getItem("completedLevels")) || [];
import { createEmptyBoard } from "@/utils/board";
import { levels } from "@/utils/levels";

if (!completedLevels.includes(levelNumber)) {
completedLevels.push(levelNumber);
localStorage.setItem("completedLevels", JSON.stringify(completedLevels));
const LOCAL_STORAGE = {
completedLevels: {
key: "completedLevels",
defaultValue: {}
},
clashingQueensEnabled: {
key: "clashingQueensEnabled",
defaultValue: true
},
showInstructions: {
key: "showInstructions",
defaultValue: true
},
showClock: {
key: "showClock",
defaultValue: true
},
autoPlaceXs: {
key: "autoPlaceXs",
defaultValue: false
},
groupBySize: {
key: "groupBySize",
defaultValue: false
},
}

const migrateStoredLevelsLocalStorage = (storedLevels) => {
if (!Array.isArray(storedLevels)) return storedLevels;
const levelsMigrated = {}

storedLevels.forEach(completedLevel => {
const level = levels[`level${completedLevel}`]
const levelSize = level?.size
levelsMigrated[completedLevel] = {
completed: true,
board: levelSize && createEmptyBoard(levelSize),
time: 0
}
})
storeLevels(levelsMigrated);
return levelsMigrated;
}

const getStoredLevels = () => {
try {
const storedLevels = JSON.parse(localStorage.getItem(LOCAL_STORAGE.completedLevels.key)) ?? LOCAL_STORAGE.completedLevels.defaultValue;
return migrateStoredLevelsLocalStorage(storedLevels);
} catch (error) {
return LOCAL_STORAGE.completedLevels.defaultValue;
}
}

const storeLevels = (completedLevels) => {
localStorage.setItem(LOCAL_STORAGE.completedLevels.key, JSON.stringify(completedLevels));
}

export const getStoredLevel = (levelNumber) => {
const storedLevels = getStoredLevels();
return storedLevels[levelNumber];
}

export const saveLevelAsCompleted = (levelNumber, time, board) => {
saveLevel(levelNumber, time, board, true);
}

export const saveLevelAsNotCompleted = (levelNumber, time, board) => {
saveLevel(levelNumber, time, board, false);
}

const saveLevel = (levelNumber, time, board, completed) => {
const storedLevels = getStoredLevels();

storedLevels[levelNumber] = {
completed,
time,
board
};
storeLevels(storedLevels);
};

export const isLevelCompleted = (levelNumber) => {
const completedLevels =
JSON.parse(localStorage.getItem("completedLevels")) || [];
return completedLevels.includes(levelNumber);
const storedLevels = getStoredLevels();
return storedLevels[levelNumber]?.completed;
};

export const isLevelInProgress = (levelNumber) => {
const storedLevels = getStoredLevels();
return storedLevels[levelNumber]?.completed === false && storedLevels[levelNumber]?.time > 0;
}

export const resetCompletedLevels = () => {
localStorage.setItem("completedLevels", JSON.stringify([]));
storeLevels(JSON.stringify(LOCAL_STORAGE.completedLevels.defaultValue));
};

export const setClashingQueensPreference = (enabled) => {
localStorage.setItem("clashingQueensEnabled", JSON.stringify(enabled));
localStorage.setItem(LOCAL_STORAGE.clashingQueensEnabled.key, JSON.stringify(enabled));
};

export const getClashingQueensPreference = () => {
return JSON.parse(localStorage.getItem("clashingQueensEnabled")) ?? true; // Default to true
return JSON.parse(localStorage.getItem(LOCAL_STORAGE.clashingQueensEnabled.key)) ?? LOCAL_STORAGE.clashingQueensEnabled.defaultValue;
};

export const setShowInstructionsPreference = (enabled) => {
localStorage.setItem("showInstructions", JSON.stringify(enabled));
localStorage.setItem(LOCAL_STORAGE.showInstructions.key, JSON.stringify(enabled));
};

export const getShowInstructionsPreference = () => {
return JSON.parse(localStorage.getItem("showInstructions")) ?? true; // Default to true
return JSON.parse(localStorage.getItem(LOCAL_STORAGE.showInstructions.key)) ?? LOCAL_STORAGE.showInstructions.defaultValue;
};

export const setShowClockPreference = (enabled) => {
localStorage.setItem("showClock", JSON.stringify(enabled));
localStorage.setItem(LOCAL_STORAGE.showClock.key, JSON.stringify(enabled));
};

export const getShowClockPreference = () => {
return JSON.parse(localStorage.getItem("showClock")) ?? true; // Default to true
return JSON.parse(localStorage.getItem(LOCAL_STORAGE.showClock.key)) ?? LOCAL_STORAGE.showClock.defaultValue;
};

export const setAutoPlaceXsPreference = (enabled) => {
localStorage.setItem("autoPlaceXs", JSON.stringify(enabled));
localStorage.setItem(LOCAL_STORAGE.autoPlaceXs.key, JSON.stringify(enabled));
};

export const getAutoPlaceXsPreference = () => {
return JSON.parse(localStorage.getItem("autoPlaceXs")) ?? false; // Default to false
return JSON.parse(localStorage.getItem(LOCAL_STORAGE.autoPlaceXs.key)) ?? LOCAL_STORAGE.autoPlaceXs.defaultValue;
};

export const setGroupingPreference = (enabled) => {
localStorage.setItem("groupBySize", JSON.stringify(enabled));
localStorage.setItem(LOCAL_STORAGE.groupBySize.key, JSON.stringify(enabled));
};

export const getGroupingPreference = () => {
return JSON.parse(localStorage.getItem("groupBySize")) ?? false; // Default to false
return JSON.parse(localStorage.getItem(LOCAL_STORAGE.groupBySize.key)) ?? LOCAL_STORAGE.groupBySize.defaultValue;
};