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 }; +}