Files
polskalokalnie/frontend/src/routes.ts
T
debian 5eee33b986
Deploy Polska Lokalnie / deploy (push) Successful in 12s
Dodaj prawdziwe wiadomosci miedzy uzytkownikami o ogloszeniach
Dotad rozmowy z kupujacymi istnialy tylko w przegladarce nadawcy: nic nie
trafialo na serwer, "odczytano" i odpowiedz sprzedajacego byly symulowane
timerem. Wzorem Gieldy Komunalnej:

Backend (/api/conversations):
- Conversation: jeden watek na pare (ogloszenie, kupujacy), sprzedajacy to
  wlasciciel ogloszenia; tytul ogloszenia zapisany na wypadek jego usuniecia.
  Wiadomosci w istniejacej tabeli messages z nowa kolumna conversation_id,
  wiec rozmowa z administracja (conversation_id = null) dziala bez zmian.
- start/list/get/reply, blokada (UserBlock, w obu kierunkach), ukrycie
  rozmowy u siebie (nowa wiadomosc przywraca ja obu stronom). Dostep tylko
  dla stron rozmowy, filtr niedozwolonych slow jak w reszcie serwisu.
- Po zatwierdzeniu transakcji odbiorca dostaje powiadomienie w serwisie
  i - jesli ma wlaczone powiadomienia o wiadomosciach - e-mail (w tle).
- /api/messages/unread-count wlicza rozmowy o ogloszeniach.
- Zgloszenie uzytkownika z rozmowy: zgloszonego, ogloszenie i tresc rozmowy
  serwer bierze z bazy, nie z przegladarki.

Frontend:
- Adresy /wiadomosci/:conversationId, /wiadomosci/administracja
  i /wiadomosci/nowa/:listingId zamiast rozmowy w location.state.
- Lista rozmow odswiezana co 30 s, otwarta rozmowa co 4 s.
- Zdjecia w wiadomosciach usuniete - serwer ich nie przyjmuje (rozmowa
  z administracja i tak je gubila).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 23:15:06 +02:00

240 lines
8.7 KiB
TypeScript

// Centralna mapa adresów aplikacji. Jedyne źródło prawdy dla nawigacji -
// komponenty korzystają z ROUTES zamiast trzymać identyfikatory widoków w stanie.
export const ROUTES = {
home: '/',
buy: '/kupno',
rent: '/wynajem',
sell: '/jak-sprzedawac',
valuation: '/wycena',
priceHistory: '/historia-cen',
districtRanking: '/ranking-dzielnic',
negotiation: '/negocjacje',
comparison: '/konto/porownanie',
add: '/dodaj-ogloszenie',
edit: '/edytuj-ogloszenie/:id',
services: '/firmy-i-uslugi',
companies: '/firmy',
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',
account: '/konto',
accountSearches: '/konto/wyszukiwania',
accountPriceAlerts: '/konto/alerty-cenowe',
accountMeetings: '/konto/spotkania',
accountListings: '/konto/moje-oferty',
accountSettings: '/konto/ustawienia',
accountSecurity: '/konto/bezpieczenstwo',
accountProfileEdit: '/konto/profil/edycja',
accountPublicProfile: '/konto/profil',
accountHelpContact: '/konto/pomoc',
terms: '/regulamin',
privacy: '/polityka-prywatnosci',
activation: '/aktywacja/:token',
passwordReset: '/reset-hasla/:token',
notifications: '/powiadomienia',
favorites: '/ulubione',
messages: '/wiadomosci',
// Rozmowa o ogloszeniu (numer z API) albo "administracja" - rozmowa z obsluga serwisu.
messagesConversation: '/wiadomosci/:conversationId',
// Nowa rozmowa z ogloszenia - watek powstaje na serwerze dopiero z pierwsza wiadomoscia.
messagesNew: '/wiadomosci/nowa/:listingId',
admin: '/admin',
listingDetail: '/oferta/:id',
publicProfile: '/profil/:id',
// Ten sam profil pod nazwą użytkownika - adres pokazywany w ustawieniach konta.
publicProfileByNick: '/u/:nick',
} as const;
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;
}
// Szczegóły oferty: identyfikator trafia do adresu, nie do stanu Reacta.
export function listingPath(id: number): string {
return `/oferta/${id}`;
}
// Profil publiczny innego użytkownika - otwierany m.in. z rozmowy w wiadomościach.
export function publicProfilePath(id: number): string {
return `/profil/${id}`;
}
// Identyfikator rozmowy z obsluga serwisu w adresie /wiadomosci/:conversationId.
export const ADMIN_CONVERSATION_SLUG = 'administracja';
export function conversationPath(id: number | typeof ADMIN_CONVERSATION_SLUG): string {
return `/wiadomosci/${id}`;
}
export function newConversationPath(listingId: number): string {
return `/wiadomosci/nowa/${listingId}`;
}
// Dedykowana strona edycji wlasnego ogloszenia.
export function listingEditPath(id: number): string {
return `/edytuj-ogloszenie/${id}`;
}
// Profil pod nazwą użytkownika. Nick jest unikalny w skali serwisu, więc jednoznacznie
// wskazuje osobę - imię i nazwisko może się powtarzać u wielu kont.
export function publicProfileNickPath(nick: string): string {
return `/u/${encodeURIComponent(nick)}`;
}
// Negocjacje dotyczą konkretnej oferty - bez identyfikatora pokazujemy pierwszą z listy.
export function negotiationPath(offerId?: number | null): string {
return offerId ? `${ROUTES.negotiation}/${offerId}` : ROUTES.negotiation;
}
// Lista ofert kupna zawężona do miasta - miasto w query stringu, żeby link działał po odświeżeniu
// i dało się go udostępnić (np. kliknięcie kafelka miasta na stronie głównej).
export function buyCityPath(city: string): string {
const params = new URLSearchParams();
params.set('miasto', city);
return `${ROUTES.buy}?${params.toString()}`;
}
// Mapa: miasto i typ oferty jako parametry zapytania, dzięki czemu wynik da się udostępnić.
export function mapPath(city?: string, offerType?: 'SALE' | 'RENT'): string {
const params = new URLSearchParams();
if (city) {
params.set('miasto', city);
}
if (offerType === 'RENT') {
params.set('typ', 'wynajem');
}
const query = params.toString();
return query ? `${ROUTES.map}?${query}` : ROUTES.map;
}
// Historia cen z wybraną lokalizacją w adresie - wejście z alertu cenowego otwiera
// od razu to miasto, a link da się skopiować, odświeżyć i otworzyć w nowej karcie.
export function priceHistoryPath(label?: string, propertyType?: string, marketType?: string): string {
const params = new URLSearchParams();
if (label) {
params.set('lokalizacja', label);
}
if (propertyType) {
params.set('typ', propertyType);
}
if (marketType) {
params.set('rynek', marketType);
}
const query = params.toString();
return query ? `${ROUTES.priceHistory}?${query}` : ROUTES.priceHistory;
}
// Lista firm i usług: kategoria w query stringu, dzięki czemu widok da się
// skopiować, odświeżyć i otworzyć w nowej karcie.
export function companiesPath(categorySlug?: string): string {
return categorySlug ? `${ROUTES.companies}?kategoria=${encodeURIComponent(categorySlug)}` : ROUTES.companies;
}
// Strona "Jeszcze nie mamy tej usługi" z formularzem reklamowym. Kategoria trafia do query
// stringu, żeby po wejściu z kafelka pole "Rodzaj usługi" było już wypełnione.
export function advertisingPath(categorySlug?: string): string {
return categorySlug ? `${ROUTES.advertising}?kategoria=${encodeURIComponent(categorySlug)}` : ROUTES.advertising;
}
// Zakładki panelu administratora jako podścieżki /admin/*.
export const ADMIN_TAB_PATHS = {
dashboard: '',
listings: 'ogloszenia',
users: 'uzytkownicy',
messages: 'wiadomosci',
support: 'pomoc-i-kontakt',
advertisingEnquiries: 'zapytania-o-reklame',
reports: 'zgloszenia',
articleRequests: 'poradniki-prosby',
payments: 'platnosci',
stats: 'statystyki',
settings: 'ustawienia',
moderation: 'weryfikacja',
categories: 'kategorie',
locations: 'lokalizacje',
promotions: 'promocje',
forbiddenWords: 'zakazane-slowa',
leads: 'leady',
campaigns: 'kampanie',
smtpConfig: 'konfiguracja-smtp',
smsConfig: 'konfiguracja-sms',
} as const;
export type AdminTabKey = keyof typeof ADMIN_TAB_PATHS;
export function adminTabPath(tab: AdminTabKey): string {
const segment = ADMIN_TAB_PATHS[tab];
return segment ? `${ROUTES.admin}/${segment}` : ROUTES.admin;
}
export const ADMIN_PATH_TO_TAB = Object.entries(ADMIN_TAB_PATHS).reduce<Record<string, AdminTabKey>>(
(acc, [tab, segment]) => {
acc[segment] = tab as AdminTabKey;
return acc;
},
{},
);
// Trasy dostępne wyłącznie po zalogowaniu (bramka w ProtectedRoute).
export const PROTECTED_PATHS: string[] = [
ROUTES.add,
ROUTES.account,
ROUTES.accountSearches,
ROUTES.accountPriceAlerts,
ROUTES.accountMeetings,
ROUTES.accountListings,
ROUTES.accountSettings,
ROUTES.accountSecurity,
ROUTES.accountProfileEdit,
ROUTES.accountPublicProfile,
ROUTES.accountHelpContact,
ROUTES.notifications,
ROUTES.favorites,
ROUTES.messages,
ROUTES.messagesConversation,
ROUTES.messagesNew,
ROUTES.comparison,
ROUTES.guideAdd,
];