poradniki

This commit is contained in:
2026-08-20 16:24:47 +02:00
parent f9f030ba14
commit 6c35589e70
17 changed files with 4570 additions and 89 deletions
@@ -0,0 +1,90 @@
package pl.polskalokalnie.article;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
/**
* Panel administratora: moderacja propozycji artykułów użytkowników ("Poradniki - prośby").
* Dostęp tylko dla roli ADMIN (reguła /api/admin/** w SecurityConfig).
*/
@RestController
@RequestMapping("/api/admin/articles")
public class AdminArticleController {
private final ArticleService service;
public AdminArticleController(ArticleService service) {
this.service = service;
}
@GetMapping
public List<AdminArticleResponse> list(@RequestParam(value = "status", required = false) String status) {
return service.listForAdmin(parseStatus(status));
}
@GetMapping("/pending-count")
public Map<String, Long> pendingCount() {
return Map.of("count", service.pendingCount());
}
@GetMapping("/{id}")
public AdminArticleResponse get(@PathVariable Long id) {
return service.getForAdmin(id);
}
@PostMapping("/{id}/approve")
public AdminArticleResponse approve(@PathVariable Long id,
@RequestBody(required = false) ApproveRequest request,
Authentication authentication) {
return service.approve(id, authentication.getName(), parseChannel(request != null ? request.channel() : null));
}
@PostMapping("/{id}/reject")
public AdminArticleResponse reject(@PathVariable Long id,
@RequestBody(required = false) RejectRequest request,
Authentication authentication) {
return service.reject(id, request != null ? request.reason() : null, authentication.getName(),
parseChannel(request != null ? request.channel() : null));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
service.delete(id);
}
private ArticleStatus parseStatus(String status) {
if (status == null || status.isBlank() || "ALL".equalsIgnoreCase(status)) {
return null;
}
try {
return ArticleStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException ex) {
return null;
}
}
private ArticleService.NotifyChannel parseChannel(String channel) {
if (channel != null && "EMAIL".equalsIgnoreCase(channel)) {
return ArticleService.NotifyChannel.EMAIL;
}
return ArticleService.NotifyChannel.IN_APP;
}
public record ApproveRequest(String channel) {
}
public record RejectRequest(String reason, String channel) {
}
}
@@ -0,0 +1,48 @@
package pl.polskalokalnie.article;
import java.time.Instant;
import java.util.List;
/** Pełny widok propozycji artykułu dla panelu administratora (z treścią, załącznikami i statusem). */
public record AdminArticleResponse(
Long id,
String topicSlug,
String title,
String excerpt,
String content,
List<String> attachments,
String authorName,
String authorNick,
String authorEmail,
String contactName,
String contactEmail,
String contactPhone,
int readMinutes,
String status,
String rejectionReason,
String reviewedByEmail,
Instant reviewedAt,
Instant createdAt
) {
public static AdminArticleResponse from(Article article) {
return new AdminArticleResponse(
article.getId(),
article.getTopicSlug(),
article.getTitle(),
article.getExcerpt(),
article.getContent(),
List.copyOf(article.getAttachments()),
article.getAuthorName(),
article.getAuthorNick(),
article.getAuthorEmail(),
article.getContactName(),
article.getContactEmail(),
article.getContactPhone(),
article.getReadMinutes(),
article.getStatus().name(),
article.getRejectionReason(),
article.getReviewedByEmail(),
article.getReviewedAt(),
article.getCreatedAt());
}
}
@@ -0,0 +1,246 @@
package pl.polskalokalnie.article;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OrderColumn;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* Poradnik/artykuł dodany przez użytkownika w danym temacie (np. "kredyty-hipoteczne").
* Treść to oczyszczony HTML z edytora - renderowana na froncie przez DOMPurify.
*/
@Entity
@Table(name = "articles")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Slug tematu = ostatni segment adresu poradnika (kupno-mieszkania, kredyty-hipoteczne, ...).
@Column(nullable = false, length = 60)
private String topicSlug;
@Column(nullable = false, length = 200)
private String title;
@Column(nullable = false, length = 400)
private String excerpt;
@Column(nullable = false, columnDefinition = "text")
private String content;
// Autor - konto, z którego dodano artykuł. Nazwę i nick trzymamy zdenormalizowane dla listy.
@Column(nullable = false, length = 180)
private String authorEmail;
@Column(nullable = false, length = 160)
private String authorName;
@Column(length = 80)
private String authorNick;
// Dane kontaktowe podane w formularzu - po nich zespół kontaktuje się w sprawie publikacji.
// Mogą różnić się od danych konta (autor może podać inny adres do kontaktu).
@Column(length = 160)
private String contactName;
@Column(length = 180)
private String contactEmail;
@Column(length = 40)
private String contactPhone;
// Szacowany czas czytania w minutach, liczony z długości treści przy zapisie.
@Column(nullable = false)
private int readMinutes;
@Column(nullable = false)
private long views = 0;
// Opcjonalne załączniki (zdjęcia jako data URL). Osobna tabela z kolumną TEXT.
@ElementCollection
@CollectionTable(name = "article_attachments", joinColumns = @JoinColumn(name = "article_id"))
@OrderColumn(name = "position")
@Column(name = "attachment", columnDefinition = "text")
private List<String> attachments = new ArrayList<>();
// Stan moderacji. Nowe propozycje trafiają do zespołu jako PENDING.
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ArticleStatus status = ArticleStatus.PENDING;
@Column(length = 180)
private String reviewedByEmail;
private Instant reviewedAt;
@Column(length = 500)
private String rejectionReason;
@Column(nullable = false, updatable = false)
private Instant createdAt;
@PrePersist
void onCreate() {
if (createdAt == null) {
createdAt = Instant.now();
}
}
public Long getId() {
return id;
}
public String getTopicSlug() {
return topicSlug;
}
public void setTopicSlug(String topicSlug) {
this.topicSlug = topicSlug;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getExcerpt() {
return excerpt;
}
public void setExcerpt(String excerpt) {
this.excerpt = excerpt;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getAuthorEmail() {
return authorEmail;
}
public void setAuthorEmail(String authorEmail) {
this.authorEmail = authorEmail;
}
public String getAuthorName() {
return authorName;
}
public void setAuthorName(String authorName) {
this.authorName = authorName;
}
public String getAuthorNick() {
return authorNick;
}
public void setAuthorNick(String authorNick) {
this.authorNick = authorNick;
}
public String getContactName() {
return contactName;
}
public void setContactName(String contactName) {
this.contactName = contactName;
}
public String getContactEmail() {
return contactEmail;
}
public void setContactEmail(String contactEmail) {
this.contactEmail = contactEmail;
}
public String getContactPhone() {
return contactPhone;
}
public void setContactPhone(String contactPhone) {
this.contactPhone = contactPhone;
}
public int getReadMinutes() {
return readMinutes;
}
public void setReadMinutes(int readMinutes) {
this.readMinutes = readMinutes;
}
public long getViews() {
return views;
}
public void setViews(long views) {
this.views = views;
}
public List<String> getAttachments() {
return attachments;
}
public void setAttachments(List<String> attachments) {
this.attachments = attachments;
}
public ArticleStatus getStatus() {
return status;
}
public void setStatus(ArticleStatus status) {
this.status = status;
}
public String getReviewedByEmail() {
return reviewedByEmail;
}
public void setReviewedByEmail(String reviewedByEmail) {
this.reviewedByEmail = reviewedByEmail;
}
public Instant getReviewedAt() {
return reviewedAt;
}
public void setReviewedAt(Instant reviewedAt) {
this.reviewedAt = reviewedAt;
}
public String getRejectionReason() {
return rejectionReason;
}
public void setRejectionReason(String rejectionReason) {
this.rejectionReason = rejectionReason;
}
public Instant getCreatedAt() {
return createdAt;
}
}
@@ -0,0 +1,29 @@
package pl.polskalokalnie.article;
import java.time.Instant;
/** Skrócona wersja artykułu na listę w danym temacie (bez pełnej treści). */
public record ArticleCardResponse(
Long id,
String topicSlug,
String title,
String excerpt,
String authorName,
String authorNick,
int readMinutes,
long views,
Instant createdAt
) {
public static ArticleCardResponse from(Article article) {
return new ArticleCardResponse(
article.getId(),
article.getTopicSlug(),
article.getTitle(),
article.getExcerpt(),
article.getAuthorName(),
article.getAuthorNick(),
article.getReadMinutes(),
article.getViews(),
article.getCreatedAt());
}
}
@@ -0,0 +1,48 @@
package pl.polskalokalnie.article;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
/** Poradniki dodawane przez użytkowników. Odczyt publiczny, dodawanie tylko dla zalogowanych. */
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
private final ArticleService service;
public ArticleController(ArticleService service) {
this.service = service;
}
@GetMapping
public List<ArticleCardResponse> listByTopic(@RequestParam("topic") String topic) {
return service.listByTopic(topic);
}
@GetMapping("/counts")
public Map<String, Long> counts() {
return service.countsByTopic();
}
@GetMapping("/{id}")
public ArticleDetailResponse get(@PathVariable Long id) {
return service.get(id);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ArticleDetailResponse create(@Valid @RequestBody ArticleCreateRequest request, Authentication authentication) {
return service.create(request, authentication.getName());
}
}
@@ -0,0 +1,20 @@
package pl.polskalokalnie.article;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.List;
/** Dane formularza "Zaproponuj artykuł". Treść to HTML z edytora, załączniki to data URL zdjęć. */
public record ArticleCreateRequest(
@NotBlank @Size(max = 60) String topicSlug,
@NotBlank @Size(min = 5, max = 200) String title,
@NotBlank @Size(min = 10, max = 400) String excerpt,
@NotBlank @Size(min = 30, max = 40000) String content,
// Dane kontaktowe autora - po nich zespół skontaktuje się w sprawie publikacji.
@NotBlank @Size(min = 3, max = 160) String contactName,
@NotBlank @Email @Size(max = 180) String contactEmail,
@Size(max = 40) String contactPhone,
List<String> attachments
) {
}
@@ -0,0 +1,37 @@
package pl.polskalokalnie.article;
import java.time.Instant;
import java.util.List;
/** Pełny artykuł do widoku szczegółowego. */
public record ArticleDetailResponse(
Long id,
String topicSlug,
String title,
String excerpt,
String content,
List<String> attachments,
String authorName,
String authorNick,
int readMinutes,
long views,
String status,
Instant createdAt
) {
public static ArticleDetailResponse from(Article article) {
return new ArticleDetailResponse(
article.getId(),
article.getTopicSlug(),
article.getTitle(),
article.getExcerpt(),
article.getContent(),
List.copyOf(article.getAttachments()),
article.getAuthorName(),
article.getAuthorNick(),
article.getReadMinutes(),
article.getViews(),
article.getStatus().name(),
article.getCreatedAt());
}
}
@@ -0,0 +1,25 @@
package pl.polskalokalnie.article;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
public interface ArticleRepository extends JpaRepository<Article, Long> {
// Widok publiczny - tylko opublikowane.
List<Article> findByTopicSlugAndStatusOrderByCreatedAtDesc(String topicSlug, ArticleStatus status);
Optional<Article> findByIdAndStatus(Long id, ArticleStatus status);
// Panel admina.
List<Article> findByStatusOrderByCreatedAtDesc(ArticleStatus status);
List<Article> findAllByOrderByCreatedAtDesc();
long countByStatus(ArticleStatus status);
// Liczba opublikowanych artykułów w rozbiciu na tematy - do liczników na kafelkach /poradniki.
@Query("select a.topicSlug, count(a) from Article a where a.status = :status group by a.topicSlug")
List<Object[]> countGroupedByTopic(ArticleStatus status);
}
@@ -0,0 +1,247 @@
package pl.polskalokalnie.article;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import pl.polskalokalnie.auth.AccountEmailService;
import pl.polskalokalnie.moderation.TextModerationService;
import pl.polskalokalnie.notification.NotificationService;
import pl.polskalokalnie.user.AppUser;
import pl.polskalokalnie.user.UserRepository;
@Service
public class ArticleService {
/** Kanał, którym powiadamiamy autora o decyzji. */
public enum NotifyChannel { IN_APP, EMAIL }
// Tematy zgodne z ostatnim segmentem adresów poradników na froncie (ROUTES.guide*).
private static final Set<String> ALLOWED_TOPICS = Set.of(
"kupno-mieszkania",
"kredyty-hipoteczne",
"wynajem",
"inwestowanie",
"remont-i-wykonczenie",
"prawo-i-podatki");
private static final int MAX_ATTACHMENTS = 6;
private final ArticleRepository repository;
private final UserRepository userRepository;
private final TextModerationService textModerationService;
private final NotificationService notificationService;
private final AccountEmailService accountEmailService;
public ArticleService(ArticleRepository repository,
UserRepository userRepository,
TextModerationService textModerationService,
NotificationService notificationService,
AccountEmailService accountEmailService) {
this.repository = repository;
this.userRepository = userRepository;
this.textModerationService = textModerationService;
this.notificationService = notificationService;
this.accountEmailService = accountEmailService;
}
// ---- Widok publiczny (tylko opublikowane) ----
public List<ArticleCardResponse> listByTopic(String topicSlug) {
requireAllowedTopic(topicSlug);
return repository.findByTopicSlugAndStatusOrderByCreatedAtDesc(topicSlug, ArticleStatus.PUBLISHED).stream()
.map(ArticleCardResponse::from)
.toList();
}
/** Liczba opublikowanych artykułów w każdym temacie (0 dla tematów bez wpisów). */
public Map<String, Long> countsByTopic() {
Map<String, Long> counts = new LinkedHashMap<>();
for (String topic : ALLOWED_TOPICS) {
counts.put(topic, 0L);
}
for (Object[] row : repository.countGroupedByTopic(ArticleStatus.PUBLISHED)) {
String slug = (String) row[0];
if (counts.containsKey(slug)) {
counts.put(slug, (Long) row[1]);
}
}
return counts;
}
@Transactional
public ArticleDetailResponse get(Long id) {
Article article = repository.findByIdAndStatus(id, ArticleStatus.PUBLISHED)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Artykuł nie istnieje"));
article.setViews(article.getViews() + 1);
return ArticleDetailResponse.from(article);
}
@Transactional
public ArticleDetailResponse create(ArticleCreateRequest request, String authorEmail) {
String topicSlug = request.topicSlug().trim();
requireAllowedTopic(topicSlug);
String plainContent = stripHtml(request.content());
if (plainContent.length() < 30) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Treść artykułu jest zbyt krótka");
}
// Filtr wulgaryzmów na widocznym tekście (tytuł, zajawka, treść bez znaczników, imię kontaktu).
textModerationService.validateOrThrow(request.title(), request.excerpt(), plainContent, request.contactName());
AppUser author = userRepository.findByEmailIgnoreCase(authorEmail)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Nie znaleziono konta autora"));
Article article = new Article();
article.setTopicSlug(topicSlug);
article.setTitle(request.title().trim());
article.setExcerpt(request.excerpt().trim());
article.setContent(request.content());
article.setAttachments(sanitizeAttachments(request.attachments()));
article.setAuthorEmail(author.getEmail());
article.setAuthorName(author.getFullName() != null ? author.getFullName() : author.getEmail());
article.setAuthorNick(author.getNick());
article.setContactName(request.contactName().trim());
article.setContactEmail(request.contactEmail().trim());
article.setContactPhone(request.contactPhone() != null && !request.contactPhone().isBlank()
? request.contactPhone().trim() : null);
article.setReadMinutes(estimateReadMinutes(plainContent));
// Nowa propozycja czeka na akceptację zespołu.
article.setStatus(ArticleStatus.PENDING);
return ArticleDetailResponse.from(repository.save(article));
}
// ---- Panel administratora ----
@Transactional(readOnly = true)
public List<AdminArticleResponse> listForAdmin(ArticleStatus status) {
List<Article> articles = status != null
? repository.findByStatusOrderByCreatedAtDesc(status)
: repository.findAllByOrderByCreatedAtDesc();
return articles.stream().map(AdminArticleResponse::from).toList();
}
@Transactional(readOnly = true)
public AdminArticleResponse getForAdmin(Long id) {
return AdminArticleResponse.from(findOrThrow(id));
}
/** Liczba propozycji oczekujących na moderację (badge w panelu). */
public long pendingCount() {
return repository.countByStatus(ArticleStatus.PENDING);
}
@Transactional
public AdminArticleResponse approve(Long id, String adminEmail, NotifyChannel channel) {
Article article = findOrThrow(id);
article.setStatus(ArticleStatus.PUBLISHED);
article.setRejectionReason(null);
article.setReviewedByEmail(adminEmail);
article.setReviewedAt(Instant.now());
Article saved = repository.save(article);
notifyAuthor(saved, true, null, channel);
return AdminArticleResponse.from(saved);
}
@Transactional
public AdminArticleResponse reject(Long id, String reason, String adminEmail, NotifyChannel channel) {
Article article = findOrThrow(id);
String cleanReason = reason != null && !reason.isBlank() ? reason.trim() : null;
article.setStatus(ArticleStatus.REJECTED);
article.setRejectionReason(cleanReason);
article.setReviewedByEmail(adminEmail);
article.setReviewedAt(Instant.now());
Article saved = repository.save(article);
notifyAuthor(saved, false, cleanReason, channel);
return AdminArticleResponse.from(saved);
}
// Powiadamia autora o decyzji wybranym przez admina kanałem. Błąd powiadomienia nie przerywa
// samej moderacji - status i tak został zapisany.
private void notifyAuthor(Article article, boolean approved, String reason, NotifyChannel channel) {
NotifyChannel effective = channel != null ? channel : NotifyChannel.IN_APP;
String path = approved
? "/poradniki/artykul/" + article.getId()
: "/poradniki/dodaj?temat=" + article.getTopicSlug();
try {
if (effective == NotifyChannel.EMAIL) {
// E-mail idzie na adres kontaktowy podany w formularzu (fallback: adres konta).
String toEmail = article.getContactEmail() != null && !article.getContactEmail().isBlank()
? article.getContactEmail() : article.getAuthorEmail();
String toName = article.getContactName() != null && !article.getContactName().isBlank()
? article.getContactName() : article.getAuthorName();
accountEmailService.sendArticleDecision(
toEmail, toName, article.getTitle(), approved, reason, path);
} else {
String title = approved ? "Twój artykuł został opublikowany" : "Twój artykuł nie został przyjęty";
String body = approved
? "„" + article.getTitle() + "” jest już widoczny w poradnikach. Dziękujemy!"
: (reason != null
? "„" + article.getTitle() + "” nie został przyjęty. Powód: " + reason
: "„" + article.getTitle() + "” nie został przyjęty. Możesz poprawić treść i wysłać ponownie.");
notificationService.create(article.getAuthorEmail(), "system", approved ? "check" : "warning",
title, body, path, "article-decision-" + article.getId() + "-" + article.getStatus().name());
}
} catch (Exception ex) {
// Świadomie pomijamy - decyzja moderacyjna jest ważniejsza niż dostarczenie powiadomienia.
}
}
@Transactional
public void delete(Long id) {
if (!repository.existsById(id)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Artykuł nie istnieje");
}
repository.deleteById(id);
}
// ---- Pomocnicze ----
private Article findOrThrow(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Artykuł nie istnieje"));
}
private void requireAllowedTopic(String topicSlug) {
if (topicSlug == null || !ALLOWED_TOPICS.contains(topicSlug)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Nieznany temat poradnika");
}
}
// Przyjmujemy tylko obrazy w formie data URL, maksymalnie kilka sztuk.
private List<String> sanitizeAttachments(List<String> attachments) {
List<String> cleaned = new ArrayList<>();
if (attachments == null) {
return cleaned;
}
for (String attachment : attachments) {
if (attachment != null && attachment.startsWith("data:image/")) {
cleaned.add(attachment);
if (cleaned.size() >= MAX_ATTACHMENTS) {
break;
}
}
}
return cleaned;
}
// Prosty czas czytania: ~200 słów na minutę, minimum 1 minuta.
private int estimateReadMinutes(String plainText) {
int words = plainText.isBlank() ? 0 : plainText.trim().split("\\s+").length;
return Math.max(1, (int) Math.ceil(words / 200.0));
}
private String stripHtml(String html) {
if (html == null) {
return "";
}
return html.replaceAll("<[^>]*>", " ").replaceAll("&nbsp;", " ").replaceAll("\\s+", " ").trim();
}
}
@@ -0,0 +1,8 @@
package pl.polskalokalnie.article;
/** Stan propozycji artykułu w procesie moderacji. */
public enum ArticleStatus {
PENDING,
PUBLISHED,
REJECTED
}
@@ -61,6 +61,35 @@ public class AccountEmailService {
send(email, "Reset hasła w serwisie Polska Lokalnie", html, "RESET HASŁA", link);
}
/**
* Powiadomienie autora o decyzji zespołu w sprawie zaproponowanego artykułu.
* approved = true → artykuł opublikowany, link prowadzi do artykułu; false → odrzucony,
* link prowadzi do formularza, aby poprawić i wysłać ponownie.
*/
public void sendArticleDecision(String email, String name, String articleTitle,
boolean approved, String reason, String path) {
String link = baseUrl + (path == null ? "" : path);
String heading = approved ? "Twój artykuł został opublikowany" : "Twój artykuł nie został przyjęty";
String safeTitle = articleTitle == null ? "" : articleTitle;
String body;
if (approved) {
body = "<p>Dobra wiadomość! Twoja propozycja artykułu <strong>„" + safeTitle + "”</strong> została"
+ " zaakceptowana przez nasz zespół i jest już widoczna w poradnikach.</p>"
+ "<p>Dziękujemy, że dzielisz się swoją wiedzą z innymi.</p>";
} else {
String reasonHtml = reason == null || reason.isBlank() ? ""
: "<p style=\"background:#fbe9ec;border-radius:8px;padding:12px 14px;color:#a23144;\"><strong>Powód:</strong> "
+ reason.trim() + "</p>";
body = "<p>Dziękujemy za propozycję artykułu <strong>„" + safeTitle + "”</strong>. Po analizie nasz zespół"
+ " zdecydował, że w obecnej formie nie zostanie on opublikowany.</p>"
+ reasonHtml
+ "<p>Zachęcamy do poprawienia treści i wysłania jej ponownie.</p>";
}
String ctaLabel = approved ? "Zobacz artykuł" : "Popraw i wyślij ponownie";
send(email, "Polska Lokalnie - " + heading, template(heading, greeting(name), body, ctaLabel, link),
approved ? "ARTYKUŁ ZAAKCEPTOWANY" : "ARTYKUŁ ODRZUCONY", link);
}
private void send(String email, String subject, String html, String label, String link) {
SmtpConfig smtp = mailConfigService.getSmtp();
boolean configured = smtp.isEnabled()
@@ -47,6 +47,8 @@ public class SecurityConfig {
.requestMatchers("/api/auth/me").authenticated()
.requestMatchers(HttpMethod.GET, "/api/listings/mine").authenticated()
.requestMatchers(HttpMethod.GET, "/api/listings", "/api/listings/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/articles", "/api/articles/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/articles").authenticated()
.requestMatchers(HttpMethod.GET, "/api/banners").permitAll()
.requestMatchers(HttpMethod.POST, "/api/banners/*/click").permitAll()
.requestMatchers(HttpMethod.POST, "/api/payments/autopay/itn").permitAll()
+1136 -73
View File
File diff suppressed because it is too large Load Diff
+272
View File
@@ -0,0 +1,272 @@
/**
* Panel administratora: moderacja propozycji artykułów użytkowników ("Poradniki - prośby").
* Filtr po statusie, podgląd treści i załączników, akcje: akceptuj / odrzuć / usuń.
* Komponent `Icon` przekazywany jest propem, aby uniknąć cyklicznego importu z App.tsx.
*/
import { useCallback, useEffect, useState } from 'react';
import { apiFetch } from '../auth';
import { SafeHtml } from '../RichText';
import { GUIDE_TOPICS, articlePath } from '../routes';
import { Link } from 'react-router-dom';
type IconComponent = (props: { name: string }) => JSX.Element | null;
type ArticleStatus = 'PENDING' | 'PUBLISHED' | 'REJECTED';
type AdminArticle = {
id: number;
topicSlug: string;
title: string;
excerpt: string;
content: string;
attachments: string[];
authorName: string;
authorNick: string | null;
authorEmail: string;
contactName: string | null;
contactEmail: string | null;
contactPhone: string | null;
readMinutes: number;
status: ArticleStatus;
rejectionReason: string | null;
reviewedByEmail: string | null;
reviewedAt: string | null;
createdAt: string;
};
const STATUS_FILTERS: { key: string; label: string }[] = [
{ key: 'PENDING', label: 'Oczekujące' },
{ key: 'PUBLISHED', label: 'Opublikowane' },
{ key: 'REJECTED', label: 'Odrzucone' },
{ key: 'ALL', label: 'Wszystkie' },
];
const STATUS_LABEL: Record<ArticleStatus, string> = {
PENDING: 'Oczekuje',
PUBLISHED: 'Opublikowany',
REJECTED: 'Odrzucony',
};
const STATUS_CLASS: Record<ArticleStatus, string> = {
PENDING: 'pending',
PUBLISHED: 'approved',
REJECTED: 'rejected',
};
function topicTitle(slug: string): string {
return GUIDE_TOPICS.find((topic) => topic.slug === slug)?.title ?? slug;
}
function formatDate(iso: string | null): string {
if (!iso) {
return '-';
}
try {
return new Intl.DateTimeFormat('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(iso));
} catch {
return '-';
}
}
type NotifyChannel = 'IN_APP' | 'EMAIL';
export function AdminArticleRequestsView({ Icon, onModerated }: { Icon: IconComponent; onModerated?: () => void }) {
const [statusFilter, setStatusFilter] = useState('PENDING');
const [items, setItems] = useState<AdminArticle[]>([]);
const [loading, setLoading] = useState(true);
const [expandedId, setExpandedId] = useState<number | null>(null);
const [busyId, setBusyId] = useState<number | null>(null);
// Otwarty panel decyzji: dla którego artykułu i jaki typ (akceptacja / odrzucenie).
const [decision, setDecision] = useState<{ id: number; type: 'approve' | 'reject' } | null>(null);
const [channel, setChannel] = useState<NotifyChannel>('IN_APP');
const [rejectReason, setRejectReason] = useState('');
const load = useCallback(async () => {
setLoading(true);
try {
const data = await apiFetch<AdminArticle[]>(`/admin/articles?status=${statusFilter}`);
setItems(Array.isArray(data) ? data : []);
} catch {
setItems([]);
} finally {
setLoading(false);
}
}, [statusFilter]);
useEffect(() => {
void load();
}, [load]);
const openDecision = (id: number, type: 'approve' | 'reject') => {
setDecision({ id, type });
setChannel('IN_APP');
setRejectReason('');
};
const runAction = async (id: number, action: () => Promise<unknown>) => {
setBusyId(id);
try {
await action();
await load();
onModerated?.();
} catch (err) {
window.alert(err instanceof Error ? err.message : 'Nie udało się wykonać akcji.');
} finally {
setBusyId(null);
}
};
const confirmDecision = (id: number, type: 'approve' | 'reject') =>
runAction(id, async () => {
const path = type === 'approve' ? 'approve' : 'reject';
const payload = type === 'approve' ? { channel } : { channel, reason: rejectReason.trim() };
await apiFetch(`/admin/articles/${id}/${path}`, { method: 'POST', body: JSON.stringify(payload) });
setDecision(null);
setRejectReason('');
});
const remove = (id: number) => {
if (!window.confirm('Usunąć tę propozycję na stałe? Tej operacji nie można cofnąć.')) {
return;
}
void runAction(id, () => apiFetch(`/admin/articles/${id}`, { method: 'DELETE' }));
};
const channelPicker = (
<div className="admin-article-channel">
<span>Powiadom autora przez:</span>
<label className={channel === 'IN_APP' ? 'active' : ''}>
<input type="radio" name="channel" checked={channel === 'IN_APP'} onChange={() => setChannel('IN_APP')} />
<Icon name="bell" /> Wiadomość w aplikacji
</label>
<label className={channel === 'EMAIL' ? 'active' : ''}>
<input type="radio" name="channel" checked={channel === 'EMAIL'} onChange={() => setChannel('EMAIL')} />
<Icon name="mail" /> E-mail
</label>
</div>
);
return (
<div className="admin-articles">
<div className="admin-articles-filters">
{STATUS_FILTERS.map((filter) => (
<button
key={filter.key}
type="button"
className={statusFilter === filter.key ? 'active' : ''}
onClick={() => setStatusFilter(filter.key)}
>
{filter.label}
</button>
))}
</div>
{loading ? (
<div className="admin-card"><p className="admin-empty">Wczytywanie propozycji...</p></div>
) : items.length === 0 ? (
<div className="admin-card"><p className="admin-empty">Brak propozycji w tym widoku.</p></div>
) : (
<div className="admin-articles-list">
{items.map((item) => {
const expanded = expandedId === item.id;
return (
<article className={`admin-article-card status-${STATUS_CLASS[item.status]}`} key={item.id}>
<div className="admin-article-head">
<div className="admin-article-title">
<span className="admin-article-topic"><Icon name="document" /> {topicTitle(item.topicSlug)}</span>
<h3>{item.title}</h3>
<p>{item.excerpt}</p>
</div>
<em className={`admin-status ${STATUS_CLASS[item.status]}`}>{STATUS_LABEL[item.status]}</em>
</div>
<div className="admin-article-meta">
<span><Icon name="user" /> {item.authorName}{item.authorNick ? ` (@${item.authorNick})` : ''}</span>
<span><Icon name="clock" /> {item.readMinutes} min • {formatDate(item.createdAt)}</span>
{item.attachments.length > 0 && <span><Icon name="camera" /> {item.attachments.length} zał.</span>}
</div>
<div className="admin-article-contact">
<span className="admin-article-contact-label"><Icon name="phone" /> Kontakt:</span>
<strong>{item.contactName || item.authorName}</strong>
<a href={`mailto:${item.contactEmail || item.authorEmail}`}>{item.contactEmail || item.authorEmail}</a>
{item.contactPhone && <a href={`tel:${item.contactPhone.replace(/\s+/g, '')}`}>{item.contactPhone}</a>}
</div>
{item.status === 'REJECTED' && item.rejectionReason && (
<p className="admin-article-reject-note"><Icon name="warning" /> Powód odrzucenia: {item.rejectionReason}</p>
)}
{item.reviewedByEmail && (
<p className="admin-article-reviewed">Sprawdzone przez {item.reviewedByEmail} • {formatDate(item.reviewedAt)}</p>
)}
<button type="button" className="admin-article-toggle" onClick={() => setExpandedId(expanded ? null : item.id)}>
<Icon name="eye" /> {expanded ? 'Ukryj podgląd' : 'Pokaż pełną treść'}
</button>
{expanded && (
<div className="admin-article-preview">
<SafeHtml className="guide-user-content" html={item.content} />
{item.attachments.length > 0 && (
<div className="admin-article-attachments">
{item.attachments.map((src, index) => (
<img key={index} src={src} alt={`Załącznik ${index + 1}`} />
))}
</div>
)}
</div>
)}
{decision?.id === item.id ? (
<div className="admin-article-decision">
{decision.type === 'reject' && (
<textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="Powód odrzucenia (opcjonalny) - trafi do autora w powiadomieniu."
rows={2}
/>
)}
{channelPicker}
<div className="admin-article-actions">
<button type="button" className="admin-btn" onClick={() => setDecision(null)}>Anuluj</button>
<button
type="button"
className={`admin-btn ${decision.type === 'approve' ? 'approve' : 'reject'}`}
disabled={busyId === item.id}
onClick={() => confirmDecision(item.id, decision.type)}
>
{decision.type === 'approve' ? 'Potwierdź publikację i powiadom' : 'Potwierdź odrzucenie i powiadom'}
</button>
</div>
</div>
) : (
<div className="admin-article-actions">
{item.status === 'PUBLISHED' && (
<Link className="admin-btn view" to={articlePath(item.id)} target="_blank" rel="noopener">
Zobacz na stronie
</Link>
)}
{item.status !== 'PUBLISHED' && (
<button type="button" className="admin-btn approve" disabled={busyId === item.id} onClick={() => openDecision(item.id, 'approve')}>
<Icon name="check" /> Akceptuj i publikuj
</button>
)}
{item.status !== 'REJECTED' && (
<button type="button" className="admin-btn reject" disabled={busyId === item.id} onClick={() => openDecision(item.id, 'reject')}>
Odrzuć
</button>
)}
<button type="button" className="admin-btn danger" disabled={busyId === item.id} onClick={() => remove(item.id)}>
<Icon name="trash" /> Usuń
</button>
</div>
)}
</article>
);
})}
</div>
)}
</div>
);
}
+843
View File
@@ -0,0 +1,843 @@
// Treść poradników tematycznych na /poradniki/*.
//
// Kupno mieszkania ma własny, ręcznie napisany komponent (GuideBuyingPage w App.tsx).
// Pozostałe tematy - kredyty, wynajem, inwestowanie, remont, prawo i podatki - opisujemy tutaj
// jako czyste dane i renderujemy wspólnym szablonem GuideArticleTemplate. Dzięki temu wszystkie
// artykuły wyglądają identycznie (te same klasy CSS), a treść trzymamy w jednym miejscu.
import { ROUTES } from './routes';
// Blok treści wewnątrz rozdziału. Każdy typ ma odpowiednik w istniejącym poradniku kupna,
// więc szablon korzysta z tych samych klas CSS co GuideBuyingPage.
export type GuideBlock =
| { type: 'p'; text: string }
| { type: 'callout'; variant: 'tip' | 'warn' | 'info'; icon: string; title: string; text: string }
| { type: 'table'; head: string[]; rows: string[][] }
| { type: 'twocol'; cols: { tone: 'pro' | 'con' | 'market'; icon: string; title: string; items: string[] }[] }
| { type: 'list'; items: ({ label: string; text: string } | string)[] };
export type GuideChapter = { id: string; label: string; heading: string; blocks: GuideBlock[] };
export type GuideContent = {
// Kolor motywu dopasowany do koloru ikony tematu na liście poradników. Steruje akcentami
// w całym artykule (nagłówki, numery, checklista, przyciski, baner CTA) przez zmienne CSS.
accent: string;
accentDark: string;
badgeIcon: string;
badgeLabel: string;
title: string;
lead: string;
chips: { icon: string; text: string }[];
processTitle: string;
processHint: string;
process: { icon: string; title: string; text: string }[];
chapters: GuideChapter[];
checklistTitle: string;
checklistHint: string;
checklist: string[];
pdfTitle: string;
pdfSubtitle: string;
pdfFileName: string;
mistakes: { title: string; text: string }[];
faq: { q: string; a: string }[];
summary: string;
ctaTitle: string;
ctaText: string;
ctaLink: string;
ctaLabel: string;
asideCalculators: { to: string; title: string; sub: string }[];
};
// Standardowa nota pod podsumowaniem - materiał ma charakter informacyjny.
const DISCLAIMER =
'Ten poradnik ma charakter informacyjny i nie stanowi porady prawnej, podatkowej ani finansowej. Szczegóły potwierdzaj u doradcy kredytowego, notariusza, doradcy podatkowego lub prawnika.';
// ------------------------------------------------------------------ KREDYTY HIPOTECZNE
const credit: GuideContent = {
accent: '#9b5cff',
accentDark: '#7b3fe0',
badgeIcon: 'percent',
badgeLabel: 'Kredyty hipoteczne',
title: 'Kredyt hipoteczny krok po kroku',
lead: 'Uczciwy przewodnik po finansowaniu zakupu nieruchomości - od zdolności kredytowej i wkładu własnego, przez wybór oferty i RRSO, aż po podpisanie umowy i bezpieczną spłatę. Prostym językiem, z przykładami i listami kontrolnymi.',
chips: [
{ icon: 'check', text: 'Aktualna wiedza na 2026 rok' },
{ icon: 'calculator', text: 'Przykłady i kalkulatory' },
{ icon: 'list', text: 'Listy kontrolne i PDF' },
{ icon: 'shield', text: 'Oparte na zasadach rynku' },
],
processTitle: 'Droga do kredytu w 10 krokach',
processHint: 'Cały proces w skrócie. Poniżej rozwijamy każdy etap dokładnie.',
process: [
{ icon: 'chart', title: 'Ocena zdolności', text: 'Sprawdź, na jaką kwotę realnie Cię stać.' },
{ icon: 'coin', title: 'Wkład własny', text: 'Zbierz minimum 10-20% wartości nieruchomości.' },
{ icon: 'shield', title: 'Historia w BIK', text: 'Uporządkuj zobowiązania i zbuduj dobrą historię.' },
{ icon: 'search', title: 'Porównanie ofert', text: 'Zestaw RRSO, marżę i koszty w kilku bankach.' },
{ icon: 'document', title: 'Wniosek kredytowy', text: 'Złóż wnioski do 2-3 banków równolegle.' },
{ icon: 'house', title: 'Wycena nieruchomości', text: 'Bank zleca operat szacunkowy.' },
{ icon: 'clock', title: 'Decyzja kredytowa', text: 'Analityk weryfikuje dochody i zabezpieczenie.' },
{ icon: 'scale', title: 'Umowa kredytowa', text: 'Przeczytaj i podpisz umowę z bankiem.' },
{ icon: 'key', title: 'Uruchomienie kredytu', text: 'Bank wypłaca środki na konto sprzedającego.' },
{ icon: 'calendar', title: 'Spłata rat', text: 'Pilnuj terminów i rozważ nadpłaty.' },
],
chapters: [
{
id: 'czym-jest',
label: 'Czym jest kredyt hipoteczny?',
heading: 'Czym jest kredyt hipoteczny?',
blocks: [
{ type: 'p', text: 'Kredyt hipoteczny to długoterminowe zobowiązanie zabezpieczone hipoteką na nieruchomości. Bank pożycza Ci brakującą część ceny, a w zamian wpisuje się do księgi wieczystej. Jeśli przestaniesz spłacać, może dochodzić swoich praw z nieruchomości - dlatego oprocentowanie jest niższe niż przy zwykłej pożyczce.' },
{ type: 'p', text: 'To najczęściej najtańszy sposób sfinansowania mieszkania, ale i najdłuższy - umowy zawiera się zwykle na 20-30 lat. Przez ten czas całkowity koszt kredytu (odsetki, prowizja, ubezpieczenia) potrafi sięgnąć nawet równowartości pożyczonej kwoty, dlatego liczy się każdy szczegół oferty.' },
{ type: 'callout', variant: 'info', icon: 'document', title: 'Rata stała czy zmienna?', text: 'Rata zmienna zależy od stopy referencyjnej (WIBOR/WIRON) i marży banku - zmienia się w czasie. Rata okresowo stała jest niezmienna przez pierwsze lata (najczęściej 5), co daje przewidywalność kosztem nieco wyższego startu. Dla wielu osób spokój okresowo stałej raty jest wart tej różnicy.' },
],
},
{
id: 'zdolnosc',
label: 'Zdolność kredytowa',
heading: 'Jak banki liczą zdolność kredytową?',
blocks: [
{ type: 'p', text: 'Zdolność kredytowa to maksymalna kwota, jaką bank jest gotów Ci pożyczyć przy Twoich dochodach i wydatkach. Bank sumuje stałe dochody, odejmuje koszty utrzymania, raty innych kredytów, limity na kartach i liczbę osób na utrzymaniu, a następnie dolicza bufor na wypadek wzrostu stóp procentowych.' },
{ type: 'p', text: 'Każdy bank liczy inaczej, dlatego różnice w oferowanej kwocie potrafią sięgać kilkudziesięciu tysięcy złotych. Zanim złożysz wnioski, uporządkuj finanse: spłać drobne zobowiązania, zamknij nieużywane karty i limity, a jeśli masz nieregularne dochody - zadbaj o ich udokumentowanie.' },
{ type: 'twocol', cols: [
{ tone: 'pro', icon: 'check', title: 'Podnosi zdolność', items: ['Stabilne, udokumentowane dochody', 'Umowa na czas nieokreślony', 'Brak innych kredytów i limitów', 'Dłuższy okres kredytowania', 'Wspólny wniosek z drugą osobą'] },
{ tone: 'con', icon: 'warning', title: 'Obniża zdolność', items: ['Karty kredytowe i limity w koncie', 'Kredyty konsumpcyjne i raty', 'Osoby na utrzymaniu', 'Krótka lub negatywna historia w BIK', 'Nieregularne lub gotówkowe dochody'] },
] },
{ type: 'callout', variant: 'tip', icon: 'calculator', title: 'Policz orientacyjnie', text: 'Skorzystaj z kalkulatora zdolności kredytowej po prawej stronie. To dobry punkt startu, ale ostateczną kwotę zawsze potwierdza bank na podstawie dokumentów i wewnętrznych procedur.' },
],
},
{
id: 'wklad',
label: 'Wkład własny',
heading: 'Ile wynosi wkład własny?',
blocks: [
{ type: 'p', text: 'Wkład własny to część wartości nieruchomości, którą pokrywasz samodzielnie. Standardowo banki wymagają co najmniej 20%, choć część akceptuje 10% pod warunkiem dodatkowego ubezpieczenia niskiego wkładu. Im wyższy wkład, tym niższa marża, mniejsza rata i większa szansa na pozytywną decyzję.' },
{ type: 'p', text: 'Pamiętaj, że wkład własny to nie to samo co gotówka na formalności. Oprócz niego potrzebujesz środków na podatek, taksę notarialną, wycenę, prowizję i ewentualny remont. Zaplanuj te kwoty osobno, żeby nie zabrakło Ci pieniędzy tuż przed transakcją.' },
{ type: 'callout', variant: 'warn', icon: 'coin', title: 'Nie zostawaj bez rezerwy', text: 'Nie przeznaczaj na wkład własny wszystkich oszczędności. Poduszka na kilka rat i nieprzewidziane wydatki to element bezpiecznego finansowania, a nie luksus.' },
],
},
{
id: 'rrso',
label: 'RRSO i koszty',
heading: 'RRSO, marża i całkowity koszt kredytu',
blocks: [
{ type: 'p', text: 'Nie porównuj ofert po samej racie ani po samym oprocentowaniu. Najuczciwszy wskaźnik to RRSO - rzeczywista roczna stopa oprocentowania, która ujmuje odsetki, prowizję i obowiązkowe koszty. Dwie oferty z tą samą ratą mogą mieć różne RRSO, bo różnią się prowizją i ubezpieczeniami.' },
{ type: 'table', head: ['Element kosztu', 'Czego dotyczy', 'Uwagi'], rows: [
['Marża banku', 'Stały składnik oprocentowania', 'Najważniejszy element - negocjowalny, obowiązuje przez cały okres.'],
['Stopa referencyjna', 'Zmienna część oprocentowania', 'WIBOR/WIRON - zmienia się z rynkiem przy racie zmiennej.'],
['Prowizja za udzielenie', 'Opłata jednorazowa', 'Bywa zerowa w zamian za droższe ubezpieczenia.'],
['Ubezpieczenia', 'Nieruchomości, na życie, pomostowe', 'Część obowiązkowa, część to warunek niższej marży.'],
['Wycena nieruchomości', 'Operat szacunkowy', 'Jednorazowy koszt na starcie procesu.'],
] },
{ type: 'callout', variant: 'tip', icon: 'spark', title: 'Negocjuj marżę i „cross-sell”', text: 'Bank obniża marżę w zamian za konto z wpływami, kartę czy ubezpieczenie. Policz, czy te produkty faktycznie się opłacają - czasem niższa marża oznacza wyższe koszty gdzie indziej.' },
],
},
{
id: 'oferta',
label: 'Wybór oferty',
heading: 'Jak wybrać i porównać oferty banków?',
blocks: [
{ type: 'p', text: 'Zbierz oferty z kilku banków w tym samym momencie, bo warunki zmieniają się z tygodnia na tydzień. Poproś o symulacje na tę samą kwotę, okres i wkład własny, żeby liczby były porównywalne. Zwróć uwagę na RRSO, wysokość marży, koszt wcześniejszej spłaty i wymagane produkty dodatkowe.' },
{ type: 'p', text: 'Możesz działać samodzielnie lub skorzystać z doradcy kredytowego. Dobry doradca zna procedury i przyspiesza proces, ale zawsze zapytaj, z iloma bankami współpracuje i jak jest wynagradzany. Ostateczną decyzję i tak podejmujesz Ty - to Ty będziesz spłacać ten kredyt.' },
],
},
{
id: 'wniosek',
label: 'Wniosek i dokumenty',
heading: 'Wniosek kredytowy i potrzebne dokumenty',
blocks: [
{ type: 'p', text: 'Składając wniosek, przygotuj komplet dokumentów - im mniej braków, tym szybsza decyzja. Warto złożyć wnioski do 2-3 banków równolegle, żeby mieć wybór i plan awaryjny, gdyby jeden bank odmówił lub zaproponował gorsze warunki.' },
{ type: 'list', items: [
{ label: 'Dokument tożsamości', text: 'dowód osobisty, czasem dodatkowy dokument.' },
{ label: 'Dochody', text: 'zaświadczenie od pracodawcy, PIT, wyciągi z konta lub dokumenty firmy.' },
{ label: 'Nieruchomość', text: 'umowa przedwstępna, numer księgi wieczystej, dane sprzedającego.' },
{ label: 'Zobowiązania', text: 'informacje o innych kredytach, limitach i kartach.' },
] },
{ type: 'callout', variant: 'info', icon: 'clock', title: 'Ile to trwa?', text: 'Od złożenia kompletnego wniosku do decyzji mija zwykle 2-4 tygodnie, a cały proces do uruchomienia kredytu to często 4-8 tygodni. Uwzględnij ten czas w umowie przedwstępnej ze sprzedającym.' },
],
},
{
id: 'umowa',
label: 'Umowa kredytowa',
heading: 'Na co uważać w umowie kredytowej?',
blocks: [
{ type: 'p', text: 'Umowa kredytowa to dokument na lata - przeczytaj ją w całości, także załączniki i tabelę opłat. Sprawdź wysokość marży i sposób ustalania oprocentowania, harmonogram spłat, koszt wcześniejszej spłaty, warunki ubezpieczenia pomostowego oraz sytuacje, w których bank może wypowiedzieć umowę.' },
{ type: 'callout', variant: 'warn', icon: 'shield', title: 'Ubezpieczenie pomostowe', text: 'Do czasu wpisu hipoteki do księgi wieczystej bank pobiera podwyższoną marżę (ubezpieczenie pomostowe). Po wpisie masz prawo do jej obniżenia i zwrotu nadpłaconej części - dopilnuj, aby bank to uwzględnił.' },
],
},
{
id: 'nadplata',
label: 'Nadpłata i refinansowanie',
heading: 'Nadpłata, refinansowanie i wcześniejsza spłata',
blocks: [
{ type: 'p', text: 'Nadpłata kredytu skraca okres spłaty lub obniża ratę i potrafi zaoszczędzić dużo na odsetkach - zwłaszcza w pierwszych latach, gdy odsetki stanowią większość raty. Sprawdź w umowie, czy w pierwszych latach nie obowiązuje prowizja za wcześniejszą spłatę.' },
{ type: 'p', text: 'Refinansowanie to przeniesienie kredytu do innego banku na lepszych warunkach. Ma sens, gdy różnica w marży realnie przewyższa koszty przeniesienia (wycena, prowizje, formalności). Raz na jakiś czas warto sprawdzić, czy Twój kredyt nadal jest konkurencyjny.' },
],
},
],
checklistTitle: 'Checklista przed złożeniem wniosku',
checklistHint: 'Wydrukuj lub pobierz w PDF i sprawdź, zanim pójdziesz do banku.',
checklist: [
'Sprawdź swoją historię i raport w BIK, wyjaśnij ewentualne błędy.',
'Spłać drobne kredyty i zamknij nieużywane karty oraz limity.',
'Uzbieraj wkład własny oraz osobno środki na koszty transakcyjne.',
'Policz zdolność kredytową w kilku bankach lub u doradcy.',
'Zbierz komplet dokumentów dochodowych i dotyczących nieruchomości.',
'Porównaj oferty po RRSO, marży i koszcie wcześniejszej spłaty.',
'Sprawdź, jakie produkty dodatkowe są warunkiem niższej marży.',
'Ustal, czy rata ma być stała czy zmienna i przelicz oba warianty.',
'Uwzględnij czas na decyzję banku w umowie przedwstępnej.',
'Zaplanuj poduszkę finansową na kilka rat po zakupie.',
],
pdfTitle: 'Lista kontrolna przed kredytem hipotecznym',
pdfSubtitle: '10 rzeczy, które warto zrobić przed złożeniem wniosku',
pdfFileName: 'lista-kontrolna-kredyt-hipoteczny.pdf',
mistakes: [
{ title: 'Porównywanie ofert po samej racie', text: 'Niska rata może kryć wysoką prowizję i drogie ubezpieczenia. Dopiero RRSO pokazuje realny koszt kredytu.' },
{ title: 'Brak rezerwy finansowej', text: 'Kredyt na maksimum zdolności nie zostawia poduszki na wzrost rat, remont czy nagłe wydatki.' },
{ title: 'Ignorowanie historii w BIK', text: 'Zaległości i chaotyczne zobowiązania obniżają zdolność. Warto uporządkować finanse kilka miesięcy przed wnioskiem.' },
{ title: 'Zaciąganie kredytów tuż przed wnioskiem', text: 'Nowa karta, zakupy na raty czy limit w koncie potrafią zepsuć zdolność w kluczowym momencie.' },
{ title: 'Podpisywanie umowy bez czytania', text: 'Warunki wcześniejszej spłaty, ubezpieczenia i marża pomostowa to zapisy, które realnie kosztują.' },
{ title: 'Zapominanie o obniżce marży po wpisie hipoteki', text: 'Po wpisie do księgi wieczystej ubezpieczenie pomostowe powinno zniknąć - trzeba tego dopilnować.' },
],
faq: [
{ q: 'Jaki wkład własny jest potrzebny do kredytu?', a: 'Standardowo banki wymagają co najmniej 20% wartości nieruchomości, ale część akceptuje 10% pod warunkiem dodatkowego ubezpieczenia niskiego wkładu. Im wyższy wkład własny, tym niższa marża i lepsze warunki. Pamiętaj, że oprócz wkładu potrzebujesz też środków na koszty transakcyjne.' },
{ q: 'Rata stała czy zmienna - co wybrać?', a: 'Rata zmienna zmienia się wraz ze stopą referencyjną, więc może rosnąć i maleć. Rata okresowo stała jest niezmienna przez pierwsze lata (najczęściej 5) i daje przewidywalność kosztem nieco wyższego startu. Jeśli cenisz spokój i stały budżet, okresowo stała rata bywa lepszym wyborem.' },
{ q: 'Czy warto korzystać z doradcy kredytowego?', a: 'Doradca zna procedury wielu banków i potrafi przyspieszyć proces oraz znaleźć ofertę, o której byś nie pomyślał. Zapytaj jednak, z iloma bankami współpracuje i jak jest wynagradzany. Nawet z doradcą warto samodzielnie porównać RRSO wybranych ofert.' },
{ q: 'Ile trwa uzyskanie kredytu hipotecznego?', a: 'Od złożenia kompletnego wniosku do decyzji mija zwykle 2-4 tygodnie, a cały proces do uruchomienia kredytu to często 4-8 tygodni. Dochodzi analiza wniosku, wycena nieruchomości i formalności. Ten czas warto uwzględnić w umowie przedwstępnej ze sprzedającym.' },
{ q: 'Czy mogę nadpłacać kredyt?', a: 'Tak, nadpłata skraca okres spłaty lub obniża ratę i oszczędza na odsetkach - najbardziej w pierwszych latach. Sprawdź w umowie, czy w początkowym okresie nie obowiązuje prowizja za wcześniejszą spłatę, bo może wpłynąć na opłacalność.' },
{ q: 'Co to jest RRSO?', a: 'RRSO to rzeczywista roczna stopa oprocentowania - wskaźnik, który ujmuje odsetki, prowizję i obowiązkowe koszty w jednej liczbie. Dzięki niemu porównasz oferty uczciwie, bo dwa kredyty z tą samą ratą mogą mieć różne RRSO przez różnice w prowizji i ubezpieczeniach.' },
],
summary: 'Kredyt hipoteczny to zobowiązanie na lata, ale dobrze przygotowany staje się bezpiecznym narzędziem. Zadbaj o zdolność i wkład własny, porównuj oferty po RRSO, czytaj umowę i zostaw sobie rezerwę. Uporządkowane finanse i chłodna kalkulacja są tu warte więcej niż pośpiech.',
ctaTitle: 'Sprawdź, na jaki kredyt Cię stać',
ctaText: 'Policz orientacyjną zdolność kredytową i przeglądaj oferty dopasowane do budżetu.',
ctaLink: ROUTES.creditCalculator,
ctaLabel: 'Otwórz kalkulator zdolności',
asideCalculators: [
{ to: ROUTES.creditCalculator, title: 'Kalkulator zdolności kredytowej', sub: 'Sprawdź, na jaki kredyt Cię stać' },
{ to: ROUTES.valuation, title: 'Kalkulator wyceny mieszkania', sub: 'Oszacuj wartość nieruchomości' },
{ to: ROUTES.priceHistory, title: 'Historia cen', sub: 'Zobacz, jak zmieniały się ceny' },
{ to: ROUTES.buy, title: 'Oferty mieszkań', sub: 'Znajdź nieruchomość na kredyt' },
],
};
// ------------------------------------------------------------------ WYNAJEM
const rent: GuideContent = {
accent: '#13a360',
accentDark: '#0e7d49',
badgeIcon: 'key',
badgeLabel: 'Wynajem',
title: 'Wynajem mieszkania - poradnik dla obu stron',
lead: 'Praktyczny przewodnik dla najemców i wynajmujących - od wyboru mieszkania i sprawdzenia właściciela, przez rodzaje umów najmu, kaucję i protokół, aż po rozwiązanie umowy i zwrot mieszkania. Prostym językiem, z listami kontrolnymi.',
chips: [
{ icon: 'check', text: 'Dla najemcy i wynajmującego' },
{ icon: 'document', text: 'Rodzaje umów najmu' },
{ icon: 'list', text: 'Protokół i listy kontrolne' },
{ icon: 'shield', text: 'Bezpieczna transakcja' },
],
processTitle: 'Bezpieczny wynajem w 8 krokach',
processHint: 'Cały proces w skrócie - z perspektywy najemcy i wynajmującego.',
process: [
{ icon: 'search', title: 'Określ potrzeby', text: 'Ustal budżet, lokalizację i wymagania.' },
{ icon: 'eye', title: 'Obejrzyj mieszkanie', text: 'Sprawdź stan techniczny i okolicę.' },
{ icon: 'shield', title: 'Zweryfikuj drugą stronę', text: 'Potwierdź tożsamość i tytuł prawny do lokalu.' },
{ icon: 'document', title: 'Wybierz rodzaj umowy', text: 'Zwykła, okazjonalna lub instytucjonalna.' },
{ icon: 'scale', title: 'Spisz umowę', text: 'Określ czynsz, kaucję i obowiązki stron.' },
{ icon: 'list', title: 'Protokół zdawczy', text: 'Opisz stan i liczniki przy przekazaniu kluczy.' },
{ icon: 'key', title: 'Przekazanie mieszkania', text: 'Wydanie kluczy i wpłata kaucji.' },
{ icon: 'calendar', title: 'Trwanie i zakończenie', text: 'Rozliczenia, wypowiedzenie i zwrot kaucji.' },
],
chapters: [
{
id: 'rodzaje-umow',
label: 'Rodzaje umów najmu',
heading: 'Rodzaje umów najmu - którą wybrać?',
blocks: [
{ type: 'p', text: 'W Polsce funkcjonują trzy główne formy najmu mieszkania. Wybór wpływa na poziom ochrony obu stron, formalności i sposób ewentualnej eksmisji. Warto znać różnice, zanim podpiszesz cokolwiek - to jedna z najważniejszych decyzji przy wynajmie.' },
{ type: 'twocol', cols: [
{ tone: 'market', icon: 'document', title: 'Najem zwykły', items: ['Najprostszy w zawarciu', 'Silna ochrona najemcy', 'Trudniejsza eksmisja', 'Dobry dla najemcy, ryzykowny dla właściciela'] },
{ tone: 'pro', icon: 'shield', title: 'Najem okazjonalny', items: ['Wymaga aktu notarialnego o poddaniu się egzekucji', 'Wskazanie lokalu zastępczego', 'Zgłoszenie do urzędu skarbowego', 'Większe bezpieczeństwo właściciela'] },
] },
{ type: 'callout', variant: 'info', icon: 'letter', title: 'Najem instytucjonalny', text: 'To odmiana dla podmiotów prowadzących wynajem zawodowo (firm). Podobnie jak najem okazjonalny ułatwia odzyskanie lokalu, ale przeznaczony jest dla wynajmujących będących przedsiębiorcami, nie dla osób prywatnych.' },
],
},
{
id: 'najemca',
label: 'Poradnik najemcy',
heading: 'Wynajmujesz mieszkanie? Na co uważać?',
blocks: [
{ type: 'p', text: 'Zanim podpiszesz umowę, potwierdź, że osoba podająca się za właściciela ma prawo wynająć lokal. Poproś o dokument tożsamości i podstawę prawną (np. akt własności lub wpis w księdze wieczystej). To najprostsza ochrona przed oszustem, który wynajmuje cudze mieszkanie.' },
{ type: 'p', text: 'Dokładnie obejrzyj mieszkanie i sprawdź, co dokładnie wchodzi w czynsz. Ustal, kto płaci za media, jak rozliczana jest kaucja i w jakich sytuacjach właściciel może ją zatrzymać. Wszystko, co ustnie obiecane, musi znaleźć się w umowie i w protokole - inaczej trudno tego dochodzić.' },
{ type: 'callout', variant: 'warn', icon: 'warning', title: 'Uwaga na oszustwa', text: 'Nigdy nie wpłacaj kaucji ani „rezerwacji” przed obejrzeniem mieszkania i podpisaniem umowy. Zaniżona cena, presja czasu i brak możliwości spotkania to typowe sygnały ostrzegawcze.' },
],
},
{
id: 'wynajmujacy',
label: 'Poradnik właściciela',
heading: 'Wynajmujesz swoje mieszkanie? O czym pamiętać?',
blocks: [
{ type: 'p', text: 'Zweryfikuj najemcę: potwierdź tożsamość, a przy dłuższym najmie zapytaj o źródło i stabilność dochodów. Rozważ najem okazjonalny, który realnie ułatwia odzyskanie lokalu, gdy najemca przestaje płacić i nie chce się wyprowadzić.' },
{ type: 'p', text: 'Zadbaj o rzetelną umowę i protokół zdawczo-odbiorczy ze zdjęciami oraz stanami liczników. To Twój najlepszy dowód w razie sporu o zniszczenia lub rozliczenie kaucji. Pamiętaj też o rozliczeniu podatku od przychodów z najmu.' },
{ type: 'callout', variant: 'tip', icon: 'percent', title: 'Podatek od najmu', text: 'Przychody z najmu prywatnego rozlicza się ryczałtem od przychodów ewidencjonowanych. Obowiązują dwie stawki zależne od rocznej kwoty przychodu. Szczegóły i aktualne progi potwierdź u doradcy podatkowego lub w urzędzie skarbowym.' },
],
},
{
id: 'umowa',
label: 'Co musi być w umowie',
heading: 'Co powinna zawierać dobra umowa najmu?',
blocks: [
{ type: 'p', text: 'Dobra umowa chroni obie strony, bo z góry rozstrzyga sytuacje, które w innym razie kończą się sporem. Im precyzyjniejsze zapisy, tym mniej pola do nieporozumień - zwłaszcza przy rozliczeniach i zwrocie kaucji.' },
{ type: 'list', items: [
{ label: 'Strony i lokal', text: 'dane najemcy i wynajmującego oraz dokładny adres i opis mieszkania.' },
{ label: 'Czynsz i media', text: 'wysokość czynszu, co obejmuje i jak rozliczane są opłaty za media.' },
{ label: 'Kaucja', text: 'wysokość, sposób wpłaty i warunki zwrotu po zakończeniu najmu.' },
{ label: 'Czas trwania', text: 'na czas określony czy nieokreślony oraz zasady wypowiedzenia.' },
{ label: 'Stan i wyposażenie', text: 'odwołanie do protokołu i listy wyposażenia.' },
{ label: 'Obowiązki stron', text: 'kto odpowiada za drobne naprawy, a kto za poważniejsze awarie.' },
] },
],
},
{
id: 'kaucja',
label: 'Kaucja i protokół',
heading: 'Kaucja, protokół i liczniki',
blocks: [
{ type: 'p', text: 'Kaucja zabezpiecza właściciela przed zniszczeniami i zaległościami. Zwykle wynosi równowartość jedno- lub dwumiesięcznego czynszu. Po zakończeniu najmu podlega zwrotowi, pomniejszona jedynie o udokumentowane szkody wykraczające poza normalne zużycie.' },
{ type: 'p', text: 'Protokół zdawczo-odbiorczy to fundament uczciwego rozliczenia. Spiszcie stan każdego pomieszczenia, wyposażenie, usterki i stany wszystkich liczników. Dołączcie zdjęcia z datą. Ten sam protokół wypełnijcie przy zwrocie mieszkania - różnice pokażą, co faktycznie się zmieniło.' },
{ type: 'callout', variant: 'warn', icon: 'list', title: 'Normalne zużycie to nie szkoda', text: 'Wytarty dywan po latach czy drobne ślady na ścianie to naturalne zużycie, którego nie można potrącić z kaucji. Potrącenia dotyczą realnych zniszczeń - dlatego tak ważny jest dokładny protokół na wejściu.' },
],
},
{
id: 'wypowiedzenie',
label: 'Zakończenie najmu',
heading: 'Wypowiedzenie i zwrot mieszkania',
blocks: [
{ type: 'p', text: 'Zasady wypowiedzenia zależą od rodzaju umowy i tego, czy zawarto ją na czas określony, czy nieokreślony. Umowa powinna jasno określać okres wypowiedzenia i sytuacje, w których każda ze stron może ją zakończyć. Wypowiedzenie najlepiej złożyć na piśmie za potwierdzeniem.' },
{ type: 'p', text: 'Przy zwrocie mieszkania ponownie spiszcie protokół i stany liczników, a właściciel rozlicza kaucję. Uczciwe zakończenie - bez zaległości i z mieszkaniem w umówionym stanie - leży w interesie obu stron i buduje dobre referencje na przyszłość.' },
],
},
],
checklistTitle: 'Checklista bezpiecznego wynajmu',
checklistHint: 'Dla najemcy i wynajmującego. Wydrukuj lub pobierz w PDF.',
checklist: [
'Potwierdź tożsamość drugiej strony i tytuł prawny do lokalu.',
'Obejrzyj mieszkanie osobiście, także wieczorem i przy dziennym świetle.',
'Ustal dokładnie, co obejmuje czynsz i jak rozliczane są media.',
'Sprawdź wysokość kaucji i warunki jej zwrotu.',
'Wybierz świadomie rodzaj umowy (zwykła, okazjonalna, instytucjonalna).',
'Spisz szczegółową umowę na piśmie, bez ustnych obietnic.',
'Wypełnij protokół zdawczo-odbiorczy ze zdjęciami i stanami liczników.',
'Zapisz listę wyposażenia i stan każdego pomieszczenia.',
'Ustal zasady drobnych napraw i zgłaszania awarii.',
'Nigdy nie płać kaucji przed obejrzeniem lokalu i podpisaniem umowy.',
],
pdfTitle: 'Lista kontrolna bezpiecznego wynajmu',
pdfSubtitle: '10 rzeczy do sprawdzenia przez najemcę i wynajmującego',
pdfFileName: 'lista-kontrolna-wynajem.pdf',
mistakes: [
{ title: 'Brak weryfikacji drugiej strony', text: 'Najemca nie sprawdza, czy wynajmujący ma prawo do lokalu, a właściciel nie weryfikuje najemcy. To otwiera drogę do oszustw.' },
{ title: 'Umowa ustna lub „na gębę”', text: 'Bez pisemnej umowy trudno dochodzić czegokolwiek - od czynszu po zwrot kaucji.' },
{ title: 'Pominięcie protokołu', text: 'Bez protokołu ze zdjęciami rozliczenie kaucji zamienia się w słowo przeciw słowu.' },
{ title: 'Niejasne rozliczenie mediów', text: 'Brak zapisu, kto i za co płaci, to najczęstsze źródło konfliktów w trakcie najmu.' },
{ title: 'Wpłata kaucji przed obejrzeniem', text: 'Płacenie „rezerwacji” zdalnie, bez umowy i wizyty, to klasyczny schemat oszustwa.' },
{ title: 'Ignorowanie podatku od najmu', text: 'Właściciel, który nie rozlicza przychodów z najmu, naraża się na zaległości i odsetki.' },
],
faq: [
{ q: 'Czym różni się najem okazjonalny od zwykłego?', a: 'Najem okazjonalny wymaga aktu notarialnego, w którym najemca dobrowolnie poddaje się egzekucji, wskazania lokalu zastępczego oraz zgłoszenia do urzędu skarbowego. W zamian daje właścicielowi znacznie łatwiejsze odzyskanie mieszkania, gdy najemca nie płaci i nie chce się wyprowadzić. Najem zwykły jest prostszy w zawarciu, ale mocniej chroni najemcę.' },
{ q: 'Ile wynosi kaucja i kiedy się ją odzyskuje?', a: 'Kaucja to zwykle równowartość jedno- lub dwumiesięcznego czynszu. Zwraca się ją po zakończeniu najmu, pomniejszoną jedynie o udokumentowane szkody wykraczające poza normalne zużycie oraz ewentualne zaległości. Dokładny protokół na wejściu i wyjściu chroni tu obie strony.' },
{ q: 'Kto płaci za naprawy w wynajmowanym mieszkaniu?', a: 'Zasadniczo drobne bieżące naprawy związane z codziennym używaniem obciążają najemcę, a poważniejsze awarie i naprawy wynikające ze zużycia - właściciela. Warto jednak dokładnie opisać podział w umowie, żeby uniknąć sporów przy każdej usterce.' },
{ q: 'Jak rozlicza się podatek od najmu?', a: 'Najem prywatny rozlicza się ryczałtem od przychodów ewidencjonowanych, z dwiema stawkami zależnymi od rocznej kwoty przychodu. Podatek płaci wynajmujący. Aktualne stawki, progi i terminy potwierdź u doradcy podatkowego lub w urzędzie skarbowym.' },
{ q: 'Czy właściciel może wejść do mieszkania bez zgody najemcy?', a: 'Nie. W czasie trwania najmu to najemca faktycznie zajmuje lokal, a właściciel nie może wchodzić bez uzgodnienia, poza sytuacjami awaryjnymi. Zasady wizyt i przeglądów najlepiej opisać w umowie, aby były jasne dla obu stron.' },
{ q: 'Jak wypowiedzieć umowę najmu?', a: 'Sposób i termin wypowiedzenia zależą od rodzaju umowy oraz od tego, czy zawarto ją na czas określony, czy nieokreślony. Umowa powinna wprost określać okres wypowiedzenia i dopuszczalne przyczyny. Wypowiedzenie warto złożyć na piśmie za potwierdzeniem odbioru.' },
],
summary: 'Udany wynajem opiera się na trzech filarach: weryfikacji drugiej strony, dobrej umowie i rzetelnym protokole. Niezależnie od tego, czy wynajmujesz, czy szukasz mieszkania, spisuj ustalenia na piśmie, dokumentuj stan lokalu i nie działaj pod presją czasu. To najprostsza droga do najmu bez sporów.',
ctaTitle: 'Szukasz mieszkania na wynajem?',
ctaText: 'Przeglądaj aktualne oferty najmu dopasowane do Twojego budżetu i lokalizacji.',
ctaLink: ROUTES.rent,
ctaLabel: 'Zobacz oferty wynajmu',
asideCalculators: [
{ to: ROUTES.rent, title: 'Oferty wynajmu', sub: 'Znajdź mieszkanie do wynajęcia' },
{ to: ROUTES.priceHistory, title: 'Historia cen', sub: 'Sprawdź poziom stawek w okolicy' },
{ to: ROUTES.districtRanking, title: 'Ranking dzielnic', sub: 'Porównaj lokalizacje' },
{ to: ROUTES.map, title: 'Mapa ofert', sub: 'Zobacz wynajem na mapie' },
],
};
// ------------------------------------------------------------------ INWESTOWANIE
const investing: GuideContent = {
accent: '#3f7df1',
accentDark: '#2c62c6',
badgeIcon: 'chart',
badgeLabel: 'Inwestowanie',
title: 'Inwestowanie w nieruchomości - od podstaw do strategii',
lead: 'Przewodnik po inwestowaniu w nieruchomości na wynajem - od liczenia rentowności i doboru lokalizacji, przez strategie najmu, po ryzyka i podatki. Prostym językiem, z konkretnymi wskaźnikami i przykładami, bez obietnic łatwego zysku.',
chips: [
{ icon: 'check', text: 'Aktualna wiedza na 2026 rok' },
{ icon: 'calculator', text: 'Wskaźniki rentowności' },
{ icon: 'chart', text: 'Strategie i analizy' },
{ icon: 'shield', text: 'Świadome zarządzanie ryzykiem' },
],
processTitle: 'Proces inwestycji w 8 krokach',
processHint: 'Od strategii do zarabiającej nieruchomości - w skrócie.',
process: [
{ icon: 'target', title: 'Określ cel', text: 'Zdecyduj: dochód z najmu czy wzrost wartości.' },
{ icon: 'coin', title: 'Zbadaj kapitał', text: 'Policz środki własne i możliwości kredytu.' },
{ icon: 'pin', title: 'Wybierz rynek', text: 'Analizuj miasto, dzielnicę i popyt na najem.' },
{ icon: 'calculator', title: 'Policz rentowność', text: 'Oszacuj ROI i realny zysk netto.' },
{ icon: 'search', title: 'Znajdź okazję', text: 'Szukaj mieszkań z potencjałem i poniżej rynku.' },
{ icon: 'hammer', title: 'Przygotuj lokal', text: 'Wykończ lub odśwież pod grupę najemców.' },
{ icon: 'key', title: 'Wynajmij', text: 'Dobierz strategię najmu i rzetelnego najemcę.' },
{ icon: 'trend', title: 'Zarządzaj', text: 'Monitoruj koszty, wynik i wartość nieruchomości.' },
],
chapters: [
{
id: 'dlaczego',
label: 'Dlaczego nieruchomości?',
heading: 'Dlaczego inwestować w nieruchomości?',
blocks: [
{ type: 'p', text: 'Nieruchomości od lat uchodzą za sposób na ochronę kapitału i budowanie majątku. Dają dwa źródła korzyści naraz: bieżący dochód z najmu oraz potencjalny wzrost wartości w czasie. To aktywo namacalne, mniej zmienne z dnia na dzień niż giełda, a przy tym można je finansować kredytem, czyli inwestować częściowo cudzym kapitałem.' },
{ type: 'p', text: 'To jednak nie jest inwestycja bez ryzyka ani „pewny zysk”. Ceny potrafią spadać, najem bywa nieregularny, a nieruchomość jest mało płynna - nie sprzedasz jej w jeden dzień. Dobre wyniki wynikają z liczb i cierpliwości, a nie z emocji czy modnych haseł.' },
{ type: 'callout', variant: 'warn', icon: 'warning', title: 'Nie ma zysku bez ryzyka', text: 'Traktuj inwestycję jak biznes: licz na chłodno, zakładaj gorsze scenariusze i nie inwestuj pieniędzy, których możesz pilnie potrzebować. Dźwignia kredytowa zwiększa nie tylko potencjalny zysk, ale i stratę.' },
],
},
{
id: 'strategie',
label: 'Strategie inwestowania',
heading: 'Główne strategie inwestowania',
blocks: [
{ type: 'p', text: 'Nie ma jednej słusznej strategii - jest ta, która pasuje do Twojego kapitału, czasu i akceptacji ryzyka. Poniżej najczęstsze podejścia. Wiele osób łączy kilka z nich w miarę zdobywania doświadczenia.' },
{ type: 'table', head: ['Strategia', 'Na czym polega', 'Uwagi'], rows: [
['Najem długoterminowy', 'Wynajem na wiele miesięcy lub lat', 'Stabilny dochód, mniej pracy, niższa rentowność.'],
['Najem krótkoterminowy', 'Wynajem na doby (turystyka, biznes)', 'Wyższy potencjał, ale dużo pracy i sezonowość.'],
['Najem na pokoje', 'Wynajem osobno każdego pokoju', 'Wyższa rentowność, większa rotacja i zarządzanie.'],
['Kup, odśwież, sprzedaj', 'Zakup poniżej rynku i sprzedaż po remoncie', 'Zysk jednorazowy, ryzyko kosztów i czasu remontu.'],
['Zakup pod wzrost wartości', 'Trzymanie w rozwijającej się lokalizacji', 'Zysk odroczony, zależny od rynku i cierpliwości.'],
] },
{ type: 'twocol', cols: [
{ tone: 'pro', icon: 'check', title: 'Najem długoterminowy', items: ['Przewidywalny, stały dochód', 'Mniej codziennej obsługi', 'Niższe ryzyko pustostanu', 'Uwaga: niższa rentowność niż najem krótkoterminowy'] },
{ tone: 'con', icon: 'clock', title: 'Najem krótkoterminowy', items: ['Wyższy potencjalny przychód', 'Elastyczność w korzystaniu z lokalu', 'Uwaga: sezonowość i puste okresy', 'Uwaga: dużo pracy, sprzątanie, obsługa gości'] },
] },
],
},
{
id: 'rentownosc',
label: 'Jak liczyć rentowność',
heading: 'Jak policzyć rentowność inwestycji?',
blocks: [
{ type: 'p', text: 'Podstawowy błąd początkujących to liczenie „na oko” tylko z ceny i czynszu. Rzetelna kalkulacja uwzględnia wszystkie koszty: podatki, ubezpieczenie, czynsz administracyjny w okresach pustostanu, drobne remonty, ewentualną obsługę oraz koszt kredytu. Dopiero zysk netto pokazuje prawdę.' },
{ type: 'list', items: [
{ label: 'Rentowność brutto', text: 'roczny przychód z najmu podzielony przez cenę zakupu - szybki, orientacyjny wskaźnik.' },
{ label: 'Rentowność netto', text: 'roczny zysk po wszystkich kosztach i podatku podzielony przez pełny koszt inwestycji.' },
{ label: 'ROI', text: 'zwrot z realnie zaangażowanego kapitału własnego - kluczowy przy zakupie na kredyt.' },
{ label: 'Pustostan', text: 'założ kilka tygodni w roku bez najemcy - to koszt, który psuje zbyt optymistyczne wyliczenia.' },
] },
{ type: 'callout', variant: 'tip', icon: 'calculator', title: 'Licz scenariusz pesymistyczny', text: 'Zanim kupisz, policz wynik przy niższym czynszu i dłuższym pustostanie. Jeśli inwestycja broni się także w gorszym scenariuszu, jest bezpieczna. Jeśli działa tylko idealnie - to sygnał ostrzegawczy.' },
],
},
{
id: 'lokalizacja',
label: 'Wybór lokalizacji',
heading: 'Gdzie inwestować? Analiza lokalizacji',
blocks: [
{ type: 'p', text: 'W inwestycji na wynajem lokalizacja jest ważniejsza niż samo mieszkanie. Szukaj miejsc z realnym, trwałym popytem: w pobliżu uczelni, dużych pracodawców, dobrej komunikacji i infrastruktury. Zwróć uwagę, kto będzie Twoim najemcą - student, młody pracownik, rodzina - i dobierz lokal do tej grupy.' },
{ type: 'p', text: 'Analizuj dane, nie przeczucia: poziom czynszów, ceny transakcyjne, plany rozwoju okolicy i kierunek zmian. Rozwijająca się dzielnica z nowymi inwestycjami i komunikacją ma większy potencjał niż miejsce, które już „zjadło” swój wzrost.' },
{ type: 'callout', variant: 'info', icon: 'pin', title: 'Sprawdź konkretne dane', text: 'Skorzystaj z historii cen i rankingu dzielnic po prawej stronie, aby porównać lokalizacje liczbami, a nie wrażeniem. Popyt na najem i trend cen to fundament decyzji inwestycyjnej.' },
],
},
{
id: 'finansowanie',
label: 'Finansowanie i dźwignia',
heading: 'Gotówka czy kredyt - dźwignia w inwestycji',
blocks: [
{ type: 'p', text: 'Kredyt to dźwignia: pozwala zainwestować w większą lub kolejną nieruchomość mniejszym kapitałem własnym i podnosi zwrot z tego kapitału (ROI), gdy dochód z najmu przewyższa koszt raty. Ta sama dźwignia działa jednak w obie strony - przy pustostanie lub wzroście rat obciąża wynik i Twój budżet.' },
{ type: 'p', text: 'Zakup za gotówkę daje spokój i pełną odporność na wzrost stóp, ale zamraża kapitał w jednym aktywie. Wielu inwestorów szuka równowagi: część wkładu własnego, rozsądny kredyt i zawsze rezerwa na kilka rat oraz nieprzewidziane koszty.' },
],
},
{
id: 'podatki',
label: 'Podatki i koszty',
heading: 'Podatki i koszty przy inwestycji',
blocks: [
{ type: 'p', text: 'Podatki potrafią istotnie zmienić rentowność, dlatego uwzględnij je od początku. Przychody z najmu prywatnego rozlicza się ryczałtem, a przy sprzedaży nieruchomości przed upływem określonego czasu od nabycia może wystąpić podatek dochodowy od zysku. Sposób rozliczenia zależy od skali i formy Twojej działalności.' },
{ type: 'callout', variant: 'warn', icon: 'scale', title: 'Skonsultuj podatki', text: 'Przy większej skali (kilka mieszkań, najem krótkoterminowy) rozliczenia bywają złożone i mogą wchodzić w zakres działalności gospodarczej oraz VAT. Zanim ruszysz, omów swój model z doradcą podatkowym - to inwestycja, która się zwraca.' },
],
},
{
id: 'ryzyka',
label: 'Ryzyka i błędy',
heading: 'Ryzyka, o których musisz wiedzieć',
blocks: [
{ type: 'p', text: 'Świadomy inwestor nie unika ryzyka, tylko je rozumie i ogranicza. Najważniejsze zagrożenia to: pustostany, nierzetelny najemca, nieprzewidziane koszty remontów, wzrost rat kredytu, spadek cen oraz niska płynność - nieruchomości nie sprzedasz z dnia na dzień bez obniżki ceny.' },
{ type: 'p', text: 'Ograniczaj ryzyko przez rezerwę finansową, dobrą umowę najmu, ubezpieczenie, staranny wybór najemcy i dywersyfikację, gdy masz więcej niż jedną nieruchomość. Nie kupuj na granicy możliwości i nie zakładaj, że wszystko pójdzie idealnie.' },
],
},
],
checklistTitle: 'Checklista przed zakupem inwestycyjnym',
checklistHint: 'Policz i sprawdź, zanim podpiszesz umowę. Pobierz w PDF.',
checklist: [
'Zdefiniuj cel: bieżący dochód czy wzrost wartości w czasie.',
'Policz rentowność netto po wszystkich kosztach i podatku.',
'Uwzględnij pustostan i nieprzewidziane koszty w kalkulacji.',
'Sprawdź realny popyt na najem w danej lokalizacji.',
'Zweryfikuj poziom czynszów i ceny transakcyjne, nie ofertowe.',
'Określ grupę docelową najemców i dopasuj lokal.',
'Przelicz wpływ kredytu na ROI i scenariusz wzrostu rat.',
'Zaplanuj rozliczenie podatkowe najmu i ewentualnej sprzedaży.',
'Zabezpiecz rezerwę na kilka rat i remonty.',
'Policz wynik w scenariuszu pesymistycznym - musi się bronić.',
],
pdfTitle: 'Lista kontrolna inwestycji w nieruchomości',
pdfSubtitle: '10 rzeczy do policzenia przed zakupem pod wynajem',
pdfFileName: 'lista-kontrolna-inwestowanie.pdf',
mistakes: [
{ title: 'Liczenie tylko ceny i czynszu', text: 'Pominięcie kosztów, podatku i pustostanu zawyża rentowność i prowadzi do rozczarowania.' },
{ title: 'Zakup pod wpływem emocji lub mody', text: 'Inwestycja to liczby. „Wszyscy kupują” to nie jest analiza rynku.' },
{ title: 'Ignorowanie pustostanu', text: 'Zakładanie 12 miesięcy najmu w roku to iluzja - zawsze pojawiają się przerwy między najemcami.' },
{ title: 'Brak rezerwy finansowej', text: 'Bez poduszki na raty i remonty jeden dłuższy pustostan potrafi zachwiać całą inwestycją.' },
{ title: 'Przecenianie najmu krótkoterminowego', text: 'Wysokie stawki za dobę kuszą, ale sezonowość, sprzątanie i obsługa mocno obniżają realny zysk.' },
{ title: 'Zlekceważenie podatków', text: 'Nieuwzględnienie podatku od najmu i od sprzedaży potrafi zamienić „zysk” w stratę.' },
],
faq: [
{ q: 'Ile kapitału potrzeba, żeby zacząć inwestować?', a: 'To zależy od rynku i strategii. Przy zakupie na kredyt potrzebujesz przede wszystkim wkładu własnego (zwykle 20% wartości) oraz środków na koszty transakcyjne, ewentualny remont i rezerwę. Kluczowe jest, żeby po zakupie zostać z poduszką finansową, a nie wejść w inwestycję na granicy możliwości.' },
{ q: 'Jaka rentowność najmu jest dobra?', a: 'Nie ma jednej magicznej liczby - ważniejsze od wskaźnika brutto jest liczenie netto po wszystkich kosztach i podatku. Porównuj rentowność z alternatywami i realnym ryzykiem. Inwestycja, która broni się także w gorszym scenariuszu (niższy czynsz, dłuższy pustostan), jest bezpieczniejsza niż taka o wyższej rentowności „na papierze”.' },
{ q: 'Najem długoterminowy czy krótkoterminowy?', a: 'Długoterminowy daje stabilny dochód przy mniejszym nakładzie pracy, ale niższą rentowność. Krótkoterminowy ma wyższy potencjał, lecz wiąże się z sezonowością, dużą obsługą i większym ryzykiem pustych okresów. Wybór zależy od lokalizacji, Twojego czasu i akceptacji ryzyka.' },
{ q: 'Czy kredyt na inwestycję to dobry pomysł?', a: 'Kredyt jako dźwignia może zwiększyć zwrot z kapitału własnego, gdy dochód z najmu przewyższa koszt raty. Ta sama dźwignia zwiększa jednak stratę przy pustostanie lub wzroście stóp. To narzędzie dla osób, które liczą na chłodno i utrzymują rezerwę finansową.' },
{ q: 'Jak rozliczyć podatek od najmu inwestycyjnego?', a: 'Najem prywatny rozlicza się ryczałtem od przychodów, z dwiema stawkami zależnymi od rocznej kwoty przychodu. Przy większej skali lub najmie krótkoterminowym rozliczenia bywają złożone i mogą wejść w zakres działalności gospodarczej oraz VAT. Model podatkowy warto omówić z doradcą.' },
{ q: 'Czy nieruchomość zawsze zyskuje na wartości?', a: 'Nie. W długim terminie ceny nieruchomości historycznie rosły, ale bywają okresy stagnacji i spadków. Nieruchomość jest też mało płynna - sprzedaż zajmuje czas. Dlatego traktuj wzrost wartości jako możliwy bonus, a nie pewnik, i opieraj decyzję głównie na dochodzie z najmu.' },
],
summary: 'Inwestowanie w nieruchomości potrafi budować majątek, ale nagradza liczby i cierpliwość, nie emocje. Zdefiniuj cel, policz rentowność netto ze scenariuszem pesymistycznym, wybierz lokalizację z realnym popytem i zawsze trzymaj rezerwę. Rozumiej ryzyko, a dźwignię kredytową stosuj rozważnie.',
ctaTitle: 'Szukasz mieszkania inwestycyjnego?',
ctaText: 'Porównaj lokalizacje danymi i przeglądaj oferty z potencjałem na wynajem.',
ctaLink: ROUTES.districtRanking,
ctaLabel: 'Zobacz ranking dzielnic',
asideCalculators: [
{ to: ROUTES.districtRanking, title: 'Ranking dzielnic', sub: 'Porównaj lokalizacje inwestycyjne' },
{ to: ROUTES.priceHistory, title: 'Historia cen', sub: 'Zobacz trend cen w okolicy' },
{ to: ROUTES.valuation, title: 'Kalkulator wyceny', sub: 'Oszacuj wartość nieruchomości' },
{ to: ROUTES.buy, title: 'Oferty mieszkań', sub: 'Znajdź okazję inwestycyjną' },
],
};
// ------------------------------------------------------------------ REMONT I WYKOŃCZENIE
const renovation: GuideContent = {
accent: '#ec6a1a',
accentDark: '#c85610',
badgeIcon: 'paint',
badgeLabel: 'Remont i wykończenie',
title: 'Remont mieszkania krok po kroku',
lead: 'Praktyczny przewodnik po remoncie i wykończeniu - od planu i budżetu, przez kolejność prac i wybór ekipy, aż po odbiór i najczęstsze błędy. Prostym językiem, z orientacyjnymi kosztami i listami kontrolnymi, które oszczędzą Ci nerwów i pieniędzy.',
chips: [
{ icon: 'check', text: 'Aktualne realia 2026 roku' },
{ icon: 'calculator', text: 'Orientacyjne koszty' },
{ icon: 'list', text: 'Kolejność prac i PDF' },
{ icon: 'hammer', text: 'Wybór i kontrola ekipy' },
],
processTitle: 'Remont w 9 krokach',
processHint: 'Od pomysłu do gotowego mieszkania - w skrócie.',
process: [
{ icon: 'document', title: 'Plan i zakres', text: 'Ustal, co i po co remontujesz.' },
{ icon: 'calculator', title: 'Budżet', text: 'Policz materiały, robociznę i rezerwę.' },
{ icon: 'screen', title: 'Projekt', text: 'Rozplanuj układ, instalacje i punkty.' },
{ icon: 'group', title: 'Wybór ekipy', text: 'Sprawdź referencje i spisz umowę.' },
{ icon: 'hammer', title: 'Prace rozbiórkowe', text: 'Wyburzenia i przygotowanie podłoża.' },
{ icon: 'gear', title: 'Instalacje', text: 'Elektryka, hydraulika, wentylacja.' },
{ icon: 'paint', title: 'Wykończenie', text: 'Tynki, podłogi, malowanie, biały montaż.' },
{ icon: 'sofa', title: 'Meble i aranżacja', text: 'Zabudowa, meble i wyposażenie.' },
{ icon: 'check', title: 'Odbiór prac', text: 'Sprawdź jakość i rozlicz ekipę.' },
],
chapters: [
{
id: 'plan',
label: 'Plan i zakres',
heading: 'Od czego zacząć remont?',
blocks: [
{ type: 'p', text: 'Remont zaczyna się na kartce, nie na budowie. Najpierw ustal zakres: odświeżenie (malowanie, drobne poprawki), remont częściowy (np. łazienka i kuchnia) czy generalny (instalacje, układ, wszystko od nowa). Od zakresu zależy budżet, czas i to, czy w mieszkaniu da się mieszkać podczas prac.' },
{ type: 'p', text: 'Zapisz, co chcesz osiągnąć i dlaczego - inaczej wygląda remont pod własne potrzeby, a inaczej pod wynajem czy sprzedaż. Jasny cel chroni przed przepłacaniem za rozwiązania, których nie potrzebujesz, i przed rozrastaniem się zakresu w trakcie prac.' },
{ type: 'callout', variant: 'tip', icon: 'spark', title: 'Zdecyduj wszystko przed startem', text: 'Najdroższe są zmiany decyzji w trakcie remontu. Wybierz płytki, układ, punkty elektryczne i materiały jeszcze przed rozpoczęciem prac - przeróbki „na gotowym” kosztują wielokrotnie więcej.' },
],
},
{
id: 'budzet',
label: 'Budżet remontu',
heading: 'Jak zaplanować budżet remontu?',
blocks: [
{ type: 'p', text: 'Budżet remontu to suma trzech elementów: materiałów, robocizny i rezerwy na niespodzianki. W starszych mieszkaniach niespodzianek jest najwięcej - po skuciu tynków czy odkryciu instalacji zakres często rośnie. Dlatego rezerwa nie jest opcją, tylko obowiązkowym elementem planu.' },
{ type: 'table', head: ['Zakres remontu', 'Co obejmuje', 'Uwagi'], rows: [
['Odświeżenie', 'Malowanie, drobne naprawy, biały montaż', 'Najtańsze i najszybsze, możliwe „mieszkając”.'],
['Remont częściowy', 'Kuchnia lub łazienka, wybrane pomieszczenia', 'Średni koszt, częściowa dezorganizacja.'],
['Remont generalny', 'Instalacje, układ, podłogi, wszystko od nowa', 'Najdroższy i najdłuższy, wymaga wyprowadzki.'],
['Wykończenie od dewelopera', 'Stan deweloperski „pod klucz”', 'Cały metraż od zera - materiały plus pełna robocizna.'],
] },
{ type: 'callout', variant: 'warn', icon: 'coin', title: 'Zaplanuj rezerwę', text: 'Do budżetu doliczaj rezerwę na nieprzewidziane koszty - w starszych mieszkaniach warto założyć spory zapas. Remont bez rezerwy niemal zawsze kończy się przekroczeniem planu i stresem.' },
],
},
{
id: 'kolejnosc',
label: 'Kolejność prac',
heading: 'Kolejność prac remontowych',
blocks: [
{ type: 'p', text: 'Właściwa kolejność to podstawa - odwrócenie etapów oznacza niszczenie tego, co już zrobione. Prace idą zawsze „od brudnych do czystych” i od tego, co ukryte, do tego, co widoczne. Trzymanie się tej sekwencji chroni przed poprawkami i podwójnym płaceniem.' },
{ type: 'list', items: [
{ label: 'Rozbiórka', text: 'wyburzenia, skuwanie tynków i płytek, demontaż starych elementów.' },
{ label: 'Instalacje', text: 'nowa elektryka, hydraulika, wentylacja - zanim zakryjesz ściany.' },
{ label: 'Prace mokre', text: 'tynki, wylewki, gładzie - z czasem na wyschnięcie.' },
{ label: 'Podłogi i płytki', text: 'układanie posadzek, glazury i terakoty.' },
{ label: 'Malowanie', text: 'gruntowanie i malowanie ścian oraz sufitów.' },
{ label: 'Biały montaż i meble', text: 'armatura, drzwi, zabudowa i wyposażenie na końcu.' },
] },
],
},
{
id: 'ekipa',
label: 'Wybór ekipy',
heading: 'Jak wybrać i rozliczyć ekipę remontową?',
blocks: [
{ type: 'p', text: 'Dobra ekipa to połowa sukcesu, a terminy u sprawdzonych fachowców bywają odległe - szukaj z wyprzedzeniem. Nie wybieraj wyłącznie po cenie: najniższa oferta często oznacza pośpiech, dopłaty w trakcie lub poprawki. Poproś o referencje i, jeśli możesz, zobacz wcześniejsze realizacje.' },
{ type: 'p', text: 'Zawsze spisz umowę z zakresem prac, terminami, harmonogramem płatności i zasadami na wypadek opóźnień lub usterek. Rozliczaj etapami, po odbiorze poszczególnych prac, a nie z góry. Unikniesz sytuacji, w której zapłacona ekipa znika przed dokończeniem.' },
{ type: 'callout', variant: 'warn', icon: 'document', title: 'Nie płać całości z góry', text: 'Płatność powinna iść za postępem prac. Duża zaliczka bez zabezpieczenia to najczęstsze źródło problemów - od porzuconego remontu po brak motywacji do poprawek.' },
],
},
{
id: 'materialy',
label: 'Materiały i oszczędzanie',
heading: 'Materiały - na czym oszczędzać, a na czym nie?',
blocks: [
{ type: 'p', text: 'Nie wszędzie warto oszczędzać tak samo. Na elementach, których wymiana jest kosztowna i uciążliwa - instalacje, hydroizolacja łazienki, podłogi w salonie - stawiaj na jakość. Na elementach łatwych do zmiany, jak kolor ścian czy dodatki, można szukać oszczędności bez szkody dla trwałości.' },
{ type: 'twocol', cols: [
{ tone: 'pro', icon: 'shield', title: 'Nie oszczędzaj na', items: ['Instalacji elektrycznej i hydraulicznej', 'Hydroizolacji łazienki', 'Robociźnie przy pracach ukrytych', 'Drzwiach i oknach', 'Podłodze w salonie'] },
{ tone: 'market', icon: 'paint', title: 'Możesz oszczędzić na', items: ['Farbach i kolorystyce', 'Dodatkach i dekoracjach', 'Meblach do wymiany w przyszłości', 'Oświetleniu dekoracyjnym', 'Elementach łatwych do zmiany'] },
] },
],
},
{
id: 'formalnosci',
label: 'Formalności',
heading: 'Kiedy remont wymaga formalności?',
blocks: [
{ type: 'p', text: 'Większość prac wykończeniowych i odświeżających nie wymaga zgód. Sprawa komplikuje się, gdy ingerujesz w konstrukcję lub układ: wyburzenie ściany nośnej, zmiana rozmieszczenia pomieszczeń czy ingerencja w piony wspólne mogą wymagać zgody wspólnoty lub spółdzielni, a czasem także urzędowych formalności.' },
{ type: 'callout', variant: 'info', icon: 'scale', title: 'Ściana nośna to nie żart', text: 'Nigdy nie wyburzaj ściany bez pewności, że nie jest nośna. Naruszenie konstrukcji zagraża bezpieczeństwu całego budynku. W razie wątpliwości skonsultuj się z konstruktorem i sprawdź wymagane zgody.' },
],
},
{
id: 'odbior',
label: 'Odbiór prac',
heading: 'Odbiór remontu i usuwanie usterek',
blocks: [
{ type: 'p', text: 'Odbiór to nie formalność, tylko moment, w którym sprawdzasz jakość i egzekwujesz poprawki. Oglądaj prace przy dobrym świetle: równość ścian, jakość fug, spadki w łazience, działanie gniazdek i punktów wodnych, domykanie drzwi. Wszystkie usterki spisz na liście i ustal termin ich usunięcia.' },
{ type: 'p', text: 'Ostatnią transzę płatności zatrzymaj do czasu usunięcia usterek z listy. To najprostszy sposób, by poprawki faktycznie zostały wykonane. Zachowaj gwarancje na materiały i sprzęt oraz dane kontaktowe ekipy na wypadek późniejszych reklamacji.' },
],
},
],
checklistTitle: 'Checklista przed i w trakcie remontu',
checklistHint: 'Wydrukuj lub pobierz w PDF i miej pod ręką na budowie.',
checklist: [
'Ustal zakres remontu i cel (dla siebie, pod wynajem, pod sprzedaż).',
'Policz budżet: materiały, robocizna i rezerwa na niespodzianki.',
'Zaplanuj układ, instalacje i punkty elektryczne przed startem.',
'Wybierz materiały i płytki jeszcze przed rozpoczęciem prac.',
'Sprawdź referencje ekipy i zobacz wcześniejsze realizacje.',
'Spisz umowę z zakresem, terminami i harmonogramem płatności.',
'Rozliczaj prace etapami, nie płać całości z góry.',
'Zachowaj właściwą kolejność prac - od brudnych do czystych.',
'Sprawdź, czy zmiany nie wymagają zgód wspólnoty lub urzędu.',
'Zrób rzetelny odbiór i zatrzymaj ostatnią transzę do poprawek.',
],
pdfTitle: 'Lista kontrolna remontu mieszkania',
pdfSubtitle: '10 rzeczy do zaplanowania przed i w trakcie remontu',
pdfFileName: 'lista-kontrolna-remont.pdf',
mistakes: [
{ title: 'Brak rezerwy w budżecie', text: 'Remont bez zapasu na niespodzianki niemal zawsze przekracza plan - zwłaszcza w starszych mieszkaniach.' },
{ title: 'Zmiana decyzji w trakcie', text: 'Przeróbki na gotowym są najdroższe. Wszystko warto przemyśleć przed startem prac.' },
{ title: 'Wybór ekipy tylko po cenie', text: 'Najtańsza oferta często oznacza pośpiech, dopłaty i poprawki. Referencje są ważniejsze niż niska stawka.' },
{ title: 'Płatność całości z góry', text: 'Zaliczka bez zabezpieczenia to ryzyko porzuconego remontu lub braku poprawek.' },
{ title: 'Odwrócona kolejność prac', text: 'Malowanie przed układaniem podłóg czy instalacje po tynkach to niszczenie tego, co już gotowe.' },
{ title: 'Oszczędzanie na instalacjach', text: 'Tania elektryka czy zła hydroizolacja łazienki to koszty i awarie, które wracają po latach.' },
],
faq: [
{ q: 'Ile kosztuje remont mieszkania?', a: 'Koszt zależy od zakresu i standardu. Odświeżenie jest najtańsze, remont częściowy (kuchnia, łazienka) droższy, a remont generalny lub wykończenie stanu deweloperskiego to koszt całego metrażu - materiały plus pełna robocizna. Zawsze licz z rezerwą na niespodzianki, szczególnie w starszych lokalach. Aktualne stawki najlepiej zebrać od kilku ekip.' },
{ q: 'W jakiej kolejności prowadzić prace?', a: 'Od brudnych do czystych i od ukrytych do widocznych: rozbiórka, instalacje, prace mokre (tynki, wylewki), podłogi i płytki, malowanie, a na końcu biały montaż, drzwi i meble. Odwrócenie tej kolejności oznacza niszczenie już wykonanych prac i podwójne koszty.' },
{ q: 'Jak wybrać dobrą ekipę remontową?', a: 'Nie kieruj się wyłącznie ceną. Poproś o referencje i zobacz wcześniejsze realizacje, sprawdź opinie i zawsze spisz umowę z zakresem prac, terminami i harmonogramem płatności. Rozliczaj etapami po odbiorze, nie płać całości z góry. Terminy u dobrych fachowców rezerwuj z wyprzedzeniem.' },
{ q: 'Czy remont wymaga zgód i pozwoleń?', a: 'Prace wykończeniowe i odświeżające zwykle nie wymagają formalności. Ingerencja w konstrukcję (ściany nośne), zmiana układu pomieszczeń czy piony wspólne mogą wymagać zgody wspólnoty lub spółdzielni, a czasem urzędowych formalności. Ściany nośnej nigdy nie ruszaj bez konsultacji z konstruktorem.' },
{ q: 'Na czym nie warto oszczędzać?', a: 'Na elementach ukrytych i trudnych do wymiany: instalacji elektrycznej i hydraulicznej, hydroizolacji łazienki, robociźnie przy pracach zakrywanych oraz na drzwiach, oknach i podłodze w salonie. Oszczędności szukaj tam, gdzie zmiana jest łatwa - farby, dodatki, oświetlenie dekoracyjne.' },
{ q: 'Jak wygląda odbiór remontu?', a: 'Oglądaj prace przy dobrym świetle: równość ścian, jakość fug, spadki w łazience, działanie gniazdek i punktów wodnych, domykanie drzwi. Wszystkie usterki spisz na liście z terminem usunięcia, a ostatnią transzę płatności zatrzymaj do czasu poprawek. Zachowaj gwarancje na materiały i sprzęt.' },
],
summary: 'Udany remont to przede wszystkim dobry plan i właściwa kolejność. Ustal zakres i budżet z rezerwą, podejmij wszystkie decyzje przed startem, wybierz sprawdzoną ekipę i rozliczaj ją etapami. Nie oszczędzaj na tym, co ukryte, a na koniec zrób rzetelny odbiór - to on decyduje o jakości na lata.',
ctaTitle: 'Szukasz fachowca do remontu?',
ctaText: 'Przeglądaj firmy remontowe i wykończeniowe albo znajdź mieszkanie do odświeżenia.',
ctaLink: ROUTES.services,
ctaLabel: 'Zobacz firmy i usługi',
asideCalculators: [
{ to: ROUTES.services, title: 'Firmy i usługi', sub: 'Znajdź ekipę remontową' },
{ to: ROUTES.valuation, title: 'Kalkulator wyceny', sub: 'Sprawdź, jak remont wpływa na wartość' },
{ to: ROUTES.buy, title: 'Oferty mieszkań', sub: 'Znajdź lokal do wykończenia' },
{ to: ROUTES.guides, title: 'Więcej poradników', sub: 'Wróć do centrum wiedzy' },
],
};
// ------------------------------------------------------------------ PRAWO I PODATKI
const law: GuideContent = {
accent: '#7867ed',
accentDark: '#5b4bd0',
badgeIcon: 'scale',
badgeLabel: 'Prawo i podatki',
title: 'Prawo i podatki w nieruchomościach',
lead: 'Przewodnik po prawnej i podatkowej stronie nieruchomości - księga wieczysta, rodzaje umów, podatki PCC i od sprzedaży, opłaty notarialne oraz bezpieczeństwo transakcji. Prostym językiem, z listami kontrolnymi. Materiał informacyjny, nie porada prawna.',
chips: [
{ icon: 'check', text: 'Aktualna wiedza na 2026 rok' },
{ icon: 'scale', text: 'Umowy i księga wieczysta' },
{ icon: 'percent', text: 'Podatki i opłaty' },
{ icon: 'shield', text: 'Bezpieczeństwo transakcji' },
],
processTitle: 'Prawna strona transakcji w 7 krokach',
processHint: 'Co sprawdzić i dopilnować, żeby transakcja była bezpieczna.',
process: [
{ icon: 'search', title: 'Księga wieczysta', text: 'Sprawdź stan prawny nieruchomości.' },
{ icon: 'user', title: 'Weryfikacja stron', text: 'Potwierdź tożsamość i prawo do sprzedaży.' },
{ icon: 'document', title: 'Umowa przedwstępna', text: 'Zabezpiecz warunki i terminy.' },
{ icon: 'calculator', title: 'Podatki i opłaty', text: 'Policz PCC, taksę i koszty sądowe.' },
{ icon: 'scale', title: 'Akt notarialny', text: 'Przeniesienie własności u notariusza.' },
{ icon: 'letter', title: 'Wpis do księgi', text: 'Aktualizacja właściciela i hipoteki.' },
{ icon: 'shield', title: 'Formalności po', text: 'Zgłoszenia, podatki i rozliczenia.' },
],
chapters: [
{
id: 'ksiega-wieczysta',
label: 'Księga wieczysta',
heading: 'Księga wieczysta - fundament bezpieczeństwa',
blocks: [
{ type: 'p', text: 'Księga wieczysta to publiczny rejestr pokazujący stan prawny nieruchomości. Znając jej numer, sprawdzisz odpis online. To pierwsza i najważniejsza rzecz do weryfikacji przed każdą transakcją - chroni przed zakupem mieszkania z ukrytym obciążeniem lub od osoby, która wcale nie jest właścicielem.' },
{ type: 'list', items: [
{ label: 'Dział I', text: 'oznaczenie nieruchomości - adres, metraż, pomieszczenia przynależne.' },
{ label: 'Dział II', text: 'właściciel - musi zgadzać się z osobą, która sprzedaje.' },
{ label: 'Dział III', text: 'prawa, ograniczenia, służebności i roszczenia osób trzecich.' },
{ label: 'Dział IV', text: 'hipoteki - jeśli są, ustal sposób ich wykreślenia.' },
] },
{ type: 'callout', variant: 'warn', icon: 'shield', title: 'Obciążenie przechodzi z nieruchomością', text: 'Wpis w dziale III lub IV może przejść na Ciebie razem z zakupem. Sprawdzenie księgi to kilka minut, a chroni przed poważnymi i kosztownymi problemami. Nigdy nie pomijaj tego kroku.' },
],
},
{
id: 'umowy',
label: 'Rodzaje umów',
heading: 'Umowy w obrocie nieruchomościami',
blocks: [
{ type: 'p', text: 'W obrocie nieruchomościami spotkasz kilka rodzajów umów, a forma ma znaczenie prawne. Przeniesienie własności nieruchomości wymaga formy aktu notarialnego - umowa „na kartce” jest w tym zakresie nieważna. Inne umowy, jak przedwstępna czy rezerwacyjna, mogą mieć formę zwykłą pisemną, choć notarialna daje silniejszą ochronę.' },
{ type: 'table', head: ['Umowa', 'Do czego służy', 'Forma'], rows: [
['Rezerwacyjna', 'Czasowa rezerwacja oferty', 'Pisemna, często z opłatą rezerwacyjną.'],
['Przedwstępna', 'Zobowiązanie do zawarcia umowy właściwej', 'Pisemna lub notarialna (silniejsza ochrona).'],
['Przyrzeczona (sprzedaży)', 'Przeniesienie własności', 'Wyłącznie akt notarialny.'],
['Najmu', 'Oddanie lokalu do używania', 'Pisemna; okazjonalny wymaga aktu notarialnego.'],
['Darowizny nieruchomości', 'Nieodpłatne przekazanie', 'Wyłącznie akt notarialny.'],
] },
{ type: 'callout', variant: 'info', icon: 'document', title: 'Zadatek a zaliczka', text: 'Zadatek działa dyscyplinująco: jeśli od umowy odstąpi kupujący, traci zadatek; jeśli sprzedający - zwraca go w podwójnej wysokości. Zaliczka jest po prostu zwracana. Dla bezpieczeństwa transakcji częściej stosuje się zadatek.' },
],
},
{
id: 'pcc',
label: 'Podatek PCC',
heading: 'Podatek PCC przy zakupie',
blocks: [
{ type: 'p', text: 'Podatek od czynności cywilnoprawnych (PCC) dotyczy przede wszystkim zakupu nieruchomości z rynku wtórnego - standardowo wynosi 2% wartości i pobiera go notariusz przy akcie. Zakup nowego mieszkania od dewelopera podlega zwykle VAT (wliczonemu w cenie), a nie PCC.' },
{ type: 'callout', variant: 'tip', icon: 'percent', title: 'Zwolnienie dla pierwszego mieszkania', text: 'Dla osób kupujących swoje pierwsze mieszkanie lub dom przewidziano zwolnienie z podatku PCC na rynku wtórnym. Warunki i zakres zwolnienia potwierdź u notariusza, bo to on nalicza i odprowadza ten podatek.' },
],
},
{
id: 'podatek-sprzedaz',
label: 'Podatek od sprzedaży',
heading: 'Podatek przy sprzedaży nieruchomości',
blocks: [
{ type: 'p', text: 'Sprzedaż nieruchomości przed upływem określonego czasu od jej nabycia (liczonego w latach) może wiązać się z podatkiem dochodowym od osiągniętego zysku. Po upływie tego okresu sprzedaż jest z podatku zwolniona. To jeden z ważniejszych elementów planowania, zwłaszcza przy szybkiej odsprzedaży.' },
{ type: 'p', text: 'Nawet gdy sprzedajesz wcześniej, podatku można uniknąć, przeznaczając przychód na własne cele mieszkaniowe w przewidzianym terminie. Zasady tzw. ulgi mieszkaniowej bywają szczegółowe, dlatego przy sprzedaży „na szybko” warto skonsultować rozliczenie z doradcą podatkowym.' },
{ type: 'callout', variant: 'warn', icon: 'calculator', title: 'Licz od zysku, nie od ceny', text: 'Podatek dotyczy dochodu, czyli różnicy między ceną sprzedaży a udokumentowanymi kosztami nabycia i nakładami. Zachowuj faktury za zakup i remonty - realnie obniżają podstawę opodatkowania.' },
],
},
{
id: 'oplaty-notarialne',
label: 'Opłaty notarialne',
heading: 'Opłaty notarialne i sądowe',
blocks: [
{ type: 'p', text: 'Przy akcie notarialnym poza ceną nieruchomości ponosisz kilka dodatkowych kosztów. To taksa notarialna (wynagrodzenie notariusza, z ustawowymi stawkami maksymalnymi), opłaty sądowe za wpisy do księgi wieczystej oraz podatki pobierane przez notariusza. Warto znać te pozycje, by nie zaskoczyły Cię przy podpisaniu.' },
{ type: 'list', items: [
{ label: 'Taksa notarialna', text: 'zależna od wartości; stawki maksymalne wynikają z rozporządzenia, bywają negocjowalne.' },
{ label: 'Wpis własności', text: 'opłata sądowa za ujawnienie nowego właściciela w księdze wieczystej.' },
{ label: 'Wpis hipoteki', text: 'dodatkowa opłata sądowa, gdy kupujesz na kredyt.' },
{ label: 'Podatki i wypisy', text: 'PCC (rynek wtórny) oraz koszt wypisów aktu.' },
] },
],
},
{
id: 'wspoluwlasnosc',
label: 'Współwłasność i spadki',
heading: 'Współwłasność, małżeństwo i spadki',
blocks: [
{ type: 'p', text: 'Stan cywilny i sposób nabycia wpływają na to, czyją własnością jest nieruchomość. Zakup w trakcie małżeństwa bez umowy majątkowej zwykle wchodzi do majątku wspólnego. Nieruchomość może też być współwłasnością kilku osób lub pochodzić ze spadku - a to rodzi dodatkowe formalności i podatki.' },
{ type: 'callout', variant: 'info', icon: 'group', title: 'Sprawdź, kto naprawdę sprzedaje', text: 'Przy współwłasności do sprzedaży potrzebna jest zgoda wszystkich współwłaścicieli. Przy nieruchomości ze spadku sprawdź, czy postępowanie spadkowe jest zakończone i uregulowany jest podatek. To typowe źródła problemów prawnych.' },
],
},
{
id: 'bezpieczenstwo',
label: 'Bezpieczeństwo transakcji',
heading: 'Jak bezpiecznie przeprowadzić transakcję?',
blocks: [
{ type: 'p', text: 'Bezpieczeństwo transakcji opiera się na weryfikacji i dokumentach, nie na zaufaniu. Sprawdź księgę wieczystą, potwierdź tożsamość i tytuł prawny sprzedającego, żądaj zaświadczeń o braku zaległości, a wszystkie ustalenia przelewaj na papier. Notariusz jest tu Twoim sojusznikiem - to on kontroluje zgodność i legalność umowy.' },
{ type: 'p', text: 'Uważaj na presję czasu, zaniżone ceny i prośby o płatności poza aktem lub gotówką bez pokwitowania. Uczciwa transakcja niczego nie ukrywa i znosi weryfikację. Jeśli coś budzi wątpliwości - zatrzymaj się i skonsultuj z prawnikiem lub notariuszem, zanim cokolwiek podpiszesz.' },
],
},
],
checklistTitle: 'Checklista prawna przy transakcji',
checklistHint: 'Sprawdź, zanim podpiszesz cokolwiek. Pobierz w PDF.',
checklist: [
'Sprawdź aktualny odpis z księgi wieczystej - wszystkie cztery działy.',
'Potwierdź, że sprzedający jest właścicielem z działu II.',
'Zweryfikuj dział III i IV: obciążenia, służebności, hipoteki.',
'Przy współwłasności uzyskaj zgodę wszystkich współwłaścicieli.',
'Poproś o zaświadczenia o braku zaległości w czynszu i mediach.',
'Ustal formę umowy - własność tylko aktem notarialnym.',
'Rozważ zadatek zamiast zaliczki w umowie przedwstępnej.',
'Policz podatki i opłaty: PCC, taksę, koszty sądowe.',
'Przy sprzedaży sprawdź podatek od zysku i ulgę mieszkaniową.',
'Zachowaj wszystkie dokumenty i faktury dotyczące transakcji.',
],
pdfTitle: 'Lista kontrolna prawna przy transakcji',
pdfSubtitle: '10 rzeczy do sprawdzenia przed podpisaniem umowy',
pdfFileName: 'lista-kontrolna-prawo-podatki.pdf',
mistakes: [
{ title: 'Pominięcie księgi wieczystej', text: 'Brak sprawdzenia działu III i IV grozi zakupem mieszkania z hipoteką, służebnością lub roszczeniem osoby trzeciej.' },
{ title: 'Zaliczka zamiast zadatku', text: 'Przy zaliczce trudniej dochodzić rekompensaty, gdy druga strona wycofa się z transakcji.' },
{ title: 'Zapominanie o podatku od sprzedaży', text: 'Sprzedaż przed upływem ustawowego okresu bez planu na ulgę mieszkaniową to niespodziewany podatek od zysku.' },
{ title: 'Nieuwzględnienie kosztów okołoaktowych', text: 'Taksa notarialna, wpisy sądowe i PCC potrafią podnieść budżet o kilka procent ceny.' },
{ title: 'Brak zgody współwłaścicieli', text: 'Sprzedaż udziału bez zgody pozostałych współwłaścicieli to źródło poważnych sporów.' },
{ title: 'Płatności poza aktem', text: 'Gotówka bez pokwitowania i płatności „obok umowy” to ryzyko podatkowe i dowodowe.' },
],
faq: [
{ q: 'Jak sprawdzić księgę wieczystą?', a: 'Znając numer księgi wieczystej, sprawdzisz jej odpis online. Zwróć uwagę na cztery działy: oznaczenie nieruchomości (I), właściciela (II), prawa i ograniczenia (III) oraz hipoteki (IV). Właściciel z działu II musi zgadzać się ze sprzedającym, a wpisy w III i IV pokazują obciążenia, które mogą przejść na Ciebie wraz z zakupem.' },
{ q: 'Kiedy płacę podatek PCC, a kiedy VAT?', a: 'Zakup z rynku wtórnego standardowo wiąże się z podatkiem PCC w wysokości 2% wartości, pobieranym przez notariusza. Zakup nowego mieszkania od dewelopera podlega zwykle VAT, który jest już wliczony w cenę. Przy pierwszym mieszkaniu z rynku wtórnego może przysługiwać zwolnienie z PCC - potwierdź to u notariusza.' },
{ q: 'Czy zapłacę podatek, gdy sprzedam mieszkanie?', a: 'Sprzedaż przed upływem ustawowego okresu od nabycia może wiązać się z podatkiem dochodowym od zysku, czyli różnicy między ceną sprzedaży a kosztami nabycia i nakładami. Po upływie tego okresu sprzedaż jest zwolniona. Wcześniejszej sprzedaży można uniknąć podatku, przeznaczając przychód na własne cele mieszkaniowe - zasady warto skonsultować z doradcą.' },
{ q: 'Czym różni się zadatek od zaliczki?', a: 'Zadatek pełni funkcję zabezpieczającą: jeśli od umowy odstąpi kupujący, traci zadatek; jeśli sprzedający, zwraca go w podwójnej wysokości. Zaliczka jest natomiast po prostu zwracana, bez dodatkowych konsekwencji. Dlatego w umowach przedwstępnych częściej stosuje się zadatek - mocniej dyscyplinuje obie strony.' },
{ q: 'Jakie koszty ponoszę u notariusza?', a: 'Poza ceną nieruchomości płacisz taksę notarialną (z ustawowymi stawkami maksymalnymi, zależną od wartości), opłaty sądowe za wpisy do księgi wieczystej (własności, a przy kredycie także hipoteki), podatek PCC na rynku wtórnym oraz koszt wypisów aktu. Warto poprosić notariusza o wcześniejsze zestawienie tych kosztów.' },
{ q: 'Czy do sprzedaży potrzebna jest zgoda współmałżonka lub współwłaścicieli?', a: 'Jeśli nieruchomość należy do majątku wspólnego małżonków, do sprzedaży potrzebna jest zgoda obojga. Przy współwłasności kilku osób wymagana jest zgoda wszystkich współwłaścicieli. Dlatego zawsze warto ustalić z księgi wieczystej i dokumentów, kto faktycznie ma prawo rozporządzać nieruchomością.' },
],
summary: 'Prawna strona nieruchomości nie jest skomplikowana, jeśli podejdzie się do niej metodycznie. Sprawdź księgę wieczystą, zweryfikuj strony i tytuł prawny, dobierz właściwą formę umowy i policz wszystkie podatki oraz opłaty. W razie wątpliwości korzystaj z notariusza i doradcy - to najtańsze ubezpieczenie transakcji.',
ctaTitle: 'Planujesz zakup lub sprzedaż?',
ctaText: 'Przeglądaj oferty i korzystaj z narzędzi, które pomogą bezpiecznie przejść przez transakcję.',
ctaLink: ROUTES.buy,
ctaLabel: 'Zobacz oferty nieruchomości',
asideCalculators: [
{ to: ROUTES.creditCalculator, title: 'Kalkulator zdolności', sub: 'Sprawdź budżet na zakup' },
{ to: ROUTES.valuation, title: 'Kalkulator wyceny', sub: 'Oszacuj wartość nieruchomości' },
{ to: ROUTES.guideBuying, title: 'Poradnik kupna', sub: 'Cały proces krok po kroku' },
{ to: ROUTES.buy, title: 'Oferty mieszkań', sub: 'Znajdź nieruchomość' },
],
};
// Klucz to ścieżka trasy - dzięki temu komponent strony pobiera treść po własnym adresie.
export const guideContents: Record<string, GuideContent> = {
[ROUTES.guideCredit]: credit,
[ROUTES.guideRent]: rent,
[ROUTES.guideInvesting]: investing,
[ROUTES.guideRenovation]: renovation,
[ROUTES.guideLaw]: law,
};
+47
View File
@@ -17,6 +17,14 @@ export const ROUTES = {
advertising: '/reklama',
guides: '/poradniki',
guideBuying: '/poradniki/kupno-mieszkania',
guideCredit: '/poradniki/kredyty-hipoteczne',
guideRent: '/poradniki/wynajem',
guideInvesting: '/poradniki/inwestowanie',
guideRenovation: '/poradniki/remont-i-wykonczenie',
guideLaw: '/poradniki/prawo-i-podatki',
guideTopicList: '/poradniki/temat/:topicSlug',
guideArticle: '/poradniki/artykul/:id',
guideAdd: '/poradniki/dodaj',
creditCalculator: '/kalkulator-zdolnosci',
map: '/mapa',
login: '/logowanie',
@@ -46,6 +54,43 @@ export const ROUTES = {
export type RoutePath = (typeof ROUTES)[keyof typeof ROUTES];
// Kanoniczna lista tematów poradników. Slug = ostatni segment adresu i zarazem klucz w API
// (/api/articles?topic=slug). `guide` to wbudowany, kompletny poradnik danego tematu.
export type GuideTopicMeta = { slug: string; title: string; icon: string; guide: string };
export const GUIDE_TOPICS: GuideTopicMeta[] = [
{ slug: 'kupno-mieszkania', title: 'Kupno mieszkania', icon: 'house', guide: ROUTES.guideBuying },
{ slug: 'kredyty-hipoteczne', title: 'Kredyty hipoteczne', icon: 'percent', guide: ROUTES.guideCredit },
{ slug: 'wynajem', title: 'Wynajem', icon: 'key', guide: ROUTES.guideRent },
{ slug: 'inwestowanie', title: 'Inwestowanie', icon: 'chart', guide: ROUTES.guideInvesting },
{ slug: 'remont-i-wykonczenie', title: 'Remont i wykończenie', icon: 'paint', guide: ROUTES.guideRenovation },
{ slug: 'prawo-i-podatki', title: 'Prawo i podatki', icon: 'scale', guide: ROUTES.guideLaw },
];
export function guideTopicBySlug(slug: string | undefined): GuideTopicMeta | undefined {
return GUIDE_TOPICS.find((topic) => topic.slug === slug);
}
// Kafelki tematów na /poradniki prowadzą do listy poradników danego tematu.
export function guideTopicListPath(slug: string): string {
return `/poradniki/temat/${slug}`;
}
// Pojedynczy artykuł użytkownika.
export function articlePath(id: number): string {
return `/poradniki/artykul/${id}`;
}
// Formularz dodawania artykułu z opcjonalnym wstępnie wybranym tematem.
export function guideAddPath(topicSlug?: string): string {
return topicSlug ? `${ROUTES.guideAdd}?temat=${encodeURIComponent(topicSlug)}` : ROUTES.guideAdd;
}
// Zgodność wstecz: mapa tytuł tematu -> wbudowany poradnik (używana przy kartach polecanych artykułów).
export const GUIDE_TOPIC_ROUTES: Record<string, string> = Object.fromEntries(
GUIDE_TOPICS.map((topic) => [topic.title, topic.guide]),
);
// Szczegóły oferty: identyfikator trafia do adresu, nie do stanu Reacta.
export function listingPath(id: number): string {
return `/oferta/${id}`;
@@ -123,6 +168,7 @@ export const ADMIN_TAB_PATHS = {
support: 'pomoc-i-kontakt',
advertisingEnquiries: 'zapytania-o-reklame',
reports: 'zgloszenia',
articleRequests: 'poradniki-prosby',
payments: 'platnosci',
stats: 'statystyki',
settings: 'ustawienia',
@@ -169,4 +215,5 @@ export const PROTECTED_PATHS: string[] = [
ROUTES.favorites,
ROUTES.messages,
ROUTES.comparison,
ROUTES.guideAdd,
];
+1443 -16
View File
File diff suppressed because it is too large Load Diff