Ulubione na koncie - koniec kasowania zapisow przez pusta przegladarke
Deploy Polska Lokalnie / deploy (push) Successful in 12s
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:
@@ -17,8 +17,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Zapisywanie ogloszen do ulubionych. Frontend trzyma liste ulubionych lokalnie (szybkie UI),
|
||||
* a tutaj rejestrujemy sam fakt zapisania - dzieki temu wlasciciel widzi, ile osob zapisalo oferte.
|
||||
* Ulubione ogloszenia uzytkownika - trzymane na koncie, wiec sa te same na kazdym urzadzeniu.
|
||||
* Z tych samych danych liczymy wlascicielowi, ile osob zapisalo jego oferte.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/listings")
|
||||
@@ -49,30 +49,38 @@ public class ListingFavoriteController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ustawia ulubione uzytkownika dokladnie na przeslana liste. Front trzyma ulubione lokalnie
|
||||
* i po kazdej zmianie wysyla caly zbior - dzieki temu serwer zna stan niezaleznie od tego,
|
||||
* z ktorego miejsca w aplikacji uzytkownik dodal lub usunal ogloszenie.
|
||||
*/
|
||||
@PostMapping("/favorites/sync")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> sync(@RequestBody SyncRequest request, Authentication authentication) {
|
||||
if (request == null || request.listingIds() == null) {
|
||||
return ResponseEntity.noContent().build();
|
||||
/** Ulubione zalogowanego uzytkownika - te same na kazdym urzadzeniu. */
|
||||
@GetMapping("/favorites")
|
||||
public List<ListingResponse> myFavorites(Authentication authentication) {
|
||||
List<Long> listingIds = favoriteRepository.findByUserEmailIgnoreCase(authentication.getName()).stream()
|
||||
.map(ListingFavorite::getListingId)
|
||||
.toList();
|
||||
if (listingIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// Ogloszenie skasowane albo odrzucone po zapisaniu po prostu znika z listy.
|
||||
return listingRepository.findAllById(listingIds).stream()
|
||||
.filter(listing -> listing.getStatus() == ListingStatus.APPROVED)
|
||||
.map(listing -> ListingResponse.from(listing, listing.isApproximateLocation()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dopisanie ulubionych zapisanych wczesniej w przegladarce. Tylko dodaje - nigdy nie kasuje,
|
||||
* zeby wejscie z pustej przegladarki nie wyczyscilo listy zebranej na innym urzadzeniu.
|
||||
*/
|
||||
@PostMapping("/favorites/import")
|
||||
@Transactional
|
||||
public List<ListingResponse> importLocal(@RequestBody SyncRequest request, Authentication authentication) {
|
||||
if (request != null && request.listingIds() != null) {
|
||||
String userEmail = authentication.getName();
|
||||
List<Long> wanted = request.listingIds().stream()
|
||||
request.listingIds().stream()
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.distinct()
|
||||
.filter(listingRepository::existsById)
|
||||
.toList();
|
||||
|
||||
favoriteRepository.findByUserEmailIgnoreCase(userEmail).stream()
|
||||
.filter(favorite -> !wanted.contains(favorite.getListingId()))
|
||||
.forEach(favoriteRepository::delete);
|
||||
|
||||
wanted.forEach(listingId -> save(listingId, userEmail));
|
||||
return ResponseEntity.noContent().build();
|
||||
.forEach(listingId -> save(listingId, userEmail));
|
||||
}
|
||||
return myFavorites(authentication);
|
||||
}
|
||||
|
||||
/** Liczba zapisan dla ogloszen zalogowanego uzytkownika: {listingId: liczba}. */
|
||||
|
||||
+51
-21
@@ -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.');
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user