Zapisane wyszukiwania na koncie zamiast w przegladarce
Deploy Polska Lokalnie / deploy (push) Successful in 12s
Deploy Polska Lokalnie / deploy (push) Successful in 12s
Nowa tabela saved_searches i /api/saved-searches (lista, dodanie, usuniecie, jednorazowy import wpisow z localStorage). Front czyta liste z serwera, wiec wyszukiwania sa te same na kazdym urzadzeniu i przezywaja wyczyszczenie danych strony. Bez powiadomien. Skrypt czyszczacy usuwa wpisy skasowanych kont. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+107
-24
@@ -384,7 +384,71 @@ const CITY_PRICE_ALERTS_STORAGE_SUFFIX = 'city-price-alert-subscriptions';
|
||||
const LISTING_PRICE_ALERTS_STORAGE_SUFFIX = 'listing-price-alert-subscriptions';
|
||||
const CITY_PRICE_ALERT_SNAPSHOTS_STORAGE_SUFFIX = 'city-price-alert-snapshots';
|
||||
const CITY_PRICE_ALERT_EVENTS_STORAGE_SUFFIX = 'city-price-alert-events';
|
||||
// Klucz zostaje tylko po to, zeby raz przeniesc na konto wyszukiwania zapisane w przegladarce
|
||||
// przed przeniesieniem tej funkcji na serwer.
|
||||
const SAVED_SEARCHES_STORAGE_SUFFIX = 'saved-searches';
|
||||
|
||||
// Zapisane wyszukiwanie w postaci z API - id nadaje serwer.
|
||||
type ApiSavedSearch = {
|
||||
id: number;
|
||||
mode: SavedSearchMode;
|
||||
city: string | null;
|
||||
propertyType: string | null;
|
||||
priceMin: number | null;
|
||||
priceMax: number | null;
|
||||
areaMin: number | null;
|
||||
areaMax: number | null;
|
||||
rooms: number | null;
|
||||
wtorny: boolean;
|
||||
pierwotny: boolean;
|
||||
noFeeOnly: boolean;
|
||||
more: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
function fromApiSavedSearch(item: ApiSavedSearch): SavedSearchRecord {
|
||||
let more: Record<string, string> = {};
|
||||
try {
|
||||
more = item.more ? JSON.parse(item.more) as Record<string, string> : {};
|
||||
} catch {
|
||||
more = {};
|
||||
}
|
||||
return {
|
||||
id: String(item.id),
|
||||
mode: item.mode,
|
||||
city: item.city ?? '',
|
||||
propertyType: item.propertyType ?? '',
|
||||
priceMin: item.priceMin,
|
||||
priceMax: item.priceMax,
|
||||
areaMin: item.areaMin,
|
||||
areaMax: item.areaMax,
|
||||
rooms: item.rooms,
|
||||
wtorny: item.wtorny,
|
||||
pierwotny: item.pierwotny,
|
||||
noFeeOnly: item.noFeeOnly,
|
||||
more,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function toSavedSearchPayload(search: SavedSearchRecord | SavedSearchDraft) {
|
||||
return {
|
||||
mode: search.mode,
|
||||
city: search.city,
|
||||
propertyType: search.propertyType,
|
||||
priceMin: search.priceMin,
|
||||
priceMax: search.priceMax,
|
||||
areaMin: search.areaMin,
|
||||
areaMax: search.areaMax,
|
||||
rooms: search.rooms,
|
||||
wtorny: search.wtorny,
|
||||
pierwotny: search.pierwotny,
|
||||
noFeeOnly: search.noFeeOnly,
|
||||
more: JSON.stringify(search.more ?? {}),
|
||||
};
|
||||
}
|
||||
// Lokalizacje, ktorych cene za m2 sprawdzil uzytkownik - najnowsza pierwsza, przechowywane per konto.
|
||||
const PRICE_HISTORY_MARKETS_SUFFIX = 'price-history-markets';
|
||||
const PRICE_HISTORY_MARKETS_LIMIT = 5;
|
||||
@@ -2703,12 +2767,26 @@ function App() {
|
||||
navigate(ROUTES.buy);
|
||||
};
|
||||
|
||||
// Zapisane wyszukiwania trzyma serwer (/api/saved-searches), wiec sa te same na kazdym
|
||||
// urzadzeniu. Wpisy zapisane wczesniej w przegladarce przenosimy raz przy pierwszym wejsciu.
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
setSavedSearchesState([]);
|
||||
return;
|
||||
}
|
||||
setSavedSearchesState(readUserJsonStorage<SavedSearchRecord[]>(user, SAVED_SEARCHES_STORAGE_SUFFIX, []));
|
||||
let cancelled = false;
|
||||
const local = readUserJsonStorage<SavedSearchRecord[]>(user, SAVED_SEARCHES_STORAGE_SUFFIX, []);
|
||||
const load = local.length > 0
|
||||
? apiFetch<ApiSavedSearch[]>('/saved-searches/import', { method: 'POST', body: JSON.stringify(local.map(toSavedSearchPayload)) })
|
||||
.then((items) => {
|
||||
writeUserJsonStorage<SavedSearchRecord[]>(user, SAVED_SEARCHES_STORAGE_SUFFIX, []);
|
||||
return items;
|
||||
})
|
||||
: apiFetch<ApiSavedSearch[]>('/saved-searches');
|
||||
load
|
||||
.then((items) => { if (!cancelled) { setSavedSearchesState(items.map(fromApiSavedSearch)); } })
|
||||
.catch(() => { if (!cancelled) { setSavedSearchesState([]); } });
|
||||
return () => { cancelled = true; };
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -2717,12 +2795,6 @@ function App() {
|
||||
setListingPriceAlertSubscriptionsState(readUserJsonStorage<ListingPriceAlertSubscription[]>(user, LISTING_PRICE_ALERTS_STORAGE_SUFFIX, []));
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
writeUserJsonStorage(user, SAVED_SEARCHES_STORAGE_SUFFIX, savedSearchesState);
|
||||
}, [savedSearchesState, user]);
|
||||
|
||||
useEffect(() => {
|
||||
writeUserJsonStorage(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, favoriteListingsState);
|
||||
@@ -3141,20 +3213,19 @@ function App() {
|
||||
}
|
||||
|
||||
const draft = buildSavedSearchDraft(mode, filters);
|
||||
setSavedSearchesState((current) => {
|
||||
const existing = current.find((item) => sameSavedSearchCriteria(item, draft));
|
||||
if (existing) {
|
||||
return current.filter((item) => item.id !== existing.id);
|
||||
}
|
||||
const nowIso = new Date().toISOString();
|
||||
const next: SavedSearchRecord = {
|
||||
id: `saved-search-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
||||
createdAt: nowIso,
|
||||
updatedAt: nowIso,
|
||||
...draft,
|
||||
};
|
||||
return [next, ...current].slice(0, SAVED_SEARCHES_LIMIT);
|
||||
});
|
||||
const existing = savedSearchesState.find((item) => sameSavedSearchCriteria(item, draft));
|
||||
if (existing) {
|
||||
removeSavedSearch(existing.id);
|
||||
return;
|
||||
}
|
||||
apiFetch<ApiSavedSearch>('/saved-searches', { method: 'POST', body: JSON.stringify(toSavedSearchPayload(draft)) })
|
||||
.then((saved) => {
|
||||
const record = fromApiSavedSearch(saved);
|
||||
setSavedSearchesState((current) => (
|
||||
current.some((item) => item.id === record.id) ? current : [record, ...current].slice(0, SAVED_SEARCHES_LIMIT)
|
||||
));
|
||||
})
|
||||
.catch(() => { /* brak sieci - lista zostaje bez zmian, uzytkownik moze sprobowac ponownie */ });
|
||||
};
|
||||
|
||||
const openSavedSearch = (savedSearch: SavedSearchRecord) => {
|
||||
@@ -3164,6 +3235,8 @@ function App() {
|
||||
|
||||
const removeSavedSearch = (savedSearchId: string) => {
|
||||
setSavedSearchesState((current) => current.filter((item) => item.id !== savedSearchId));
|
||||
apiFetch<void>(`/saved-searches/${encodeURIComponent(savedSearchId)}`, { method: 'DELETE' })
|
||||
.catch(() => { /* usuniecie na serwerze nie wyszlo - wroci przy nastepnym wejsciu */ });
|
||||
};
|
||||
|
||||
const buySearchSaved = savedSearchesState.some((item) => sameSavedSearchCriteria(item, buildSavedSearchDraft('buy', filters)));
|
||||
@@ -6417,9 +6490,19 @@ function AccountSidebar() {
|
||||
return 0;
|
||||
}
|
||||
})();
|
||||
const savedSearchCount = user
|
||||
? readUserJsonStorage<SavedSearchRecord[]>(user, SAVED_SEARCHES_STORAGE_SUFFIX, []).length
|
||||
: 0;
|
||||
// Zapisane wyszukiwania sa na koncie, wiec licznik bierzemy z serwera - tak jak liczbe ofert.
|
||||
const [savedSearchCount, setSavedSearchCount] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
setSavedSearchCount(0);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
apiFetch<ApiSavedSearch[]>('/saved-searches')
|
||||
.then((items) => { if (!cancelled) { setSavedSearchCount(items.length); } })
|
||||
.catch(() => { if (!cancelled) { setSavedSearchCount(0); } });
|
||||
return () => { cancelled = true; };
|
||||
}, [user, location.pathname]);
|
||||
const favoritesCount = user
|
||||
? readUserJsonStorage<FavoriteListing[]>(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, []).length
|
||||
: 0;
|
||||
|
||||
@@ -500,3 +500,34 @@ test.describe('Standard: przyciski robia to, co obiecuja', () => {
|
||||
await expect(page.locator('a.help-contact-channel-action[href^="tel:"]')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Zapisane wyszukiwania trzyma serwer, wiec sa te same w kazdej przegladarce i przezywaja
|
||||
// wyczyszczenie danych strony. Bez powiadomien - to tylko lista filtrow do wygodnego powrotu.
|
||||
test.describe('Zapisane wyszukiwania', () => {
|
||||
test('zapis przezywa wyczyszczenie pamieci przegladarki', async ({ page }) => {
|
||||
await login(page);
|
||||
await goto(page, `${ROUTES.buy}?miasto=Gdynia`);
|
||||
const saveButton = page.getByRole('button', { name: /wyszukiwanie/ }).first();
|
||||
if ((await saveButton.innerText()).includes('Zapisano')) {
|
||||
await saveButton.click();
|
||||
await expect(saveButton).toHaveText(/Zapisz wyszukiwanie/);
|
||||
}
|
||||
await saveButton.click();
|
||||
await expect(saveButton).toHaveText(/Zapisano wyszukiwanie/);
|
||||
|
||||
// Symulacja innego urzadzenia: zostaje sam token logowania, reszta pamieci znika.
|
||||
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.accountSearches);
|
||||
await expect(page.locator('.account-searches-list, main').first()).toContainText('Gdynia');
|
||||
|
||||
// Sprzatanie, zeby kolejne uruchomienie testu zaczynalo od czystej listy.
|
||||
await page.locator('.account-search-remove').first().click();
|
||||
await expect(page.locator('main')).not.toContainText('Gdynia');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user