Popraw rejestracje, kod SMS, opisy wysylki i widok wiadomosci
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:
2026-09-22 09:30:21 +02:00
parent 9615f131f4
commit 72af24469a
4 changed files with 125 additions and 26 deletions
+66 -10
View File
@@ -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>