From c64e40775772075d69b30ed39470779734fd2032 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:41:48 +0000
Subject: [PATCH 1/2] Initial plan
From c6fcf1b0b7b0e31305ac9f2e46a61704e40b3f4f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:51:04 +0000
Subject: [PATCH 2/2] feat: add favorite events feature
- Add useFavoriteEvents hook using AsyncStorage for persistence
- Update EventSelector to show Favorites section at top with star toggle buttons
- Add event.type.favorites localization key to en.json and es.json
- Add AsyncStorage jest mock to global test setup
- Delete stale EventSelector snapshot for regeneration
Agent-Logs-Url: https://github.com/SpeedcuberOSS/speedcuber-timer/sessions/e0a858fc-748a-4bbe-ba7b-0b95bd4452ff
Co-authored-by: thehale <47901316+thehale@users.noreply.github.com>
---
src/__mocks__/globalMock.js | 4 +
src/localization/locales/en.json | 3 +-
src/localization/locales/es.json | 3 +-
src/ui/components/events/EventSelector.tsx | 80 +-
.../__snapshots__/EventSelector.test.ts.snap | 5991 -----------------
src/ui/hooks/useFavoriteEvents.ts | 50 +
6 files changed, 124 insertions(+), 6007 deletions(-)
delete mode 100644 src/ui/components/events/__tests__/__snapshots__/EventSelector.test.ts.snap
create mode 100644 src/ui/hooks/useFavoriteEvents.ts
diff --git a/src/__mocks__/globalMock.js b/src/__mocks__/globalMock.js
index ebdbcd1..f979dd6 100644
--- a/src/__mocks__/globalMock.js
+++ b/src/__mocks__/globalMock.js
@@ -4,6 +4,10 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+jest.mock('@react-native-async-storage/async-storage', () =>
+ require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
+);
+
jest.mock('react-native-webview', () => {
const { View } = require('react-native');
return {
diff --git a/src/localization/locales/en.json b/src/localization/locales/en.json
index 4a19c80..34cea17 100644
--- a/src/localization/locales/en.json
+++ b/src/localization/locales/en.json
@@ -80,7 +80,8 @@
"type": {
"official": "Official",
"unofficial": "Unofficial",
- "retired": "Retired"
+ "retired": "Retired",
+ "favorites": "Favorites"
}
},
"events": {
diff --git a/src/localization/locales/es.json b/src/localization/locales/es.json
index 315263a..a426f78 100644
--- a/src/localization/locales/es.json
+++ b/src/localization/locales/es.json
@@ -33,7 +33,8 @@
"type": {
"official": "Oficial",
"unofficial": "No oficial",
- "retired": "Retirado"
+ "retired": "Retirado",
+ "favorites": "Favoritos"
}
},
"scramble": {
diff --git a/src/ui/components/events/EventSelector.tsx b/src/ui/components/events/EventSelector.tsx
index 2d580a5..d8880f1 100644
--- a/src/ui/components/events/EventSelector.tsx
+++ b/src/ui/components/events/EventSelector.tsx
@@ -6,13 +6,14 @@
import * as Events from '../../../lib/stif/builtins/CompetitiveEvents';
-import { Divider, List } from 'react-native-paper';
+import { Divider, IconButton, List } from 'react-native-paper';
import { Fragment, useCallback, useState } from 'react';
import { ScrollView, View } from 'react-native';
import Icons from '../../icons/iconHelper';
import { STIF } from '../../../lib/stif';
import Ticker from '../ticker/Ticker';
+import { useFavoriteEvents } from '../../hooks/useFavoriteEvents';
import { useTranslation } from 'react-i18next';
interface EventSelectorProps {
@@ -22,12 +23,14 @@ interface EventSelectorProps {
interface EventItemProps {
event: STIF.CompetitiveEvent;
onSelect: (event: STIF.CompetitiveEvent) => void;
+ isFavorite: boolean;
+ onToggleFavorite: (eventId: string) => void;
}
-function EventItem({ event, onSelect }: EventItemProps) {
+function EventItem({ event, onSelect, isFavorite, onToggleFavorite }: EventItemProps) {
const { t } = useTranslation();
const [multiCount, setMultiCount] = useState(2);
- const isMultiEvent = () => ['333mbf', "333m", "222m"].includes(event.id)
+ const isMultiEvent = () => ['333mbf', '333m', '222m'].includes(event.id)
const pressHandler = useCallback(() => {
if (isMultiEvent()) {
onSelect({ ...event, puzzles: new Array(multiCount).fill(event.puzzles[0]) });
@@ -43,16 +46,29 @@ function EventItem({ event, onSelect }: EventItemProps) {
left={props => }
right={props =>
isMultiEvent() ? (
-
- setMultiCount(value)}
- orientation="horizontal"
+
+
+ setMultiCount(value)}
+ orientation="horizontal"
+ />
+
+ onToggleFavorite(event.id)}
/>
- ) : null
+ ) : (
+ onToggleFavorite(event.id)}
+ />
+ )
}
/>
);
@@ -60,12 +76,30 @@ function EventItem({ event, onSelect }: EventItemProps) {
export default function EventSelector({ onSelect }: EventSelectorProps) {
const { t } = useTranslation();
+ const { favoriteEventIds, toggleFavorite, isFavorite } = useFavoriteEvents();
const unsupportedEvents = ['unknown', '333bf-team', '23relay'];
const events = Object.values(Events).filter(
e => !unsupportedEvents.includes(e.id),
);
+ const favoriteEvents = events.filter(e => isFavorite(e.id));
return (
+ {favoriteEvents.length > 0 && (
+
+ {t('event.type.favorites')}
+ {favoriteEvents.map((e, idx) => (
+
+ {!!idx && }
+
+
+ ))}
+
+ )}
{t('event.type.official')}
{events
@@ -74,7 +108,13 @@ export default function EventSelector({ onSelect }: EventSelectorProps) {
// Inspired by: https://www.codemzy.com/blog/joining-arrays-react-components
{!!idx && }
-
+
))}
@@ -86,7 +126,13 @@ export default function EventSelector({ onSelect }: EventSelectorProps) {
// Inspired by: https://www.codemzy.com/blog/joining-arrays-react-components
{!!idx && }
-
+
))}
@@ -98,7 +144,13 @@ export default function EventSelector({ onSelect }: EventSelectorProps) {
// Inspired by: https://www.codemzy.com/blog/joining-arrays-react-components
{!!idx && }
-
+
))}
diff --git a/src/ui/components/events/__tests__/__snapshots__/EventSelector.test.ts.snap b/src/ui/components/events/__tests__/__snapshots__/EventSelector.test.ts.snap
deleted file mode 100644
index 9f85ce4..0000000
--- a/src/ui/components/events/__tests__/__snapshots__/EventSelector.test.ts.snap
+++ /dev/null
@@ -1,5991 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Event Selector [Default] matches snapshot 1`] = `
-
-
-
-
-
-
- event.type.official
-
-
-
-
-
-
-
-
-
-
- events.222
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.333
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.333bf
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.333mbf
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.333fm
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.333oh
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.444
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.444bf
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.555
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.555bf
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.666
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.777
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.clock
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.minx
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.pyram
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.skewb
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.sq1
-
-
-
-
-
-
-
- event.type.unofficial
-
-
-
-
-
-
-
-
-
-
- events.111
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.222bf
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.222m
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.333m
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.666bf
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.777bf
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.234relay
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.2345relay
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.23456relay
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- events.234567relay
-
-
-
-
-
-
-
- event.type.retired
-
-
-
-
-
-
-
-
-
-
- events.333ft
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/ui/hooks/useFavoriteEvents.ts b/src/ui/hooks/useFavoriteEvents.ts
new file mode 100644
index 0000000..bc0720f
--- /dev/null
+++ b/src/ui/hooks/useFavoriteEvents.ts
@@ -0,0 +1,50 @@
+// Copyright (c) 2023 Joseph Hale
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { useCallback, useEffect, useState } from 'react';
+
+const STORAGE_KEY = '@favorite_events';
+
+export function useFavoriteEvents() {
+ const [favoriteEventIds, setFavoriteEventIds] = useState([]);
+
+ useEffect(() => {
+ AsyncStorage.getItem(STORAGE_KEY)
+ .then(value => {
+ if (value !== null) {
+ try {
+ setFavoriteEventIds(JSON.parse(value));
+ } catch {
+ setFavoriteEventIds([]);
+ }
+ }
+ })
+ .catch(() => {
+ setFavoriteEventIds([]);
+ });
+ }, []);
+
+ const toggleFavorite = useCallback(
+ (eventId: string) => {
+ const updated = favoriteEventIds.includes(eventId)
+ ? favoriteEventIds.filter(id => id !== eventId)
+ : [...favoriteEventIds, eventId];
+ setFavoriteEventIds(updated);
+ AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated)).catch(() => {
+ setFavoriteEventIds(favoriteEventIds);
+ });
+ },
+ [favoriteEventIds],
+ );
+
+ const isFavorite = useCallback(
+ (eventId: string) => favoriteEventIds.includes(eventId),
+ [favoriteEventIds],
+ );
+
+ return { favoriteEventIds, toggleFavorite, isFavorite };
+}