Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ec73959a7 | |||
| 3e394998a3 | |||
| e1ab61ec7c | |||
| f966b4539a | |||
| 5f5bec7232 | |||
| af21991d9f | |||
| ec5a7289f6 | |||
| f072ed0115 | |||
| 498ea07b9e |
@@ -10,6 +10,8 @@ backend/target/
|
|||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/.vite/
|
frontend/.vite/
|
||||||
|
frontend/test-results/
|
||||||
|
frontend/playwright-report/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -44,6 +44,31 @@ Pull requests should include a short summary, testing performed, linked issues w
|
|||||||
|
|
||||||
Do not commit secrets, local credentials, generated build output, or machine-specific configuration. Store required environment variables in an ignored `.env` file and document safe example values in `.env.example`.
|
Do not commit secrets, local credentials, generated build output, or machine-specific configuration. Store required environment variables in an ignored `.env` file and document safe example values in `.env.example`.
|
||||||
|
|
||||||
|
## Standard architektoniczny: routing (obowiązkowy)
|
||||||
|
|
||||||
|
Każdy nowy widok, ekran, zakładka lub podstrona **musi mieć własny, jednoznaczny adres URL** i być obsługiwana przez React Router. To twardy standard całego projektu, nie sugestia.
|
||||||
|
|
||||||
|
Zasady:
|
||||||
|
|
||||||
|
- **Nie buduj nawigacji na stanie.** Zakazane jako mechanizm przełączania stron: `useState('view')`, `setView`, `activeView`, `selectedPage`, `selectedTab` i pochodne.
|
||||||
|
- **Nie przełączaj całych stron warunkowym renderowaniem** bez zmiany adresu (`{view === 'x' && <Page />}`).
|
||||||
|
- **Do nawigacji używaj** `Link`, `NavLink`, `useNavigate` oraz tras zadeklarowanych w `Routes`.
|
||||||
|
- **Identyfikatory obiektów trzymaj w parametrach URL**: `/oferta/:id`, `/profil/:id`, `/wiadomosci/:conversationId` — nie w stanie Reacta.
|
||||||
|
- **Filtry, sortowanie, wyszukiwanie i paginację zapisuj w query string** wszędzie tam, gdzie użytkownik powinien móc skopiować lub odświeżyć widok.
|
||||||
|
- **Zakładki paneli twórz jako trasy zagnieżdżone**: `/konto/ustawienia`, `/admin/uzytkownicy`.
|
||||||
|
- **Trasy wymagające logowania zabezpieczaj `ProtectedRoute`** (`frontend/src/ProtectedRoute.tsx`). Pamiętaj, że to zabezpieczenie interfejsu — autoryzację egzekwuje backend.
|
||||||
|
- **Nie używaj `href="#"`** ani przycisków imitujących linki.
|
||||||
|
- **Każdy link musi działać** po odświeżeniu (F5), przy bezpośrednim wejściu z adresu, po użyciu Wstecz/Dalej i po otwarciu w nowej karcie.
|
||||||
|
|
||||||
|
Gdzie co leży:
|
||||||
|
|
||||||
|
- `frontend/src/routes.ts` — centralna mapa adresów (`ROUTES`) i helpery (`listingPath`, `mapPath`, `adminTabPath`). Nowy widok zaczynaj od wpisu tutaj, nie od literału ścieżki w JSX.
|
||||||
|
- `frontend/src/ProtectedRoute.tsx` — bramka autoryzacji, obsługuje stan `loading` z `AuthProvider`.
|
||||||
|
- `frontend/src/App.tsx` — deklaracja `<Routes>`.
|
||||||
|
- `nginx/default.conf` — `try_files $uri $uri/ /index.html` (SPA fallback, już skonfigurowany).
|
||||||
|
|
||||||
|
Przed zakończeniem każdego zadania sprawdź, czy dodane widoki mają trasy i czy adres zmienia się podczas nawigacji. Jeżeli istniejący kod łamie ten standard, nie powielaj starego rozwiązania — dostosuj go do React Routera.
|
||||||
|
|
||||||
## Behavioral Guidelines To Reduce Common LLM Coding Mistakes
|
## Behavioral Guidelines To Reduce Common LLM Coding Mistakes
|
||||||
|
|
||||||
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||||
|
|||||||
Generated
+123
-1
@@ -11,9 +11,11 @@
|
|||||||
"@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": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@vitejs/plugin-react": "6.0.2",
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
@@ -108,6 +110,22 @@
|
|||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||||
@@ -536,6 +554,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",
|
||||||
@@ -977,6 +1008,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/png-js": {
|
"node_modules/png-js": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.1.0.tgz",
|
||||||
@@ -1039,6 +1117,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 +1213,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",
|
||||||
|
|||||||
@@ -6,15 +6,18 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"preview": "vite preview --host 0.0.0.0"
|
"preview": "vite preview --host 0.0.0.0",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@vitejs/plugin-react": "6.0.2",
|
"@vitejs/plugin-react": "6.0.2",
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
// Testy routingu uruchamiamy na systemowym Chrome (bez pobierania przegladarek Playwright).
|
||||||
|
// Domyslnie startujemy wlasny dev server; ustaw E2E_BASE_URL, zeby przetestowac
|
||||||
|
// build produkcyjny za nginx, np. E2E_BASE_URL=http://localhost npm run test:e2e
|
||||||
|
const baseURL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: { timeout: 15_000 },
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
reporter: [['list']],
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
channel: 'chrome',
|
||||||
|
viewport: { width: 1680, height: 1050 },
|
||||||
|
actionTimeout: 15_000,
|
||||||
|
},
|
||||||
|
webServer: process.env.E2E_BASE_URL
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:5173',
|
||||||
|
reuseExistingServer: true,
|
||||||
|
timeout: 120_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
+599
-436
File diff suppressed because it is too large
Load Diff
@@ -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 />;
|
||||||
|
}
|
||||||
+27
-6
@@ -104,21 +104,42 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
apiFetch<AuthUser>('/auth/me')
|
|
||||||
.then((me) => {
|
// Sesje konczymy wylacznie wtedy, gdy backend odrzuci token (401/403).
|
||||||
|
// Chwilowy blad sieci albo 5xx nie moze wylogowywac uzytkownika - taki blad
|
||||||
|
// ponawiamy raz, a token zostaje, wiec kolejne wejscie na strone go odzyska.
|
||||||
|
const loadProfile = async (canRetry: boolean): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const me = await apiFetch<AuthUser>('/auth/me');
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setUser(me);
|
setUser(me);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch(() => {
|
if (cancelled) {
|
||||||
if (!cancelled) {
|
return;
|
||||||
|
}
|
||||||
|
const status = (error as { status?: number }).status;
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
window.localStorage.removeItem(TOKEN_KEY);
|
window.localStorage.removeItem(TOKEN_KEY);
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
if (canRetry) {
|
||||||
|
await new Promise((resolve) => { window.setTimeout(resolve, 400); });
|
||||||
|
if (!cancelled) {
|
||||||
|
await loadProfile(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUser(null);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadProfile(true);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,121 @@
|
|||||||
|
// 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',
|
||||||
|
companies: '/firmy',
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
];
|
||||||
+235
-47
@@ -86,13 +86,13 @@ svg {
|
|||||||
row-gap: 6px;
|
row-gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-admin .nav button {
|
.header-admin .nav button, .header-admin .nav a {
|
||||||
color: #dbe4f0;
|
color: #dbe4f0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-admin .nav button.active::after {
|
.header-admin .nav button.active::after, .header-admin .nav a.active::after {
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ svg {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav button {
|
.nav button, .nav a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #111a2e;
|
color: #111a2e;
|
||||||
@@ -206,7 +206,7 @@ svg {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav button.active::after {
|
.nav button.active::after, .nav a.active::after {
|
||||||
background: #13a360;
|
background: #13a360;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
@@ -589,7 +589,7 @@ svg {
|
|||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-tabs button {
|
.search-tabs button, .search-tabs a {
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: rgba(255, 255, 255, 0.88);
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #657284;
|
color: #657284;
|
||||||
@@ -600,11 +600,11 @@ svg {
|
|||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-tabs button:first-child {
|
.search-tabs button:first-child, .search-tabs a:first-child {
|
||||||
border-radius: 6px 0 0 0;
|
border-radius: 6px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-tabs button:last-child {
|
.search-tabs button:last-child, .search-tabs a:last-child {
|
||||||
border-radius: 0 6px 0 0;
|
border-radius: 0 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,7 +798,7 @@ svg {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-card button {
|
.tool-card button, .tool-card a {
|
||||||
align-self: end;
|
align-self: end;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px solid #e2e8ef;
|
border: 1px solid #e2e8ef;
|
||||||
@@ -3183,7 +3183,7 @@ svg {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-side button {
|
.result-side button, .result-side a {
|
||||||
align-self: end;
|
align-self: end;
|
||||||
background: #0b9f5c;
|
background: #0b9f5c;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -5922,7 +5922,7 @@ svg {
|
|||||||
padding: 15px 16px;
|
padding: 15px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sell-valuation-card button {
|
.sell-valuation-card button, .sell-valuation-card a {
|
||||||
background: #0b9f5c;
|
background: #0b9f5c;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -6000,7 +6000,7 @@ svg {
|
|||||||
margin-bottom: 22px;
|
margin-bottom: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.valuation-breadcrumb button {
|
.valuation-breadcrumb button, .valuation-breadcrumb a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #768398;
|
color: #768398;
|
||||||
@@ -6009,7 +6009,7 @@ svg {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.valuation-breadcrumb button.active {
|
.valuation-breadcrumb button.active, .valuation-breadcrumb a.active {
|
||||||
color: #14223a;
|
color: #14223a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6876,7 +6876,7 @@ svg {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-breadcrumb button {
|
.history-breadcrumb button, .history-breadcrumb a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #7a8798;
|
color: #7a8798;
|
||||||
@@ -6885,7 +6885,7 @@ svg {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.history-breadcrumb button.active {
|
.history-breadcrumb button.active, .history-breadcrumb a.active {
|
||||||
color: #14223a;
|
color: #14223a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8331,7 +8331,7 @@ svg {
|
|||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sell-bottom-cta button {
|
.sell-bottom-cta button, .sell-bottom-cta a {
|
||||||
background: #0b9f5c;
|
background: #0b9f5c;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -9553,7 +9553,7 @@ svg {
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.district-breadcrumb button {
|
.district-breadcrumb button, .district-breadcrumb a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #768398;
|
color: #768398;
|
||||||
@@ -9562,7 +9562,7 @@ svg {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.district-breadcrumb button.active {
|
.district-breadcrumb button.active, .district-breadcrumb a.active {
|
||||||
color: #14223a;
|
color: #14223a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10090,7 +10090,7 @@ svg {
|
|||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.negotiation-breadcrumb button {
|
.negotiation-breadcrumb button, .negotiation-breadcrumb a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #768398;
|
color: #768398;
|
||||||
@@ -10099,7 +10099,7 @@ svg {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.negotiation-breadcrumb button.active {
|
.negotiation-breadcrumb button.active, .negotiation-breadcrumb a.active {
|
||||||
color: #14223a;
|
color: #14223a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10798,7 +10798,7 @@ svg {
|
|||||||
gap: 17px;
|
gap: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav button,
|
.nav button, .nav a,
|
||||||
.icon-text span {
|
.icon-text span {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -11003,11 +11003,11 @@ svg {
|
|||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav button {
|
.nav button, .nav a {
|
||||||
padding: 8px 0 12px;
|
padding: 8px 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav button.active::after {
|
.nav button.active::after, .nav a.active::after {
|
||||||
bottom: 3px;
|
bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11417,7 +11417,7 @@ svg {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-tabs button {
|
.search-tabs button, .search-tabs a {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
@@ -13947,7 +13947,7 @@ svg {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-menu button {
|
.account-menu button, .account-menu a {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -13964,12 +13964,12 @@ svg {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-menu button svg {
|
.account-menu button svg, .account-menu a svg {
|
||||||
color: #7d8ca0;
|
color: #7d8ca0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-menu button span {
|
.account-menu button span, .account-menu a span {
|
||||||
background: #e8f8ef;
|
background: #e8f8ef;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
color: #109253;
|
color: #109253;
|
||||||
@@ -13978,13 +13978,13 @@ svg {
|
|||||||
padding: 1px 6px;
|
padding: 1px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-menu button .tag {
|
.account-menu button .tag, .account-menu a .tag {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
padding: 1px 5px;
|
padding: 1px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-menu button.active {
|
.account-menu button.active, .account-menu a.active {
|
||||||
background: #ebf6f0;
|
background: #ebf6f0;
|
||||||
color: #109253;
|
color: #109253;
|
||||||
}
|
}
|
||||||
@@ -15192,7 +15192,7 @@ svg {
|
|||||||
margin: 2px 0 8px;
|
margin: 2px 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-page .messages-breadcrumb button {
|
.messages-page .messages-breadcrumb button, .messages-page .messages-breadcrumb a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #718094;
|
color: #718094;
|
||||||
@@ -15201,7 +15201,7 @@ svg {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-page .messages-breadcrumb button.active {
|
.messages-page .messages-breadcrumb button.active, .messages-page .messages-breadcrumb a.active {
|
||||||
color: #14223a;
|
color: #14223a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17044,7 +17044,7 @@ svg {
|
|||||||
.account-alerts article > span svg,
|
.account-alerts article > span svg,
|
||||||
.account-searches article > span svg,
|
.account-searches article > span svg,
|
||||||
.account-tools article > span svg,
|
.account-tools article > span svg,
|
||||||
.account-menu button svg {
|
.account-menu button svg, .account-menu a svg {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
}
|
}
|
||||||
@@ -18271,7 +18271,7 @@ svg {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-alerts-active-items button {
|
.price-alerts-active-items button, .price-alerts-active-items a {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #fbfcfe;
|
background: #fbfcfe;
|
||||||
border: 1px solid #e2eaf3;
|
border: 1px solid #e2eaf3;
|
||||||
@@ -18405,7 +18405,7 @@ svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-card-head.split > button,
|
.settings-card-head.split > button,
|
||||||
.settings-profile-row > button {
|
.settings-profile-row > button, .settings-profile-row > a {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #d9e2ec;
|
border: 1px solid #d9e2ec;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -20757,7 +20757,7 @@ svg {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-profile-head > button {
|
.public-profile-head > button, .public-profile-head > a {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #d9e2ec;
|
border: 1px solid #d9e2ec;
|
||||||
@@ -20771,7 +20771,7 @@ svg {
|
|||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.public-profile-head > button svg {
|
.public-profile-head > button svg, .public-profile-head > a svg {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
}
|
}
|
||||||
@@ -21387,7 +21387,7 @@ svg {
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav button {
|
.admin-nav button, .admin-nav a {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -21404,22 +21404,22 @@ svg {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav button svg {
|
.admin-nav button svg, .admin-nav a svg {
|
||||||
color: #7186a0;
|
color: #7186a0;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav button.active {
|
.admin-nav button.active, .admin-nav a.active {
|
||||||
background: #ebf6f0;
|
background: #ebf6f0;
|
||||||
color: #109253;
|
color: #109253;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav button.active svg {
|
.admin-nav button.active svg, .admin-nav a.active svg {
|
||||||
color: #109253;
|
color: #109253;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-nav button b {
|
.admin-nav button b, .admin-nav a b {
|
||||||
background: #129357;
|
background: #129357;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@@ -21559,7 +21559,7 @@ svg {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-card-head button {
|
.admin-card-head button, .admin-card-head a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #159e5d;
|
color: #159e5d;
|
||||||
@@ -21636,7 +21636,7 @@ svg {
|
|||||||
padding: 12px 18px 18px;
|
padding: 12px 18px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-quick-actions-grid button {
|
.admin-quick-actions-grid button, .admin-quick-actions-grid a {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f8fbff;
|
background: #f8fbff;
|
||||||
border: 1px solid #e0e8f2;
|
border: 1px solid #e0e8f2;
|
||||||
@@ -21650,11 +21650,11 @@ svg {
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-quick-actions-grid button svg:first-child {
|
.admin-quick-actions-grid button svg:first-child, .admin-quick-actions-grid a svg:first-child {
|
||||||
color: #109253;
|
color: #109253;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-quick-actions-grid button svg:last-child {
|
.admin-quick-actions-grid button svg:last-child, .admin-quick-actions-grid a svg:last-child {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
@@ -22853,7 +22853,7 @@ svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.listing-detail-back,
|
.listing-detail-back,
|
||||||
.listing-detail-crumbs button {
|
.listing-detail-crumbs button, .listing-detail-crumbs a {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -24390,7 +24390,7 @@ svg {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-breadcrumb button {
|
.comparison-breadcrumb button, .comparison-breadcrumb a {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@@ -24398,7 +24398,7 @@ svg {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comparison-breadcrumb button.active {
|
.comparison-breadcrumb button.active, .comparison-breadcrumb a.active {
|
||||||
color: #152640;
|
color: #152640;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26749,3 +26749,191 @@ 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; }
|
||||||
|
|
||||||
|
/* Routing: NavLink/Link renderuja <a> - zerujemy domyslne style linku,
|
||||||
|
zeby menu wygladalo dokladnie tak jak wczesniejsze <button>. */
|
||||||
|
.nav a,
|
||||||
|
.account-menu a,
|
||||||
|
.brand,
|
||||||
|
.icon-text {
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-page { display: grid; padding: 90px 24px 120px; place-items: center; }
|
||||||
|
.not-found-shell { max-width: 520px; text-align: center; }
|
||||||
|
.not-found-shell h1 { color: #13a360; font-size: 88px; font-weight: 900; line-height: 1; margin: 0; }
|
||||||
|
.not-found-shell h2 { color: #111a2e; font-size: 24px; font-weight: 800; margin: 16px 0 8px; }
|
||||||
|
.not-found-shell p { color: #5b6472; font-size: 15px; margin: 0 0 28px; }
|
||||||
|
.not-found-actions { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
|
||||||
|
.not-found-actions a { border-radius: 10px; font-size: 14px; font-weight: 800; padding: 12px 22px; text-decoration: none; }
|
||||||
|
.not-found-primary { background: #13a360; color: #ffffff; }
|
||||||
|
.not-found-secondary { border: 1px solid #d5dde7; color: #27384f; }
|
||||||
|
|
||||||
|
/* Routing: pozycje menu admina to teraz <a> (NavLink) */
|
||||||
|
.admin-nav a { color: inherit; cursor: pointer; text-decoration: none; }
|
||||||
|
|
||||||
|
/* Routing: okruszki nawigacyjne to teraz <Link> */
|
||||||
|
.breadcrumb a,
|
||||||
|
.district-breadcrumb a,
|
||||||
|
.negotiation-breadcrumb a,
|
||||||
|
.comparison-breadcrumb a,
|
||||||
|
.messages-breadcrumb a,
|
||||||
|
.valuation-breadcrumb a,
|
||||||
|
.history-breadcrumb a,
|
||||||
|
.credit-breadcrumb a { color: inherit; cursor: pointer; text-decoration: none; }
|
||||||
|
|
||||||
|
/* Routing: naglowkowe CTA jako <Link> - zachowanie wygladu przyciskow */
|
||||||
|
.post-button { align-content: center; text-decoration: none; }
|
||||||
|
|
||||||
|
/* Lista firm i uslug (/firmy) - korzysta z istniejacych kart .directory-card */
|
||||||
|
.companies-frame {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 1420px;
|
||||||
|
padding: 0 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-frame .services-main { padding-right: 0; }
|
||||||
|
|
||||||
|
.companies-head { margin: 18px 0 20px; }
|
||||||
|
|
||||||
|
.companies-head h1 {
|
||||||
|
color: #111a2e;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.15;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-head p {
|
||||||
|
color: #5b6472;
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-count {
|
||||||
|
color: #109253;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-chip {
|
||||||
|
align-items: center;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #dce4ec;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #27384f;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-chip svg { color: #7d8ca0; font-size: 14px; }
|
||||||
|
|
||||||
|
.companies-chip span {
|
||||||
|
background: #eef3f8;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #5b6472;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
padding: 1px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-chip:hover { border-color: #b9c6d4; }
|
||||||
|
|
||||||
|
.companies-chip.active {
|
||||||
|
background: #ebf6f0;
|
||||||
|
border-color: #13a360;
|
||||||
|
color: #109253;
|
||||||
|
}
|
||||||
|
|
||||||
|
.companies-chip.active svg { color: #109253; }
|
||||||
|
.companies-chip.active span { background: #d7efe2; color: #0d7a45; }
|
||||||
|
|
||||||
|
.companies-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.companies-frame { padding: 0 24px; }
|
||||||
|
.companies-list { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.companies-head h1 { font-size: 24px; }
|
||||||
|
.companies-frame { padding: 0 14px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Routing: CTA nawigacyjne renderuja sie jako <a> (Link). :where() ma zerowa
|
||||||
|
specyficznosc, wiec kazda istniejaca regula klasy nadal wygrywa - to tylko
|
||||||
|
zdejmuje domyslne podkreslenie i niebieski kolor linku. */
|
||||||
|
:where(a) {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Routing: CTA o ukladzie blokowym odtwarzaja centrowanie tresci znane z <button> */
|
||||||
|
a.sell-primary,
|
||||||
|
a.account-new-search,
|
||||||
|
a.price-alerts-link,
|
||||||
|
a.price-alerts-manage,
|
||||||
|
a.cover-preview-button,
|
||||||
|
a.back-step-button,
|
||||||
|
a.listing-detail-back {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kontenery, ktorych reguly nie ustawialy display - <button> centrowal tresc sam,
|
||||||
|
<a> wymaga tego jawnie. Pudelko (padding, min-height, tlo) bez zmian. */
|
||||||
|
.search-tabs a,
|
||||||
|
.tool-card a,
|
||||||
|
.sell-valuation-card a,
|
||||||
|
.sell-bottom-cta a,
|
||||||
|
.settings-profile-row > a {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Karty prowadzace do osobnej strony: link rozciagniety na cala karte.
|
||||||
|
Daje prawdziwy <a href> (kopiowanie, nowa karta, indeksowanie) bez
|
||||||
|
zagniezdzania przyciskow w <a>. Kontrolki karty zostaja klikalne nad nakladka. */
|
||||||
|
.real-result-card { position: relative; }
|
||||||
|
|
||||||
|
.card-stretched-link::after {
|
||||||
|
content: "";
|
||||||
|
inset: 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.real-result-card .real-favorite-button,
|
||||||
|
.real-result-card .real-card-cta {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.real-card-cta {
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
|
import { ROUTES } from '../src/routes';
|
||||||
|
|
||||||
|
// Standard projektu (AGENTS.md): kazdy widok ma wlasny URL obslugiwany przez React Router,
|
||||||
|
// a nawigacja odbywa sie przez <Link>/<NavLink>. Te testy pilnuja tej zasady.
|
||||||
|
|
||||||
|
const ADMIN = { email: 'admin@mieszko.pl', password: 'Admin123!' };
|
||||||
|
|
||||||
|
const PUBLIC_ROUTES = [
|
||||||
|
ROUTES.home,
|
||||||
|
ROUTES.buy,
|
||||||
|
ROUTES.rent,
|
||||||
|
ROUTES.sell,
|
||||||
|
ROUTES.valuation,
|
||||||
|
ROUTES.priceHistory,
|
||||||
|
ROUTES.districtRanking,
|
||||||
|
ROUTES.negotiation,
|
||||||
|
ROUTES.creditCalculator,
|
||||||
|
ROUTES.map,
|
||||||
|
ROUTES.services,
|
||||||
|
ROUTES.companies,
|
||||||
|
ROUTES.guides,
|
||||||
|
ROUTES.guideBuying,
|
||||||
|
ROUTES.login,
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROTECTED_ROUTES = [
|
||||||
|
ROUTES.account,
|
||||||
|
ROUTES.favorites,
|
||||||
|
ROUTES.messages,
|
||||||
|
ROUTES.add,
|
||||||
|
ROUTES.admin,
|
||||||
|
`${ROUTES.admin}/leady`,
|
||||||
|
];
|
||||||
|
|
||||||
|
async function goto(page: Page, path: string) {
|
||||||
|
await page.goto(path, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.locator('main').first().waitFor({ state: 'attached' });
|
||||||
|
// ProtectedRoute dociaga profil z /auth/me - poczekaj, az skonczy stan ladowania
|
||||||
|
await page.locator('.route-loading').waitFor({ state: 'detached' }).catch(() => {});
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(page: Page) {
|
||||||
|
await goto(page, ROUTES.login);
|
||||||
|
await page.getByPlaceholder('Wpisz swój adres e-mail').fill(ADMIN.email);
|
||||||
|
await page.getByPlaceholder('Wpisz swoje hasło').fill(ADMIN.password);
|
||||||
|
await page.getByRole('button', { name: 'Zaloguj się', exact: true }).click();
|
||||||
|
// token w localStorage to pewny dowod zalogowania - adres moze sie jeszcze zmieniac
|
||||||
|
await page.waitForFunction(() => Boolean(window.localStorage.getItem('polskalokalnie-auth-token')));
|
||||||
|
await page.waitForFunction(() => window.location.pathname !== '/logowanie');
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('Trasy publiczne', () => {
|
||||||
|
for (const path of PUBLIC_ROUTES) {
|
||||||
|
test(`bezposrednie wejscie na ${path} (F5 / nowa karta / wklejony link)`, async ({ page }) => {
|
||||||
|
await goto(page, path);
|
||||||
|
expect(new URL(page.url()).pathname).toBe(path);
|
||||||
|
await expect(page.locator('main')).not.toBeEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('nieznany adres pokazuje strone 404', async ({ page }) => {
|
||||||
|
await goto(page, '/adres-ktory-nie-istnieje');
|
||||||
|
await expect(page.locator('.not-found-page')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('odswiezenie strony zachowuje widok', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.districtRanking);
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
expect(new URL(page.url()).pathname).toBe(ROUTES.districtRanking);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Lista firm i uslug (/firmy)', () => {
|
||||||
|
test('wejscie na /firmy pokazuje pelna liste', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.companies);
|
||||||
|
await expect(page.locator('.companies-head h1')).toHaveText('Wszystkie firmy i usługi');
|
||||||
|
const cards = await page.locator('.directory-card').count();
|
||||||
|
expect(cards).toBeGreaterThan(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('filtr kategorii dziala przez query string', async ({ page }) => {
|
||||||
|
await goto(page, `${ROUTES.companies}?kategoria=remonty`);
|
||||||
|
await expect(page.locator('.companies-head h1')).toHaveText('Remont i wykończenie');
|
||||||
|
|
||||||
|
const filtered = await page.locator('.directory-card').count();
|
||||||
|
await goto(page, ROUTES.companies);
|
||||||
|
const all = await page.locator('.directory-card').count();
|
||||||
|
expect(filtered).toBeGreaterThan(0);
|
||||||
|
expect(filtered).toBeLessThan(all);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nieznana kategoria nie wywraca widoku - pokazuje pelna liste', async ({ page }) => {
|
||||||
|
await goto(page, `${ROUTES.companies}?kategoria=nie-ma-takiej`);
|
||||||
|
await expect(page.locator('.companies-head h1')).toHaveText('Wszystkie firmy i usługi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('klik w chip kategorii zmienia adres i wynik', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.companies);
|
||||||
|
await page.locator('.companies-chip', { hasText: 'Finanse' }).first().click();
|
||||||
|
await page.waitForFunction(() => window.location.search.includes('kategoria=finanse'));
|
||||||
|
await expect(page.locator('.companies-head h1')).toHaveText('Finanse');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('"Zobacz firmy" prowadzi do listy z wybrana kategoria', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.services);
|
||||||
|
const link = page.locator('.service-category-card', { hasText: 'Remont i wykończenie' }).getByRole('link');
|
||||||
|
await expect(link).toHaveAttribute('href', '/firmy?kategoria=remonty');
|
||||||
|
await link.click();
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/firmy');
|
||||||
|
await expect(page.locator('.companies-head h1')).toHaveText('Remont i wykończenie');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('"Zobacz wszystkie" prowadzi do pelnej listy', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.services);
|
||||||
|
const link = page.locator('.services-section .section-head', { hasText: 'Polecane firmy' }).getByRole('link');
|
||||||
|
await expect(link).toHaveAttribute('href', ROUTES.companies);
|
||||||
|
await link.click();
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/firmy');
|
||||||
|
await expect(page.locator('.companies-head h1')).toHaveText('Wszystkie firmy i usługi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Wstecz i Dalej dzialaja na filtrze kategorii', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.services);
|
||||||
|
await page.locator('.service-category-card', { hasText: 'Finanse' }).getByRole('link').click();
|
||||||
|
await page.waitForFunction(() => window.location.search.includes('kategoria=finanse'));
|
||||||
|
|
||||||
|
await page.goBack({ waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/firmy-i-uslugi');
|
||||||
|
|
||||||
|
await page.goForward({ waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => window.location.search.includes('kategoria=finanse'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Nawigacja przez linki', () => {
|
||||||
|
test('menu glowne to <a href> - mozna otworzyc w nowej karcie', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.home);
|
||||||
|
for (const [label, expected] of [
|
||||||
|
['Kupuję', ROUTES.buy],
|
||||||
|
['Wynajmuję', ROUTES.rent],
|
||||||
|
['Poradniki', ROUTES.guides],
|
||||||
|
['Firmy i usługi', ROUTES.services],
|
||||||
|
] as const) {
|
||||||
|
const link = page.getByRole('link', { name: label, exact: true }).first();
|
||||||
|
await expect(link).toHaveAttribute('href', expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('aktywna pozycja menu wynika z adresu', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.buy);
|
||||||
|
await expect(page.getByRole('link', { name: 'Kupuję', exact: true }).first()).toHaveClass(/active/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CTA naglowka maja poprawne adresy', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.home);
|
||||||
|
await expect(page.locator('a.post-button')).toHaveAttribute('href', ROUTES.add);
|
||||||
|
await expect(page.getByRole('link', { name: 'Moje konto' })).toHaveAttribute('href', ROUTES.login);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CTA w tresci strony sa linkami z href', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.sell);
|
||||||
|
await expect(page.locator('a.sell-primary')).toHaveAttribute('href', ROUTES.add);
|
||||||
|
await expect(page.locator('.sell-valuation-card a')).toHaveAttribute('href', ROUTES.valuation);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('karta oferty ma prawdziwy link do strony oferty', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.buy);
|
||||||
|
await page.locator('.real-result-card').first().waitFor();
|
||||||
|
const card = page.locator('.real-result-card').first();
|
||||||
|
const href = await card.locator('.card-stretched-link').getAttribute('href');
|
||||||
|
expect(href).toMatch(/^\/oferta\/\d+$/);
|
||||||
|
await expect(card.locator('a.real-card-cta')).toHaveAttribute('href', href!);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('klik w serce na karcie nie nawiguje', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.buy);
|
||||||
|
await page.locator('.real-result-card').first().waitFor();
|
||||||
|
await page.locator('.real-result-card').first().locator('.real-favorite-button').click();
|
||||||
|
await page.waitForTimeout(700);
|
||||||
|
expect(new URL(page.url()).pathname).toBe(ROUTES.buy);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('klik w menu zmienia adres, Wstecz i Dalej dzialaja', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.home);
|
||||||
|
await page.getByRole('link', { name: 'Kupuję', exact: true }).first().click();
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/kupno');
|
||||||
|
|
||||||
|
await page.getByRole('link', { name: 'Poradniki', exact: true }).first().click();
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/poradniki');
|
||||||
|
|
||||||
|
await page.goBack({ waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/kupno');
|
||||||
|
|
||||||
|
await page.goForward({ waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/poradniki');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Standard: brak atrap linkow', () => {
|
||||||
|
const PAGES = [ROUTES.home, ROUTES.services, ROUTES.companies, ROUTES.guides, ROUTES.sell, ROUTES.buy];
|
||||||
|
|
||||||
|
for (const path of PAGES) {
|
||||||
|
test(`${path} nie zawiera href="#" ani javascript:void`, async ({ page }) => {
|
||||||
|
await goto(page, path);
|
||||||
|
const bad = await page.locator('a[href="#"], a[href^="javascript:"]').count();
|
||||||
|
expect(bad).toBe(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Trasy chronione', () => {
|
||||||
|
test('bez logowania przekierowuja na /logowanie', async ({ page }) => {
|
||||||
|
for (const path of PROTECTED_ROUTES) {
|
||||||
|
await goto(page, path);
|
||||||
|
expect(new URL(page.url()).pathname).toBe(ROUTES.login);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('po zalogowaniu wracamy na zamierzona strone', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.accountPriceAlerts);
|
||||||
|
expect(new URL(page.url()).pathname).toBe(ROUTES.login);
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Wpisz swój adres e-mail').fill(ADMIN.email);
|
||||||
|
await page.getByPlaceholder('Wpisz swoje hasło').fill(ADMIN.password);
|
||||||
|
await page.getByRole('button', { name: 'Zaloguj się', exact: true }).click();
|
||||||
|
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/konto/alerty-cenowe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zakladki panelu admina maja wlasne adresy i przezywaja F5', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await goto(page, ROUTES.admin);
|
||||||
|
|
||||||
|
await page.locator('.admin-nav a', { hasText: 'Leady' }).first().click();
|
||||||
|
await page.waitForFunction(() => window.location.pathname === '/admin/leady');
|
||||||
|
|
||||||
|
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||||
|
await page.locator('.admin-nav').first().waitFor({ state: 'attached' });
|
||||||
|
expect(new URL(page.url()).pathname).toBe('/admin/leady');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nieznana sekcja panelu admina pokazuje 404', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await goto(page, `${ROUTES.admin}/nie-ma-takiej-sekcji`);
|
||||||
|
await expect(page.locator('.not-found-page')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user