Ulubione na koncie - koniec kasowania zapisow przez pusta przegladarke
Deploy Polska Lokalnie / deploy (push) Successful in 12s

Dotad front po kazdej zmianie dyktowal serwerowi cala liste ulubionych.
Zalogowanie sie na innym urzadzeniu wysylalo "mam zero ulubionych"
i kasowalo zapisy, przez co wlasciciel ogloszenia tracil licznik zapisan.

Teraz: GET /api/listings/favorites zwraca ulubione konta, dodanie
i usuniecie ida pojedynczo (POST/DELETE /{id}/favorite), a wpisy z
przegladarki dopisuje jednorazowy /favorites/import. Endpoint
/favorites/sync usuniety razem z jego jedynym wywolaniem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 11:26:00 +02:00
parent b9fecdea85
commit 557c76257b
3 changed files with 114 additions and 45 deletions
+51 -21
View File
@@ -2782,33 +2782,40 @@ function App() {
}, [user]);
useEffect(() => {
setFavoriteListingsState(readUserJsonStorage<FavoriteListing[]>(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, []));
setFavoritesPreferredTab(readUserJsonStorage<FavoriteTab>(user, FAVORITES_PREFERRED_TAB_STORAGE_SUFFIX, 'buy'));
setListingPriceAlertSubscriptionsState(readUserJsonStorage<ListingPriceAlertSubscription[]>(user, LISTING_PRICE_ALERTS_STORAGE_SUFFIX, []));
}, [user]);
useEffect(() => {
writeUserJsonStorage(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, favoriteListingsState);
}, [favoriteListingsState, user]);
// Ulubione zyja lokalnie (szybkie UI), ale wlasciciel ogloszenia musi widziec, ile osob je zapisalo.
// Po kazdej zmianie wysylamy caly zbior - dziala niezaleznie od tego, gdzie uzytkownik kliknal serce.
// Ulubione trzyma konto (/api/listings/favorites), wiec sa te same na kazdym urzadzeniu.
// Wpisy zapisane wczesniej w przegladarce dopisujemy raz przy pierwszym wejsciu.
useEffect(() => {
if (!user) {
setFavoriteListingsState([]);
return;
}
const listingIds = favoriteListingsState
.map((favorite) => favorite.listingId)
.filter((listingId): listingId is number => typeof listingId === 'number');
let cancelled = false;
const local = readUserJsonStorage<FavoriteListing[]>(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, []);
const localIds = local.map((item) => item.listingId).filter((id): id is number => typeof id === 'number');
const load = localIds.length > 0
? apiFetch<ApiListingSummary[]>('/listings/favorites/import', { method: 'POST', body: JSON.stringify({ listingIds: localIds }) })
.then((items) => {
writeUserJsonStorage<FavoriteListing[]>(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, []);
return items;
})
: apiFetch<ApiListingSummary[]>('/listings/favorites');
load
.then((items) => { if (!cancelled) { setFavoriteListingsState(items.map(toFavoriteListingFromSummary)); } })
.catch(() => { if (!cancelled) { setFavoriteListingsState([]); } });
return () => { cancelled = true; };
}, [user]);
apiFetch<void>('/listings/favorites/sync', {
method: 'POST',
body: JSON.stringify({ listingIds }),
}).catch(() => {
// Brak polaczenia - licznik zapisan dogoni stan przy nastepnej zmianie ulubionych.
});
}, [favoriteListingsState, user]);
// Zapis i usuniecie ulubionego ida na serwer pojedynczo - stan przegladarki nigdy nie
// dyktuje serwerowi calej listy, wiec wejscie z nowego urzadzenia niczego nie kasuje.
const saveFavoriteOnServer = (listingId: number, add: boolean) => {
apiFetch<void>(`/listings/${listingId}/favorite`, { method: add ? 'POST' : 'DELETE' })
.catch(() => { /* brak sieci - zmiana wroci przy nastepnym wejsciu na konto */ });
};
useEffect(() => {
writeUserJsonStorage(user, FAVORITES_PREFERRED_TAB_STORAGE_SUFFIX, favoritesPreferredTab);
@@ -3241,6 +3248,7 @@ function App() {
const toggleListingFavorite = (listing: ApiListingDetail) => {
const listingKey = favoriteListingKey(listing.id);
const alreadyFavorite = favoriteListingsState.some((item) => item.id === listingKey);
saveFavoriteOnServer(listing.id, !alreadyFavorite);
if (!alreadyFavorite) {
notificationsCenter.notify({
category: 'listing',
@@ -3332,6 +3340,7 @@ function App() {
const toggleSummaryFavorite = async (listing: ApiListingSummary) => {
const listingKey = favoriteListingKey(listing.id);
const exists = favoriteListingsState.some((item) => item.id === listingKey);
saveFavoriteOnServer(listing.id, !exists);
if (exists) {
setUnreadFavoriteIds((prev) => {
if (!prev.has(listingKey)) {
@@ -6497,9 +6506,19 @@ function AccountSidebar() {
.catch(() => { if (!cancelled) { setSavedSearchCount(0); } });
return () => { cancelled = true; };
}, [user, location.pathname]);
const favoritesCount = user
? readUserJsonStorage<FavoriteListing[]>(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, []).length
: 0;
// Ulubione sa na koncie - licznik bierzemy z serwera, tak jak liczbe ofert i wyszukiwan.
const [favoritesCount, setFavoritesCount] = useState(0);
useEffect(() => {
if (!user) {
setFavoritesCount(0);
return;
}
let cancelled = false;
apiFetch<ApiListingSummary[]>('/listings/favorites')
.then((items) => { if (!cancelled) { setFavoritesCount(items.length); } })
.catch(() => { if (!cancelled) { setFavoritesCount(0); } });
return () => { cancelled = true; };
}, [user, location.pathname]);
// Licznik obejmuje oba rodzaje alertow - lokalizacji i pojedynczych ogloszen -
// dokladnie tak, jak lista na /konto/alerty-cenowe.
const priceAlertsCount = user
@@ -26297,7 +26316,17 @@ function FavoritesPage({
return () => window.clearTimeout(timeoutId);
}, [favoritesFeedback]);
// Usuniecie idzie takze na serwer - ulubione sa na koncie, nie w tej jednej przegladarce.
const forgetOnServer = (listingId: number) => {
apiFetch<void>(`/listings/${listingId}/favorite`, { method: 'DELETE' })
.catch(() => { /* brak sieci - wpis wroci przy nastepnym wejsciu */ });
};
const removeFavoriteListing = (id: string) => {
const removed = favoritesState.find((listing) => listing.id === id);
if (removed) {
forgetOnServer(removed.listingId);
}
setFavoritesState((current) => current.filter((listing) => listing.id !== id));
setMenuListingId((current) => (current === id ? null : current));
setFavoritesFeedback('Usunięto ogłoszenie z ulubionych.');
@@ -26363,6 +26392,7 @@ function FavoritesPage({
return;
}
favoritesState.forEach((listing) => forgetOnServer(listing.listingId));
setFavoritesState([]);
setMenuListingId(null);
setFavoritesFeedback('Usunięto wszystkie ogłoszenia z ulubionych.');
+31
View File
@@ -566,3 +566,34 @@ test.describe('Spotkania', () => {
await expect(page.locator('.messages-meeting-form')).toBeVisible();
});
});
// Ulubione sa na koncie. Test pilnuje regresji: wejscie z pustej przegladarki NIE moze
// skasowac ulubionych zapisanych wczesniej (kiedys front dyktowal serwerowi cala liste).
test.describe('Ulubione', () => {
test('przezywaja wyczyszczenie pamieci przegladarki', async ({ page }) => {
await login(page);
await goto(page, ROUTES.buy);
await page.locator('.real-result-card').first().waitFor();
const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href');
await goto(page, href!);
const title = await page.locator('.listing-detail-title, h1').first().innerText();
const heart = page.locator('button[aria-label*="ulubion"], .listing-detail-fav').first();
await heart.click();
await goto(page, ROUTES.favorites);
await expect(page.locator('main')).toContainText(title.trim().slice(0, 20));
await page.evaluate(() => {
const token = window.localStorage.getItem('polskalokalnie-auth-token');
window.localStorage.clear();
if (token) {
window.localStorage.setItem('polskalokalnie-auth-token', token);
}
});
await goto(page, ROUTES.favorites);
await expect(page.locator('main')).toContainText(title.trim().slice(0, 20));
await page.locator('button[aria-label*="Usuń"], .favorites-card-remove').first().click();
await expect(page.locator('main')).not.toContainText(title.trim().slice(0, 20));
});
});