Regulamin i polityka prywatnosci zgodne ze stanem serwisu
Deploy Polska Lokalnie / deploy (push) Successful in 22s

Audyt pokazal rozbieznosci w obie strony: dokumenty opisywaly funkcje,
ktorych nie ma (logowanie Google/Facebook), a kod robil rzeczy, ktorych
dokumenty nie ujawnialy.

Kod:
- zgody marketingowe domyslnie WYLACZONE (byly true dla kazdego nowego
  konta) + skrypt fix-marketing-consents.sql czyszczacy stare wpisy,
- usuniete tlumaczenie przez nieoficjalne API Google - przy zmianie
  jezyka wysylalo do USA tresci stron, w tym wiadomosci i nazwiska,
- osadzone spacery (YouTube, Matterport, Kuula) laduja sie dopiero po
  zgodzie; YouTube w trybie bez ciasteczek; mapy jak dotad,
- usuniecie konta kasuje dane osobowe (AccountErasureService), a nie
  tylko wiersz app_users,
- wygasle tokeny i kody SMS kasowane po 7 dniach (ExpiredTokenCleaner),
- zapisujemy moment akceptacji regulaminu przy rejestracji.

Dokumenty: opisuja realne funkcje, wszystkich odbiorcow danych (OVH,
Autopay, SMTP, SMS, Esri, OpenStreetMap, YouTube/Matterport/Kuula, GUS),
transfery poza EOG, konkretne okresy przechowywania, procedure zgloszen
z DSA, brak profilowania i tabele pamieci przegladarki zamiast fikcyjnych
cookies analitycznych. Zakres NIS2 i DORA opisany zgodnie z prawda.

Uklad stron prawnych jak reszta serwisu: kontener 1760 px, karty,
spis tresci z boku.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 21:59:15 +02:00
parent a4457df5f8
commit ca5f3ebfc1
19 changed files with 1187 additions and 603 deletions
+29 -209
View File
@@ -19,7 +19,7 @@ import { BannerSlot } from './Banners';
import { AdminBannersView } from './admin/banners';
import { AdminPromotionView } from './admin/promotion';
import { AdminSettingsView } from './admin/settings';
import { TermsPage, PrivacyPolicyPage, CookieBanner } from './legal';
import { TermsPage, PrivacyPolicyPage, CookieBanner, hasExternalEmbedConsent } from './legal';
import { SiteFooter } from './Footer';
import { ActivationPage, PasswordResetPage, PaymentRedirectPage, PhoneOtpModal } from './AuthPages';
@@ -1995,7 +1995,8 @@ function getVirtualTourEmbed(url: string): VirtualTourEmbedInfo | null {
const id = pathParts[0];
if (id) {
return {
embedUrl: `https://www.youtube.com/embed/${encodeURIComponent(id)}`,
// Tryb bez ciasteczek: YouTube nie zaklada ciasteczek sledzacych przed odtworzeniem.
embedUrl: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(id)}`,
providerLabel: 'YouTube',
};
}
@@ -2007,7 +2008,7 @@ function getVirtualTourEmbed(url: string): VirtualTourEmbedInfo | null {
|| (pathParts[0] === 'shorts' ? pathParts[1] : null);
if (videoId) {
return {
embedUrl: `https://www.youtube.com/embed/${encodeURIComponent(videoId)}`,
embedUrl: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}`,
providerLabel: 'YouTube',
};
}
@@ -2636,11 +2637,6 @@ function App() {
[favoriteListingIdList, listingPriceAlertIdList],
);
const monitoredListingIdsSignature = useMemo(() => monitoredListingIdList.join(','), [monitoredListingIdList]);
const translatedOriginalTextRef = useRef<Map<Text, string>>(new Map());
const translatedOriginalAttrRef = useRef<Map<Element, Record<string, string>>>(new Map());
const translationObserverRef = useRef<MutationObserver | null>(null);
const translationTimerRef = useRef<number | null>(null);
const translationInFlightRef = useRef(false);
const filters = useFilterState();
const notificationsCenter = useNotifications();
const [savedSearchesState, setSavedSearchesState] = useState<SavedSearchRecord[]>([]);
@@ -2805,198 +2801,6 @@ function App() {
document.documentElement.lang = htmlLang;
}, [user?.preferredLanguage]);
useEffect(() => {
const targetLanguage = user?.preferredLanguage ?? 'PL';
const textNodeSources = translatedOriginalTextRef.current;
const attrSources = translatedOriginalAttrRef.current;
const restorePolish = () => {
textNodeSources.forEach((source, node) => {
if (node.isConnected) {
node.textContent = source;
}
});
attrSources.forEach((sourceAttrs, element) => {
if (!element.isConnected) {
return;
}
Object.entries(sourceAttrs).forEach(([attr, value]) => {
element.setAttribute(attr, value);
});
});
};
const shouldTranslateSource = (value: string) => {
const trimmed = value.trim();
if (!trimmed) {
return false;
}
// Skip pure numbers and separators.
if (!/[a-zA-Z\u00C0-\u024F\u0400-\u04FF\u0104\u0105\u0106\u0107\u0118\u0119\u0141\u0142\u0143\u0144\u00D3\u00F3\u015A\u015B\u0179\u017A\u017B\u017C]/.test(trimmed)) {
return false;
}
return true;
};
const collectTargets = () => {
const textTargets: Array<{ node: Text; source: string }> = [];
const attrTargets: Array<{ element: Element; attr: string; source: string }> = [];
const root = document.querySelector('.site-shell') ?? document.body;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
let current = walker.nextNode();
while (current) {
const textNode = current as Text;
const parent = textNode.parentElement;
if (parent && !['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA'].includes(parent.tagName)) {
const source = textNodeSources.get(textNode) ?? textNode.textContent ?? '';
if (!textNodeSources.has(textNode)) {
textNodeSources.set(textNode, source);
}
if (shouldTranslateSource(source)) {
textTargets.push({ node: textNode, source });
}
}
current = walker.nextNode();
}
root.querySelectorAll('[placeholder], [title], [aria-label]').forEach((element) => {
const tracked = attrSources.get(element) ?? {};
['placeholder', 'title', 'aria-label'].forEach((attr) => {
const raw = element.getAttribute(attr);
if (!raw) {
return;
}
const source = tracked[attr] ?? raw;
if (!tracked[attr]) {
tracked[attr] = source;
}
if (shouldTranslateSource(source)) {
attrTargets.push({ element, attr, source });
}
});
if (Object.keys(tracked).length > 0) {
attrSources.set(element, tracked);
}
});
return { textTargets, attrTargets };
};
const translateVisibleContent = async () => {
if (targetLanguage === 'PL') {
restorePolish();
return;
}
if (translationInFlightRef.current) {
return;
}
const { textTargets, attrTargets } = collectTargets();
const sources = Array.from(new Set([
...textTargets.map((item) => item.source),
...attrTargets.map((item) => item.source),
]));
if (sources.length === 0) {
return;
}
translationInFlightRef.current = true;
try {
const mergedTranslations: Record<string, string> = {};
const missingForApi: string[] = [];
sources.forEach((source) => {
const manual = getManualTranslation(targetLanguage, source);
if (manual != null) {
mergedTranslations[source] = manual;
} else if (source.length <= 380) {
missingForApi.push(source);
} else {
mergedTranslations[source] = source;
}
});
if (missingForApi.length > 0) {
const fallbackChunks: string[][] = [];
for (let index = 0; index < missingForApi.length; index += 150) {
fallbackChunks.push(missingForApi.slice(index, index + 150));
}
for (const chunk of fallbackChunks) {
const response = await apiFetch<{ language: PreferredLanguage; translations: Record<string, string> }>('/i18n/translate', {
method: 'POST',
body: JSON.stringify({ language: targetLanguage, texts: chunk }),
});
Object.assign(mergedTranslations, response.translations);
}
}
textTargets.forEach(({ node, source }) => {
if (!node.isConnected) {
return;
}
const translated = mergedTranslations[source];
if (translated && node.textContent !== translated) {
node.textContent = translated;
}
});
attrTargets.forEach(({ element, attr, source }) => {
if (!element.isConnected) {
return;
}
const translated = mergedTranslations[source];
if (translated) {
element.setAttribute(attr, translated);
}
});
} catch {
// If translation fails, leave original text untouched.
} finally {
translationInFlightRef.current = false;
}
};
const queueTranslate = () => {
if (translationTimerRef.current != null) {
window.clearTimeout(translationTimerRef.current);
}
translationTimerRef.current = window.setTimeout(() => {
void translateVisibleContent();
}, 80);
};
translationObserverRef.current?.disconnect();
if (targetLanguage === 'PL') {
restorePolish();
return;
}
queueTranslate();
const observer = new MutationObserver(() => {
queueTranslate();
});
observer.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ['placeholder', 'title', 'aria-label'],
});
translationObserverRef.current = observer;
return () => {
observer.disconnect();
translationObserverRef.current = null;
if (translationTimerRef.current != null) {
window.clearTimeout(translationTimerRef.current);
translationTimerRef.current = null;
}
};
}, [user?.preferredLanguage, location.pathname]);
// Miasto i typ oferty trafiaja do query stringa - wynik na mapie da sie udostepnic linkiem.
const openMap = (name: string, source: 'buy' | 'rent' = 'buy') => {
navigate(mapPath(name, source === 'rent' ? 'RENT' : 'SALE'));
@@ -26777,6 +26581,9 @@ function ListingDetailPage({
}) {
const navigate = useNavigate();
const { user, loading: authLoading } = useAuth();
// Osadzony spacer z obcego serwisu ladujemy dopiero za zgoda ogladajacego (baner cookie
// "akceptuje wszystkie" ja obejmuje) - ramka przekazuje dostawcy adres IP.
const [tourEmbedAllowed, setTourEmbedAllowed] = useState(() => hasExternalEmbedConsent());
const { formatPrice, formatPricePerArea, formatArea, areaSuffix } = useDisplay();
const [listing, setListing] = useState<ApiListingDetail | null>(null);
// Historia ogladania jest per konto, wiec zapisujemy ja dopiero, gdy wiadomo, kto oglada -
@@ -27390,15 +27197,28 @@ function ListingDetailPage({
<div className="listing-detail-card">
<h2>Wirtualny spacer 3D</h2>
{virtualTourEmbed ? (
<div className="listing-detail-tour-embed">
<iframe
src={virtualTourEmbed.embedUrl}
title={`Wirtualny spacer (${virtualTourEmbed.providerLabel})`}
loading="lazy"
allow="xr-spatial-tracking; vr; gyroscope; accelerometer; fullscreen"
allowFullScreen
/>
</div>
/* Ramka z obcego serwisu przekazuje mu adres IP ogladajacego, wiec bez zgody
pokazujemy zastepnik i mowimy wprost, kto te dane dostanie. */
tourEmbedAllowed ? (
<div className="listing-detail-tour-embed">
<iframe
src={virtualTourEmbed.embedUrl}
title={`Wirtualny spacer (${virtualTourEmbed.providerLabel})`}
loading="lazy"
allow="xr-spatial-tracking; vr; gyroscope; accelerometer; fullscreen"
allowFullScreen
/>
</div>
) : (
<div className="listing-detail-tour-consent">
<span><Icon name="tour" /></span>
<p>
Spacer jest osadzony z serwisu <strong>{virtualTourEmbed.providerLabel}</strong>.
Po włączeniu Twój adres IP trafi do tego dostawcy.
</p>
<button type="button" onClick={() => setTourEmbedAllowed(true)}>Pokaż wirtualny spacer</button>
</div>
)
) : (
<p className="listing-detail-description">Ten typ linku nie obsługuje osadzenia w karcie. Otwórz spacer w nowej karcie.</p>
)}