From 3e00962828298100305a55a8be974540c54dc3f0 Mon Sep 17 00:00:00 2001 From: pascal Date: Sat, 11 Jul 2026 01:45:13 +0200 Subject: [PATCH 1/3] dodawanie alerow cenowych --- frontend/src/App.tsx | 451 +++++++++++++++++++++++++++++++++++++++- frontend/src/styles.css | 397 +++++++++++++++++++++++++++++++++++ 2 files changed, 842 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 80f8e97..d6efb40 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,7 +9,7 @@ const cityImage = new URL('./assets/city-panorama.png', import.meta.url).href; const loginRoomImage = new URL('./assets/login-room.png', import.meta.url).href; const SMS_VERIFICATION_CODE = '123456'; -type View = 'home' | 'buy' | 'rent' | 'sell' | 'valuation' | 'priceHistory' | 'districtRanking' | 'negotiation' | 'comparison' | 'add' | 'services' | 'guides' | 'map' | 'login' | 'account' | 'accountMeetings' | 'accountListings' | 'accountSettings' | 'accountSecurity' | 'accountProfileEdit' | 'accountPublicProfile' | 'notifications' | 'favorites' | 'messages' | 'admin' | 'listingDetail'; +type View = 'home' | 'buy' | 'rent' | 'sell' | 'valuation' | 'priceHistory' | 'districtRanking' | 'negotiation' | 'comparison' | 'add' | 'services' | 'guides' | 'map' | 'login' | 'account' | 'accountPriceAlerts' | 'accountMeetings' | 'accountListings' | 'accountSettings' | 'accountSecurity' | 'accountProfileEdit' | 'accountPublicProfile' | 'notifications' | 'favorites' | 'messages' | 'admin' | 'listingDetail'; type MessageThreadListing = { listingId: number; @@ -109,7 +109,7 @@ type LiveListingSummary = ApiListingSummary & { const LIVE_LISTINGS_REFRESH_MS = 15000; const PROTECTED_VIEWS = new Set([ - 'add', 'admin', 'account', 'accountMeetings', 'accountListings', 'accountSettings', + 'add', 'admin', 'account', 'accountPriceAlerts', 'accountMeetings', 'accountListings', 'accountSettings', 'accountSecurity', 'accountProfileEdit', 'accountPublicProfile', 'notifications', 'favorites', 'messages', 'comparison', ]); @@ -362,6 +362,73 @@ type FavoriteListing = { badge: string; }; +type FavoritePriceAlertEvent = { + id: string; + listingId: number; + title: string; + location: string; + area: string; + previousPrice: number; + currentPrice: number; + changedAt: string; +}; + +type CityPriceAlertSubscription = { + key: string; + label: string; + city: string; + region: string; + propertyType: HistoryPropertyType; + marketType: HistoryMarketType; + createdAt: string; +}; + +const FAVORITE_PRICE_ALERTS_STORAGE_SUFFIX = 'favorite-price-alert-events'; +const FAVORITE_PRICE_SNAPSHOTS_STORAGE_SUFFIX = 'favorite-price-snapshots'; +const CITY_PRICE_ALERTS_STORAGE_SUFFIX = 'city-price-alert-subscriptions'; +const PRICE_ALERT_EVENTS_LIMIT = 40; + +function readUserJsonStorage(user: AuthUser | null, suffix: string, fallback: T): T { + if (typeof window === 'undefined') { + return fallback; + } + try { + const raw = window.localStorage.getItem(accountStorageKey(suffix, user)); + if (!raw) { + return fallback; + } + return JSON.parse(raw) as T; + } catch { + return fallback; + } +} + +function writeUserJsonStorage(user: AuthUser | null, suffix: string, value: T): void { + if (typeof window === 'undefined') { + return; + } + window.localStorage.setItem(accountStorageKey(suffix, user), JSON.stringify(value)); +} + +function mergePriceAlertEvents( + incoming: FavoritePriceAlertEvent[], + existing: FavoritePriceAlertEvent[], +): FavoritePriceAlertEvent[] { + if (incoming.length === 0) { + return existing; + } + + const known = new Set(existing.map((item) => item.id)); + const uniqueIncoming = incoming.filter((item) => !known.has(item.id)); + if (uniqueIncoming.length === 0) { + return existing; + } + + return [...uniqueIncoming, ...existing] + .sort((left, right) => new Date(right.changedAt).getTime() - new Date(left.changedAt).getTime()) + .slice(0, PRICE_ALERT_EVENTS_LIMIT); +} + const favoriteListings: FavoriteListing[] = []; function favoriteListingKey(listingId: number): string { @@ -2325,6 +2392,11 @@ function App() { const [favoritesPreferredTab, setFavoritesPreferredTab] = useState('buy'); const [unreadFavoriteIds, setUnreadFavoriteIds] = useState>(new Set()); const hydratedFavoriteIdsRef = useRef>(new Set()); + const favoriteListingIdList = useMemo( + () => Array.from(new Set(favoriteListingsState.map((item) => item.listingId))).sort((left, right) => left - right), + [favoriteListingsState], + ); + const favoriteListingIdsSignature = useMemo(() => favoriteListingIdList.join(','), [favoriteListingIdList]); const translatedOriginalTextRef = useRef>(new Map()); const translatedOriginalAttrRef = useRef>>(new Map()); const translationObserverRef = useRef(null); @@ -2735,6 +2807,98 @@ function App() { }; }, [favoriteListingsState]); + useEffect(() => { + if (!user || favoriteListingIdList.length === 0) { + return; + } + + let cancelled = false; + let snapshots = readUserJsonStorage>(user, FAVORITE_PRICE_SNAPSHOTS_STORAGE_SUFFIX, {}); + + const syncFavoritePrices = async () => { + const refreshed = await Promise.all( + favoriteListingIdList.map(async (listingId) => { + try { + return await apiFetch(`/listings/${listingId}`); + } catch { + return null; + } + }), + ); + + if (cancelled) { + return; + } + + const nowIso = new Date().toISOString(); + const incomingEvents: FavoritePriceAlertEvent[] = []; + const nextSnapshots = { ...snapshots }; + const refreshedFavorites = new Map(); + + refreshed.forEach((listing) => { + if (!listing) { + return; + } + + const listingIdKey = String(listing.id); + const previousPrice = nextSnapshots[listingIdKey]; + const currentPrice = Number(listing.price); + if (Number.isFinite(currentPrice)) { + if (Number.isFinite(previousPrice) && previousPrice !== currentPrice) { + const location = [listing.city, listing.district].filter(Boolean).join(', '); + incomingEvents.push({ + id: `${listing.id}:${previousPrice}->${currentPrice}`, + listingId: listing.id, + title: listing.title, + location: location || listing.city, + area: `${`${listing.area}`.replace('.', ',')} m²`, + previousPrice, + currentPrice, + changedAt: nowIso, + }); + } + nextSnapshots[listingIdKey] = currentPrice; + } + + refreshedFavorites.set(listing.id, toFavoriteListing(listing)); + }); + + snapshots = nextSnapshots; + writeUserJsonStorage(user, FAVORITE_PRICE_SNAPSHOTS_STORAGE_SUFFIX, nextSnapshots); + + if (incomingEvents.length > 0) { + const existingEvents = readUserJsonStorage(user, FAVORITE_PRICE_ALERTS_STORAGE_SUFFIX, []); + const merged = mergePriceAlertEvents(incomingEvents, existingEvents); + writeUserJsonStorage(user, FAVORITE_PRICE_ALERTS_STORAGE_SUFFIX, merged); + } + + if (refreshedFavorites.size > 0) { + setFavoriteListingsState((current) => current.map((item) => { + const refreshedItem = refreshedFavorites.get(item.listingId); + if (!refreshedItem) { + return item; + } + return { + ...item, + ...refreshedItem, + id: item.id, + listingId: item.listingId, + }; + })); + } + }; + + void syncFavoritePrices(); + const intervalId = window.setInterval(() => { + void syncFavoritePrices(); + }, 45000); + + return () => { + cancelled = true; + window.clearInterval(intervalId); + }; + }, [user, favoriteListingIdList, favoriteListingIdsSignature]); + return (
0} /> @@ -2742,6 +2906,7 @@ function App() { {view === 'login' && } {view === 'admin' && } {view === 'account' && } + {view === 'accountPriceAlerts' && } {view === 'comparison' && } {view === 'accountMeetings' && } {view === 'accountListings' && } @@ -2789,7 +2954,7 @@ function App() { )} {view === 'sell' && } {view === 'valuation' && } - {view === 'priceHistory' && } + {view === 'priceHistory' && } {view === 'districtRanking' && } {view === 'negotiation' && ( Zapisane wyszukiwania 5 - +
@@ -4776,6 +4942,228 @@ function AccountDashboardPage({ onNavigate, activeView }: { onNavigate: (view: V ); } +function AccountPriceAlertsPage({ + onNavigate, + activeView, + user, + onOpenListing, +}: { + onNavigate: (view: View) => void; + activeView: View; + user: AuthUser | null; + onOpenListing: (id: number) => void; +}) { + const [cityAlerts, setCityAlerts] = useState([]); + const [priceEvents, setPriceEvents] = useState([]); + + useEffect(() => { + if (!user) { + setCityAlerts([]); + setPriceEvents([]); + return; + } + + const hydrate = () => { + setCityAlerts(readUserJsonStorage(user, CITY_PRICE_ALERTS_STORAGE_SUFFIX, [])); + setPriceEvents(readUserJsonStorage(user, FAVORITE_PRICE_ALERTS_STORAGE_SUFFIX, [])); + }; + + hydrate(); + const intervalId = window.setInterval(hydrate, 10000); + return () => window.clearInterval(intervalId); + }, [user]); + + const latestEvent = priceEvents[0] ?? null; + const cityInsights = useMemo(() => cityAlerts.map((subscription, index) => { + const market = HISTORY_PRICE_MARKETS.find((item) => item.key === subscription.key) ?? findHistoryMarket(subscription.label); + const district = historyShortMarketLabel(market ?? resolveHistoryMarket(subscription.label)); + const rawPrice = market?.apartmentM2 ?? estimateHistoryLocationPrice(subscription.label).price; + const price = Math.round(rawPrice * HISTORY_PROPERTY_MULTIPLIERS[subscription.propertyType] * HISTORY_MARKET_MULTIPLIERS[subscription.marketType]); + const delta = (market?.trend24 ?? (0.03 + ((index % 4) * 0.008))) * 100; + return { + key: subscription.key, + district, + price, + delta, + barWidth: Math.max(28, 78 - (index * 9)), + }; + }), [cityAlerts]); + + const cityAveragePrice = cityInsights.length > 0 + ? Math.round(cityInsights.reduce((sum, item) => sum + item.price, 0) / cityInsights.length) + : 0; + const cityAverageDelta = cityInsights.length > 0 + ? cityInsights.reduce((sum, item) => sum + item.delta, 0) / cityInsights.length + : 0; + + const activeAlerts = cityAlerts.map((subscription) => ({ + title: subscription.label, + meta: `${subscription.propertyType} • ${subscription.marketType}`, + key: subscription.key, + })); + + const removeAlert = (key: string) => { + if (!user) { + return; + } + setCityAlerts((current) => { + const next = current.filter((item) => item.key !== key); + writeUserJsonStorage(user, CITY_PRICE_ALERTS_STORAGE_SUFFIX, next); + return next; + }); + }; + + const hasPriceDrop = latestEvent ? latestEvent.currentPrice < latestEvent.previousPrice : false; + const latestDelta = latestEvent ? latestEvent.currentPrice - latestEvent.previousPrice : 0; + const latestDeltaPercent = latestEvent && latestEvent.previousPrice > 0 + ? (Math.abs(latestDelta) / latestEvent.previousPrice) * 100 + : 0; + + return ( +
+
+ + +
+
+ Strona główna Powiadomienia i alerty +
+

Powiadomienia i alerty

+

Śledź zmiany cen w wybranych lokalizacjach i otrzymuj powiadomienia, które pomogą Ci podjąć lepszą decyzję.

+
+ +
+ +
+
+ Alert cenowy +

+ {latestEvent + ? (hasPriceDrop ? 'Cena mieszkania spadła!' : 'Cena mieszkania zmieniła się') + : 'Brak nowych zmian cen'} +

+

{latestEvent ? latestEvent.title : 'Dodaj oferty do ulubionych, aby śledzić zmiany cen.'}

+ {latestEvent ? latestEvent.location : 'Wybierz mieszkania, które Cię interesują'} +
+ {latestEvent ? formatPln(latestEvent.currentPrice) : '0 zł'} + {latestEvent ? formatPln(latestEvent.previousPrice) : '0 zł'} + + {latestEvent + ? `${latestDelta <= 0 ? '-' : '+'} ${formatPln(Math.abs(latestDelta))} (${latestDeltaPercent.toFixed(1).replace('.', ',')}%)` + : 'Brak porównań'} + +
+
+ Powierzchnia: {latestEvent ? latestEvent.area : '—'} + + Cena za m²: + {latestEvent + ? formatM2(Math.round(latestEvent.currentPrice / Math.max(1, Number(latestEvent.area.replace(/[^\d,]/g, '').replace(',', '.')) || 1))) + : '—'} + + +
+
+ +
+ + +
+ +
+
+
+

Średnia zmiana ceny za m² w alertach

+

{cityInsights.length > 0 ? `Na podstawie ${cityInsights.length} aktywnych lokalizacji` : 'Dodaj lokalizację, aby aktywować wykres'}

+
+
+ {cityInsights.length > 0 ? formatM2(cityAveragePrice) : '0 zł/m²'} + {cityInsights.length > 0 ? `${cityAverageDelta >= 0 ? '+' : ''}${cityAverageDelta.toFixed(1).replace('.', ',')}%` : '0,0%'} +
+ +
+ +
+
+

Zmiana ceny za m² w dzielnicach

+

Ostatnie 12 miesięcy

+
+
+ {cityInsights.length > 0 ? cityInsights.slice(0, 5).map((item) => ( +
+ {item.district} + + {formatM2(item.price)} + {item.delta >= 0 ? '+' : ''}{item.delta.toFixed(1).replace('.', ',')}% +
+ )) : ( +
+ Brak danych + + 0 zł/m² + 0,0% +
+ )} +
+ +
+ +
+
+

Twoje alerty

+
+
+ {activeAlerts.length > 0 ? activeAlerts.map((item) => ( + + )) : ( + + )} +
+ +
+
+
+
+
+ ); +} + function AccountComparisonPage({ onNavigate, activeView, onOpenListing }: { onNavigate: (view: View) => void; activeView: View; onOpenListing: (id: number) => void }) { type ComparisonPropertyFilter = 'ALL' | ApiPropertyType | 'LAND' | 'COMMERCIAL' | 'ROOM'; @@ -13039,12 +13427,14 @@ function historyShortMarketLabel(market: PriceHistoryMarket) { return tail; } -function PriceHistoryPage({ onNavigate }: { onNavigate: (view: View) => void }) { +function PriceHistoryPage({ onNavigate, user }: { onNavigate: (view: View) => void; user: AuthUser | null }) { const [locationInput, setLocationInput] = useState(''); const [selectedMarket, setSelectedMarket] = useState(null); const [propertyType, setPropertyType] = useState('Mieszkanie'); const [marketType, setMarketType] = useState('Wtórny i pierwotny'); const [period, setPeriod] = useState('2026'); + const [createAlertFeedback, setCreateAlertFeedback] = useState(null); + const [cityAlertSubscriptions, setCityAlertSubscriptions] = useState([]); const [suggestions, setSuggestions] = useState([]); const [suggestOpen, setSuggestOpen] = useState(false); const [suggestLoading, setSuggestLoading] = useState(false); @@ -13119,6 +13509,9 @@ function PriceHistoryPage({ onNavigate }: { onNavigate: (view: View) => void }) const tooltipHeight = 54; const tooltipX = hoveredPoint ? Math.min(chartWidth - tooltipWidth - 8, Math.max(8, hoveredPoint.x - tooltipWidth / 2)) : 0; const tooltipY = hoveredPoint ? Math.max(8, hoveredPoint.y - tooltipHeight - 14) : 0; + const selectedMarketAlreadyAlerted = selectedMarket + ? cityAlertSubscriptions.some((item) => item.key === selectedMarket.key) + : false; const handleHistoryChartMouseMove = (event: ReactMouseEvent) => { if (!hasSelectedMarket) { @@ -13131,6 +13524,14 @@ function PriceHistoryPage({ onNavigate }: { onNavigate: (view: View) => void }) setHoveredHistoryIndex(nextIndex); }; + useEffect(() => { + if (!user) { + setCityAlertSubscriptions([]); + return; + } + setCityAlertSubscriptions(readUserJsonStorage(user, CITY_PRICE_ALERTS_STORAGE_SUFFIX, [])); + }, [user]); + useEffect(() => { if (skipNextFetch.current) { skipNextFetch.current = false; @@ -13203,6 +13604,43 @@ function PriceHistoryPage({ onNavigate }: { onNavigate: (view: View) => void }) setPeriod('2026'); }; + const createCityAlert = () => { + if (!user) { + setCreateAlertFeedback('Zaloguj się, aby dodać alert cenowy.'); + onNavigate('login'); + return; + } + if (!selectedMarket) { + setCreateAlertFeedback('Najpierw wybierz lokalizację do monitorowania.'); + return; + } + + const subscription: CityPriceAlertSubscription = { + key: selectedMarket.key, + label: selectedMarket.label, + city: selectedMarket.city, + region: selectedMarket.region, + propertyType, + marketType, + createdAt: new Date().toISOString(), + }; + + setCityAlertSubscriptions((current) => { + if (current.some((item) => item.key === subscription.key)) { + setCreateAlertFeedback('Ten alert już jest aktywny.'); + return current; + } + const next = [subscription, ...current]; + writeUserJsonStorage(user, CITY_PRICE_ALERTS_STORAGE_SUFFIX, next); + setCreateAlertFeedback('Alert został dodany do Twojego panelu.'); + return next; + }); + + window.setTimeout(() => { + onNavigate('accountPriceAlerts'); + }, 280); + }; + return (
@@ -13351,7 +13789,8 @@ function PriceHistoryPage({ onNavigate }: { onNavigate: (view: View) => void })

Chcesz być na bieżąco?Ustaw alert cenowy i otrzymuj powiadomienia o zmianach cen w wybranej lokalizacji.

- + + {createAlertFeedback && {createAlertFeedback}}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 5b5d799..6fef8b9 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -17099,6 +17099,403 @@ svg { align-content: start; } +.price-alerts-page { + background: linear-gradient(180deg, #f4f7fb 0%, #eef3f9 100%); +} + +.price-alerts-heading { + display: grid; + gap: 10px; +} + +.price-alerts-heading small { + align-items: center; + color: #7a8ba0; + display: inline-flex; + font-size: 11px; + font-weight: 850; + gap: 6px; +} + +.price-alerts-heading small svg { + color: #a2afc1; + font-size: 12px; + transform: rotate(-45deg); +} + +.price-alerts-heading h1 { + color: #162742; + font-size: 38px; + letter-spacing: -0.01em; + margin: 0; +} + +.price-alerts-heading p { + color: #5d7088; + font-size: 13px; + font-weight: 760; + margin: 8px 0 0; + max-width: 780px; +} + +.price-alerts-settings { + align-items: center; + background: #ffffff; + border: 1px solid #dbe5f0; + border-radius: 9px; + color: #2a3e58; + display: inline-flex; + font-size: 12px; + font-weight: 900; + gap: 7px; + height: 38px; + justify-self: end; + padding: 0 13px; +} + +.price-alerts-hero { + background: #ffffff; + border: 1px solid #dfe8f1; + border-radius: 14px; + display: grid; + gap: 14px; + grid-template-columns: minmax(280px, 1fr) minmax(220px, 360px) minmax(200px, 280px); + padding: 16px; +} + +.price-alerts-hero-main h2 { + color: #152841; + font-size: 33px; + line-height: 1.05; + margin: 8px 0 2px; +} + +.price-alerts-hero-main p { + color: #2b4059; + font-size: 17px; + font-weight: 800; + margin: 0; +} + +.price-alerts-hero-main > small { + align-items: center; + color: #5f738c; + display: inline-flex; + font-size: 12px; + font-weight: 700; + gap: 5px; + margin-top: 9px; +} + +.price-alerts-badge { + background: #e9f8ef; + border-radius: 999px; + color: #0f9657; + display: inline-block; + font-size: 10px; + font-weight: 900; + letter-spacing: 0.04em; + padding: 3px 9px; + text-transform: uppercase; +} + +.price-alerts-pricing { + align-items: baseline; + display: flex; + flex-wrap: wrap; + gap: 8px 11px; + margin-top: 14px; +} + +.price-alerts-pricing strong { + color: #129256; + font-size: 39px; + line-height: 1; +} + +.price-alerts-pricing em { + color: #8191a5; + font-size: 19px; + font-style: normal; + font-weight: 700; + text-decoration: line-through; +} + +.price-alerts-pricing span { + color: #f04444; + font-size: 15px; + font-weight: 900; +} + +.price-alerts-meta { + color: #71839b; + display: flex; + flex-wrap: wrap; + font-size: 12px; + font-weight: 750; + gap: 8px 18px; + margin-top: 8px; +} + +.price-alerts-meta b { + color: #2a405a; +} + +.price-alerts-hero-image { + background-position: center; + background-repeat: no-repeat; + background-size: cover; + border-radius: 12px; + min-height: 210px; +} + +.price-alerts-hero-side { + background: #f4fbf7; + border: 1px solid #dfeee6; + border-radius: 12px; + display: grid; + gap: 10px; + padding: 14px; +} + +.price-alerts-hero-side strong { + color: #148c53; + font-size: 14px; +} + +.price-alerts-hero-side ul { + color: #3d556d; + display: grid; + font-size: 12px; + font-weight: 760; + gap: 6px; + list-style: none; + margin: 0; + padding: 0; +} + +.price-alerts-hero-side b { + color: #1f354f; +} + +.price-alerts-hero-side button { + align-items: center; + background: #17a762; + border: 0; + border-radius: 8px; + color: #ffffff; + display: inline-flex; + font-size: 12px; + font-weight: 900; + gap: 7px; + height: 35px; + justify-content: center; +} + +.price-alerts-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.price-alerts-card { + background: #ffffff; + border: 1px solid #dfe8f1; + border-radius: 12px; + display: grid; + gap: 10px; + padding: 14px; +} + +.price-alerts-card header h3 { + color: #182a45; + font-size: 18px; + margin: 0; +} + +.price-alerts-card header p { + color: #6b7d93; + font-size: 11px; + font-weight: 800; + margin: 5px 0 0; +} + +.price-alerts-kpi { + align-items: baseline; + display: flex; + gap: 12px; +} + +.price-alerts-kpi strong { + color: #162a44; + font-size: 30px; + line-height: 1; +} + +.price-alerts-kpi span { + color: #14a35f; + font-size: 18px; + font-weight: 900; +} + +.price-alerts-chart { + height: 132px; + width: 100%; +} + +.price-alerts-chart-line { + fill: none; + stroke: #26b36a; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 3; +} + +.price-alerts-districts { + display: grid; + gap: 9px; +} + +.price-alerts-districts div { + align-items: center; + display: grid; + gap: 8px; + grid-template-columns: 88px minmax(90px, 1fr) auto auto; +} + +.price-alerts-districts strong { + color: #233852; + font-size: 12px; +} + +.price-alerts-district-bar { + background: #edf1f6; + border-radius: 999px; + display: block; + height: 8px; + overflow: hidden; +} + +.price-alerts-district-bar i { + background: linear-gradient(90deg, #25b269 0%, #5ac987 100%); + border-radius: 999px; + display: block; + height: 100%; +} + +.price-alerts-districts em { + color: #2d435e; + font-size: 12px; + font-style: normal; + font-weight: 820; +} + +.price-alerts-districts b { + color: #15a35f; + font-size: 12px; +} + +.price-alerts-link { + background: transparent; + border: 0; + color: #18a661; + font-size: 12px; + font-weight: 900; + justify-self: start; + padding: 0; +} + +.price-alerts-active-list { + align-content: start; +} + +.price-alerts-active-items { + display: grid; + gap: 8px; +} + +.price-alerts-active-items button { + align-items: center; + background: #fbfcfe; + border: 1px solid #e2eaf3; + border-radius: 9px; + color: #21364f; + display: grid; + gap: 8px; + grid-template-columns: minmax(0, 1fr) auto; + padding: 9px; + text-align: left; +} + +.price-alerts-active-items strong { + color: #1d3049; + display: block; + font-size: 12px; +} + +.price-alerts-active-items small { + color: #6d7f96; + display: block; + font-size: 10px; + font-weight: 750; + margin-top: 2px; +} + +.price-alerts-active-items span { + background: #e9f8ef; + border-radius: 999px; + color: #119659; + font-size: 10px; + font-weight: 900; + padding: 2px 8px; +} + +.price-alerts-manage { + background: #f5f8fc; + border: 1px solid #dce6f1; + border-radius: 8px; + color: #334a64; + font-size: 12px; + font-weight: 900; + height: 35px; +} + +@media (max-width: 1300px) { + .price-alerts-grid { + grid-template-columns: 1fr; + } + + .price-alerts-hero { + grid-template-columns: 1fr; + } + + .price-alerts-heading h1 { + font-size: 31px; + } + + .price-alerts-settings { + justify-self: start; + } +} + +@media (max-width: 740px) { + .price-alerts-pricing strong { + font-size: 30px; + } + + .price-alerts-pricing em { + font-size: 15px; + } + + .price-alerts-pricing span { + font-size: 12px; + } + + .price-alerts-districts div { + grid-template-columns: 1fr; + } +} + .settings-head h1 { color: #1a2c46; font-size: 43px; From 0efc31b000c0e47951e0661f6de2e6844737551d Mon Sep 17 00:00:00 2001 From: pascal Date: Sun, 12 Jul 2026 18:06:15 +0200 Subject: [PATCH 2/3] Dodaj widok Pomoc i kontakt w panelu uzytkownika --- frontend/src/App.tsx | 191 ++++++++++++++++++- frontend/src/styles.css | 406 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 594 insertions(+), 3 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d6efb40..242ad4b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,7 +9,7 @@ const cityImage = new URL('./assets/city-panorama.png', import.meta.url).href; const loginRoomImage = new URL('./assets/login-room.png', import.meta.url).href; const SMS_VERIFICATION_CODE = '123456'; -type View = 'home' | 'buy' | 'rent' | 'sell' | 'valuation' | 'priceHistory' | 'districtRanking' | 'negotiation' | 'comparison' | 'add' | 'services' | 'guides' | 'map' | 'login' | 'account' | 'accountPriceAlerts' | 'accountMeetings' | 'accountListings' | 'accountSettings' | 'accountSecurity' | 'accountProfileEdit' | 'accountPublicProfile' | 'notifications' | 'favorites' | 'messages' | 'admin' | 'listingDetail'; +type View = 'home' | 'buy' | 'rent' | 'sell' | 'valuation' | 'priceHistory' | 'districtRanking' | 'negotiation' | 'comparison' | 'add' | 'services' | 'guides' | 'map' | 'login' | 'account' | 'accountPriceAlerts' | 'accountMeetings' | 'accountListings' | 'accountSettings' | 'accountSecurity' | 'accountProfileEdit' | 'accountPublicProfile' | 'accountHelpContact' | 'notifications' | 'favorites' | 'messages' | 'admin' | 'listingDetail'; type MessageThreadListing = { listingId: number; @@ -110,7 +110,7 @@ const LIVE_LISTINGS_REFRESH_MS = 15000; const PROTECTED_VIEWS = new Set([ 'add', 'admin', 'account', 'accountPriceAlerts', 'accountMeetings', 'accountListings', 'accountSettings', - 'accountSecurity', 'accountProfileEdit', 'accountPublicProfile', 'notifications', 'favorites', 'messages', 'comparison', + 'accountSecurity', 'accountProfileEdit', 'accountPublicProfile', 'accountHelpContact', 'notifications', 'favorites', 'messages', 'comparison', ]); type NotificationCategory = 'listing' | 'messages' | 'system'; @@ -2914,6 +2914,7 @@ function App() { {view === 'accountSecurity' && } {view === 'accountProfileEdit' && } {view === 'accountPublicProfile' && } + {view === 'accountHelpContact' && } {view === 'notifications' && } {view === 'messages' && ( onNavigate('accountSettings')}> Dane i ustawienia - +
@@ -4942,6 +4944,189 @@ function AccountDashboardPage({ onNavigate, activeView }: { onNavigate: (view: V ); } +function AccountHelpContactPage({ onNavigate, activeView }: { onNavigate: (view: View) => void; activeView: View }) { + const supportChannels = [ + { + id: 'live-chat', + icon: 'message', + iconTone: 'green', + title: 'Czat na żywo', + badge: 'Najszybsza pomoc', + description: 'Porozmawiaj z naszym konsultantem w czasie rzeczywistym.', + action: 'Dostępny 8:00 - 20:00', + }, + { + id: 'email', + icon: 'mail', + iconTone: 'blue', + title: 'E-mail', + description: 'Napisz do nas, odpowiemy najszybciej jak to możliwe.', + action: 'bok@mieszko.pl', + }, + { + id: 'phone', + icon: 'phone', + iconTone: 'violet', + title: 'Telefon', + description: 'Zadzwoń do nas, chętnie odpowiemy na Twoje pytania.', + action: '+48 22 123 45 67', + }, + { + id: 'form', + icon: 'document', + iconTone: 'orange', + title: 'Formularz kontaktowy', + description: 'Wyślij nam wiadomość, a my skontaktujemy się z Tobą.', + action: 'Przejdź do formularza', + }, + ] as const; + + const faqItems = [ + 'Jak dodać ogłoszenie?', + 'Ile kosztuje dodanie ogłoszenia?', + 'Jak wyróżnić moje ogłoszenie?', + 'Jak edytować lub usunąć ogłoszenie?', + 'Jak działa porównywarka mieszkań?', + 'Jak ustawić alerty cenowe?', + ] as const; + + const moreHelpCards = [ + { + id: 'guides', + icon: 'document', + iconTone: 'green', + title: 'Poradniki', + description: 'Praktyczne artykuły i wskazówki dotyczące kupna, sprzedaży i wynajmu nieruchomości.', + action: 'Zobacz poradniki', + }, + { + id: 'security', + icon: 'shield', + iconTone: 'gold', + title: 'Bezpieczeństwo', + description: 'Dowiedz się jak bezpiecznie korzystać z serwisu i unikać oszustw.', + action: 'Zasady bezpieczeństwa', + }, + { + id: 'rules', + icon: 'list', + iconTone: 'blue', + title: 'Regulamin i zasady', + description: 'Zapoznaj się z regulaminem serwisu oraz zasadami korzystania.', + action: 'Przeczytaj regulamin', + }, + { + id: 'report', + icon: 'warning', + iconTone: 'red', + title: 'Zgłoś problem', + description: 'Zgłoś nieodpowiednie treści lub problemy techniczne.', + action: 'Zgłoś', + }, + ] as const; + + return ( +
+
+ + +
+
+
+
+

Pomoc i kontakt

+

Jesteśmy tutaj, aby Ci pomóc. Skontaktuj się z nami lub znajdź odpowiedzi na najczęściej zadawane pytania.

+
+ +
+ +
+

Jak możemy Ci pomóc?

+
+ {supportChannels.map((channel) => ( +
+
+ +
+

{channel.title}

+ {'badge' in channel && channel.badge ? {channel.badge} : null} +
+
+

{channel.description}

+ +
+ ))} +
+
+ +
+
+
+

Najczęściej zadawane pytania

+ +
+
+ {faqItems.map((question) => ( +
+ + {question} + + +

Znajdziesz odpowiedź w naszym centrum pomocy. W razie potrzeby skorzystaj z formularza kontaktowego obok.

+
+ ))} +
+
+ +
+

Napisz do nas

+
+
+ + +
+ +