Dodaj react-router-dom, mape tras i ProtectedRoute
Fundament migracji z useState(view) na prawdziwy routing: - react-router-dom 7 w zaleznosciach - BrowserRouter w main.tsx (nginx ma juz try_files, wiec czyste sciezki dzialaja) - routes.ts: centralna mapa adresow + helpery listingPath/mapPath/adminTabPath - ProtectedRoute z obsluga stanu loading z AuthProvider Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+59
-1
@@ -11,7 +11,8 @@
|
|||||||
"@types/pdfmake": "^0.3.3",
|
"@types/pdfmake": "^0.3.3",
|
||||||
"pdfmake": "^0.3.11",
|
"pdfmake": "^0.3.11",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1",
|
||||||
|
"react-router-dom": "^7.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
@@ -536,6 +537,19 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1039,6 +1053,44 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
|
||||||
|
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
|
||||||
|
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.18.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/restructure": {
|
"node_modules/restructure": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
|
||||||
@@ -1097,6 +1149,12 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"@types/pdfmake": "^0.3.3",
|
"@types/pdfmake": "^0.3.3",
|
||||||
"pdfmake": "^0.3.11",
|
"pdfmake": "^0.3.11",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1",
|
||||||
|
"react-router-dom": "^7.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuth } from './auth';
|
||||||
|
import { ROUTES } from './routes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bramka dostępu dla tras wymagających zalogowania (i opcjonalnie roli ADMIN).
|
||||||
|
*
|
||||||
|
* Uwaga na `loading`: po odświeżeniu strony token jest w localStorage, ale profil
|
||||||
|
* dociąga się asynchronicznie z /auth/me. Bez tego warunku bezpośrednie wejście na
|
||||||
|
* /konto wyrzucałoby zalogowanego użytkownika na logowanie, zanim profil dotrze.
|
||||||
|
*
|
||||||
|
* To zabezpieczenie interfejsu, nie kontrola dostępu do danych — autoryzację
|
||||||
|
* egzekwuje backend przy każdym endpoincie.
|
||||||
|
*/
|
||||||
|
export function ProtectedRoute({ requireAdmin = false }: { requireAdmin?: boolean }) {
|
||||||
|
const { user, loading } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="route-loading" role="status" aria-live="polite">
|
||||||
|
Wczytywanie…
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
// Zapamiętujemy cel, żeby po zalogowaniu wrócić dokładnie tam, gdzie użytkownik zmierzał.
|
||||||
|
return <Navigate to={ROUTES.login} state={{ from: `${location.pathname}${location.search}` }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireAdmin && user.role !== 'ADMIN') {
|
||||||
|
return <Navigate to={ROUTES.account} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { AuthProvider } from './auth';
|
import { AuthProvider } from './auth';
|
||||||
import { NotificationsProvider } from './notifications';
|
import { NotificationsProvider } from './notifications';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<NotificationsProvider>
|
<NotificationsProvider>
|
||||||
<App />
|
<App />
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</BrowserRouter>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// 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',
|
||||||
|
services: '/firmy-i-uslugi',
|
||||||
|
guides: '/poradniki',
|
||||||
|
guideBuying: '/poradniki/kupno-mieszkania',
|
||||||
|
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',
|
||||||
|
notifications: '/powiadomienia',
|
||||||
|
favorites: '/ulubione',
|
||||||
|
messages: '/wiadomosci',
|
||||||
|
admin: '/admin',
|
||||||
|
listingDetail: '/oferta/:id',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type RoutePath = (typeof ROUTES)[keyof typeof ROUTES];
|
||||||
|
|
||||||
|
// Szczegóły oferty: identyfikator trafia do adresu, nie do stanu Reacta.
|
||||||
|
export function listingPath(id: number): string {
|
||||||
|
return `/oferta/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negocjacje dotyczą konkretnej oferty — bez identyfikatora pokazujemy pierwszą z listy.
|
||||||
|
export function negotiationPath(offerId?: number | null): string {
|
||||||
|
return offerId ? `${ROUTES.negotiation}/${offerId}` : ROUTES.negotiation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zakładki panelu administratora jako podścieżki /admin/*.
|
||||||
|
export const ADMIN_TAB_PATHS = {
|
||||||
|
dashboard: '',
|
||||||
|
listings: 'ogloszenia',
|
||||||
|
users: 'uzytkownicy',
|
||||||
|
messages: 'wiadomosci',
|
||||||
|
reports: 'zgloszenia',
|
||||||
|
payments: 'platnosci',
|
||||||
|
stats: 'statystyki',
|
||||||
|
settings: 'ustawienia',
|
||||||
|
moderation: 'weryfikacja',
|
||||||
|
categories: 'kategorie',
|
||||||
|
locations: 'lokalizacje',
|
||||||
|
promotions: 'promocje',
|
||||||
|
forbiddenWords: 'zakazane-slowa',
|
||||||
|
leads: 'leady',
|
||||||
|
campaigns: 'kampanie',
|
||||||
|
mailConfig: 'konfiguracja-wysylki',
|
||||||
|
} 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.comparison,
|
||||||
|
];
|
||||||
@@ -26749,3 +26749,6 @@ svg {
|
|||||||
|
|
||||||
.mkt-error { color: #b3261e; font-size: 13px; font-weight: 600; margin: 0; }
|
.mkt-error { color: #b3261e; font-size: 13px; font-weight: 600; margin: 0; }
|
||||||
.mkt-ok { color: #12784a; font-size: 13px; font-weight: 600; margin: 0; }
|
.mkt-ok { color: #12784a; font-size: 13px; font-weight: 600; margin: 0; }
|
||||||
|
|
||||||
|
/* Routing: stan wczytywania profilu przy wejsciu na trase chroniona */
|
||||||
|
.route-loading { padding: 80px 24px; text-align: center; color: #5b6472; font-size: 15px; font-weight: 600; }
|
||||||
|
|||||||
Reference in New Issue
Block a user