Kalkulator zdolności, integracja Moich ofert i system powiadomień
- Kalkulator zdolności kredytowej: nowy widok z live-obliczeniami (rata annuitetowa/malejąca, DTI/DSTI, rekomendacje, donut), wpięty w poradnik, panel i stronę główną; pasek boczny konta dla zalogowanych. - Moje oferty: prawdziwe ogłoszenia (/listings/mine) scalone z listą, realne akcje Wstrzymaj/Wznów (PATCH /status, nowy status PAUSED), Usuń (DELETE) z kontrolą właściciela; realne wyświetlenia (viewsCount) i zapisane z ulubionych. Fix CHECK constraintu enuma w SchemaFixer. - System powiadomień: encja Notification + serwis + kontroler + SSE realtime (token w query param w JwtAuthFilter). Podpięte źródła serwerowe (wiadomości, zmiana statusu ogłoszenia, zdarzenia konta) i klienckie (ulubione, spadek ceny, spotkania, weryfikacja telefonu). Centralny NotificationsProvider; dzwonek i strona bez zmian designu, klikalne z deep-linkami i oznaczaniem jako przeczytane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+756
-101
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,14 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { AuthProvider } from './auth';
|
||||
import { NotificationsProvider } from './notifications';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
<NotificationsProvider>
|
||||
<App />
|
||||
</NotificationsProvider>
|
||||
</AuthProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { apiFetch, getToken, useAuth } from './auth';
|
||||
|
||||
export type NotificationCategoryKey = 'listing' | 'messages' | 'system';
|
||||
|
||||
// Powiadomienie w formie z backendu.
|
||||
export type ServerNotification = {
|
||||
id: number;
|
||||
category: NotificationCategoryKey;
|
||||
icon: string;
|
||||
title: string;
|
||||
body: string | null;
|
||||
link: string | null;
|
||||
read: boolean;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
// Zdarzenia liczone po stronie klienta (alerty cen, spotkania, ulubione, telefon, dopasowania).
|
||||
export type NotifyInput = {
|
||||
category: NotificationCategoryKey;
|
||||
icon?: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
link?: string;
|
||||
dedupeKey?: string;
|
||||
};
|
||||
|
||||
// Kształt do renderu (zgodny z istniejącym UI: title/description/timeLabel/dayLabel/category/icon/unread/action).
|
||||
export type DisplayNotification = ServerNotification & {
|
||||
description: string;
|
||||
timeLabel: string;
|
||||
dayLabel: 'Dzisiaj' | 'Wczoraj';
|
||||
unread: boolean;
|
||||
action?: 'arrow';
|
||||
};
|
||||
|
||||
type NotificationsContextValue = {
|
||||
notifications: ServerNotification[];
|
||||
unreadCount: number;
|
||||
markRead: (id: number) => void;
|
||||
markAllRead: () => void;
|
||||
notify: (input: NotifyInput) => void;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
const NotificationsContext = createContext<NotificationsContextValue | null>(null);
|
||||
|
||||
function pad(value: number): string {
|
||||
return value < 10 ? `0${value}` : String(value);
|
||||
}
|
||||
|
||||
// Mapowanie listy z backendu na kształt oczekiwany przez istniejący widok (bez zmiany designu).
|
||||
export function toDisplayNotifications(list: ServerNotification[]): DisplayNotification[] {
|
||||
const now = new Date();
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
|
||||
|
||||
return list.map((item) => {
|
||||
const created = new Date(item.createdAt);
|
||||
const createdMs = created.getTime();
|
||||
const time = `${pad(created.getHours())}:${pad(created.getMinutes())}`;
|
||||
const isToday = createdMs >= startOfToday;
|
||||
const isYesterday = createdMs >= startOfYesterday && createdMs < startOfToday;
|
||||
|
||||
let timeLabel: string;
|
||||
if (isToday) {
|
||||
timeLabel = time;
|
||||
} else if (isYesterday) {
|
||||
timeLabel = `Wczoraj, ${time}`;
|
||||
} else {
|
||||
timeLabel = `${pad(created.getDate())}.${pad(created.getMonth() + 1)}, ${time}`;
|
||||
}
|
||||
|
||||
const unread = !item.read;
|
||||
return {
|
||||
...item,
|
||||
description: item.body ?? '',
|
||||
timeLabel,
|
||||
// Widok grupuje wyłącznie na "Dzisiaj"/"Wczoraj" — starsze trafiają do "Wczoraj".
|
||||
dayLabel: isToday ? 'Dzisiaj' : 'Wczoraj',
|
||||
unread,
|
||||
action: !unread && item.link ? 'arrow' : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function NotificationsProvider({ children }: { children: ReactNode }) {
|
||||
const { user } = useAuth();
|
||||
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
|
||||
const sentDedupeKeys = useRef<Set<string>>(new Set());
|
||||
|
||||
const unreadCount = notifications.reduce((count, item) => (item.read ? count : count + 1), 0);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (!getToken()) {
|
||||
setNotifications([]);
|
||||
return;
|
||||
}
|
||||
apiFetch<ServerNotification[]>('/notifications')
|
||||
.then((data) => setNotifications(Array.isArray(data) ? data : []))
|
||||
.catch(() => { /* cicho - brak sieci/tokenu */ });
|
||||
}, []);
|
||||
|
||||
// Ładowanie listy + realtime (SSE) gdy użytkownik zalogowany. Token w query param (EventSource nie ustawia nagłówków).
|
||||
useEffect(() => {
|
||||
const token = getToken();
|
||||
if (!user || !token) {
|
||||
setNotifications([]);
|
||||
sentDedupeKeys.current = new Set();
|
||||
return;
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
const source = new EventSource(`/api/notifications/stream?access_token=${encodeURIComponent(token)}`);
|
||||
source.addEventListener('notification', (event) => {
|
||||
try {
|
||||
const incoming = JSON.parse((event as MessageEvent).data) as ServerNotification;
|
||||
setNotifications((current) => (current.some((item) => item.id === incoming.id) ? current : [incoming, ...current]));
|
||||
} catch {
|
||||
/* ignoruj nieparsowalne zdarzenie */
|
||||
}
|
||||
});
|
||||
// onerror: EventSource sam ponawia połączenie.
|
||||
|
||||
// Zapasowy polling na wypadek zerwanego SSE.
|
||||
const pollId = window.setInterval(refresh, 60000);
|
||||
|
||||
return () => {
|
||||
source.close();
|
||||
window.clearInterval(pollId);
|
||||
};
|
||||
}, [user, refresh]);
|
||||
|
||||
const markRead = useCallback((id: number) => {
|
||||
setNotifications((current) => current.map((item) => (item.id === id ? { ...item, read: true } : item)));
|
||||
apiFetch(`/notifications/${id}/read`, { method: 'POST' }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const markAllRead = useCallback(() => {
|
||||
setNotifications((current) => current.map((item) => ({ ...item, read: true })));
|
||||
apiFetch('/notifications/read-all', { method: 'POST' }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const notify = useCallback((input: NotifyInput) => {
|
||||
if (!getToken() || !input.title) {
|
||||
return;
|
||||
}
|
||||
if (input.dedupeKey) {
|
||||
if (sentDedupeKeys.current.has(input.dedupeKey)) {
|
||||
return;
|
||||
}
|
||||
sentDedupeKeys.current.add(input.dedupeKey);
|
||||
}
|
||||
apiFetch<ServerNotification | null>('/notifications', { method: 'POST', body: JSON.stringify(input) })
|
||||
.then((created) => {
|
||||
if (created && created.id) {
|
||||
setNotifications((current) => (current.some((item) => item.id === created.id) ? current : [created, ...current]));
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NotificationsContext.Provider value={{ notifications, unreadCount, markRead, markAllRead, notify, refresh }}>
|
||||
{children}
|
||||
</NotificationsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useNotifications(): NotificationsContextValue {
|
||||
const context = useContext(NotificationsContext);
|
||||
if (!context) {
|
||||
throw new Error('useNotifications musi być użyte wewnątrz NotificationsProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -25842,3 +25842,805 @@ svg {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Kalkulator zdolności kredytowej ---- */
|
||||
.credit-page {
|
||||
background: #f4f7fb;
|
||||
padding: 34px 20px 70px;
|
||||
}
|
||||
|
||||
.credit-shell {
|
||||
margin: 0 auto;
|
||||
max-width: 1240px;
|
||||
}
|
||||
|
||||
.credit-breadcrumb {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.credit-title {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.credit-title-icon {
|
||||
align-items: center;
|
||||
background: #e6f6ee;
|
||||
border-radius: 14px;
|
||||
color: #0b9f5c;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 26px;
|
||||
height: 56px;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.credit-title h1 {
|
||||
color: #14223a;
|
||||
font-size: 30px;
|
||||
font-weight: 950;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.credit-title p {
|
||||
color: #526171;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.55;
|
||||
margin: 0;
|
||||
max-width: 780px;
|
||||
}
|
||||
|
||||
.credit-layout {
|
||||
align-items: start;
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
grid-template-columns: minmax(320px, 430px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.credit-form-column,
|
||||
.credit-result-column {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.credit-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfe7ef;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 32px rgba(24, 35, 57, 0.05);
|
||||
padding: 22px 22px 20px;
|
||||
}
|
||||
|
||||
.credit-card h2 {
|
||||
align-items: center;
|
||||
color: #14223a;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
font-weight: 950;
|
||||
gap: 12px;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.credit-step-badge {
|
||||
align-items: center;
|
||||
background: #0b9f5c;
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
font-size: 13px;
|
||||
font-weight: 950;
|
||||
height: 26px;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.credit-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.credit-grid-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.credit-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.credit-field-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.credit-field > span {
|
||||
color: #27364d;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.credit-field small {
|
||||
color: #7a8798;
|
||||
font-size: 11.5px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.credit-field input,
|
||||
.credit-field select {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d5dee8;
|
||||
border-radius: 8px;
|
||||
color: #14223a;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
min-height: 48px;
|
||||
padding: 0 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.credit-field input:focus,
|
||||
.credit-field select:focus {
|
||||
border-color: #0b9f5c;
|
||||
box-shadow: 0 0 0 3px rgba(11, 159, 92, 0.14);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.credit-input-suffix {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d5dee8;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.credit-input-suffix:focus-within {
|
||||
border-color: #0b9f5c;
|
||||
box-shadow: 0 0 0 3px rgba(11, 159, 92, 0.14);
|
||||
}
|
||||
|
||||
.credit-input-suffix input {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.credit-input-suffix input:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.credit-input-suffix em {
|
||||
color: #7a8798;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.credit-stepper {
|
||||
align-items: center;
|
||||
border: 1px solid #d5dee8;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr) 48px;
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.credit-stepper button {
|
||||
background: #f4f7fb;
|
||||
border: none;
|
||||
color: #0b9f5c;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.credit-stepper button:hover {
|
||||
background: #e6f6ee;
|
||||
}
|
||||
|
||||
.credit-stepper strong {
|
||||
color: #14223a;
|
||||
font-size: 16px;
|
||||
font-weight: 950;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.credit-segmented {
|
||||
background: #eef2f7;
|
||||
border-radius: 8px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.credit-segmented button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #526171;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
padding: 11px 8px;
|
||||
}
|
||||
|
||||
.credit-segmented button.active {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(24, 35, 57, 0.12);
|
||||
color: #0b9f5c;
|
||||
}
|
||||
|
||||
.credit-range-value {
|
||||
color: #0b9f5c;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.credit-range {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: linear-gradient(#d5dee8, #d5dee8) no-repeat;
|
||||
background-size: 100% 6px;
|
||||
background-position: 0 center;
|
||||
border-radius: 6px;
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.credit-range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: #0b9f5c;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(11, 159, 92, 0.4);
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.credit-range::-moz-range-thumb {
|
||||
background: #0b9f5c;
|
||||
border: 3px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.credit-range-scale {
|
||||
color: #97a3b2;
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.credit-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.credit-btn-primary,
|
||||
.credit-btn-ghost {
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
gap: 9px;
|
||||
justify-content: center;
|
||||
min-height: 50px;
|
||||
padding: 0 22px;
|
||||
}
|
||||
|
||||
.credit-btn-primary {
|
||||
background: #0b9f5c;
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.credit-btn-primary:hover {
|
||||
background: #0a8b50;
|
||||
}
|
||||
|
||||
.credit-btn-ghost {
|
||||
background: #ffffff;
|
||||
border: 1px solid #d5dee8;
|
||||
color: #27364d;
|
||||
}
|
||||
|
||||
.credit-btn-ghost:hover {
|
||||
border-color: #b7c2cf;
|
||||
}
|
||||
|
||||
/* Wynik */
|
||||
.credit-result-hero {
|
||||
border-radius: 14px;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
padding: 26px 28px 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.credit-tone-verysafe,
|
||||
.credit-tone-safe {
|
||||
background: linear-gradient(135deg, #0b9f5c, #0a8b50);
|
||||
}
|
||||
|
||||
.credit-tone-moderate {
|
||||
background: linear-gradient(135deg, #d98a13, #c2790a);
|
||||
}
|
||||
|
||||
.credit-tone-high {
|
||||
background: linear-gradient(135deg, #e0592f, #c8461f);
|
||||
}
|
||||
|
||||
.credit-tone-none,
|
||||
.credit-tone-empty {
|
||||
background: linear-gradient(135deg, #4a5a70, #35435a);
|
||||
}
|
||||
|
||||
.credit-result-hero.pulse {
|
||||
animation: creditPulse 0.9s ease;
|
||||
}
|
||||
|
||||
@keyframes creditPulse {
|
||||
0% { transform: scale(1); }
|
||||
40% { transform: scale(1.014); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.credit-hero-top {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.credit-hero-top > span:first-child {
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.credit-badge {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
padding: 6px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.credit-result-hero strong {
|
||||
display: block;
|
||||
font-size: 44px;
|
||||
font-weight: 950;
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1;
|
||||
margin: 18px 0 12px;
|
||||
}
|
||||
|
||||
.credit-stars {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.credit-stars span {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.credit-stars span.on {
|
||||
color: #ffd873;
|
||||
}
|
||||
|
||||
.credit-stars span svg {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.credit-result-hero p {
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
line-height: 1.5;
|
||||
margin: 14px 0 0;
|
||||
opacity: 0.94;
|
||||
}
|
||||
|
||||
.credit-tiles {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.credit-tile {
|
||||
background: #ffffff;
|
||||
border: 1px solid #dfe7ef;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 32px rgba(24, 35, 57, 0.05);
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 16px 16px 15px;
|
||||
}
|
||||
|
||||
.credit-tile small {
|
||||
color: #7a8798;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.credit-tile strong {
|
||||
color: #14223a;
|
||||
font-size: 20px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.credit-tile em {
|
||||
color: #0b9f5c;
|
||||
font-size: 11.5px;
|
||||
font-style: normal;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.credit-details-head {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: minmax(0, 1fr) 250px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.credit-details h3 {
|
||||
color: #14223a;
|
||||
font-size: 16px;
|
||||
font-weight: 950;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.credit-donut-wrap {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.credit-donut {
|
||||
height: 128px;
|
||||
width: 128px;
|
||||
}
|
||||
|
||||
.credit-donut circle {
|
||||
fill: none;
|
||||
stroke-width: 13;
|
||||
}
|
||||
|
||||
.credit-donut-track {
|
||||
stroke: #eef2f7;
|
||||
}
|
||||
|
||||
.credit-donut-you {
|
||||
stroke: #0b9f5c;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.credit-donut-partner {
|
||||
stroke: #3f8bff;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.credit-donut-value {
|
||||
dominant-baseline: middle;
|
||||
fill: #14223a;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
stroke: none;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.credit-donut-label {
|
||||
dominant-baseline: middle;
|
||||
fill: #7a8798;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
stroke: none;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.credit-donut-legend {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
list-style: none;
|
||||
margin: 4px 0 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.credit-donut-legend li {
|
||||
align-items: center;
|
||||
color: #526171;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.credit-donut-legend b {
|
||||
color: #14223a;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.credit-donut-legend i {
|
||||
border-radius: 3px;
|
||||
flex: 0 0 auto;
|
||||
height: 11px;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.credit-dot-you {
|
||||
background: #0b9f5c;
|
||||
}
|
||||
|
||||
.credit-dot-partner {
|
||||
background: #3f8bff;
|
||||
}
|
||||
|
||||
.credit-detail-rows {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.credit-detail-row {
|
||||
align-items: center;
|
||||
border-top: 1px solid #eef2f7;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
padding: 11px 0;
|
||||
}
|
||||
|
||||
.credit-detail-row dt {
|
||||
color: #526171;
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.credit-detail-row dd {
|
||||
color: #14223a;
|
||||
font-size: 14px;
|
||||
font-weight: 950;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.credit-detail-positive dd {
|
||||
color: #0b9f5c;
|
||||
}
|
||||
|
||||
.credit-detail-negative dd {
|
||||
color: #d64545;
|
||||
}
|
||||
|
||||
.credit-cta-band {
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #e6f6ee, #f0f9f4);
|
||||
border: 1px solid #bfe6d2;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.credit-cta-band strong {
|
||||
color: #14223a;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.credit-cta-band span {
|
||||
color: #4b6a58;
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.credit-cta-band button {
|
||||
align-items: center;
|
||||
background: #0b9f5c;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
gap: 8px;
|
||||
min-height: 48px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.credit-cta-band button:hover:not(:disabled) {
|
||||
background: #0a8b50;
|
||||
}
|
||||
|
||||
.credit-cta-band button:disabled {
|
||||
background: #b7c2cf;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.credit-recos h3 {
|
||||
align-items: center;
|
||||
color: #14223a;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
font-weight: 950;
|
||||
gap: 9px;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.credit-reco-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.credit-reco {
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e7edf3;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
gap: 13px;
|
||||
padding: 13px 15px;
|
||||
}
|
||||
|
||||
.credit-reco-icon {
|
||||
align-items: center;
|
||||
background: #e6f6ee;
|
||||
border-radius: 9px;
|
||||
color: #0b9f5c;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 17px;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.credit-reco-body {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.credit-reco-body strong {
|
||||
color: #14223a;
|
||||
font-size: 13.5px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.credit-reco-body small {
|
||||
color: #7a8798;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.credit-reco-delta {
|
||||
color: #0b9f5c;
|
||||
flex: 0 0 auto;
|
||||
font-size: 14px;
|
||||
font-weight: 950;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.credit-disclaimer {
|
||||
align-items: flex-start;
|
||||
color: #7a8798;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
gap: 9px;
|
||||
line-height: 1.55;
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.credit-disclaimer svg {
|
||||
color: #0b9f5c;
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.credit-layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.credit-grid-2 {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.credit-tiles {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.credit-details-head {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.credit-cta-band {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.credit-title h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.credit-result-hero strong {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prawdziwe ogloszenie z serwisu w liscie "Moje oferty" */
|
||||
.listing-photo.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.listing-status em.listing-live-tag {
|
||||
background: #e6f6ee;
|
||||
border-radius: 999px;
|
||||
color: #0b9f5c;
|
||||
font-size: 10.5px;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.02em;
|
||||
padding: 2px 8px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Kalkulator osadzony w panelu konta (z paskiem bocznym) */
|
||||
.credit-account-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.credit-account-main .credit-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.credit-account-main .credit-layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Powiadomienia - klikalnosc (logika, bez zmiany wygladu) */
|
||||
.notification-row,
|
||||
.notification-popover-item[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-popover-empty {
|
||||
color: #7a8798;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
padding: 18px 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user