From a3dacbd788ce8414eb45e12184c53abdea282666 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 15 Jul 2024 16:57:12 +0300 Subject: [PATCH] feat: added ability for users to select initial cube by default (#330) * feat: allow users to select a default starting cube * fix: updating settings if they differ from the old ones * fix: refactor styles, highlight selected cube, and close select on outside click * refactor styles to match menu schema. --------- Co-authored-by: Bryan Lundberg --- messages/en.json | 5 +- messages/ru.json | 5 +- src/components/Select.tsx | 2 +- src/components/menu-settings/Menu.tsx | 12 ++- .../MenuSelectDefaultStartCube.tsx | 97 +++++++++++++++++++ src/hooks/useModalCube.ts | 23 +++++ src/hooks/usePreloadSettings.ts | 7 +- src/interfaces/Settings.ts | 6 ++ src/lib/const/defaultSettings.ts | 3 + src/lib/loadSettings.ts | 27 +++++- 10 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 src/components/menu-settings/MenuSelectDefaultStartCube.tsx diff --git a/messages/en.json b/messages/en.json index 51ce30c9..a1c8e3b5 100644 --- a/messages/en.json +++ b/messages/en.json @@ -89,7 +89,8 @@ "confirm": "Confirm", "cancel": "Cancel", "save": "Save", - "delete": "Delete" + "delete": "Delete", + "none": "None" }, "Settings-menu": { "title": "Settings", @@ -116,6 +117,8 @@ "allowed-file-types": "Only image files (JPEG, PNG, GIF) are allowed.", "max-file-size": "Maximum file size allowed is", "data": "App Data", + "preferences": "Preferences", + "default-cube": "Default cube", "import-from-file": "Import from file", "export-to-file": "Export to file", "about": "About", diff --git a/messages/ru.json b/messages/ru.json index 9e5de279..094d074e 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -89,7 +89,8 @@ "confirm": "Подтвердить", "cancel": "Отмена", "save": "Сохранить", - "delete": "Удалить" + "delete": "Удалить", + "none": "Нет" }, "Settings-menu": { "title": "Настройки", @@ -111,6 +112,8 @@ "best-average": "Лучшая средняя", "worst-time": "Худшее время", "theme": "Тема", + "preferences": "Предпочтения", + "default-cube": "Кубик по умолчанию", "custom-background-image": "Пользовательское фоновое изображение", "format": "Формат", "allowed-file-types": "Разрешены только файлы изображений (JPEG, PNG, GIF).", diff --git a/src/components/Select.tsx b/src/components/Select.tsx index 590d6159..fb4cb7ab 100644 --- a/src/components/Select.tsx +++ b/src/components/Select.tsx @@ -96,7 +96,7 @@ export default function Select() { ); } -function MiniatureIcon({ category }: { category: Categories }) { +export function MiniatureIcon({ category }: { category: Categories }) { const images = cubeCollection.map((option) => { if (option.name === category) { return ( diff --git a/src/components/menu-settings/Menu.tsx b/src/components/menu-settings/Menu.tsx index aabc4ec4..4331c421 100644 --- a/src/components/menu-settings/Menu.tsx +++ b/src/components/menu-settings/Menu.tsx @@ -14,14 +14,14 @@ import MenuSelectLanguage from "./MenuSelectLanguage"; import { ArrowLeftIcon, BellAlertIcon, - ClockIcon, CogIcon, CpuChipIcon, FolderIcon, IdentificationIcon, - ShieldCheckIcon, SparklesIcon, + ViewColumnsIcon, } from "@heroicons/react/24/solid"; +import MenuSelectDefaultStartCube from "./MenuSelectDefaultStartCube"; export default function MenuSettings() { const { settingsOpen, setSettingsOpen, settings } = useSettingsModalStore(); @@ -139,6 +139,14 @@ export default function MenuSettings() { > + + } + title={t("preferences")} + > + + + } title={t("about")} diff --git a/src/components/menu-settings/MenuSelectDefaultStartCube.tsx b/src/components/menu-settings/MenuSelectDefaultStartCube.tsx new file mode 100644 index 00000000..b4fc35d5 --- /dev/null +++ b/src/components/menu-settings/MenuSelectDefaultStartCube.tsx @@ -0,0 +1,97 @@ +import { useTranslations } from "next-intl"; +import { useRef, useState } from "react"; +import { useSettingsModalStore } from "@/store/SettingsModalStore"; +import useClickOutside from "@/hooks/useClickOutside"; +import { useTimerStore } from "@/store/timerStore"; +import { MiniatureIcon } from "../Select"; +import { AnimatePresence, motion } from "framer-motion"; +import loadSettings from "@/lib/loadSettings"; +import { Cube } from "@/interfaces/Cube"; + +export default function MenuSelectDefaultStartCube() { + const { settings, setSettings } = useSettingsModalStore(); + const t = useTranslations("Index"); + + const [open, setOpen] = useState(false); + const { cubes } = useTimerStore(); + const componentRef = useRef(null); + + const handleClose = () => { + setOpen(false); + }; + + const handleCubeSelect = (cube: Cube | null) => { + const currentSettings = loadSettings(); + currentSettings.preferences.defaultCube.cube = cube; + setSettings(currentSettings); + window.localStorage.setItem("settings", JSON.stringify(currentSettings)); + handleClose(); + }; + + const defaultCube = settings.preferences.defaultCube.cube; + + useClickOutside(componentRef, handleClose); + + return ( +
+
{t("Settings-menu.default-cube")}
+
+ + + {open && ( + +
handleCubeSelect(null)} + className={`cursor-pointer transition duration-200 p-1 select-none rounded-md ps-2 flex overflow-hidden ${ + defaultCube === null + ? "bg-neutral-700 text-neutral-50" + : "text-neutral-900 hover:bg-neutral-500 hover:text-neutral-100" + }`} + > +
{t("Inputs.none")}
+
+ {cubes?.map((cube) => ( +
handleCubeSelect(cube)} + className={`cursor-pointer transition duration-200 p-1 select-none rounded-md ps-2 flex overflow-hidden ${ + defaultCube?.id === cube.id + ? "bg-neutral-700 text-neutral-50" + : "text-neutral-900 hover:bg-neutral-500 hover:text-neutral-100" + }`} + > +
+ +
{cube.name}
+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/hooks/useModalCube.ts b/src/hooks/useModalCube.ts index bd57e95e..6b7c147f 100644 --- a/src/hooks/useModalCube.ts +++ b/src/hooks/useModalCube.ts @@ -8,6 +8,8 @@ import { DeleteCubeDetails } from "@/interfaces/DeleteCubeDetails"; import formatTime from "@/lib/formatTime"; import useEscape from "./useEscape"; import { deleteCubeById, getAllCubes, saveCube } from "@/db/dbOperations"; +import loadSettings from "@/lib/loadSettings"; +import { useSettingsModalStore } from "@/store/SettingsModalStore"; export default function useModalCube() { const { @@ -20,6 +22,8 @@ export default function useModalCube() { setCubeName, } = useCubesModalStore(); + const { setSettings } = useSettingsModalStore(); + const { setCubes, setSelectedCube, @@ -33,6 +37,8 @@ export default function useModalCube() { const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); const [cubeData, setCubeData] = useState(null); + const defaultCubeId = loadSettings().preferences.defaultCube.cube?.id; + useEscape(() => setModalOpen(false)); const handleClickRadio = (category: Categories) => { @@ -88,6 +94,16 @@ export default function useModalCube() { } } + if (editingCube.id === defaultCubeId) { + const currentSettings = loadSettings(); + if (currentSettings.preferences.defaultCube.cube) { + currentSettings.preferences.defaultCube.cube.name = name; + currentSettings.preferences.defaultCube.cube.category = category; + } + setSettings(currentSettings); + window.localStorage.setItem("settings", JSON.stringify(currentSettings)); + } + const updatedCube = await saveCube({ ...editingCube, name: name.trim(), @@ -153,6 +169,13 @@ export default function useModalCube() { setTimerStatistics(); } + if (editingCube.id === defaultCubeId) { + const currentSettings = loadSettings(); + currentSettings.preferences.defaultCube.cube = null; + setSettings(currentSettings); + window.localStorage.setItem("settings", JSON.stringify(currentSettings)); + } + await deleteCubeById(editingCube.id); const cubes = await getAllCubes(); setCubes(cubes); diff --git a/src/hooks/usePreloadSettings.ts b/src/hooks/usePreloadSettings.ts index 04c4150c..75f9caea 100644 --- a/src/hooks/usePreloadSettings.ts +++ b/src/hooks/usePreloadSettings.ts @@ -6,13 +6,18 @@ import { getAllCubes } from "@/db/dbOperations"; import { useBackgroundImageStore } from "@/store/BackgroundThemeStore"; export function usePreloadSettings() { - const { setCubes } = useTimerStore(); + const { setCubes, setSelectedCube, setTimerStatistics, setNewScramble } = + useTimerStore(); const { setSettings } = useSettingsModalStore(); const { setBackgroundImage } = useBackgroundImageStore(); useEffect(() => { const getSettings = loadSettings(); + const defaultCube = getSettings.preferences.defaultCube.cube; + setSelectedCube(defaultCube); setSettings(getSettings); + setTimerStatistics(); + setNewScramble(defaultCube); }, [setSettings]); useEffect(() => { diff --git a/src/interfaces/Settings.ts b/src/interfaces/Settings.ts index 292fc662..609491f9 100644 --- a/src/interfaces/Settings.ts +++ b/src/interfaces/Settings.ts @@ -1,3 +1,4 @@ +import { Cube } from "./Cube"; import { Themes } from "./types/Themes"; interface Timer { @@ -26,9 +27,14 @@ interface Theme { content: { color: string; key: string }; } +interface Preferences { + defaultCube: { cube: Cube | null; key: string }; +} + export interface Settings { timer: Timer; features: Features; alerts: Alerts; theme: Theme; + preferences: Preferences; } diff --git a/src/lib/const/defaultSettings.ts b/src/lib/const/defaultSettings.ts index 28a230db..a10d2447 100644 --- a/src/lib/const/defaultSettings.ts +++ b/src/lib/const/defaultSettings.ts @@ -23,4 +23,7 @@ export const defaultSettings: Settings = { background: { color: "dark", key: "background-color" }, content: { color: "dark", key: "letter-color" }, }, + preferences: { + defaultCube: { cube: null, key: "default-cube" }, + }, }; diff --git a/src/lib/loadSettings.ts b/src/lib/loadSettings.ts index bd57a80c..7659e49b 100644 --- a/src/lib/loadSettings.ts +++ b/src/lib/loadSettings.ts @@ -1,6 +1,20 @@ import { Settings } from "@/interfaces/Settings"; import { defaultSettings } from "./const/defaultSettings"; +function areSettingsDifferent( + currentSettings: Settings, + defaultSettings: Settings +) { + return JSON.stringify(currentSettings) !== JSON.stringify(defaultSettings); +} + +function mergeSettings(currentSettings: Settings, defaultSettings: Settings) { + return { + ...defaultSettings, + ...currentSettings, + }; +} + /** * Retrieves the user settings from local storage. * If no settings are found, it initializes and saves the default settings. @@ -20,7 +34,14 @@ export default function loadSettings(): Settings { return defaultSettings; } - // Parse and return the retrieved settings - const settings = JSON.parse(data); - return settings; + const currentSettings: Settings = JSON.parse(data); + + if (areSettingsDifferent(currentSettings, defaultSettings)) { + const updatedSettings = mergeSettings(currentSettings, defaultSettings); + window.localStorage.setItem("settings", JSON.stringify(updatedSettings)); + + return updatedSettings; + } + + return currentSettings; }