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
@@ -38,11 +38,13 @@ import pl.polskalokalnie.lead.AdvertisingEnquiryResponse;
import pl.polskalokalnie.lead.AdvertisingEnquiryService; import pl.polskalokalnie.lead.AdvertisingEnquiryService;
import pl.polskalokalnie.support.SupportMessageResponse; import pl.polskalokalnie.support.SupportMessageResponse;
import pl.polskalokalnie.support.SupportMessageService; import pl.polskalokalnie.support.SupportMessageService;
import pl.polskalokalnie.user.AccountErasureService;
import pl.polskalokalnie.user.AppUser; import pl.polskalokalnie.user.AppUser;
import pl.polskalokalnie.user.BlockedEmail; import pl.polskalokalnie.user.BlockedEmail;
import pl.polskalokalnie.user.BlockedEmailRepository; import pl.polskalokalnie.user.BlockedEmailRepository;
import pl.polskalokalnie.user.Role; import pl.polskalokalnie.user.Role;
import pl.polskalokalnie.user.UserRepository; import pl.polskalokalnie.user.UserRepository;
import pl.polskalokalnie.user.UserSettingsService;
@RestController @RestController
@RequestMapping("/api/admin") @RequestMapping("/api/admin")
@@ -64,6 +66,8 @@ public class AdminController {
private final AdminStatsService adminStatsService; private final AdminStatsService adminStatsService;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final NotificationService notificationService; private final NotificationService notificationService;
private final AccountErasureService accountErasureService;
private final UserSettingsService userSettingsService;
public AdminController(UserRepository userRepository, BlockedEmailRepository blockedEmailRepository, public AdminController(UserRepository userRepository, BlockedEmailRepository blockedEmailRepository,
ListingService listingService, ListingReportService listingReportService, ListingService listingService, ListingReportService listingReportService,
@@ -74,7 +78,9 @@ public class AdminController {
TextModerationService textModerationService, TextModerationService textModerationService,
AdminStatsService adminStatsService, AdminStatsService adminStatsService,
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
NotificationService notificationService) { NotificationService notificationService,
AccountErasureService accountErasureService,
UserSettingsService userSettingsService) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.blockedEmailRepository = blockedEmailRepository; this.blockedEmailRepository = blockedEmailRepository;
this.listingService = listingService; this.listingService = listingService;
@@ -87,6 +93,8 @@ public class AdminController {
this.adminStatsService = adminStatsService; this.adminStatsService = adminStatsService;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.notificationService = notificationService; this.notificationService = notificationService;
this.accountErasureService = accountErasureService;
this.userSettingsService = userSettingsService;
} }
// --- Uzytkownicy --- // --- Uzytkownicy ---
@@ -22,6 +22,7 @@ import pl.polskalokalnie.auth.dto.RegisterRequest;
import pl.polskalokalnie.auth.dto.RegisterResponse; import pl.polskalokalnie.auth.dto.RegisterResponse;
import pl.polskalokalnie.auth.dto.UpdateProfileRequest; import pl.polskalokalnie.auth.dto.UpdateProfileRequest;
import pl.polskalokalnie.auth.dto.UserResponse; import pl.polskalokalnie.auth.dto.UserResponse;
import pl.polskalokalnie.user.AccountErasureService;
import pl.polskalokalnie.user.AccountType; import pl.polskalokalnie.user.AccountType;
import pl.polskalokalnie.user.AppUser; import pl.polskalokalnie.user.AppUser;
import pl.polskalokalnie.user.AuthProvider; import pl.polskalokalnie.user.AuthProvider;
@@ -53,6 +54,7 @@ public class AuthService {
private final PhoneVerificationService phoneVerificationService; private final PhoneVerificationService phoneVerificationService;
private final NickService nickService; private final NickService nickService;
private final UserSettingsService userSettingsService; private final UserSettingsService userSettingsService;
private final AccountErasureService accountErasureService;
private final ListingService listingService; private final ListingService listingService;
public AuthService( public AuthService(
@@ -67,6 +69,7 @@ public class AuthService {
PhoneVerificationService phoneVerificationService, PhoneVerificationService phoneVerificationService,
NickService nickService, NickService nickService,
UserSettingsService userSettingsService, UserSettingsService userSettingsService,
AccountErasureService accountErasureService,
ListingService listingService ListingService listingService
) { ) {
this.userRepository = userRepository; this.userRepository = userRepository;
@@ -80,6 +83,7 @@ public class AuthService {
this.phoneVerificationService = phoneVerificationService; this.phoneVerificationService = phoneVerificationService;
this.nickService = nickService; this.nickService = nickService;
this.userSettingsService = userSettingsService; this.userSettingsService = userSettingsService;
this.accountErasureService = accountErasureService;
this.listingService = listingService; this.listingService = listingService;
} }
@@ -109,6 +113,10 @@ public class AuthService {
user.setPhone(phone); user.setPhone(phone);
user.setNip(request.nip() != null && !request.nip().isBlank() ? request.nip().trim() : null); user.setNip(request.nip() != null && !request.nip().isBlank() ? request.nip().trim() : null);
user.setAddress(request.address() != null && !request.address().isBlank() ? request.address().trim() : null); user.setAddress(request.address() != null && !request.address().isBlank() ? request.address().trim() : null);
// Rejestracja jest mozliwa tylko po zaznaczeniu zgody w formularzu - zapisujemy jej moment.
if (Boolean.TRUE.equals(request.acceptTerms())) {
user.setTermsAcceptedAt(Instant.now());
}
// Konto wymaga aktywacji przez uzytkownika (link e-mail). Do tego czasu logowanie jest zablokowane. // Konto wymaga aktywacji przez uzytkownika (link e-mail). Do tego czasu logowanie jest zablokowane.
user.setVerified(false); user.setVerified(false);
user.setPhoneVerified(false); user.setPhoneVerified(false);
@@ -442,6 +450,7 @@ public class AuthService {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Hasło jest nieprawidłowe"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Hasło jest nieprawidłowe");
} }
accountErasureService.eraseFor(user.getId(), user.getEmail());
userSettingsService.deleteForUser(user.getId()); userSettingsService.deleteForUser(user.getId());
listingService.deleteAllOfOwner(user.getEmail()); listingService.deleteAllOfOwner(user.getEmail());
userRepository.delete(user); userRepository.delete(user);
@@ -0,0 +1,49 @@
package pl.polskalokalnie.auth;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* Sprzatanie jednorazowych tokenow i kodow SMS. Wczesniej zostawaly w bazie bezterminowo,
* mimo ze po wygasnieciu sa bezuzyteczne - a wiersz phone_otps zawiera numer telefonu
* i kod w jawnej postaci. Zasada ograniczenia przechowywania (art. 5 ust. 1 lit. e RODO)
* wymaga, zeby znikaly, gdy przestaja byc potrzebne.
*
* <p>Zostawiamy krotki bufor od wygasniecia: uzytkownik, ktory kliknie stary link, dostaje
* czytelny komunikat zamiast bledu "nie znaleziono".</p>
*/
@Component
public class ExpiredTokenCleaner {
private static final Logger log = LoggerFactory.getLogger(ExpiredTokenCleaner.class);
/** Bufor po wygasnieciu - tyle jeszcze trzymamy zuzyty albo przeterminowany wpis. */
private static final int GRACE_DAYS = 7;
private final JdbcTemplate jdbcTemplate;
public ExpiredTokenCleaner(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Scheduled(cron = "0 15 3 * * *")
@Transactional
public void cleanDaily() {
Instant cutoff = Instant.now().minus(GRACE_DAYS, ChronoUnit.DAYS);
try {
int tokens = jdbcTemplate.update("DELETE FROM auth_tokens WHERE expires_at < ?", cutoff);
int otps = jdbcTemplate.update("DELETE FROM phone_otps WHERE expires_at < ?", cutoff);
if (tokens > 0 || otps > 0) {
log.info("Sprzatanie: usunieto {} wygaslych tokenow i {} kodow SMS", tokens, otps);
}
} catch (RuntimeException ex) {
log.warn("Sprzatanie wygaslych tokenow nie powiodlo sie: {}", ex.getMessage());
}
}
}
@@ -39,7 +39,6 @@ public class SecurityConfig {
.requestMatchers("/api/auth/activate", "/api/auth/resend-activation", .requestMatchers("/api/auth/activate", "/api/auth/resend-activation",
"/api/auth/forgot-password", "/api/auth/reset-password", "/api/auth/forgot-password", "/api/auth/reset-password",
"/api/auth/verify-phone", "/api/auth/resend-phone-otp").permitAll() "/api/auth/verify-phone", "/api/auth/resend-phone-otp").permitAll()
.requestMatchers("/api/i18n/translate").permitAll()
.requestMatchers(HttpMethod.GET, "/api/fx/rates").permitAll() .requestMatchers(HttpMethod.GET, "/api/fx/rates").permitAll()
.requestMatchers(HttpMethod.POST, "/api/advertising-enquiries").permitAll() .requestMatchers(HttpMethod.POST, "/api/advertising-enquiries").permitAll()
.requestMatchers(HttpMethod.GET, "/api/gus/company").permitAll() .requestMatchers(HttpMethod.GET, "/api/gus/company").permitAll()
@@ -12,6 +12,8 @@ public record RegisterRequest(
AccountType accountType, AccountType accountType,
String phone, String phone,
String nip, String nip,
String address String address,
/** Potwierdzenie akceptacji Regulaminu i Polityki prywatnosci - zapisujemy moment zgody. */
Boolean acceptTerms
) { ) {
} }
@@ -1,132 +0,0 @@
package pl.polskalokalnie.i18n;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import pl.polskalokalnie.user.PreferredLanguage;
@Service
public class RuntimeTranslationService {
private static final int MAX_TEXTS_PER_REQUEST = 180;
private static final int MAX_TEXT_LENGTH = 400;
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(6))
.build();
private final ObjectMapper objectMapper = new ObjectMapper();
private final Map<String, String> cache = new ConcurrentHashMap<>();
public Map<String, String> translateBatch(PreferredLanguage language, List<String> texts) {
if (texts == null || texts.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Brak tekstów do tłumaczenia");
}
if (texts.size() > MAX_TEXTS_PER_REQUEST) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Zbyt wiele tekstów do tłumaczenia w jednym żądaniu");
}
if (language == PreferredLanguage.PL) {
Map<String, String> passthrough = new LinkedHashMap<>();
texts.forEach((text) -> passthrough.put(text, text));
return passthrough;
}
String targetCode = toGoogleLang(language);
Map<String, String> results = new LinkedHashMap<>();
List<String> toTranslate = new ArrayList<>();
for (String text : texts) {
String safeText = text != null ? text : "";
if (safeText.length() > MAX_TEXT_LENGTH) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Jeden z tekstów jest zbyt długi do tłumaczenia");
}
String cacheKey = targetCode + "|" + safeText;
String cached = cache.get(cacheKey);
if (cached != null) {
results.put(safeText, cached);
} else {
toTranslate.add(safeText);
}
}
for (String text : toTranslate) {
String translated = translateSingle(text, targetCode);
cache.put(targetCode + "|" + text, translated);
results.put(text, translated);
}
return results;
}
private String translateSingle(String source, String targetCode) {
if (source.isBlank()) {
return source;
}
String encoded = URLEncoder.encode(source, StandardCharsets.UTF_8);
String url = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=pl&tl="
+ targetCode + "&dt=t&q=" + encoded;
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.GET()
.timeout(Duration.ofSeconds(10))
.header("Accept", "application/json")
.header("User-Agent", "PolskaLokalnie/1.0")
.build();
try {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Usługa tłumaczeń jest chwilowo niedostępna");
}
return parseTranslatedText(response.body(), source);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Nie udało się pobrać tłumaczenia", ex);
} catch (IOException ex) {
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Nie udało się pobrać tłumaczenia", ex);
}
}
private String parseTranslatedText(String payload, String fallback) throws IOException {
JsonNode root = objectMapper.readTree(payload);
JsonNode topSegments = root.path(0);
if (!topSegments.isArray() || topSegments.isEmpty()) {
return fallback;
}
StringBuilder translated = new StringBuilder();
for (JsonNode segment : topSegments) {
JsonNode part = segment.path(0);
if (part.isTextual()) {
translated.append(part.asText());
}
}
String value = translated.toString();
return value.isBlank() ? fallback : value;
}
private String toGoogleLang(PreferredLanguage language) {
return switch (language) {
case EN -> "en";
case UK -> "uk";
case DE -> "de";
case PL -> "pl";
};
}
}
@@ -1,12 +0,0 @@
package pl.polskalokalnie.i18n;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import pl.polskalokalnie.user.PreferredLanguage;
public record TranslateRequest(
@NotNull PreferredLanguage language,
@NotEmpty List<String> texts
) {
}
@@ -1,10 +0,0 @@
package pl.polskalokalnie.i18n;
import java.util.Map;
import pl.polskalokalnie.user.PreferredLanguage;
public record TranslateResponse(
PreferredLanguage language,
Map<String, String> translations
) {
}
@@ -1,31 +0,0 @@
package pl.polskalokalnie.i18n;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/i18n")
public class TranslationController {
private final RuntimeTranslationService runtimeTranslationService;
public TranslationController(RuntimeTranslationService runtimeTranslationService) {
this.runtimeTranslationService = runtimeTranslationService;
}
@PostMapping("/translate")
public TranslateResponse translate(@Valid @RequestBody TranslateRequest request) {
List<String> normalizedTexts = request.texts().stream()
.map(text -> text == null ? "" : text)
.distinct()
.collect(Collectors.toList());
Map<String, String> translations = runtimeTranslationService.translateBatch(request.language(), normalizedTexts);
return new TranslateResponse(request.language(), translations);
}
}
@@ -67,13 +67,17 @@ public class Lead {
@Column(name = "tag", length = 60) @Column(name = "tag", length = 60)
private List<String> tags = new ArrayList<>(); private List<String> tags = new ArrayList<>();
// Zgoda marketingowa musi byc wyrazna i uprzednia (art. 4 pkt 11 i art. 7 RODO oraz Prawo
// komunikacji elektronicznej), wiec domyslnie jej NIE MA. Wlacza ja sam uzytkownik
// w ustawieniach konta (MarketingConsentController) albo administrator przy imporcie leada,
// majac podstawe zebrana poza serwisem.
@Column(nullable = false) @Column(nullable = false)
@ColumnDefault("true") @ColumnDefault("false")
private boolean emailConsent = true; private boolean emailConsent = false;
@Column(nullable = false) @Column(nullable = false)
@ColumnDefault("true") @ColumnDefault("false")
private boolean smsConsent = true; private boolean smsConsent = false;
// Trwala rezygnacja - raz ustawiona nie jest cofana przez kampanie ani synchronizacje. // Trwala rezygnacja - raz ustawiona nie jest cofana przez kampanie ani synchronizacje.
@Column(nullable = false) @Column(nullable = false)
@@ -0,0 +1,76 @@
package pl.polskalokalnie.user;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Usuniecie danych osobowych konta (art. 17 RODO). Dotad kasowany byl tylko wiersz app_users,
* ustawienia i ogloszenia - reszta zostawala w bazie jako sieroty: wiadomosci, powiadomienia,
* alerty, spotkania, tokeny, kody SMS z numerem telefonu i lead marketingowy.
*
* <p>Zostaja wylacznie dane, ktorych usuniecie byloby sprzeczne z innym obowiazkiem:
* zamowienia promowania (dokumentacja rozliczeniowa), zgloszenia ogloszen i uzytkownikow
* oraz korespondencja z obsluga (obrona przed roszczeniami i obowiazki moderacyjne).
* Te kategorie maja wlasne okresy przechowywania opisane w Polityce prywatnosci.</p>
*
* <p>Schemat nie ma kluczy obcych z kaskada (ddl-auto=update), wiec kasujemy jawnie,
* tabela po tabeli - dzieki temu widac wprost, co znika.</p>
*/
@Service
public class AccountErasureService {
private static final Logger log = LoggerFactory.getLogger(AccountErasureService.class);
private final JdbcTemplate jdbcTemplate;
public AccountErasureService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public void eraseFor(Long userId, String email) {
if (userId == null || email == null || email.isBlank()) {
return;
}
int removed = 0;
// Dane powiazane z identyfikatorem konta.
removed += update("DELETE FROM messages WHERE sender_id = ? OR recipient_id = ?", userId, userId);
removed += update("DELETE FROM conversations WHERE buyer_id = ? OR seller_id = ?", userId, userId);
removed += update("DELETE FROM user_blocks WHERE blocker_id = ? OR blocked_id = ?", userId, userId);
removed += update("DELETE FROM meetings WHERE proposer_id = ? OR counterpart_id = ?", userId, userId);
// Dane powiazane z adresem e-mail.
removed += update("DELETE FROM listing_favorites WHERE lower(user_email) = lower(?)", email);
removed += update("DELETE FROM saved_searches WHERE lower(user_email) = lower(?)", email);
removed += update("DELETE FROM price_alert_events WHERE lower(user_email) = lower(?)", email);
removed += update("DELETE FROM price_alerts WHERE lower(user_email) = lower(?)", email);
removed += update("DELETE FROM notifications WHERE lower(user_email) = lower(?)", email);
removed += update("DELETE FROM auth_tokens WHERE lower(user_email) = lower(?)", email);
removed += update("DELETE FROM phone_otps WHERE lower(user_email) = lower(?)", email);
// Baza marketingowa: lead, jego tagi i pozycje w kolejkach kampanii.
removed += update("DELETE FROM campaign_recipients WHERE lead_id IN (SELECT id FROM leads WHERE lower(email) = lower(?))", email);
removed += update("DELETE FROM lead_tags WHERE lead_id IN (SELECT id FROM leads WHERE lower(email) = lower(?))", email);
removed += update("DELETE FROM leads WHERE lower(email) = lower(?)", email);
// Poradniki uzytkownika znikaja razem z kontem - tak samo jak jego ogloszenia.
removed += update("DELETE FROM article_attachments WHERE article_id IN (SELECT id FROM articles WHERE lower(author_email) = lower(?))", email);
removed += update("DELETE FROM articles WHERE lower(author_email) = lower(?)", email);
log.info("Usunieto {} rekordow powiazanych z kontem id={}", removed, userId);
}
private int update(String sql, Object... args) {
try {
return jdbcTemplate.update(sql, args);
} catch (RuntimeException ex) {
// Brak tabeli w starszej bazie nie moze zablokowac usuniecia konta.
log.warn("Nie udalo sie wykonac czyszczenia: {} ({})", sql, ex.getMessage());
return 0;
}
}
}
@@ -103,6 +103,13 @@ public class AppUser {
// Na karcie sprzedajacego pokazujemy z tego "ostatnio aktywny/a"; null = jeszcze nie odnotowano. // Na karcie sprzedajacego pokazujemy z tego "ostatnio aktywny/a"; null = jeszcze nie odnotowano.
private Instant lastActiveAt; private Instant lastActiveAt;
/**
* Moment akceptacji Regulaminu i Polityki prywatnosci przy rejestracji. Zapisujemy go,
* zeby mozna bylo wykazac, kiedy uzytkownik zaakceptowal dokumenty (art. 7 ust. 1 RODO).
*/
@Column(name = "terms_accepted_at")
private Instant termsAcceptedAt;
@PrePersist @PrePersist
void onCreate() { void onCreate() {
if (createdAt == null) { if (createdAt == null) {
@@ -274,6 +281,14 @@ public class AppUser {
this.sessionsValidFrom = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.SECONDS); this.sessionsValidFrom = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.SECONDS);
} }
public Instant getTermsAcceptedAt() {
return termsAcceptedAt;
}
public void setTermsAcceptedAt(Instant termsAcceptedAt) {
this.termsAcceptedAt = termsAcceptedAt;
}
public Instant getLastActiveAt() { public Instant getLastActiveAt() {
return lastActiveAt; return lastActiveAt;
} }
+29 -209
View File
@@ -19,7 +19,7 @@ import { BannerSlot } from './Banners';
import { AdminBannersView } from './admin/banners'; import { AdminBannersView } from './admin/banners';
import { AdminPromotionView } from './admin/promotion'; import { AdminPromotionView } from './admin/promotion';
import { AdminSettingsView } from './admin/settings'; import { AdminSettingsView } from './admin/settings';
import { TermsPage, PrivacyPolicyPage, CookieBanner } from './legal'; import { TermsPage, PrivacyPolicyPage, CookieBanner, hasExternalEmbedConsent } from './legal';
import { SiteFooter } from './Footer'; import { SiteFooter } from './Footer';
import { ActivationPage, PasswordResetPage, PaymentRedirectPage, PhoneOtpModal } from './AuthPages'; import { ActivationPage, PasswordResetPage, PaymentRedirectPage, PhoneOtpModal } from './AuthPages';
@@ -1995,7 +1995,8 @@ function getVirtualTourEmbed(url: string): VirtualTourEmbedInfo | null {
const id = pathParts[0]; const id = pathParts[0];
if (id) { if (id) {
return { 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', providerLabel: 'YouTube',
}; };
} }
@@ -2007,7 +2008,7 @@ function getVirtualTourEmbed(url: string): VirtualTourEmbedInfo | null {
|| (pathParts[0] === 'shorts' ? pathParts[1] : null); || (pathParts[0] === 'shorts' ? pathParts[1] : null);
if (videoId) { if (videoId) {
return { return {
embedUrl: `https://www.youtube.com/embed/${encodeURIComponent(videoId)}`, embedUrl: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(videoId)}`,
providerLabel: 'YouTube', providerLabel: 'YouTube',
}; };
} }
@@ -2636,11 +2637,6 @@ function App() {
[favoriteListingIdList, listingPriceAlertIdList], [favoriteListingIdList, listingPriceAlertIdList],
); );
const monitoredListingIdsSignature = useMemo(() => monitoredListingIdList.join(','), [monitoredListingIdList]); 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 filters = useFilterState();
const notificationsCenter = useNotifications(); const notificationsCenter = useNotifications();
const [savedSearchesState, setSavedSearchesState] = useState<SavedSearchRecord[]>([]); const [savedSearchesState, setSavedSearchesState] = useState<SavedSearchRecord[]>([]);
@@ -2805,198 +2801,6 @@ function App() {
document.documentElement.lang = htmlLang; document.documentElement.lang = htmlLang;
}, [user?.preferredLanguage]); }, [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. // Miasto i typ oferty trafiaja do query stringa - wynik na mapie da sie udostepnic linkiem.
const openMap = (name: string, source: 'buy' | 'rent' = 'buy') => { const openMap = (name: string, source: 'buy' | 'rent' = 'buy') => {
navigate(mapPath(name, source === 'rent' ? 'RENT' : 'SALE')); navigate(mapPath(name, source === 'rent' ? 'RENT' : 'SALE'));
@@ -26777,6 +26581,9 @@ function ListingDetailPage({
}) { }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { user, loading: authLoading } = useAuth(); 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 { formatPrice, formatPricePerArea, formatArea, areaSuffix } = useDisplay();
const [listing, setListing] = useState<ApiListingDetail | null>(null); const [listing, setListing] = useState<ApiListingDetail | null>(null);
// Historia ogladania jest per konto, wiec zapisujemy ja dopiero, gdy wiadomo, kto oglada - // Historia ogladania jest per konto, wiec zapisujemy ja dopiero, gdy wiadomo, kto oglada -
@@ -27390,15 +27197,28 @@ function ListingDetailPage({
<div className="listing-detail-card"> <div className="listing-detail-card">
<h2>Wirtualny spacer 3D</h2> <h2>Wirtualny spacer 3D</h2>
{virtualTourEmbed ? ( {virtualTourEmbed ? (
<div className="listing-detail-tour-embed"> /* Ramka z obcego serwisu przekazuje mu adres IP ogladajacego, wiec bez zgody
<iframe pokazujemy zastepnik i mowimy wprost, kto te dane dostanie. */
src={virtualTourEmbed.embedUrl} tourEmbedAllowed ? (
title={`Wirtualny spacer (${virtualTourEmbed.providerLabel})`} <div className="listing-detail-tour-embed">
loading="lazy" <iframe
allow="xr-spatial-tracking; vr; gyroscope; accelerometer; fullscreen" src={virtualTourEmbed.embedUrl}
allowFullScreen title={`Wirtualny spacer (${virtualTourEmbed.providerLabel})`}
/> loading="lazy"
</div> 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> <p className="listing-detail-description">Ten typ linku nie obsługuje osadzenia w karcie. Otwórz spacer w nowej karcie.</p>
)} )}
+3 -1
View File
@@ -262,7 +262,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
async (email: string, password: string, fullName: string, details?: RegisterDetails) => async (email: string, password: string, fullName: string, details?: RegisterDetails) =>
apiFetch<RegisterPending>('/auth/register', { apiFetch<RegisterPending>('/auth/register', {
method: 'POST', method: 'POST',
body: JSON.stringify({ email, password, fullName, ...details }), // acceptTerms zapisuje na serwerze moment akceptacji Regulaminu i Polityki prywatnosci
// (rozliczalnosc z art. 7 ust. 1 RODO) - checkbox w formularzu jest warunkiem wyslania.
body: JSON.stringify({ email, password, fullName, acceptTerms: true, ...details }),
}), }),
[], [],
); );
+635 -174
View File
@@ -1,6 +1,8 @@
// Strony prawne serwisu: Regulamin, Polityka prywatności oraz baner zgody na pliki cookie. // Strony prawne serwisu: Regulamin, Polityka prywatnosci oraz baner zgody.
// Treść dostosowana do usług portalu Polska Lokalnie (ogłoszenia nieruchomości bez prowizji, //
// płatne promowanie ofert przez AutoPay). Dostawca: SoftSPM, hosting: OVHcloud. // ZASADA: dokumenty opisuja wylacznie to, co serwis naprawde robi. Kazda wymieniona tu funkcja,
// kazdy odbiorca danych i kazdy okres przechowywania ma pokrycie w kodzie. Gdy zmienia sie
// funkcjonalnosc albo lista uslug zewnetrznych, te pliki trzeba poprawic razem z nia.
import { useEffect, useState, type ReactNode } from 'react'; import { useEffect, useState, type ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ROUTES } from './routes'; import { ROUTES } from './routes';
@@ -12,202 +14,660 @@ const PROVIDER = {
nip: '7691956407', nip: '7691956407',
regon: '100629083', regon: '100629083',
email: 'info@polskalokalnie.pl', email: 'info@polskalokalnie.pl',
phone: '+48 453 193 605',
hosting: 'OVHcloud (OVH Sp. z o.o., ul. Swobodna 1, 50-088 Wrocław)', hosting: 'OVHcloud (OVH Sp. z o.o., ul. Swobodna 1, 50-088 Wrocław)',
dataCenter: 'centrum danych w Warszawie',
}; };
const SERVICE_NAME = 'Polska Lokalnie'; const SERVICE_NAME = 'Polska Lokalnie';
const LAST_UPDATED = '9 sierpnia 2026 r.'; const LAST_UPDATED = '23 września 2026 r.';
function LegalPage({ title, subtitle, children }: { title: string; subtitle: string; children: ReactNode }) { type LegalSection = {
id: string;
title: string;
body: ReactNode;
};
/**
* Wspolny uklad stron prawnych: ta sama szerokosc i ten sam jezyk wizualny co reszta serwisu
* (/kupno, /jak-sprzedawac), plus spis tresci przyklejony z boku - dokumenty sa dlugie,
* a uzytkownik zwykle szuka jednej konkretnej sekcji.
*/
function LegalDocument({ title, subtitle, intro, sections }: {
title: string;
subtitle: string;
intro: ReactNode;
sections: LegalSection[];
}) {
return ( return (
<section className="legal-page" aria-label={title}> <section className="legal-page" aria-label={title}>
<div className="legal-shell"> <div className="legal-container">
<header className="legal-head"> {/* Bez okruszkow - w projekcie kazdy widok zaczyna sie od naglowka H1 (patrz styles.css). */}
<p className="legal-brand">Polska Lokalnie <span>|</span> SoftSPM</p> <header className="legal-title">
<h1>{title}</h1> <h1>{title}</h1>
<p>{subtitle}</p> <p>{subtitle}</p>
<p className="legal-updated">Data ostatniej aktualizacji: {LAST_UPDATED}</p> <div className="legal-meta">
<span><strong>{SERVICE_NAME}</strong> · {PROVIDER.name}</span>
<span>Obowiązuje od {LAST_UPDATED}</span>
</div>
</header> </header>
<div className="legal-body">{children}</div>
<div className="legal-layout">
<aside className="legal-toc" aria-label="Spis treści">
<h2>Spis treści</h2>
<ol>
{sections.map((section) => (
<li key={section.id}><a href={`#${section.id}`}>{section.title}</a></li>
))}
</ol>
</aside>
<div className="legal-main">
<div className="legal-card legal-intro">{intro}</div>
{sections.map((section) => (
<article className="legal-card" id={section.id} key={section.id}>
<h2>{section.title}</h2>
{section.body}
</article>
))}
</div>
</div>
</div> </div>
</section> </section>
); );
} }
export function TermsPage() { function ProviderDetails() {
return ( return (
<LegalPage <ul className="legal-facts">
title="Regulamin platformy Polska Lokalnie" <li><span>Usługodawca</span><strong>{PROVIDER.legal}</strong></li>
subtitle="Zasady korzystania z platformy ogłoszeniowej oraz świadczenia usług drogą elektroniczną." <li><span>Adres</span><strong>{PROVIDER.address}</strong></li>
> <li><span>NIP</span><strong>{PROVIDER.nip}</strong></li>
<h2>§ 1. Postanowienia ogólne</h2> <li><span>REGON</span><strong>{PROVIDER.regon}</strong></li>
<ol> <li><span>E-mail</span><strong>{PROVIDER.email}</strong></li>
<li>Niniejszy Regulamin określa zasady korzystania z platformy internetowej <strong>{SERVICE_NAME}</strong>, dostępnej pod adresem polskalokalnie.pl (dalej „Platforma").</li> <li><span>Telefon</span><strong>{PROVIDER.phone}</strong></li>
<li>Właścicielem i dostawcą Platformy (Usługodawcą) jest <strong>{PROVIDER.legal}</strong>, {PROVIDER.address}, NIP: {PROVIDER.nip}, REGON: {PROVIDER.regon}, adres e-mail: {PROVIDER.email}.</li> <li><span>Hosting</span><strong>{PROVIDER.hosting}, {PROVIDER.dataCenter}</strong></li>
<li>Platforma jest utrzymywana na serwerach dostawcy hostingu {PROVIDER.hosting}, które są fizycznie zlokalizowane na terytorium Rzeczypospolitej Polskiej (w Unii Europejskiej).</li> </ul>
<li>Regulamin jest udostępniany nieodpłatnie w sposób umożliwiający jego pozyskanie, odtworzenie i utrwalenie.</li> );
<li>Korzystanie z Platformy oznacza akceptację postanowień Regulaminu oraz Polityki prywatności.</li> }
</ol>
<h2>§ 2. Definicje</h2> export function TermsPage() {
<ul> const sections: LegalSection[] = [
<li><strong>Usługodawca</strong> - {PROVIDER.name}, podmiot prowadzący Platformę.</li> {
<li><strong>Użytkownik</strong> - osoba fizyczna, osoba prawna lub jednostka organizacyjna korzystająca z Platformy.</li> id: 'postanowienia-ogolne',
<li><strong>Konsument</strong> - Użytkownik będący osobą fizyczną dokonujący czynności niezwiązanej bezpośrednio z jego działalnością gospodarczą lub zawodową.</li> title: '§ 1. Postanowienia ogólne',
<li><strong>Konto</strong> - zbiór zasobów i uprawnień przypisanych Użytkownikowi po rejestracji.</li> body: (
<li><strong>Ogłoszenie</strong> - zamieszczana przez Użytkownika oferta sprzedaży lub najmu nieruchomości.</li> <>
<li><strong>Usługa promowania (Wyróżnienie)</strong> - odpłatna usługa zwiększająca widoczność Ogłoszenia.</li> <ol>
<li><strong>Operator płatności</strong> - dostawca usług szybkich płatności obsługujący płatności w Platformie (AutoPay - Autopay S.A.).</li> <li>Regulamin określa zasady korzystania z platformy <strong>{SERVICE_NAME}</strong>, dostępnej pod adresem polskalokalnie.pl (dalej „Platforma").</li>
</ul> <li>Usługodawcą jest:</li>
</ol>
<ProviderDetails />
<ol start={3}>
<li>Platforma działa na serwerach {PROVIDER.hosting}, w {PROVIDER.dataCenter}, na terytorium Rzeczypospolitej Polskiej.</li>
<li>Usługodawca świadczy usługę hostingu treści: udostępnia narzędzia, a treść ogłoszeń i wiadomości pochodzi od Użytkowników.</li>
<li>Regulamin jest udostępniany nieodpłatnie w sposób umożliwiający jego pozyskanie, odtworzenie i utrwalenie.</li>
<li>Korzystanie z Platformy oznacza akceptację Regulaminu oraz <Link to={ROUTES.privacy}>Polityki prywatności</Link>.</li>
</ol>
</>
),
},
{
id: 'definicje',
title: '§ 2. Definicje',
body: (
<ul>
<li><strong>Użytkownik</strong> - osoba fizyczna, prawna lub jednostka organizacyjna korzystająca z Platformy.</li>
<li><strong>Konsument</strong> - Użytkownik będący osobą fizyczną, dokonujący czynności niezwiązanej bezpośrednio z działalnością gospodarczą lub zawodową.</li>
<li><strong>Konto</strong> - zbiór zasobów i uprawnień przypisanych Użytkownikowi po rejestracji i aktywacji.</li>
<li><strong>Ogłoszenie</strong> - zamieszczana przez Użytkownika oferta sprzedaży lub najmu nieruchomości.</li>
<li><strong>Wyróżnienie</strong> - odpłatna usługa zwiększająca widoczność Ogłoszenia na listach wyników.</li>
<li><strong>Operator płatności</strong> - Autopay S.A. z siedzibą w Sopocie, obsługujący płatności za usługi odpłatne.</li>
</ul>
),
},
{
id: 'zakres-uslug',
title: '§ 3. Zakres usług',
body: (
<>
<p>W ramach Platformy Użytkownik może korzystać z następujących funkcji:</p>
<ol>
<li><strong>Bezpłatne ogłoszenia</strong> - dodawanie, edycja i wstrzymywanie ofert sprzedaży oraz najmu, bez prowizji od transakcji.</li>
<li><strong>Wyszukiwanie i mapa</strong> - przeglądanie ofert z filtrami, przeglądanie ich na mapie oraz zawężanie wyników do widocznego fragmentu mapy.</li>
<li><strong>Konto i narzędzia osobiste</strong> - ulubione ogłoszenia, zapisane wyszukiwania, porównywarka ofert oraz panel z własnymi ogłoszeniami.</li>
<li><strong>Alerty cenowe</strong> - obserwowanie ceny wybranego Ogłoszenia lub średniej ceny za m² w mieście. Zmiany Platforma sprawdza raz na dobę, a informacja o nich trafia <strong>wyłącznie na listę powiadomień w Platformie</strong>. Z alertów nie są wysyłane wiadomości e-mail ani SMS.</li>
<li><strong>Wiadomości</strong> - kontakt między Użytkownikami w sprawie konkretnego Ogłoszenia, z możliwością zablokowania rozmówcy oraz kontakt z obsługą serwisu.</li>
<li><strong>Umawianie oglądania</strong> - proponowanie terminu w rozmowie o Ogłoszeniu; druga strona potwierdza go lub odrzuca, a każda ze stron może termin odwołać. Terminy są widoczne w kalendarzu obu stron.</li>
<li><strong>Narzędzia informacyjne</strong> - historia cen (na podstawie danych Narodowego Banku Polskiego, Głównego Urzędu Statystycznego oraz ogłoszeń w Platformie), orientacyjna wycena nieruchomości, analiza „ile można utargować", ranking dzielnic i kalkulator zdolności kredytowej.</li>
<li><strong>Poradniki</strong> - czytanie artykułów oraz publikowanie własnych, po akceptacji przez obsługę serwisu.</li>
<li><strong>Wyróżnianie Ogłoszeń</strong> - usługa odpłatna, opisana w § 9.</li>
</ol>
<p className="legal-note">
Narzędzia informacyjne (wycena, historia cen, analiza negocjacji, ranking, kalkulator) mają charakter
orientacyjny. Nie stanowią oferty, porady inwestycyjnej ani operatu szacunkowego w rozumieniu przepisów
o gospodarce nieruchomościami.
</p>
</>
),
},
{
id: 'wymagania-techniczne',
title: '§ 4. Wymagania techniczne',
body: (
<ol>
<li>Do korzystania z Platformy potrzebne są: urządzenie z dostępem do Internetu, aktualna przeglądarka z obsługą JavaScript oraz czynne konto poczty elektronicznej.</li>
<li>Platforma zapisuje dane w pamięci przeglądarki (localStorage). Jej zablokowanie uniemożliwia zalogowanie się.</li>
<li>Usługodawca nie odpowiada za problemy wynikające z konfiguracji urządzenia Użytkownika lub działania jego dostawcy Internetu.</li>
</ol>
),
},
{
id: 'konto',
title: '§ 5. Rejestracja i Konto',
body: (
<ol>
<li>Rejestracja wymaga podania adresu e-mail, hasła (co najmniej 8 znaków) oraz imienia i nazwiska albo nazwy firmy, a także akceptacji Regulaminu i Polityki prywatności. Moment akceptacji jest zapisywany.</li>
<li>Konto zakłada osoba pełnoletnia posiadająca pełną zdolność do czynności prawnych, we własnym imieniu albo z umocowaniem do działania za inny podmiot.</li>
<li>Konto wymaga aktywacji linkiem wysłanym na podany adres e-mail. Do tego czasu logowanie jest zablokowane.</li>
<li>Numer telefonu jest dobrowolny. Użytkownik może go potwierdzić kodem SMS; niepotwierdzony numer nie blokuje korzystania z Platformy.</li>
<li>Przy rejestracji konta firmowego dane firmy mogą zostać pobrane z rejestru REGON prowadzonego przez Główny Urząd Statystyczny na podstawie podanego numeru NIP.</li>
<li>Logowanie odbywa się wyłącznie adresem e-mail i hasłem. Platforma nie oferuje logowania za pośrednictwem kont w serwisach zewnętrznych.</li>
<li>Konto jest przypisane do jednego Użytkownika. Udostępnianie danych logowania osobom trzecim jest niedozwolone.</li>
<li>Użytkownik może w każdej chwili usunąć Konto w panelu konta. Skutki usunięcia opisuje <Link to={ROUTES.privacy}>Polityka prywatności</Link>.</li>
</ol>
),
},
{
id: 'ogloszenia',
title: '§ 6. Zasady publikacji ogłoszeń',
body: (
<ol>
<li>Ogłoszenie może dotyczyć wyłącznie rzeczywistej oferty sprzedaży lub najmu nieruchomości, do której Użytkownik ma tytuł prawny albo stosowne umocowanie.</li>
<li>Treść Ogłoszenia musi być zgodna ze stanem faktycznym, kompletna i nie może wprowadzać w błąd, w szczególności co do ceny, powierzchni i stanu nieruchomości.</li>
<li>Do Ogłoszenia można dodać <strong>do 8 zdjęć</strong> oraz odnośnik do wirtualnego spaceru umieszczonego w serwisie zewnętrznym (YouTube, Matterport, Kuula).</li>
<li>Użytkownik może ograniczyć publikowaną lokalizację do przybliżonego obszaru zamiast dokładnego punktu na mapie.</li>
<li>Zabronione jest zamieszczanie treści bezprawnych, w szczególności naruszających prawa osób trzecich, obraźliwych, dyskryminujących, wprowadzających w błąd oraz treści o charakterze spamu.</li>
<li>Ogłoszenia i artykuły podlegają moderacji przed publikacją. Usługodawca może wstrzymać publikację, wezwać do poprawienia treści albo usunąć materiał naruszający Regulamin lub przepisy prawa, informując o tym Użytkownika.</li>
<li>Użytkownik udziela Usługodawcy niewyłącznej, nieodpłatnej licencji na korzystanie z treści i zdjęć Ogłoszenia w zakresie niezbędnym do jego prezentacji w Platformie oraz w podglądach linku udostępnianego w komunikatorach i serwisach społecznościowych.</li>
</ol>
),
},
{
id: 'zglaszanie-tresci',
title: '§ 7. Zgłaszanie nielegalnych treści i moderacja',
body: (
<>
<p>
Usługodawca jest dostawcą usług hostingu w rozumieniu rozporządzenia (UE) 2022/2065 (akt o usługach
cyfrowych, DSA) i stosuje poniższy tryb zgłaszania treści.
</p>
<ol>
<li><strong>Jak zgłosić.</strong> Przy każdym Ogłoszeniu oraz w rozmowie z Użytkownikiem dostępny jest przycisk zgłoszenia. Zgłoszenie można też wysłać na adres {PROVIDER.email}. Ten sam adres jest punktem kontaktowym dla organów i dla Użytkowników.</li>
<li><strong>Co podać.</strong> Wskazanie treści (odnośnik), wyjaśnienie, dlaczego jest bezprawna lub sprzeczna z Regulaminem, oraz dane kontaktowe zgłaszającego. Do zgłoszenia można dołączyć załączniki.</li>
<li><strong>Rozpatrzenie.</strong> Zgłoszenia rozpatrujemy terminowo, rzetelnie, w sposób niearbitralny i obiektywny, z udziałem człowieka - decyzji nie podejmuje automat.</li>
<li><strong>Decyzja.</strong> Możliwe rozstrzygnięcia to: pozostawienie treści, wezwanie do jej poprawienia, ukrycie lub usunięcie treści oraz zablokowanie Konta w razie poważnych lub powtarzających się naruszeń. O decyzji i jej powodach informujemy zgłaszającego oraz autora treści.</li>
<li><strong>Odwołanie.</strong> Od decyzji przysługuje odwołanie w terminie 14 dni, na adres {PROVIDER.email}. Odwołanie rozpatruje osoba, która nie podejmowała decyzji pierwotnej. Użytkownik zachowuje prawo dochodzenia roszczeń przed sądem.</li>
<li><strong>Nadużycia.</strong> Zgłoszenia oczywiście bezzasadne, składane masowo lub w złej wierze, mogą pozostać bez rozpoznania.</li>
</ol>
</>
),
},
{
id: 'kontakt-miedzy-uzytkownikami',
title: '§ 8. Kontakt między Użytkownikami',
body: (
<ol>
<li>Wiadomości służą wyłącznie sprawom związanym z Ogłoszeniem. Zabronione jest wykorzystywanie ich do rozsyłania ofert handlowych niezwiązanych z Ogłoszeniem.</li>
<li>Użytkownik może zablokować rozmówcę; blokada wstrzymuje wymianę wiadomości w obie strony.</li>
<li>Termin oglądania uzgodniony w rozmowie jest ustaleniem między Użytkownikami. Usługodawca nie jest jego stroną i nie odpowiada za jego dotrzymanie.</li>
<li>Usługodawca zaleca ostrożność: nie należy przekazywać w wiadomościach danych logowania, numerów kart ani kodów autoryzacyjnych.</li>
</ol>
),
},
{
id: 'wyroznianie',
title: '§ 9. Wyróżnianie ogłoszeń (usługa odpłatna)',
body: (
<ol>
<li>Użytkownik może odpłatnie wyróżnić własne Ogłoszenie na czas określony zgodnie z planami dostępnymi w Platformie. Wyróżnione Ogłoszenia prezentowane są wyżej na listach wyników.</li>
<li>Dostępny jest także zakup pakietu wyróżnień; zakupione jednostki pozwalają wyróżnić dowolne własne Ogłoszenie.</li>
<li>Ceny podawane w Platformie są cenami brutto w złotych polskich.</li>
<li>Wyróżnienie aktywuje się po otrzymaniu potwierdzenia płatności od Operatora płatności i trwa przez wybrany okres. Zamknięcie przeglądarki po dokonaniu płatności nie wpływa na aktywację.</li>
<li>Dokument potwierdzający zakup Usługodawca wystawia na żądanie Użytkownika zgłoszone na adres {PROVIDER.email}.</li>
</ol>
),
},
{
id: 'platnosci',
title: '§ 10. Płatności',
body: (
<ol>
<li>Płatności obsługuje <strong>Autopay S.A.</strong>, ul. Powstańców Warszawy 6, 81-718 Sopot - krajowa instytucja płatnicza nadzorowana przez Komisję Nadzoru Finansowego. Dostępne metody to m.in. BLIK, karta płatnicza i szybki przelew.</li>
<li>Po wybraniu płatności Użytkownik zostaje przekierowany na stronę Operatora płatności. Usługodawca nie otrzymuje i nie przechowuje danych karty płatniczej ani danych logowania do bankowości.</li>
<li>Potwierdzenie płatności Operator przekazuje automatycznie; na jego podstawie usługa jest aktywowana.</li>
<li>W razie niepowodzenia płatności usługa nie zostaje uruchomiona, a Użytkownik może ponowić próbę. Jeżeli mimo obciążenia usługa nie została aktywowana, należy zgłosić to na adres {PROVIDER.email} - sprawa zostanie wyjaśniona z Operatorem płatności, a środki zwrócone.</li>
</ol>
),
},
{
id: 'odstapienie',
title: '§ 11. Odstąpienie od umowy (Konsument)',
body: (
<ol>
<li>Konsument może odstąpić od umowy zawartej na odległość w terminie 14 dni bez podania przyczyny.</li>
<li>Usługa wyróżnienia rozpoczyna się przed upływem tego terminu wyłącznie na wyraźne żądanie Konsumenta. Po pełnym wykonaniu usługi prawo odstąpienia wygasa, a przy wykonaniu częściowym zwrot pomniejsza się proporcjonalnie do spełnionego świadczenia (art. 35 ustawy o prawach konsumenta).</li>
<li>Oświadczenie o odstąpieniu można złożyć na adres {PROVIDER.email}. Zwrot następuje w terminie 14 dni, tym samym sposobem zapłaty.</li>
<li>Odstąpienie od umowy zawartej z innym Użytkownikiem (np. umowy najmu lub sprzedaży nieruchomości) reguluje ta umowa i przepisy prawa - Usługodawca nie jest jej stroną.</li>
</ol>
),
},
{
id: 'reklamacje',
title: '§ 12. Reklamacje',
body: (
<ol>
<li>Reklamacje dotyczące usług Platformy można składać na adres {PROVIDER.email} lub przez formularz w zakładce Pomoc i kontakt, wskazując dane Użytkownika i opis zastrzeżeń.</li>
<li>Reklamacje rozpatrujemy w terminie 14 dni i informujemy o wyniku na podany adres e-mail.</li>
<li>Usługodawca odpowiada wobec Konsumenta za zgodność usługi cyfrowej z umową na zasadach określonych w ustawie o prawach konsumenta.</li>
</ol>
),
},
{
id: 'odpowiedzialnosc',
title: '§ 13. Odpowiedzialność',
body: (
<ol>
<li>Usługodawca udostępnia narzędzia teleinformatyczne i nie jest stroną transakcji zawieranych między Użytkownikami.</li>
<li>Za treść Ogłoszeń oraz przebieg i skutki transakcji odpowiada Użytkownik, który Ogłoszenie zamieścił.</li>
<li>Usługodawca nie ma ogólnego obowiązku monitorowania treści. Po otrzymaniu wiarygodnej informacji o bezprawnym charakterze treści niezwłocznie uniemożliwia do niej dostęp, zgodnie z § 7.</li>
<li>Usługodawca dokłada starań, aby Platforma działała bez przerw, lecz nie gwarantuje nieprzerwanej dostępności i zastrzega prawo do przerw technicznych.</li>
<li>Powyższe ograniczenia nie wyłączają ani nie ograniczają praw Konsumenta w zakresie, w jakim przepisy bezwzględnie obowiązujące na to nie zezwalają.</li>
</ol>
),
},
{
id: 'blokada-konta',
title: '§ 14. Blokada i rozwiązanie umowy',
body: (
<ol>
<li>Usługodawca może zablokować Konto w razie: naruszenia Regulaminu lub prawa, zamieszczania treści bezprawnych, podszywania się pod inną osobę, działań zagrażających bezpieczeństwu Platformy albo zakładania kont w celu obejścia wcześniejszej blokady.</li>
<li>O blokadzie i jej przyczynie informujemy na adres e-mail przypisany do Konta. Od decyzji przysługuje odwołanie na zasadach z § 7 ust. 5.</li>
<li>Użytkownik może rozwiązać umowę w każdej chwili, usuwając Konto.</li>
<li>Usługodawca może rozwiązać umowę z zachowaniem 14-dniowego terminu, z ważnych przyczyn, informując o tym Użytkownika.</li>
</ol>
),
},
{
id: 'bezpieczenstwo',
title: '§ 15. Bezpieczeństwo i zgodność',
body: (
<>
<ol>
<li>Usługodawca stosuje środki techniczne i organizacyjne odpowiadające ryzyku, opisane w <Link to={ROUTES.privacy}>Polityce prywatności</Link>.</li>
<li>
Zakres stosowania przepisów sektorowych: Platforma nie jest podmiotem finansowym ani dostawcą usług ICT
dla podmiotów finansowych w rozumieniu rozporządzenia (UE) 2022/2554 (DORA), a ze względu na skalę
działalności Usługodawca nie jest podmiotem kluczowym ani ważnym w rozumieniu dyrektywy (UE) 2022/2555
(NIS2). Niezależnie od tego Usługodawca <strong>dobrowolnie stosuje praktyki bezpieczeństwa wzorowane
na wymaganiach obu aktów</strong>: zarządzanie ryzykiem teleinformatycznym, kontrolę dostępu,
szyfrowanie transmisji, ograniczanie liczby żądań oraz reagowanie na incydenty.
</li>
<li>Użytkownik zobowiązuje się nie podejmować działań zagrażających bezpieczeństwu Platformy, w szczególności prób nieuprawnionego dostępu, automatycznego pobierania danych na masową skalę ani zakłócania jej działania.</li>
<li>Podatności bezpieczeństwa prosimy zgłaszać na adres {PROVIDER.email}. Zgłoszenia dokonane w dobrej wierze nie będą podstawą roszczeń wobec zgłaszającego.</li>
</ol>
<p className="legal-note">
Platforma podlega w szczególności: rozporządzeniu (UE) 2016/679 (RODO), rozporządzeniu (UE) 2022/2065
(DSA), ustawie o świadczeniu usług drogą elektroniczną, ustawie o prawach konsumenta, Kodeksowi cywilnemu
oraz ustawie Prawo komunikacji elektronicznej.
</p>
</>
),
},
{
id: 'dane-osobowe',
title: '§ 16. Dane osobowe',
body: (
<p>
Zasady przetwarzania danych osobowych, listę odbiorców danych oraz okresy przechowywania określa
<Link to={ROUTES.privacy}> Polityka prywatności</Link>. Administratorem danych jest Usługodawca.
</p>
),
},
{
id: 'wlasnosc-intelektualna',
title: '§ 17. Własność intelektualna',
body: (
<p>
Platforma, jej układ, logotyp i oprogramowanie podlegają ochronie prawnej i stanowią własność Usługodawcy
lub podmiotów, z którymi Usługodawca współpracuje. Kopiowanie lub wykorzystywanie ich bez zgody jest
zabronione. Prawa do treści Ogłoszeń i artykułów pozostają przy ich autorach.
</p>
),
},
{
id: 'spory',
title: '§ 18. Pozasądowe rozwiązywanie sporów',
body: (
<>
<p>Konsument może skorzystać z pozasądowych sposobów rozpatrywania reklamacji i dochodzenia roszczeń, w szczególności:</p>
<ul>
<li>pomocy powiatowego (miejskiego) rzecznika konsumentów,</li>
<li>mediacji przy wojewódzkim inspektorze Inspekcji Handlowej,</li>
<li>stałego sądu polubownego przy wojewódzkim inspektorze Inspekcji Handlowej,</li>
<li>informacji i poradnictwa Urzędu Ochrony Konkurencji i Konsumentów (uokik.gov.pl).</li>
</ul>
<p className="legal-note">
Europejska platforma ODR zakończyła działalność 20 lipca 2025 r., dlatego nie wskazujemy jej jako drogi
rozwiązania sporu.
</p>
</>
),
},
{
id: 'postanowienia-koncowe',
title: '§ 19. Zmiany Regulaminu i postanowienia końcowe',
body: (
<ol>
<li>Usługodawca może zmienić Regulamin z ważnych przyczyn, w szczególności zmiany przepisów lub zakresu usług, informując Użytkowników z co najmniej 14-dniowym wyprzedzeniem na adres e-mail przypisany do Konta.</li>
<li>Użytkownik, który nie akceptuje zmian, może przed ich wejściem w życie usunąć Konto.</li>
<li>W sprawach nieuregulowanych stosuje się przepisy prawa polskiego.</li>
<li>Spory z Użytkownikiem niebędącym Konsumentem rozstrzyga sąd właściwy dla siedziby Usługodawcy.</li>
<li>Jeżeli którekolwiek postanowienie Regulaminu okaże się nieważne, pozostałe zachowują moc.</li>
</ol>
),
},
];
<h2>§ 3. Rodzaje i zakres usług</h2> return (
<ol> <LegalDocument
<li>Platforma umożliwia w szczególności: przeglądanie i wyszukiwanie ogłoszeń nieruchomości, <strong>bezpłatne dodawanie ogłoszeń</strong> sprzedaży i najmu, prowadzenie Konta, zapisywanie ofert do ulubionych, ustawianie alertów cenowych, korzystanie z narzędzi (m.in. orientacyjna wycena, kalkulator zdolności), kontakt między Użytkownikami oraz odpłatne <strong>promowanie ogłoszeń</strong>.</li> title="Regulamin"
<li>Dodawanie ogłoszeń jest bezpłatne i nie wiąże się z prowizją od transakcji.</li> subtitle="Zasady korzystania z platformy ogłoszeniowej Polska Lokalnie i świadczenia usług drogą elektroniczną."
<li>Usługi płatne (promowanie, pakiety promowań) są świadczone zgodnie z § 8 i cennikiem prezentowanym w Platformie.</li> intro={(
<li>Narzędzia analityczne i wyceny mają charakter wyłącznie poglądowy i nie stanowią oferty, porady ani operatu szacunkowego w rozumieniu przepisów prawa.</li> <p>
</ol> Regulamin opisuje funkcje, które Platforma rzeczywiście udostępnia, oraz zasady odpowiedzialności za treści
publikowane przez Użytkowników. Dodawanie ogłoszeń jest bezpłatne i bez prowizji; odpłatne jest wyłącznie
<h2>§ 4. Wymagania techniczne</h2> dobrowolne wyróżnianie ofert.
<ol> </p>
<li>Do korzystania z Platformy niezbędne są: urządzenie z dostępem do Internetu, aktualna przeglądarka internetowa z obsługą JavaScript i plików cookie oraz aktywne konto poczty elektronicznej.</li> )}
<li>Usługodawca nie ponosi odpowiedzialności za problemy techniczne leżące po stronie Użytkownika lub dostawców usług telekomunikacyjnych.</li> sections={sections}
</ol> />
<h2>§ 5. Rejestracja i Konto</h2>
<ol>
<li>Założenie Konta wymaga wypełnienia formularza rejestracji oraz akceptacji Regulaminu i Polityki prywatności. Możliwa jest również rejestracja i logowanie za pośrednictwem kont zewnętrznych (Google, Facebook).</li>
<li>Użytkownik zobowiązuje się podać dane prawdziwe i aktualne oraz zabezpieczyć dane logowania przed dostępem osób trzecich.</li>
<li>Konto jest przypisane do jednego Użytkownika; udostępnianie go osobom trzecim jest niedozwolone.</li>
<li>Użytkownik może w każdej chwili usunąć Konto, kontaktując się z Usługodawcą lub korzystając z odpowiedniej opcji w Platformie.</li>
</ol>
<h2>§ 6. Zasady publikacji ogłoszeń</h2>
<ol>
<li>Ogłoszenie może dotyczyć wyłącznie rzeczywistej oferty sprzedaży lub najmu nieruchomości, do której Użytkownik posiada tytuł prawny lub odpowiednie umocowanie.</li>
<li>Treść Ogłoszenia powinna być zgodna ze stanem faktycznym, kompletna i nie może wprowadzać w błąd.</li>
<li>Zabronione jest zamieszczanie treści bezprawnych, w szczególności: naruszających prawa osób trzecich, obraźliwych, wulgarnych, dyskryminujących, wprowadzających w błąd, o charakterze spamu lub niezwiązanych z przedmiotem Platformy.</li>
<li>Ogłoszenia podlegają moderacji. Usługodawca może wstrzymać publikację, edytować w niezbędnym zakresie lub usunąć Ogłoszenie naruszające Regulamin lub przepisy prawa, informując o tym Użytkownika.</li>
<li>Użytkownik udziela Usługodawcy niewyłącznej, nieodpłatnej licencji na korzystanie z treści i zdjęć zamieszczonych w Ogłoszeniu w zakresie niezbędnym do świadczenia usług (prezentacja i promocja Ogłoszenia w Platformie i kanałach powiązanych).</li>
</ol>
<h2>§ 7. Odpowiedzialność</h2>
<ol>
<li>Usługodawca udostępnia narzędzia teleinformatyczne i nie jest stroną transakcji zawieranych między Użytkownikami.</li>
<li>Za treść Ogłoszeń, ich zgodność z prawem oraz przebieg i skutki transakcji odpowiada Użytkownik zamieszczający Ogłoszenie.</li>
<li>Usługodawca dokłada starań, aby Platforma działała prawidłowo, jednak nie gwarantuje nieprzerwanej dostępności i zastrzega prawo do przerw technicznych.</li>
<li>Usługodawca działa zgodnie z ustawą o świadczeniu usług drogą elektroniczną i nie ma obowiązku uprzedniej kontroli treści; po uzyskaniu wiarygodnej wiadomości o bezprawnym charakterze treści niezwłocznie uniemożliwia do niej dostęp.</li>
</ol>
<h2>§ 8. Promowanie ogłoszeń (usługi płatne)</h2>
<ol>
<li>Użytkownik może odpłatnie wyróżnić Ogłoszenie na czas określony (np. 4, 14 lub 30 dni) zgodnie z planami dostępnymi w Platformie. Wyróżnione Ogłoszenia prezentowane są wyżej na listach wyników.</li>
<li>Dostępny jest również zakup pakietu promowań (np. 5 lub 10 sztuk) w cenie promocyjnej; zakupione kredyty pozwalają wyróżnić dowolne własne Ogłoszenie zgodnie z wybranym planem czasowym.</li>
<li>Ceny prezentowane w Platformie są cenami brutto wyrażonymi w złotych polskich (PLN).</li>
<li>Usługa promowania rozpoczyna się niezwłocznie po zaksięgowaniu płatności i trwa przez wybrany okres.</li>
</ol>
<h2>§ 9. Płatności</h2>
<ol>
<li>Płatności za usługi płatne obsługiwane są przez zewnętrznego Operatora płatności (AutoPay - Autopay S.A.), umożliwiającego m.in. płatność BLIK, kartą płatniczą oraz szybkim przelewem.</li>
<li>Rozliczenie następuje w momencie zakupu. Na żądanie Użytkownika Usługodawca wystawia dokument potwierdzający zakup zgodnie z obowiązującymi przepisami.</li>
<li>Usługodawca nie przechowuje pełnych danych kart płatniczych - są one przetwarzane wyłącznie przez Operatora płatności w bezpiecznym środowisku.</li>
</ol>
<h2>§ 10. Odstąpienie od umowy (Konsument)</h2>
<ol>
<li>Konsument, który zawarł umowę na odległość, może od niej odstąpić bez podania przyczyny w terminie 14 dni.</li>
<li>Rozpoczęcie świadczenia usługi promowania przed upływem terminu na odstąpienie następuje na wyraźne żądanie Konsumenta; Konsument przyjmuje do wiadomości, że po pełnym wykonaniu usługi traci prawo odstąpienia, a przy częściowym wykonaniu zwrot pomniejsza się proporcjonalnie do spełnionego świadczenia (art. 35 ustawy o prawach konsumenta).</li>
<li>Oświadczenie o odstąpieniu można złożyć na adres e-mail: {PROVIDER.email}.</li>
</ol>
<h2>§ 11. Reklamacje</h2>
<ol>
<li>Reklamacje dotyczące usług można składać na adres e-mail {PROVIDER.email}, wskazując dane Użytkownika i opis zastrzeżeń.</li>
<li>Usługodawca rozpatruje reklamację w terminie do 14 dni i informuje Użytkownika o wyniku.</li>
</ol>
<h2>§ 12. Dane osobowe</h2>
<p>Zasady przetwarzania danych osobowych określa <Link to={ROUTES.privacy}>Polityka prywatności</Link>. Administratorem danych jest Usługodawca.</p>
<h2>§ 13. Pliki cookie</h2>
<p>Platforma wykorzystuje pliki cookie zgodnie z Polityką prywatności. Podczas pierwszej wizyty Użytkownik wyraża zgodę na wykorzystanie plików cookie lub ogranicza ją do plików niezbędnych.</p>
<h2>§ 14. Prawa autorskie</h2>
<p>Platforma, jej układ, logotyp oraz oprogramowanie podlegają ochronie prawnej i stanowią własność Usługodawcy lub podmiotów, z którymi Usługodawca współpracuje. Kopiowanie lub wykorzystywanie bez zgody jest zabronione.</p>
<h2>§ 15. Pozasądowe rozwiązywanie sporów</h2>
<p>Konsument może skorzystać z pozasądowych sposobów rozpatrywania reklamacji i dochodzenia roszczeń, w tym z platformy ODR Komisji Europejskiej dostępnej pod adresem ec.europa.eu/consumers/odr oraz z pomocy powiatowego (miejskiego) rzecznika konsumentów.</p>
<h2>§ 16. Zmiany Regulaminu i postanowienia końcowe</h2>
<ol>
<li>Usługodawca może zmienić Regulamin z ważnych przyczyn (m.in. zmiana przepisów, zakresu usług), informując Użytkowników z odpowiednim wyprzedzeniem.</li>
<li>W sprawach nieuregulowanych stosuje się przepisy prawa polskiego, w szczególności Kodeksu cywilnego, ustawy o świadczeniu usług drogą elektroniczną oraz ustawy o prawach konsumenta.</li>
<li>Regulamin obowiązuje od dnia publikacji.</li>
</ol>
</LegalPage>
); );
} }
export function PrivacyPolicyPage() { export function PrivacyPolicyPage() {
const sections: LegalSection[] = [
{
id: 'administrator',
title: '1. Administrator danych',
body: (
<>
<p>Administratorem danych osobowych jest:</p>
<ProviderDetails />
<p>
We wszystkich sprawach dotyczących danych osobowych prosimy o kontakt na adres {PROVIDER.email}.
Administrator nie wyznaczył inspektora ochrony danych - obowiązek taki nie wynika z art. 37 RODO.
</p>
</>
),
},
{
id: 'jakie-dane',
title: '2. Jakie dane przetwarzamy',
body: (
<ul>
<li><strong>Dane konta</strong> - adres e-mail, hasło w postaci zaszyfrowanej (funkcja skrótu), imię i nazwisko lub nazwa firmy, nazwa użytkownika, opcjonalnie numer telefonu, adres i NIP.</li>
<li><strong>Dane profilu</strong> - opis, zdjęcie profilowe i tło, adres strony i profili społecznościowych, preferencje wyszukiwania - jeśli Użytkownik je poda.</li>
<li><strong>Treści</strong> - ogłoszenia wraz ze zdjęciami i lokalizacją, artykuły w poradnikach, wiadomości do innych Użytkowników i do obsługi serwisu.</li>
<li><strong>Aktywność na koncie</strong> - ulubione ogłoszenia, zapisane wyszukiwania, alerty cenowe, umówione terminy oglądania, powiadomienia.</li>
<li><strong>Zgłoszenia i moderacja</strong> - treść zgłoszenia, załączniki oraz - przy zgłoszeniu Użytkownika - zapis rozmowy, której zgłoszenie dotyczy.</li>
<li><strong>Płatności</strong> - identyfikator zamówienia, kwota, metoda i status płatności oraz adres e-mail płatnika. Nie otrzymujemy danych karty płatniczej.</li>
<li><strong>Dane techniczne</strong> - adres IP, adres żądania, typ przeglądarki i strona odsyłająca, zapisywane w logach serwera.</li>
<li><strong>Dane firmowe z rejestru</strong> - przy zakładaniu konta firmowego dane pobierane z rejestru REGON na podstawie podanego numeru NIP.</li>
</ul>
),
},
{
id: 'cele-i-podstawy',
title: '3. Cele i podstawy prawne',
body: (
<ul>
<li><strong>Prowadzenie konta i świadczenie usług</strong> (ogłoszenia, wiadomości, ulubione, alerty, terminy oglądania) - art. 6 ust. 1 lit. b RODO, czyli wykonanie umowy.</li>
<li><strong>Obsługa płatności za wyróżnienia i rozliczenia</strong> - art. 6 ust. 1 lit. b i c RODO.</li>
<li><strong>Moderacja treści i rozpatrywanie zgłoszeń</strong> - art. 6 ust. 1 lit. c RODO (obowiązki z aktu o usługach cyfrowych) oraz art. 6 ust. 1 lit. f RODO.</li>
<li><strong>Bezpieczeństwo Platformy i zapobieganie nadużyciom</strong> - art. 6 ust. 1 lit. f RODO, czyli nasz prawnie uzasadniony interes.</li>
<li><strong>Obrona przed roszczeniami i ich dochodzenie</strong> - art. 6 ust. 1 lit. f RODO.</li>
<li><strong>Obowiązki rachunkowe i podatkowe</strong> - art. 6 ust. 1 lit. c RODO.</li>
<li><strong>Informacje marketingowe</strong> (wiadomości o nowościach i ofertach) - wyłącznie na podstawie odrębnej zgody, art. 6 ust. 1 lit. a RODO. Zgody nie zakładamy z góry: domyślnie jest wyłączona, a Użytkownik włącza ją sam w ustawieniach konta i może wycofać w każdej chwili.</li>
</ul>
),
},
{
id: 'czy-obowiazkowe',
title: '4. Czy podanie danych jest obowiązkowe',
body: (
<ul>
<li>Adres e-mail, hasło oraz imię i nazwisko lub nazwa są niezbędne do założenia konta - bez nich nie możemy świadczyć usługi.</li>
<li>NIP jest niezbędny przy koncie firmowym.</li>
<li>Numer telefonu, adres, opis profilu i zdjęcia są dobrowolne. Ich brak ogranicza jedynie wybrane funkcje (np. kontakt telefoniczny z ogłoszenia).</li>
<li>Przeglądanie ogłoszeń nie wymaga zakładania konta.</li>
</ul>
),
},
{
id: 'odbiorcy',
title: '5. Odbiorcy danych',
body: (
<>
<p>Dane powierzamy wyłącznie podmiotom, które przetwarzają je na nasze zlecenie, na podstawie umów powierzenia:</p>
<ul>
<li><strong>{PROVIDER.hosting}</strong> - utrzymanie serwerów w {PROVIDER.dataCenter}.</li>
<li><strong>Dostawca poczty wychodzącej (SMTP)</strong> - wysyłka wiadomości systemowych, m.in. linku aktywacyjnego i powiadomień o wiadomościach.</li>
<li><strong>Bramka SMS</strong> - wysyłka kodów potwierdzających numer telefonu.</li>
</ul>
<p>Odrębnymi administratorami danych są natomiast:</p>
<ul>
<li><strong>Autopay S.A.</strong> - w zakresie realizacji płatności za wyróżnienia.</li>
<li><strong>Drugi Użytkownik</strong> - dane, które sam udostępniasz w ogłoszeniu lub w wiadomości (np. imię, numer telefonu, ustalony termin oglądania), trafiają do osoby, z którą się kontaktujesz.</li>
</ul>
<p>Wyświetlenie niektórych elementów strony powoduje połączenie Twojej przeglądarki z serwerami dostawców zewnętrznych, którzy otrzymują wówczas Twój adres IP:</p>
<ul>
<li><strong>Esri (ArcGIS Online)</strong> oraz <strong>OpenStreetMap</strong> - kafelki map. Mapa jest niezbędna do przeglądania ofert i ładuje się zawsze.</li>
<li><strong>OpenStreetMap (Nominatim)</strong> - podpowiedzi adresów i ustalanie współrzędnych wpisanej lokalizacji. Do usługi trafia wpisywany adres.</li>
<li><strong>flagcdn.com</strong> - obrazki flag przy wyborze kierunkowego numeru telefonu.</li>
<li><strong>YouTube (tryb bez ciasteczek), Matterport, Kuula</strong> - wirtualne spacery osadzone w ogłoszeniach. Ładują się <strong>dopiero po Twoim kliknięciu</strong>; do tego czasu w ich miejscu widnieje zastępnik.</li>
</ul>
<p>
Dane mogą być także udostępnione organom publicznym, gdy obowiązek taki wynika z przepisów prawa.
Nie sprzedajemy danych i nie udostępniamy ich partnerom marketingowym.
</p>
</>
),
},
{
id: 'poza-eog',
title: '6. Przekazywanie danych poza EOG',
body: (
<>
<p>
Dane przechowujemy na serwerach w Polsce. Poza Europejski Obszar Gospodarczy trafia jedynie adres IP
i informacje techniczne przeglądarki, w związku z wyświetlaniem elementów opisanych w punkcie 5:
</p>
<ul>
<li>kafelki map dostarcza Esri z siedzibą w Stanach Zjednoczonych,</li>
<li>wirtualne spacery - YouTube (Google), Matterport i Kuula, również ze Stanów Zjednoczonych, przy czym wyłącznie po Twoim kliknięciu.</li>
</ul>
<p>
Podstawą przekazania są mechanizmy przewidziane w rozdziale V RODO: decyzja Komisji Europejskiej
stwierdzająca odpowiedni stopień ochrony (Data Privacy Framework) albo standardowe klauzule umowne.
</p>
</>
),
},
{
id: 'okresy',
title: '7. Jak długo przechowujemy dane',
body: (
<ul>
<li><strong>Dane konta i treści</strong> - przez czas posiadania konta. Po jego usunięciu kasujemy: wiadomości i rozmowy, ogłoszenia, ulubione, zapisane wyszukiwania, alerty cenowe, terminy oglądania, powiadomienia, artykuły, dane profilu oraz wpis w bazie marketingowej.</li>
<li><strong>Dane rozliczeniowe</strong> (zamówienia wyróżnień) - 5 lat od końca roku, w którym powstał obowiązek podatkowy.</li>
<li><strong>Zgłoszenia treści i korespondencja z obsługą</strong> - do 3 lat, w celu obrony przed roszczeniami i wykazania prawidłowej moderacji. Po usunięciu konta zgłoszenia pozostają w zakresie niezbędnym do tego celu.</li>
<li><strong>Jednorazowe tokeny i kody SMS</strong> - usuwane automatycznie po 7 dniach od wygaśnięcia.</li>
<li><strong>Logi serwera</strong> - przechowywane w buforze o ograniczonym rozmiarze i nadpisywane rotacyjnie; nie tworzymy z nich archiwum.</li>
<li><strong>Dane przetwarzane na podstawie zgody</strong> - do czasu jej wycofania.</li>
</ul>
),
},
{
id: 'prawa',
title: '8. Twoje prawa',
body: (
<>
<p>Przysługuje Ci prawo do:</p>
<ul>
<li>dostępu do danych i otrzymania ich kopii,</li>
<li>sprostowania danych - większość zmienisz samodzielnie w ustawieniach konta,</li>
<li>usunięcia danych - konto usuniesz samodzielnie w panelu konta,</li>
<li>ograniczenia przetwarzania,</li>
<li>przenoszenia danych przetwarzanych na podstawie umowy lub zgody,</li>
<li><strong>sprzeciwu</strong> wobec przetwarzania opartego na prawnie uzasadnionym interesie,</li>
<li>wycofania zgody w dowolnym momencie, bez wpływu na zgodność z prawem przetwarzania sprzed wycofania.</li>
</ul>
<p>
Przysługuje Ci także prawo wniesienia skargi do Prezesa Urzędu Ochrony Danych Osobowych,
ul. Stawki 2, 00-193 Warszawa, uodo.gov.pl.
</p>
</>
),
},
{
id: 'profilowanie',
title: '9. Profilowanie i decyzje automatyczne',
body: (
<p>
Nie profilujemy Użytkowników i nie podejmujemy wobec nich decyzji wyłącznie w sposób zautomatyzowany,
które wywoływałyby skutki prawne lub podobnie istotnie na nich wpływały (art. 22 RODO). Alerty cenowe
porównują wyłącznie liczby - cenę ogłoszenia lub średnią cenę za m² - i nie budują profilu osoby.
Decyzje moderacyjne podejmuje człowiek; automat jedynie oznacza treści do sprawdzenia.
</p>
),
},
{
id: 'pamiec-przegladarki',
title: '10. Pamięć przeglądarki i pliki cookie',
body: (
<>
<p>
Platforma <strong>nie ustawia plików cookie</strong> i nie korzysta z narzędzi analitycznych ani
pikseli reklamowych. Dane zapisujemy w pamięci przeglądarki (localStorage i sessionStorage):
</p>
<div className="legal-table-wrap">
<table className="legal-table">
<thead>
<tr><th>Nazwa</th><th>Do czego służy</th><th>Jak długo</th></tr>
</thead>
<tbody>
<tr>
<td>polskalokalnie-auth-token</td>
<td>utrzymanie zalogowania (niezbędne)</td>
<td>24 godziny</td>
</tr>
<tr>
<td>polskalokalnie-cookie-consent</td>
<td>zapamiętanie Twojego wyboru w banerze</td>
<td>do wyczyszczenia danych strony</td>
</tr>
<tr>
<td>polskalokalnie.valuationDraft</td>
<td>roboczy formularz wyceny, żeby nie ginął przy odświeżeniu</td>
<td>do zamknięcia karty</td>
</tr>
<tr>
<td>accountProfile:&lt;konto&gt;:…</td>
<td>drobne wygody interfejsu przypisane do konta: ostatnio oglądane oferty, wybrana zakładka, układ kafelków panelu</td>
<td>do wyczyszczenia danych strony</td>
</tr>
</tbody>
</table>
</div>
<p>
Wybór w banerze decyduje o ładowaniu treści osadzanych z serwisów zewnętrznych (wirtualne spacery).
Możesz go zmienić w każdej chwili, klikając „Ustawienia prywatności" w stopce. Osadzony spacer możesz
też włączyć pojedynczo, przyciskiem przy konkretnym ogłoszeniu.
</p>
</>
),
},
{
id: 'bezpieczenstwo-danych',
title: '11. Bezpieczeństwo danych',
body: (
<>
<p>Stosujemy środki techniczne i organizacyjne odpowiadające ryzyku, w szczególności:</p>
<ul>
<li>szyfrowanie transmisji (HTTPS z wymuszonym HSTS),</li>
<li>przechowywanie haseł wyłącznie w postaci skrótu (bcrypt) - nie znamy Twojego hasła,</li>
<li>kontrolę dostępu i rozdzielenie uprawnień w panelu administracyjnym,</li>
<li>ograniczanie liczby żądań, chroniące przed atakami automatycznymi i nadużyciami,</li>
<li>nagłówki bezpieczeństwa przeglądarki oraz oczyszczanie treści publikowanych przez Użytkowników,</li>
<li>bieżącą aktualizację oprogramowania i przeglądy bezpieczeństwa.</li>
</ul>
<p>
Praktyki te wzorujemy na wymaganiach dyrektywy NIS2 oraz na zasadach zarządzania ryzykiem
teleinformatycznym znanych z rozporządzenia DORA, choć - ze względu na charakter i skalę działalności -
Platforma nie jest podmiotem objętym obowiązkami żadnego z tych aktów.
</p>
</>
),
},
{
id: 'zmiany',
title: '12. Zmiany Polityki prywatności',
body: (
<p>
Politykę aktualizujemy, gdy zmienia się sposób działania Platformy. Aktualna wersja jest zawsze dostępna
pod tym adresem wraz z datą obowiązywania. O istotnych zmianach informujemy na adres e-mail przypisany
do konta.
</p>
),
},
];
return ( return (
<LegalPage <LegalDocument
title="Polityka prywatności" title="Polityka prywatności"
subtitle="Informacje o przetwarzaniu danych osobowych oraz wykorzystaniu plików cookie na platformie Polska Lokalnie." subtitle="Jakie dane zbieramy, po co, komu je powierzamy i jak długo je przechowujemy."
> intro={(
<h2>1. Administrator danych</h2> <p>
<p>Administratorem danych osobowych jest <strong>{PROVIDER.legal}</strong>, {PROVIDER.address}, NIP: {PROVIDER.nip}. Kontakt w sprawach danych osobowych: {PROVIDER.email}.</p> Ten dokument opisuje rzeczywisty sposób działania serwisu. Nie używamy narzędzi analitycznych ani pikseli
reklamowych, nie profilujemy Użytkowników i nie wysyłamy wiadomości marketingowych bez wyraźnej zgody.
<h2>2. Jakie dane przetwarzamy</h2> </p>
<ul> )}
<li>dane podane przy rejestracji i w profilu (imię i nazwisko lub nazwa, adres e-mail, numer telefonu, dane firmowe),</li> sections={sections}
<li>dane zawarte w ogłoszeniach i wiadomościach,</li> />
<li>dane dotyczące płatności za usługi płatne (bez pełnych danych kart płatniczych),</li>
<li>dane techniczne i o aktywności (adres IP, identyfikatory urządzenia, pliki cookie, logi).</li>
</ul>
<h2>3. Cele i podstawy prawne</h2>
<ul>
<li>świadczenie usług i prowadzenie Konta - art. 6 ust. 1 lit. b RODO (wykonanie umowy),</li>
<li>obsługa płatności i rozliczeń - art. 6 ust. 1 lit. b i c RODO,</li>
<li>obowiązki prawne (m.in. rachunkowe, reklamacje) - art. 6 ust. 1 lit. c RODO,</li>
<li>marketing własny, statystyki i bezpieczeństwo - art. 6 ust. 1 lit. f RODO (prawnie uzasadniony interes),</li>
<li>działania marketingowe realizowane na podstawie zgody (np. newsletter) - art. 6 ust. 1 lit. a RODO.</li>
</ul>
<h2>4. Odbiorcy danych</h2>
<p>Dane mogą być przekazywane podmiotom przetwarzającym je na zlecenie Administratora, w szczególności: dostawcy hostingu {PROVIDER.hosting}, Operatorowi płatności AutoPay (Autopay S.A.) w zakresie realizacji płatności, dostawcom usług e-mail i SMS oraz podmiotom świadczącym wsparcie techniczne - wyłącznie w zakresie niezbędnym i na podstawie odpowiednich umów powierzenia.</p>
<h2>5. Okres przechowywania</h2>
<p>Dane przechowujemy przez czas trwania umowy (posiadania Konta), a po jej zakończeniu przez okres wymagany przepisami (m.in. rozliczeniowymi) lub do przedawnienia roszczeń. Dane przetwarzane na podstawie zgody - do jej wycofania.</p>
<h2>6. Prawa osób, których dane dotyczą</h2>
<p>Przysługuje Państwu prawo dostępu do danych, ich sprostowania, usunięcia, ograniczenia przetwarzania, przenoszenia, wniesienia sprzeciwu oraz cofnięcia zgody w dowolnym momencie (bez wpływu na zgodność z prawem wcześniejszego przetwarzania). Mają Państwo także prawo wniesienia skargi do Prezesa Urzędu Ochrony Danych Osobowych (ul. Stawki 2, 00-193 Warszawa).</p>
<h2>7. Przekazywanie poza EOG</h2>
<p>Dane są przetwarzane na terenie Europejskiego Obszaru Gospodarczego, a serwery Platformy znajdują się fizycznie na terytorium Rzeczypospolitej Polskiej. W razie przekazania danych poza EOG stosujemy odpowiednie zabezpieczenia (m.in. standardowe klauzule umowne).</p>
<h2>8. Pliki cookie</h2>
<ul>
<li><strong>Niezbędne</strong> - konieczne do działania Platformy (logowanie, sesja, bezpieczeństwo). Nie wymagają zgody.</li>
<li><strong>Analityczne</strong> - pomagają zrozumieć sposób korzystania z Platformy i go ulepszać.</li>
<li><strong>Marketingowe/funkcjonalne</strong> - umożliwiają dopasowanie treści i zapamiętanie preferencji.</li>
</ul>
<p>Zgodę na pliki inne niż niezbędne wyrażają Państwo w banerze cookie podczas pierwszej wizyty; można ją w każdej chwili zmienić w ustawieniach przeglądarki. Ograniczenie plików cookie może wpłynąć na dostępność części funkcji.</p>
<h2>9. Bezpieczeństwo</h2>
<p>Stosujemy środki techniczne i organizacyjne odpowiadające ryzyku, w tym szyfrowanie transmisji (SSL/TLS) oraz kontrolę dostępu do danych.</p>
<h2>10. Zmiany polityki</h2>
<p>Polityka może być aktualizowana. Aktualna wersja jest zawsze dostępna w Platformie wraz z datą ostatniej aktualizacji.</p>
</LegalPage>
); );
} }
const COOKIE_CONSENT_KEY = 'polskalokalnie-cookie-consent'; const COOKIE_CONSENT_KEY = 'polskalokalnie-cookie-consent';
/**
* Czy uzytkownik zgodzil sie na tresci osadzane z serwisow zewnetrznych (wirtualne spacery
* z YouTube, Matterport, Kuula). Takie osadzenie przekazuje dostawcy adres IP uzytkownika,
* wiec bez zgody pokazujemy zastepnik z przyciskiem zamiast ladowac ramke.
*
* Mapy sa traktowane jako element niezbedny do ogladania ogloszen i nie zaleza od tej zgody.
*/
export function hasExternalEmbedConsent(): boolean {
try {
return window.localStorage.getItem(COOKIE_CONSENT_KEY) === 'all';
} catch {
return false;
}
}
const COOKIE_OPEN_EVENT = 'pl-cookie-open'; const COOKIE_OPEN_EVENT = 'pl-cookie-open';
// Ponowne otwarcie ustawień plików cookie (np. z linku w stopce). // Ponowne otwarcie ustawień prywatności (np. z linku w stopce).
export function openCookieSettings() { export function openCookieSettings() {
try { try {
window.localStorage.removeItem(COOKIE_CONSENT_KEY); window.localStorage.removeItem(COOKIE_CONSENT_KEY);
@@ -217,7 +677,7 @@ export function openCookieSettings() {
window.dispatchEvent(new Event(COOKIE_OPEN_EVENT)); window.dispatchEvent(new Event(COOKIE_OPEN_EVENT));
} }
// Baner zgody na pliki cookie - pojawia się przy pierwszej wizycie, dopóki Użytkownik nie dokona wyboru. // Baner zgody - pojawia się przy pierwszej wizycie, dopóki Użytkownik nie dokona wyboru.
export function CookieBanner() { export function CookieBanner() {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@@ -248,14 +708,15 @@ export function CookieBanner() {
} }
return ( return (
<div className="cookie-banner" role="dialog" aria-live="polite" aria-label="Zgoda na pliki cookie"> <div className="cookie-banner" role="dialog" aria-live="polite" aria-label="Ustawienia prywatności">
<div className="cookie-banner-inner"> <div className="cookie-banner-inner">
<div className="cookie-banner-text"> <div className="cookie-banner-text">
<strong>Dbamy o Twoją prywatność</strong> <strong>Dbamy o Twoją prywatność</strong>
<p> <p>
Używamy plików cookie, aby platforma działała poprawnie, oraz - za Twoją zgodą - do celów analitycznych Nie używamy narzędzi analitycznych ani reklamowych. Zapisujemy tylko dane potrzebne do działania serwisu.
i marketingowych. Szczegóły znajdziesz w <Link to={ROUTES.privacy}>Polityce prywatności</Link> i{' '} Twoja zgoda dotyczy wyłącznie treści osadzanych z serwisów zewnętrznych - wirtualnych spacerów
<Link to={ROUTES.terms}>Regulaminie</Link>. (YouTube, Matterport, Kuula), które otrzymują wtedy Twój adres IP. Szczegóły w{' '}
<Link to={ROUTES.privacy}>Polityce prywatności</Link> i <Link to={ROUTES.terms}>Regulaminie</Link>.
</p> </p>
</div> </div>
<div className="cookie-banner-actions"> <div className="cookie-banner-actions">
+269 -20
View File
@@ -32913,38 +32913,242 @@ a.listing-detail-back {
.promo-orders-table td, .promo-orders-table th { white-space: nowrap; } .promo-orders-table td, .promo-orders-table th { white-space: nowrap; }
/* ===== STRONY PRAWNE (Regulamin, Polityka prywatnosci) ===== */ /* ===== STRONY PRAWNE (Regulamin, Polityka prywatnosci) ===== */
.legal-page { background: #f5f7fa; padding: 30px 0 50px; } /* ===== STRONY PRAWNE (regulamin, polityka prywatnosci) =====
.legal-shell { Ta sama szerokosc i ten sam jezyk wizualny co /kupno czy /jak-sprzedawac:
pelnoekranowy kontener 1760 px, karty na bialym tle i spis tresci przyklejony z boku. */
.legal-page {
background:
radial-gradient(circle at 18% 0%, rgba(255, 255, 255, 0.96), rgba(255, 255, 255, 0) 32%),
linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
padding: 26px 0 60px;
}
.legal-container {
margin: 0 auto;
max-width: 1760px;
padding: 0 48px;
}
.legal-title h1 {
color: #13243d;
font-size: clamp(26px, 3.2vw, 38px);
letter-spacing: -0.02em;
margin: 0 0 8px;
}
.legal-title > p {
color: #4f6277;
font-size: 15px;
margin: 0 0 12px;
max-width: 760px;
}
.legal-meta {
color: #7b889a;
display: flex;
flex-wrap: wrap;
font-size: 13px;
font-weight: 800;
gap: 8px 18px;
margin-bottom: 26px;
}
.legal-meta strong {
color: #13243d;
}
.legal-layout {
align-items: start;
display: grid;
gap: 24px;
grid-template-columns: 300px minmax(0, 1fr);
}
.legal-toc {
background: #ffffff; background: #ffffff;
border: 1px solid #e3e9f0; border: 1px solid #e3e9f0;
border-radius: 16px; border-radius: 14px;
margin: 0 auto; padding: 18px 20px;
max-width: 900px; position: sticky;
padding: 34px clamp(20px, 4vw, 44px); top: 96px;
width: calc(100% - 2 * var(--workspace-pad));
} }
.legal-head { border-bottom: 1px solid #eef2f6; margin-bottom: 18px; padding-bottom: 16px; }
.legal-brand { color: #13243d; font-size: 14px; font-weight: 950; letter-spacing: -0.01em; margin: 0 0 10px; } .legal-toc h2 {
.legal-brand span { color: #cdd6e2; font-weight: 400; margin: 0 4px; } color: #13243d;
.legal-head h1 { color: #13243d; font-size: clamp(24px, 3.4vw, 32px); margin: 0 0 8px; } font-size: 13px;
.legal-head > p { color: #4f6277; font-size: 15px; margin: 0; } letter-spacing: 0.04em;
.legal-updated { color: #8a99ad !important; font-size: 13px !important; font-weight: 700; margin-top: 8px !important; } margin: 0 0 12px;
.legal-body { color: #2b3b52; font-size: 15px; line-height: 1.7; } text-transform: uppercase;
.legal-body h2 { color: #13243d; font-size: 18px; font-weight: 850; margin: 26px 0 10px; } }
.legal-body p { margin: 0 0 12px; }
.legal-body ul, .legal-body ol { margin: 0 0 14px; padding-left: 22px; } .legal-toc ol {
.legal-body li { margin: 6px 0; } display: grid;
.legal-body a { color: #12a764; font-weight: 700; text-decoration: underline; } gap: 2px;
list-style: none;
margin: 0;
max-height: calc(100vh - 220px);
overflow-y: auto;
padding: 0;
}
.legal-toc a {
border-radius: 8px;
color: #4f6277;
display: block;
font-size: 13px;
font-weight: 700;
padding: 7px 10px;
text-decoration: none;
}
.legal-toc a:hover {
background: #f1f8f4;
color: #0b9f5c;
}
.legal-main {
display: grid;
gap: 16px;
}
.legal-card {
background: #ffffff;
border: 1px solid #e3e9f0;
border-radius: 14px;
color: #2b3b52;
font-size: 15px;
line-height: 1.7;
padding: 24px clamp(20px, 2.4vw, 32px);
scroll-margin-top: 96px;
}
.legal-card h2 {
color: #13243d;
font-size: 19px;
font-weight: 900;
margin: 0 0 14px;
}
.legal-intro {
background: #f1f8f4;
border-color: #cfe8db;
}
.legal-card p {
margin: 0 0 12px;
max-width: 900px;
}
.legal-card p:last-child {
margin-bottom: 0;
}
.legal-card ul,
.legal-card ol {
margin: 0 0 14px;
max-width: 900px;
padding-left: 22px;
}
.legal-card li {
margin: 8px 0;
}
.legal-card a {
color: #12a764;
font-weight: 700;
text-decoration: underline;
}
.legal-facts {
display: grid;
gap: 8px;
list-style: none;
margin: 0 0 16px;
padding: 0;
}
.legal-facts li {
display: grid;
gap: 4px 16px;
grid-template-columns: 150px minmax(0, 1fr);
margin: 0;
}
.legal-facts span {
color: #7b889a;
font-size: 13px;
font-weight: 800;
}
.legal-facts strong {
color: #24344b;
font-weight: 700;
}
.legal-note { .legal-note {
background: #f6f9fc; background: #f6f9fc;
border: 1px dashed #cdd9e6; border: 1px dashed #cdd9e6;
border-radius: 10px; border-radius: 10px;
color: #6b7f96; color: #6b7f96;
font-size: 13px; font-size: 13px;
margin-top: 24px; margin-top: 16px;
padding: 12px 14px; padding: 12px 14px;
} }
.legal-table-wrap {
margin: 0 0 14px;
overflow-x: auto;
}
.legal-table {
border-collapse: collapse;
font-size: 13px;
min-width: 620px;
width: 100%;
}
.legal-table th,
.legal-table td {
border-bottom: 1px solid #eef2f6;
padding: 10px 12px;
text-align: left;
vertical-align: top;
}
.legal-table th {
color: #7b889a;
font-size: 12px;
letter-spacing: 0.02em;
text-transform: uppercase;
}
.legal-table td:first-child {
color: #24344b;
font-weight: 800;
white-space: nowrap;
}
@media (max-width: 1100px) {
.legal-container {
padding: 0 20px;
}
.legal-layout {
grid-template-columns: 1fr;
}
.legal-toc {
position: static;
}
.legal-toc ol {
max-height: none;
}
}
/* ===== STOPKA SERWISU ===== */ /* ===== STOPKA SERWISU ===== */
.site-footer { background: #0f2340; color: #c6d3e4; margin-top: 40px; } .site-footer { background: #0f2340; color: #c6d3e4; margin-top: 40px; }
.site-footer-inner { .site-footer-inner {
@@ -35343,3 +35547,48 @@ a.listing-detail-secondary {
top: 14px; top: 14px;
z-index: 2; z-index: 2;
} }
/* Wirtualny spacer z obcego serwisu laduje sie dopiero za zgoda ogladajacego. */
.listing-detail-tour-consent {
align-items: center;
background: #f7fafc;
border: 1px dashed #cfdae6;
border-radius: 12px;
display: grid;
gap: 10px;
justify-items: center;
padding: 28px 20px;
text-align: center;
}
.listing-detail-tour-consent > span {
align-items: center;
background: #e7f8ef;
border-radius: 50%;
color: #0b9f5c;
display: grid;
font-size: 20px;
height: 46px;
place-items: center;
width: 46px;
}
.listing-detail-tour-consent p {
color: #4a5a6f;
font-size: 13px;
line-height: 1.5;
margin: 0;
max-width: 460px;
}
.listing-detail-tour-consent button {
background: #10a15e;
border: 0;
border-radius: 8px;
color: #ffffff;
cursor: pointer;
font-size: 13px;
font-weight: 900;
min-height: 42px;
padding: 0 18px;
}
+35 -1
View File
@@ -1,5 +1,5 @@
import { test, expect, type Page } from '@playwright/test'; import { test, expect, type Page } from '@playwright/test';
import { ROUTES, negotiationPath, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath, paymentPath } from '../src/routes'; import { ROUTES, listingPath, negotiationPath, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath, paymentPath } from '../src/routes';
// Standard projektu (AGENTS.md): kazdy widok ma wlasny URL obslugiwany przez React Router, // Standard projektu (AGENTS.md): kazdy widok ma wlasny URL obslugiwany przez React Router,
// a nawigacja odbywa sie przez <Link>/<NavLink>. Te testy pilnuja tej zasady. // a nawigacja odbywa sie przez <Link>/<NavLink>. Te testy pilnuja tej zasady.
@@ -655,3 +655,37 @@ test.describe('Alerty cenowe', () => {
await expect(page.locator('.listing-detail-alert')).toHaveText(/Utwórz alert ceny/); await expect(page.locator('.listing-detail-alert')).toHaveText(/Utwórz alert ceny/);
}); });
}); });
// RODO: bez zgody nie ladujemy tresci z obcych serwisow, ktore dostalyby adres IP ogladajacego.
test.describe('Prywatnosc', () => {
test('wirtualny spacer laduje sie dopiero po zgodzie', async ({ page }) => {
const externalHosts = new Set<string>();
page.on('request', (request) => {
const host = new URL(request.url()).hostname;
if (!host.includes('localhost') && !host.includes('127.0.0.1')) {
externalHosts.add(host);
}
});
// Szukamy ogloszenia ze spacerem - tylko tam jest co blokowac.
const listings = await page.request.get('/api/listings?offerType=SALE').then((r) => r.json());
let tourListingId: number | null = null;
for (const item of listings) {
const detail = await page.request.get(`/api/listings/${item.id}`).then((r) => r.json());
if (detail.virtualTourUrl) {
tourListingId = item.id;
break;
}
}
if (tourListingId == null) {
test.skip(true, 'Zadne ogloszenie nie ma wirtualnego spaceru');
}
await goto(page, listingPath(tourListingId!));
await expect(page.locator('.listing-detail-tour-consent')).toBeVisible();
await expect(page.locator('.listing-detail-tour-embed iframe')).toHaveCount(0);
expect([...externalHosts].some((host) => host.includes('youtube') || host.includes('kuula') || host.includes('matterport'))).toBe(false);
await page.getByRole('button', { name: /Pokaż wirtualny spacer/ }).click();
await expect(page.locator('.listing-detail-tour-embed iframe')).toHaveCount(1);
});
});
-6
View File
@@ -21,7 +21,6 @@ limit_req_zone $pl_limit_key zone=pl_auth:10m rate=10r/m;
# Akcje wysylajace e-mail lub platny SMS: rejestracja, reset hasla, ponowne kody, formularz reklamowy. # Akcje wysylajace e-mail lub platny SMS: rejestracja, reset hasla, ponowne kody, formularz reklamowy.
limit_req_zone $pl_limit_key zone=pl_send:10m rate=3r/m; limit_req_zone $pl_limit_key zone=pl_send:10m rate=3r/m;
# Tlumaczenia (kazde zadanie to do 150 zapytan do Google). # Tlumaczenia (kazde zadanie to do 150 zapytan do Google).
limit_req_zone $pl_limit_key zone=pl_translate:10m rate=120r/m;
# Cale API - ochrona przed zalewem zadan; zwykle korzystanie z serwisu jest daleko ponizej. # Cale API - ochrona przed zalewem zadan; zwykle korzystanie z serwisu jest daleko ponizej.
limit_req_zone $pl_limit_key zone=pl_api:10m rate=30r/s; limit_req_zone $pl_limit_key zone=pl_api:10m rate=30r/s;
limit_req_status 429; limit_req_status 429;
@@ -70,11 +69,6 @@ server {
include /etc/nginx/snippets/api-proxy.conf; include /etc/nginx/snippets/api-proxy.conf;
limit_req zone=pl_send burst=5 nodelay; limit_req zone=pl_send burst=5 nodelay;
} }
location = /api/i18n/translate {
include /etc/nginx/snippets/api-proxy.conf;
limit_req zone=pl_translate burst=60 nodelay;
}
} }
# Szczegoly ogloszenia: crawler dostaje serwerowy podglad Open Graph, czlowiek - aplikacje SPA. # Szczegoly ogloszenia: crawler dostaje serwerowy podglad Open Graph, czlowiek - aplikacje SPA.
+47
View File
@@ -0,0 +1,47 @@
-- Wycofanie domniemanych zgod marketingowych.
--
-- Do 2026-09-23 encja Lead miala emailConsent i smsConsent ustawione domyslnie na TRUE, a lead
-- powstawal automatycznie przy kazdej rejestracji. Oznacza to, ze w bazie sa zgody, ktorych nikt
-- swiadomie nie wyrazil - a zgoda marketingowa musi byc uprzednia i wyrazna (art. 4 pkt 11
-- i art. 7 RODO oraz Prawo komunikacji elektronicznej). Kod juz tego nie robi (domyslnie FALSE),
-- ten skrypt porzadkuje dane, ktore powstaly wczesniej.
--
-- Skrypt zeruje zgody WSZYSTKIM leadom, bo w bazie nie ma sladu, kto zgode wyrazil swiadomie,
-- a kto dostal ja z automatu. Kierunek jest bezpieczny: uzytkownik moze ja wlaczyc sam
-- w ustawieniach konta. Trwalych rezygnacji (opt-out) skrypt nie rusza.
--
-- Domyslnie PROBA NA SUCHO: wszystko w jednej transakcji, skrypt pokazuje stan przed i po,
-- po czym robi ROLLBACK. Zapis nastepuje tylko z -v apply=1.
--
-- docker exec -i polskalokalnie-db-1 psql -U polskalokalnie -d polskalokalnie < scripts/fix-marketing-consents.sql
-- docker exec -i polskalokalnie-db-1 psql -U polskalokalnie -d polskalokalnie -v apply=1 < scripts/fix-marketing-consents.sql
\set ON_ERROR_STOP on
\if :{?apply}
\else
\set apply 0
\endif
BEGIN;
\echo '=== Stan przed ==='
SELECT 'leady razem' AS co, count(*) AS ile FROM leads
UNION ALL SELECT 'ze zgoda na e-mail', count(*) FROM leads WHERE email_consent
UNION ALL SELECT 'ze zgoda na SMS', count(*) FROM leads WHERE sms_consent
UNION ALL SELECT 'z trwala rezygnacja (nie ruszamy)', count(*) FROM leads WHERE email_opt_out OR sms_opt_out;
UPDATE leads SET email_consent = false WHERE email_consent;
UPDATE leads SET sms_consent = false WHERE sms_consent;
\echo '=== Stan po ==='
SELECT 'leady razem' AS co, count(*) AS ile FROM leads
UNION ALL SELECT 'ze zgoda na e-mail', count(*) FROM leads WHERE email_consent
UNION ALL SELECT 'ze zgoda na SMS', count(*) FROM leads WHERE sms_consent;
\if :apply
COMMIT;
\echo '>>> ZAPISANO (COMMIT) - zgody wyzerowane, uzytkownik moze je wlaczyc w ustawieniach konta'
\else
ROLLBACK;
\echo '>>> PROBA NA SUCHO - nic nie zostalo zmienione (ROLLBACK). Zapis: -v apply=1'
\endif