`
: nothing}
- ${this._showExercises && enableExerciseDisplay
+ ${this._showExercises && showExercises
? html`
Exercises
@@ -267,7 +262,7 @@ export class HomePage extends LitElement {
| Type |
Name |
- Reps |
+ Rep(s) |
Set(s) |
Done |
@@ -447,8 +442,8 @@ export class HomePage extends LitElement {
#complete() {
const {
enableNotifications,
- exercisesCount,
showMotivationalQuote,
+ exercisesByCategory,
audioSound,
audioVolume,
} = this._settings;
@@ -475,7 +470,9 @@ export class HomePage extends LitElement {
if (isPomodoroModeSelected) {
this._showExercises = true;
- this._exercises = ExercisesStore.getRandomExercises(exercisesCount);
+ this._exercises = Object.entries(exercisesByCategory).flatMap(
+ ([category, exercises]) => exercises.map((name) => ({ category, name }))
+ );
}
}
diff --git a/src/shared/styles/tabStyles.js b/src/shared/styles/tabStyles.js
new file mode 100644
index 0000000..6655705
--- /dev/null
+++ b/src/shared/styles/tabStyles.js
@@ -0,0 +1,45 @@
+import { css } from 'lit';
+
+const tabStyles = css`
+ .tabs {
+ display: flex;
+ width: 100%;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ .tabs button.tab-item {
+ flex: 1 1 0;
+ padding: 0.75rem 1rem;
+ cursor: pointer;
+ border: none;
+ font-size: 0.9rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.375rem;
+ background-color: #1f1f27;
+ color: #c0c0c8;
+ transition:
+ background-color 0.2s,
+ color 0.2s;
+ border-radius: 0;
+ border-right: 0.0625rem solid #2c2c35;
+ }
+
+ .tabs button.tab-item:last-child {
+ border-right: none;
+ }
+
+ .tabs button.tab-item[data-tab-active='true'] {
+ font-weight: 500;
+ background-color: #3b3b48;
+ color: #ffffff;
+ }
+
+ .tabs button.tab-item:hover {
+ background-color: #353540;
+ }
+`;
+
+export { tabStyles };
diff --git a/src/stores/exercises.js b/src/stores/exercises.js
deleted file mode 100644
index 4b9af37..0000000
--- a/src/stores/exercises.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import { isNum } from '../utils/helpers.js';
-
-import { DEFAULT_SETTINGS } from './settings.js';
-
-/** @type {Readonly>} */
-const EXERCISE_CATEGORY = Object.freeze({
- UPPER_BODY: 'upperBody',
- LOWER_BODY: 'lowerBody',
- CORE: 'core',
- CARDIO: 'cardio',
- MOBILITY: 'mobility',
- BALANCE: 'balance',
- FULL_BODY: 'fullBody',
- STATIC_STRENGTH: 'staticStrength',
- YOGA: 'yoga',
-});
-
-/** @type {readonly import("../index.d.js").ExerciseEntries[]} */
-const EXERCISES_ENTRIES = Object.freeze([
- [
- EXERCISE_CATEGORY.UPPER_BODY,
- Object.freeze([
- 'Push-ups',
- 'Wide push-ups',
- 'Diamond push-ups',
- 'Decline push-ups',
- 'Incline push-ups',
- 'Shoulder taps',
- ]),
- ],
- [
- EXERCISE_CATEGORY.LOWER_BODY,
- Object.freeze([
- 'Squats',
- 'Lunges',
- 'Reverse lunges',
- 'Side lunges',
- 'Curtsy lunges',
- 'Bulgarian split squat',
- 'Calf raises',
- 'Wall sit',
- 'Hip thrusts',
- ]),
- ],
- [
- EXERCISE_CATEGORY.CORE,
- Object.freeze([
- 'Sit-ups',
- 'Crunches',
- 'Bicycle crunches',
- 'Leg raises',
- 'Flutter kicks',
- 'Scissor kicks',
- 'Russian twists',
- 'Plank',
- 'Side plank',
- 'Mountain climbers',
- 'V-ups',
- 'Supermans',
- ]),
- ],
- [
- EXERCISE_CATEGORY.CARDIO,
- Object.freeze([
- 'Jumping jacks',
- 'Burpees',
- 'Standing knee drives',
- 'Invisible jump rope',
- ]),
- ],
- [
- EXERCISE_CATEGORY.MOBILITY,
- Object.freeze([
- 'Hip flexor stretch',
- 'Hamstring stretch',
- 'Quad stretch',
- 'Ankle circles',
- 'Arm circles',
- 'Torso twists',
- ]),
- ],
- [
- EXERCISE_CATEGORY.BALANCE,
- Object.freeze([
- 'Single-leg balance hold',
- 'Single-leg toe touch',
- 'Heel-to-toe walk',
- 'Single-leg calf raise',
- ]),
- ],
- [
- EXERCISE_CATEGORY.FULL_BODY,
- Object.freeze(['Burpees', 'Mountain climbers', 'Jumping jacks']),
- ],
- [
- EXERCISE_CATEGORY.STATIC_STRENGTH,
- Object.freeze(['Static squat hold', 'Static plank hold']),
- ],
- [
- EXERCISE_CATEGORY.YOGA,
- Object.freeze([
- 'Warrior pose',
- 'Chair pose',
- 'Tree pose',
- 'Boat pose',
- 'Bridge pose',
- 'Crescent lunge hold',
- ]),
- ],
-]);
-
-class ExercisesStore {
- /**
- * @param {number} exercisesCount
- * @returns {import("../index.d.js").Exercise[]}
- */
- static getRandomExercises(exercisesCount) {
- const count = isNum(exercisesCount)
- ? exercisesCount
- : DEFAULT_SETTINGS.exercisesCount;
- const result = [];
- const pairSet = new Set();
-
- const maxPairs = EXERCISES_ENTRIES.reduce(
- (sum, [, list]) => sum + list.length,
- 0
- );
-
- const limit = Math.min(count, maxPairs);
-
- while (result.length < limit) {
- const [randomCategory, exerciseList] =
- EXERCISES_ENTRIES[Math.floor(Math.random() * EXERCISES_ENTRIES.length)];
- const randomExercise =
- exerciseList[Math.floor(Math.random() * exerciseList.length)];
-
- const key = `${randomCategory}:${randomExercise}`;
-
- if (pairSet.has(key)) {
- continue;
- } else {
- pairSet.add(key);
- }
-
- result.push({
- category: randomCategory,
- name: randomExercise,
- });
- }
-
- return result;
- }
-}
-
-export default ExercisesStore;
diff --git a/src/stores/settings.js b/src/stores/settings.js
index 306a20a..4a49cff 100644
--- a/src/stores/settings.js
+++ b/src/stores/settings.js
@@ -4,16 +4,21 @@ import {
AUDIO_VOLUME,
CLIENT_ERROR_MESSAGE,
DEFAULT_POMODORO_TIMES,
+ EXERCISES_BY_CATEGORY_MAP,
STORAGE_KEY_NAMESPACE,
} from '../utils/constants.js';
-import { isBool, isNum } from '../utils/helpers.js';
+import { isBool, isNum, isPlainObject } from '../utils/helpers.js';
+
+/** @typedef {boolean | number | import('index.d.js').ExerciseByCategory} SettingsStorageValue */
+
+/** @typedef {keyof typeof DEFAULT_SETTINGS} SettingsKey */
/** @type {import('index.d.js').Settings} */
export const DEFAULT_SETTINGS = Object.freeze({
- enableExerciseDisplay: true,
+ showExercises: true,
exerciseReps: 5,
exerciseSets: 1,
- exercisesCount: 1,
+ exercisesByCategory: {},
enableNotifications: false,
showTimerInTitle: false,
showMotivationalQuote: true,
@@ -22,6 +27,12 @@ export const DEFAULT_SETTINGS = Object.freeze({
...DEFAULT_POMODORO_TIMES,
});
+export const SETTINGS_KEY = /** @type {Record} */ (
+ Object.freeze(
+ Object.fromEntries(Object.keys(DEFAULT_SETTINGS).map((key) => [key, key]))
+ )
+);
+
class SettingsStore extends EventTarget {
/** @type {import("../index.d.js").Settings} */
#settings = { ...DEFAULT_SETTINGS };
@@ -40,33 +51,62 @@ class SettingsStore extends EventTarget {
const settingsMap = new Map(Object.entries(this.#settings));
for (const [key, defaultValue] of settingsMap.entries()) {
- const storedValue = /** @type {boolean | number | null} */ (
+ const storedValue = /** @type {SettingsStorageValue | null} */ (
this.#settingsStorage.get(key)
);
- let value = /** @type {boolean | number} */ (
+ let value =
storedValue === null
? defaultValue
: (isBool(defaultValue) && isBool(storedValue)) ||
- (isNum(defaultValue) && isNum(storedValue))
+ (isNum(defaultValue) && isNum(storedValue)) ||
+ (isPlainObject(defaultValue) && isPlainObject(storedValue))
? storedValue
- : defaultValue
- );
+ : defaultValue;
- if (key === 'audioSound' && typeof value === 'number') {
+ // Additional checks for specific settings
+ if (key === SETTINGS_KEY.audioSound) {
const matchingAudioSound = Object.values(AUDIO_SOUND).find(
({ ID }) => ID === value
);
if (matchingAudioSound === undefined) {
value = defaultValue;
}
- } else if (key === 'audioVolume' && typeof value === 'number') {
+ } else if (key === SETTINGS_KEY.audioVolume) {
const matchingAudioVolume = Object.values(AUDIO_VOLUME).find(
(volume) => volume === value
);
if (matchingAudioVolume === undefined) {
value = defaultValue;
}
+ } else if (
+ key === SETTINGS_KEY.exercisesByCategory &&
+ Object.keys(value).length > 0
+ ) {
+ const exercisesByCategoryValue =
+ /** @type {import('index.d.js').ExerciseByCategory} */ (value);
+
+ value = Object.fromEntries(
+ Object.entries(exercisesByCategoryValue)
+ .filter(
+ ([category, exercises]) =>
+ EXERCISES_BY_CATEGORY_MAP.has(
+ /** @type {import('index.d.js').ExerciseCategory} */ (
+ category
+ )
+ ) && Array.isArray(exercises)
+ )
+ .map(([category, exercises]) => [
+ category,
+ exercises.filter((exercise) =>
+ EXERCISES_BY_CATEGORY_MAP.get(
+ /** @type {import('index.d.js').ExerciseCategory} */ (
+ category
+ )
+ )?.includes(exercise)
+ ),
+ ])
+ );
}
settingsMap.set(key, value);
@@ -89,11 +129,11 @@ class SettingsStore extends EventTarget {
const settingsMap = new Map(Object.entries(this.#settings));
for (const [key, defaultValue] of Object.entries(DEFAULT_SETTINGS)) {
- const val = /** @type {boolean | number} */ (
- valueMap.has(key) ? valueMap.get(key) : defaultValue
- );
+ const val = valueMap.has(key) ? valueMap.get(key) : defaultValue;
- const newValue = isBool(val) || isNum(val) ? val : defaultValue;
+ const newValue = /** @type {SettingsStorageValue} */ (
+ isBool(val) || isNum(val) || isPlainObject(val) ? val : defaultValue
+ );
settingsMap.set(key, newValue);
this.#settingsStorage.set(key, newValue);
diff --git a/src/utils/constants.js b/src/utils/constants.js
index ce4c76c..b64cb35 100644
--- a/src/utils/constants.js
+++ b/src/utils/constants.js
@@ -44,6 +44,115 @@ const CLIENT_ERROR_MESSAGE = Object.freeze({
PRELOAD_AUDIO_FAILED: 'Failed to load audio/sound.',
});
+/** @type {Readonly>} */
+const EXERCISE_CATEGORY = Object.freeze({
+ UPPER_BODY: 'upperBody',
+ LOWER_BODY: 'lowerBody',
+ CORE: 'core',
+ CARDIO: 'cardio',
+ MOBILITY: 'mobility',
+ BALANCE: 'balance',
+ FULL_BODY: 'fullBody',
+ STATIC_STRENGTH: 'staticStrength',
+ YOGA: 'yoga',
+});
+
+/** @type {readonly import("../index.d.js").ExerciseEntries[]} */
+const EXERCISES_ENTRIES = Object.freeze([
+ [
+ EXERCISE_CATEGORY.UPPER_BODY,
+ Object.freeze([
+ 'Push-ups',
+ 'Wide push-ups',
+ 'Diamond push-ups',
+ 'Decline push-ups',
+ 'Incline push-ups',
+ 'Shoulder taps',
+ ]),
+ ],
+ [
+ EXERCISE_CATEGORY.LOWER_BODY,
+ Object.freeze([
+ 'Squats',
+ 'Lunges',
+ 'Reverse lunges',
+ 'Side lunges',
+ 'Curtsy lunges',
+ 'Bulgarian split squat',
+ 'Calf raises',
+ 'Wall sit',
+ 'Hip thrusts',
+ ]),
+ ],
+ [
+ EXERCISE_CATEGORY.CORE,
+ Object.freeze([
+ 'Sit-ups',
+ 'Crunches',
+ 'Bicycle crunches',
+ 'Leg raises',
+ 'Flutter kicks',
+ 'Scissor kicks',
+ 'Russian twists',
+ 'Plank',
+ 'Side plank',
+ 'Mountain climbers',
+ 'V-ups',
+ 'Supermans',
+ ]),
+ ],
+ [
+ EXERCISE_CATEGORY.CARDIO,
+ Object.freeze([
+ 'Jumping jacks',
+ 'Burpees',
+ 'Standing knee drives',
+ 'Invisible jump rope',
+ ]),
+ ],
+ [
+ EXERCISE_CATEGORY.MOBILITY,
+ Object.freeze([
+ 'Hip flexor stretch',
+ 'Hamstring stretch',
+ 'Quad stretch',
+ 'Ankle circles',
+ 'Arm circles',
+ 'Torso twists',
+ ]),
+ ],
+ [
+ EXERCISE_CATEGORY.BALANCE,
+ Object.freeze([
+ 'Single-leg balance hold',
+ 'Single-leg toe touch',
+ 'Heel-to-toe walk',
+ 'Single-leg calf raise',
+ ]),
+ ],
+ [
+ EXERCISE_CATEGORY.FULL_BODY,
+ Object.freeze(['Burpees', 'Mountain climbers', 'Jumping jacks']),
+ ],
+ [
+ EXERCISE_CATEGORY.STATIC_STRENGTH,
+ Object.freeze(['Static squat hold', 'Static plank hold']),
+ ],
+ [
+ EXERCISE_CATEGORY.YOGA,
+ Object.freeze([
+ 'Warrior pose',
+ 'Chair pose',
+ 'Tree pose',
+ 'Boat pose',
+ 'Bridge pose',
+ 'Crescent lunge hold',
+ ]),
+ ],
+]);
+
+const EXERCISES_BY_CATEGORY_MAP = Object.freeze(new Map(EXERCISES_ENTRIES));
+
const POMODORO_MODE = Object.freeze({
POMODORO: 'pomodoroMinutes',
SHORT_BREAK: 'shortBreakMinutes',
@@ -83,6 +192,9 @@ export {
AUDIO_SOUND,
AUDIO_VOLUME,
CLIENT_ERROR_MESSAGE,
+ EXERCISES_BY_CATEGORY_MAP,
+ EXERCISE_CATEGORY,
+ EXERCISES_ENTRIES,
DEFAULT_POMODORO_TIMES,
POMODORO_MODE,
POMODORO_TIMER_ACTION,
diff --git a/src/utils/helpers.js b/src/utils/helpers.js
index 51f8a99..fdd160a 100644
--- a/src/utils/helpers.js
+++ b/src/utils/helpers.js
@@ -23,6 +23,18 @@ function isNum(input) {
return false;
}
+/**
+ * @param {unknown} input
+ * @returns {boolean}
+ */
+function isPlainObject(input) {
+ return (
+ typeof input === 'object' &&
+ input !== null &&
+ Object.prototype.toString.call(input) === '[object Object]'
+ );
+}
+
/**
* @param {unknown} input
* @returns {string}
@@ -58,4 +70,4 @@ function toTitleCase(input) {
.join(' ');
}
-export { isBool, isNum, toTitleCase, toSentenceCase };
+export { isBool, isNum, isPlainObject, toTitleCase, toSentenceCase };