Popraw rejestracje, kod SMS, opisy wysylki i widok wiadomosci
Deploy Polska Lokalnie / deploy (push) Successful in 11s
Deploy Polska Lokalnie / deploy (push) Successful in 11s
Rejestracja (/logowanie):
- Formularz rejestracji (osoba prywatna i firma) zawsze zaczyna sie pusty.
Logowanie i rejestracja dzielily pola e-mail i hasla, wiec dane wpisane
albo podstawione przez przegladarke przy logowaniu trafialy do nowego konta.
- "Powtorz haslo" ma wlasny przelacznik widocznosci; wczesniej oko przy
"Haslo" odslanialo oba pola, a ikona przy "Powtorz haslo" nie reagowala.
- Przycisk oka ma normalny obszar klikniecia (mial zerowy rozmiar).
Kod SMS:
- Okno potwierdzania numeru odlicza czas od SMS-a z rejestracji, a odmowa
serwera ("Kolejny mozesz zamowic za N s") odlicza czas w dol zamiast
pokazywac stala liczbe sekund. To samo w ustawieniach konta.
Panel admina:
- "Wysylka kampanii e-mail aktywna" -> "Wysylka e-maili aktywna" z opisem:
przelacznik blokuje wszystkie e-maile (aktywacja, reset hasla,
powiadomienia), nie tylko kampanie. Podobny opis przy SMS.
Wiadomosci:
- Puste pole pisania bez "Napisz wiadomosc do ...".
- Naglowek rozmowy "Nazwa - opis" zamiast sklejonego tekstu.
- Na liscie rozmow jedna miniatura ogloszenia zamiast dwoch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+66
-10
@@ -10481,6 +10481,8 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
|
||||
const [otpInfo, setOtpInfo] = useState('');
|
||||
const [otpBusy, setOtpBusy] = useState(false);
|
||||
const [otpCooldown, setOtpCooldown] = useState(0);
|
||||
// Serwer odmowil kolejnego kodu - komunikat z odliczanym czasem zamiast stalej liczby sekund.
|
||||
const [otpCooldownRejected, setOtpCooldownRejected] = useState(false);
|
||||
|
||||
// --- Preferencje wyszukiwania ---
|
||||
const [searchPrefs, setSearchPrefs] = useState({
|
||||
@@ -10864,6 +10866,7 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
|
||||
setOtpBusy(true);
|
||||
setOtpError('');
|
||||
setOtpInfo('');
|
||||
setOtpCooldownRejected(false);
|
||||
try {
|
||||
await resendPhoneOtp(user.email);
|
||||
setOtpInfo(`Wysłaliśmy kod SMS na numer ${user.phone ?? ''}.`);
|
||||
@@ -10871,7 +10874,14 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
|
||||
setIsOtpOpen(true);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Nie udało się wysłać kodu SMS.';
|
||||
if (isOtpOpen) {
|
||||
// Za wczesnie na kolejny kod - poprzedni nadal jest wazny, wiec okno otwieramy
|
||||
// i odliczamy czas podany przez serwer ("Kolejny możesz zamówić za N s").
|
||||
const secondsLeft = Number(/za (\d+) s/.exec(message)?.[1]);
|
||||
if ((error as { status?: number }).status === 429 && secondsLeft > 0) {
|
||||
setOtpCooldown(secondsLeft);
|
||||
setOtpCooldownRejected(true);
|
||||
setIsOtpOpen(true);
|
||||
} else if (isOtpOpen) {
|
||||
// Okno juz otwarte (ponowna wysylka) - blad pokazujemy w nim.
|
||||
setOtpError(message);
|
||||
} else {
|
||||
@@ -11873,6 +11883,9 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
|
||||
autoFocus
|
||||
/>
|
||||
{otpError && <p className="auth-status-error" role="alert">{otpError}</p>}
|
||||
{otpCooldownRejected && otpCooldown > 0 && (
|
||||
<p className="auth-status-error" role="alert">Kod został już wysłany. Kolejny możesz zamówić za {otpCooldown} s</p>
|
||||
)}
|
||||
{otpInfo && !otpError && <p className="otp-modal-ok">{otpInfo}</p>}
|
||||
<button className="auth-status-btn" type="submit" disabled={otpBusy || otpCode.length < 6}>
|
||||
{otpBusy ? 'Sprawdzanie...' : 'Potwierdź numer'}
|
||||
@@ -12092,6 +12105,8 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
const [companyPhone, setCompanyPhone] = useState('');
|
||||
const [agreeTerms, setAgreeTerms] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
// "Powtórz hasło" ma własny przełącznik widoczności - niezależny od pola "Hasło".
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -12101,6 +12116,8 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
const [otpModalOpen, setOtpModalOpen] = useState(false);
|
||||
// Czy kod SMS faktycznie wyszedl z bramki - decyduje o tresci ekranu po rejestracji.
|
||||
const [otpCodeSent, setOtpCodeSent] = useState(false);
|
||||
// Kiedy wyszedl ostatni kod SMS - okno odlicza od tej chwili czas do ponownej wysylki.
|
||||
const [otpSentAt, setOtpSentAt] = useState<number | null>(null);
|
||||
const [notActivated, setNotActivated] = useState(false);
|
||||
|
||||
const runAuth = async (action: () => Promise<AuthUser>) => {
|
||||
@@ -12140,6 +12157,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
// Backend potwierdza, czy SMS naprawde wyszedl - przy awarii bramki nie otwieramy okna na kod.
|
||||
setOtpCodeSent(pending.phoneVerificationRequired);
|
||||
if (pending.phoneVerificationRequired) {
|
||||
setOtpSentAt(Date.now());
|
||||
setOtpModalOpen(true);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -12149,11 +12167,31 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Logowanie i rejestracja korzystaja z tych samych pol (e-mail, haslo). Formularz rejestracji
|
||||
// - prywatny i firmowy - zawsze zaczyna sie pusty, zeby dane wpisane albo podstawione przez
|
||||
// przegladarke przy logowaniu nie trafialy do nowego konta. E-mail zostaje tylko miedzy
|
||||
// logowaniem a resetem hasla, gdzie chodzi o to samo konto.
|
||||
const goToStep = (nextStep: LoginStep) => {
|
||||
const keepsEmail = (step === 'login' || step === 'forgot') && (nextStep === 'login' || nextStep === 'forgot');
|
||||
setStep(nextStep);
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
setNotActivated(false);
|
||||
if (!keepsEmail) {
|
||||
setEmail('');
|
||||
}
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setShowPassword(false);
|
||||
setShowConfirmPassword(false);
|
||||
setFirstName('');
|
||||
setLastName('');
|
||||
setPhone('');
|
||||
setCompanyName('');
|
||||
setNip('');
|
||||
setCompanyAddress('');
|
||||
setCompanyPhone('');
|
||||
setAgreeTerms(false);
|
||||
};
|
||||
|
||||
const handleLoginSubmit = (event: ReactFormEvent<HTMLFormElement>) => {
|
||||
@@ -12498,7 +12536,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
<span>Powtórz hasło</span>
|
||||
<div>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="Powtórz swoje hasło"
|
||||
value={confirmPassword}
|
||||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||||
@@ -12506,7 +12544,14 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
<Icon name="eye" />
|
||||
<button
|
||||
type="button"
|
||||
className="login-eye"
|
||||
aria-label={showConfirmPassword ? 'Ukryj hasło' : 'Pokaż hasło'}
|
||||
onClick={() => setShowConfirmPassword((current) => !current)}
|
||||
>
|
||||
<Icon name="eye" />
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -12668,7 +12713,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
<span>Powtórz hasło</span>
|
||||
<div>
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
placeholder="Powtórz swoje hasło"
|
||||
value={confirmPassword}
|
||||
onChange={(event) => setConfirmPassword(event.target.value)}
|
||||
@@ -12676,7 +12721,14 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
minLength={6}
|
||||
required
|
||||
/>
|
||||
<Icon name="eye" />
|
||||
<button
|
||||
type="button"
|
||||
className="login-eye"
|
||||
aria-label={showConfirmPassword ? 'Ukryj hasło' : 'Pokaż hasło'}
|
||||
onClick={() => setShowConfirmPassword((current) => !current)}
|
||||
>
|
||||
<Icon name="eye" />
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -12756,6 +12808,8 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
|
||||
<PhoneOtpModal
|
||||
email={pendingEmail}
|
||||
phone={pendingPhone}
|
||||
lastSentAt={otpSentAt}
|
||||
onCodeSent={() => setOtpSentAt(Date.now())}
|
||||
onClose={() => setOtpModalOpen(false)}
|
||||
onVerified={() => { setOtpModalOpen(false); setInfo('Numer telefonu został potwierdzony.'); }}
|
||||
/>
|
||||
@@ -25122,9 +25176,7 @@ function MessagesPage({
|
||||
{/* Czego dotyczy rozmowa - widac od razu, bez wchodzenia w watek. */}
|
||||
{thread.listing && (
|
||||
<span className="messages-thread-offer" title={`${thread.listing.title} • ${formatPrice(thread.listing.price)}`}>
|
||||
{thread.listing.thumbnail && (
|
||||
<i style={{ backgroundImage: `url(${thread.listing.thumbnail})` }} aria-hidden="true" />
|
||||
)}
|
||||
{/* Miniatura ogloszenia jest juz awatarem watku - tu sam tytul i cena. */}
|
||||
<em>{thread.listing.title}</em>
|
||||
<b>{formatPrice(thread.listing.price)}</b>
|
||||
</span>
|
||||
@@ -25153,7 +25205,9 @@ function MessagesPage({
|
||||
) : (
|
||||
<strong>{activeThread.name}</strong>
|
||||
)}
|
||||
<small>{activeThread.status}</small>
|
||||
{activeThread.status && (
|
||||
<small><span className="messages-chat-status-sep" aria-hidden="true"> - </span>{activeThread.status}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="messages-chat-listing">
|
||||
@@ -25222,9 +25276,11 @@ function MessagesPage({
|
||||
sendMessage();
|
||||
}
|
||||
}}
|
||||
// Puste pole do pisania; podpowiedz zostaje tylko przy blokadzie, zeby bylo jasne, czemu nie da sie pisac.
|
||||
placeholder={isActiveThreadBlocked
|
||||
? (activeThread.blockedByMe ? 'Zablokowałeś tego użytkownika. Odblokuj go, aby pisać.' : 'Nie możesz pisać do tego użytkownika.')
|
||||
: `Napisz wiadomość do ${activeThread.name}...`}
|
||||
: ''}
|
||||
aria-label={`Wiadomość do ${activeThread.name}`}
|
||||
maxLength={2000}
|
||||
/>
|
||||
<button type="button" className="send" aria-label="Wyślij wiadomość" onClick={sendMessage} disabled={isActiveThreadBlocked || isSending}><Icon name="arrow" /></button>
|
||||
|
||||
@@ -128,10 +128,16 @@ export function PasswordResetPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Odstep miedzy wysylkami kodu - ten sam, ktory wymusza backend (RESEND_COOLDOWN_SECONDS).
|
||||
const OTP_RESEND_COOLDOWN_SECONDS = 60;
|
||||
|
||||
// Okno potwierdzenia numeru telefonu kodem SMS (OTP), pokazywane po rejestracji z podanym telefonem.
|
||||
export function PhoneOtpModal({ email, phone, onClose, onVerified }: {
|
||||
// lastSentAt - kiedy wyszedl ostatni kod; od niego odliczamy czas do mozliwosci ponownej wysylki.
|
||||
export function PhoneOtpModal({ email, phone, lastSentAt, onCodeSent, onClose, onVerified }: {
|
||||
email: string;
|
||||
phone: string;
|
||||
lastSentAt?: number | null;
|
||||
onCodeSent?: () => void;
|
||||
onClose: () => void;
|
||||
onVerified: () => void;
|
||||
}) {
|
||||
@@ -140,7 +146,11 @@ export function PhoneOtpModal({ email, phone, onClose, onVerified }: {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
const [cooldown, setCooldown] = useState(() => (
|
||||
lastSentAt ? Math.max(0, OTP_RESEND_COOLDOWN_SECONDS - Math.floor((Date.now() - lastSentAt) / 1000)) : 0
|
||||
));
|
||||
// Odmowa serwera ("Kolejny możesz zamówić za N s") - pokazujemy ja z odliczanym czasem.
|
||||
const [cooldownRejected, setCooldownRejected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (cooldown <= 0) {
|
||||
@@ -167,13 +177,20 @@ export function PhoneOtpModal({ email, phone, onClose, onVerified }: {
|
||||
|
||||
const resend = async () => {
|
||||
setError(null);
|
||||
setCooldownRejected(false);
|
||||
try {
|
||||
await resendPhoneOtp(email);
|
||||
setInfo('Wysłaliśmy nowy kod SMS.');
|
||||
// Zgodne z odstepem wymuszanym przez backend (RESEND_COOLDOWN_SECONDS).
|
||||
setCooldown(60);
|
||||
setCooldown(OTP_RESEND_COOLDOWN_SECONDS);
|
||||
onCodeSent?.();
|
||||
} catch (e) {
|
||||
setError(errMsg(e));
|
||||
const secondsLeft = Number(/za (\d+) s/.exec(errMsg(e))?.[1]);
|
||||
if ((e as { status?: number }).status === 429 && secondsLeft > 0) {
|
||||
setCooldown(secondsLeft);
|
||||
setCooldownRejected(true);
|
||||
} else {
|
||||
setError(errMsg(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -197,6 +214,9 @@ export function PhoneOtpModal({ email, phone, onClose, onVerified }: {
|
||||
autoFocus
|
||||
/>
|
||||
{error && <p className="auth-status-error" role="alert">{error}</p>}
|
||||
{cooldownRejected && cooldown > 0 && (
|
||||
<p className="auth-status-error" role="alert">Kod został już wysłany. Kolejny możesz zamówić za {cooldown} s</p>
|
||||
)}
|
||||
{info && <p className="otp-modal-ok">{info}</p>}
|
||||
<button className="auth-status-btn" type="submit" disabled={busy || code.length < 6}>{busy ? 'Sprawdzanie...' : 'Potwierdź numer'}</button>
|
||||
</form>
|
||||
|
||||
@@ -844,11 +844,15 @@ function SmtpCard({ Icon }: { Icon: IconComponent }) {
|
||||
<label>Login SMTP / adres nadawcy
|
||||
<input value={config.username ?? ''} onChange={(e) => set({ username: e.target.value })} placeholder="np. marketing@twojadomena.pl" />
|
||||
</label>
|
||||
<label>Wysyłka kampanii e-mail aktywna
|
||||
<label>Wysyłka e-maili aktywna
|
||||
<select value={config.enabled ? 'on' : 'off'} onChange={(e) => set({ enabled: e.target.value === 'on' })}>
|
||||
<option value="off">Wyłączone (sandbox)</option>
|
||||
<option value="on">Włączone</option>
|
||||
</select>
|
||||
<small className="mkt-field-note">
|
||||
Dotyczy wszystkich e-maili serwisu: linków aktywacyjnych, resetu hasła, powiadomień o wiadomościach
|
||||
i kampanii. Po wyłączeniu nic nie jest wysyłane.
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
{error && <p className="mkt-error">{error}</p>}
|
||||
@@ -955,6 +959,10 @@ function SmsCard({ Icon }: { Icon: IconComponent }) {
|
||||
<option value="off">Wyłączone (sandbox)</option>
|
||||
<option value="on">Włączone</option>
|
||||
</select>
|
||||
<small className="mkt-field-note">
|
||||
Dotyczy wszystkich SMS-ów serwisu: kodów weryfikujących telefon przy rejestracji i kampanii.
|
||||
Po wyłączeniu nic nie jest wysyłane.
|
||||
</small>
|
||||
</label>
|
||||
<label>Pełny URL endpointu bramki SMS
|
||||
<input value={config.endpointUrl ?? ''} onChange={(e) => set({ endpointUrl: e.target.value })} placeholder="https://api.softspm.pl/send_sms_api.php" />
|
||||
|
||||
+25
-10
@@ -16791,15 +16791,6 @@ a.city-card:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.messages-thread-offer i {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
flex: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.messages-thread-offer em {
|
||||
color: #5f748a;
|
||||
font-size: 11px;
|
||||
@@ -16888,11 +16879,16 @@ a.city-card:hover {
|
||||
|
||||
.messages-chat-user strong {
|
||||
color: #162841;
|
||||
display: block;
|
||||
display: inline-block;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* "Nazwa - opis" w jednej linii, np. "Piotr Nowak - Sprzedający". */
|
||||
.messages-chat-status-sep {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.messages-chat-user small {
|
||||
color: #6e8199;
|
||||
font-size: 11px;
|
||||
@@ -23369,6 +23365,24 @@ a.city-card:hover {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
/* Przycisk oka w polu hasla: sam zajmuje miejsce ikony, zeby klikniecie obok rysunku
|
||||
tez przelaczalo widocznosc (wczesniej przycisk mial zerowy rozmiar). */
|
||||
.login-field .login-eye {
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.login-field .login-eye svg {
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.publish-feedback {
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
@@ -30635,6 +30649,7 @@ a.city-card:hover {
|
||||
@media (max-width: 760px) { .mkt-config-grid { grid-template-columns: 1fr; } .mkt-form-row { grid-template-columns: 1fr; } }
|
||||
.mkt-config-grid label { display: grid; gap: 6px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .03em; color: #6b7f96; }
|
||||
.mkt-config-grid input, .mkt-config-grid select { padding: 10px 12px; border: 1px solid #d8e3ef; border-radius: 8px; font-size: 13px; font-weight: 500; text-transform: none; letter-spacing: normal; color: #17293c; }
|
||||
.mkt-config-grid .mkt-field-note { font-size: 12px; font-weight: 500; line-height: 1.4; text-transform: none; letter-spacing: normal; color: #6b7f96; }
|
||||
.mkt-badge-set { display: inline-flex; align-items: center; gap: 4px; text-transform: none; letter-spacing: normal; font-size: 11px; font-weight: 800; color: #1256a0; background: #e4f0ff; border-radius: 999px; padding: 2px 8px; }
|
||||
.mkt-badge-set svg { width: 12px; height: 12px; }
|
||||
.mkt-config-save { display: flex; justify-content: flex-end; }
|
||||
|
||||
Reference in New Issue
Block a user