From 5c7d469082c13d6d3450e351c04256917935499b Mon Sep 17 00:00:00 2001 From: Anettkva Date: Sun, 13 Sep 2026 12:35:28 +0200 Subject: [PATCH 1/3] feat(favorites): add localStorage read/write for favorite cats --- t29-project-1/src/tests/favorites.test.ts | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 t29-project-1/src/tests/favorites.test.ts diff --git a/t29-project-1/src/tests/favorites.test.ts b/t29-project-1/src/tests/favorites.test.ts new file mode 100644 index 0000000..7d7148d --- /dev/null +++ b/t29-project-1/src/tests/favorites.test.ts @@ -0,0 +1,42 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { + addFavoriteCat, + getFavoriteCats, + removeFavoriteCat, +} from '../utils/favorites'; + +describe('favorite cats storage', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('returns an empty list when no favorites have been saved', () => { + expect(getFavoriteCats()).toEqual([]); + }); + + it('adds a cat to favorites', () => { + addFavoriteCat({ id: 'cat-1', url: 'https://example.com/cat-1.jpg' }); + + expect(getFavoriteCats()).toEqual([ + { id: 'cat-1', url: 'https://example.com/cat-1.jpg' }, + ]); + }); + + it('does not add the same cat twice', () => { + addFavoriteCat({ id: 'cat-1', url: 'https://example.com/cat-1.jpg' }); + addFavoriteCat({ id: 'cat-1', url: 'https://example.com/cat-1.jpg' }); + + expect(getFavoriteCats()).toHaveLength(1); + }); + + it('removes a cat from favorites', () => { + addFavoriteCat({ id: 'cat-1', url: 'https://example.com/cat-1.jpg' }); + addFavoriteCat({ id: 'cat-2', url: 'https://example.com/cat-2.jpg' }); + + removeFavoriteCat('cat-1'); + + expect(getFavoriteCats()).toEqual([ + { id: 'cat-2', url: 'https://example.com/cat-2.jpg' }, + ]); + }); +}); From 08e37727b7bd4e2507bcbf1a36580ba00c6479c4 Mon Sep 17 00:00:00 2001 From: Anettkva Date: Sun, 13 Sep 2026 12:35:47 +0200 Subject: [PATCH 2/3] test: add tests for favorite cats storage --- t29-project-1/src/utils/favorites.ts | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 t29-project-1/src/utils/favorites.ts diff --git a/t29-project-1/src/utils/favorites.ts b/t29-project-1/src/utils/favorites.ts new file mode 100644 index 0000000..80d808b --- /dev/null +++ b/t29-project-1/src/utils/favorites.ts @@ -0,0 +1,34 @@ +import type { CatData } from '../api/catApi'; + +const FAVORITE_CATS_STORAGE_KEY = 'favoriteCats'; + +export function getFavoriteCats(): CatData[] { + try { + const stored = localStorage.getItem(FAVORITE_CATS_STORAGE_KEY); + return stored ? (JSON.parse(stored) as CatData[]) : []; + } catch { + return []; + } +} + +function saveFavoriteCats(cats: CatData[]): void { + try { + localStorage.setItem(FAVORITE_CATS_STORAGE_KEY, JSON.stringify(cats)); + } catch { + // localStorage unavailable (e.g. private browsing) — favorite just won't persist + } +} + +export function addFavoriteCat(cat: CatData): void { + const favorites = getFavoriteCats(); + + if (favorites.some((favorite) => favorite.id === cat.id)) return; + + saveFavoriteCats([...favorites, cat]); +} + +export function removeFavoriteCat(catId: string): void { + const favorites = getFavoriteCats(); + + saveFavoriteCats(favorites.filter((favorite) => favorite.id !== catId)); +} From 38e09282f18011b1432fc5173eed270f131d56fe Mon Sep 17 00:00:00 2001 From: Anettkva Date: Mon, 14 Sep 2026 13:06:33 +0200 Subject: [PATCH 3/3] docs: add favorite cats storage guide --- t29-project-1/src/docs/FAVORITES_STORAGE.md | 93 +++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 t29-project-1/src/docs/FAVORITES_STORAGE.md diff --git a/t29-project-1/src/docs/FAVORITES_STORAGE.md b/t29-project-1/src/docs/FAVORITES_STORAGE.md new file mode 100644 index 0000000..8fad0ff --- /dev/null +++ b/t29-project-1/src/docs/FAVORITES_STORAGE.md @@ -0,0 +1,93 @@ +# Favorite cats storage guide + +This document explains how favorited cats are persisted in `localStorage`, so +favorites are restored after a page reload. + +## Functions + +The storage logic lives in `src/utils/favorites.ts` and exposes three +functions: + +```ts +getFavoriteCats(): CatData[] +addFavoriteCat(cat: CatData): void +removeFavoriteCat(catId: string): void +``` + +`getFavoriteCats` reads the saved list of favorites, or returns an empty array +if none have been saved yet: + +```ts +import { getFavoriteCats } from '../utils/favorites'; + +const favorites = getFavoriteCats(); +``` + +`addFavoriteCat` saves a cat to the list: + +```ts +import { addFavoriteCat } from '../utils/favorites'; + +addFavoriteCat(cat); +``` + +`removeFavoriteCat` removes a cat from the list by ID: + +```ts +import { removeFavoriteCat } from '../utils/favorites'; + +removeFavoriteCat(cat.id); +``` + +## What is stored + +Unlike the breed filter, which stores a single string, favorites are stored +as the full `CatData` object for each cat, not just its ID. This is because +there is currently no way to re-fetch a specific cat by ID from the API, so +the favorites page will need the stored `url` (and other fields) to display +each card directly from `localStorage`. + +The list is stored under the key `favoriteCats`, serialized as JSON. + +## Duplicate favorites + +`addFavoriteCat` checks the existing list for a cat with the same `id` before +adding. Favoriting the same cat twice has no effect: + +```ts +addFavoriteCat(cat); // added +addFavoriteCat(cat); // no-op, already in the list +``` + +## Error handling + +All three functions wrap their `localStorage` access in a `try/catch`. If +storage is unavailable, for example in private browsing mode, +`getFavoriteCats` returns an empty array and the add/remove functions fail +silently instead of throwing. Callers do not need their own error handling +for this. + +## Intended usage + +No favorite button exists yet. Once one is built, it can call +`addFavoriteCat`/`removeFavoriteCat` when the user toggles a cat's favorite +status, and check whether a cat is already favorited with: + +```ts +const isFavorite = getFavoriteCats().some((favorite) => favorite.id === cat.id); +``` + +## Testing + +Tests for this module are in `src/tests/favorites.test.ts` and run with the +rest of the suite: + +```bash +npm test +``` + +To run only this file, pass its path through to Vitest: + +```bash +npm test -- src/tests/favorites.test.ts +```