From e7402dbfed6ae0f8ad643d05625f378edc9c7468 Mon Sep 17 00:00:00 2001 From: Przemek Zatorski Date: Tue, 22 Sep 2026 11:26:47 +0200 Subject: [PATCH] Osadz logo w mailach, popraw naglowek i nadaj adresy krokom logowania Maile: - Logo jest zalacznikiem inline (cid:) zamiast obrazka z adresu serwisu. Wczesniej w mailach z lokalnego srodowiska (i w programach blokujacych obrazki z zewnatrz) w miejscu logo byla ikona bledu. Usuniety tekst zastepczy, ktory dublowal napis "Polska Lokalnie". Naglowek: - Menu nie nachodzi na logo ani panel przy zadnej szerokosci 1110-1920 px. Ponizej 1560 px "Ulubione" to samo serce (nazwa w dymku), mniejsze odstepy i przycisk "Dodaj ogloszenie" bez sztywnej szerokosci; w 1101-1270 px mniejszy napis logo i tekst przycisku. Logowanie i rejestracja (standard routingu): - Kazdy krok ma wlasny adres: /logowanie, /logowanie/nie-pamietam-hasla, /rejestracja, /rejestracja/osoba-prywatna, /rejestracja/firma, /rejestracja/sprawdz-poczte. Dzialaja F5, Wstecz/Dalej i nowa karta, formularz czysci sie takze przy Wstecz/Dalej, a cel powrotu po zalogowaniu nie ginie przy przejsciach miedzy krokami. Co-Authored-By: Claude Opus 5 (1M context) --- .../auth/AccountEmailService.java | 2 +- .../pl/polskalokalnie/send/EmailSender.java | 29 +++++-- backend/src/main/resources/mail/logo.png | Bin 0 -> 5813 bytes .../polskalokalnie/send/EmailSenderTest.java | 79 ++++++++++++++++++ frontend/src/App.tsx | 61 +++++++++++--- frontend/src/routes.ts | 6 ++ frontend/src/styles.css | 57 ++++++++++++- frontend/tests/routing.spec.ts | 21 +++++ 8 files changed, 238 insertions(+), 17 deletions(-) create mode 100644 backend/src/main/resources/mail/logo.png create mode 100644 backend/src/test/java/pl/polskalokalnie/send/EmailSenderTest.java diff --git a/backend/src/main/java/pl/polskalokalnie/auth/AccountEmailService.java b/backend/src/main/java/pl/polskalokalnie/auth/AccountEmailService.java index 3856190..5c4a945 100644 --- a/backend/src/main/java/pl/polskalokalnie/auth/AccountEmailService.java +++ b/backend/src/main/java/pl/polskalokalnie/auth/AccountEmailService.java @@ -132,7 +132,7 @@ public class AccountEmailService { + "
" // Naglowek z logo + nazwa + "
" - + "\"Polska" + "" + "Polska Lokalnie" diff --git a/backend/src/main/java/pl/polskalokalnie/send/EmailSender.java b/backend/src/main/java/pl/polskalokalnie/send/EmailSender.java index d1c9759..bec4e00 100644 --- a/backend/src/main/java/pl/polskalokalnie/send/EmailSender.java +++ b/backend/src/main/java/pl/polskalokalnie/send/EmailSender.java @@ -5,6 +5,7 @@ import jakarta.mail.internet.MimeMessage; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.Properties; +import org.springframework.core.io.ClassPathResource; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; @@ -18,6 +19,14 @@ import pl.polskalokalnie.mailconfig.SmtpConfig; @Component public class EmailSender { + /** + * Logo serwisu osadzone w samym mailu (obrazek inline). Szablon odwoluje sie do niego przez + * src="cid:...", wiec logo nie zalezy od pobierania z adresu serwisu - dziala tez w mailach + * ze srodowiska lokalnego i w programach, ktore blokuja obrazki z zewnatrz. + */ + public static final String LOGO_CID = "polskalokalnie-logo"; + private static final String LOGO_RESOURCE = "mail/logo.png"; + public SendResult send(SmtpConfig config, String to, String subject, String htmlBody) { if (config == null || config.getHost() == null || config.getHost().isBlank() || config.getUsername() == null || config.getUsername().isBlank()) { @@ -26,11 +35,7 @@ public class EmailSender { try { JavaMailSenderImpl mailSender = buildMailSender(config); MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, false, StandardCharsets.UTF_8.name()); - helper.setFrom(fromAddress(config)); - helper.setTo(to); - helper.setSubject(subject == null ? "" : subject); - helper.setText(htmlBody, true); + fillMessage(message, fromAddress(config), to, subject, htmlBody); mailSender.send(message); String messageId = message.getMessageID(); return SendResult.ok(messageId); @@ -39,6 +44,20 @@ public class EmailSender { } } + // Tresc HTML; gdy szablon uzywa logo (cid:), wiadomosc jest wieloczesciowa z logo jako zalacznikiem inline. + void fillMessage(MimeMessage message, InternetAddress from, String to, String subject, String htmlBody) + throws jakarta.mail.MessagingException { + boolean withLogo = htmlBody != null && htmlBody.contains("cid:" + LOGO_CID); + MimeMessageHelper helper = new MimeMessageHelper(message, withLogo, StandardCharsets.UTF_8.name()); + helper.setFrom(from); + helper.setTo(to); + helper.setSubject(subject == null ? "" : subject); + helper.setText(htmlBody, true); + if (withLogo) { + helper.addInline(LOGO_CID, new ClassPathResource(LOGO_RESOURCE), "image/png"); + } + } + private JavaMailSenderImpl buildMailSender(SmtpConfig config) { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost(config.getHost()); diff --git a/backend/src/main/resources/mail/logo.png b/backend/src/main/resources/mail/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bb90012c6d147526bd18136002c378409e3a029d GIT binary patch literal 5813 zcmV;m7E0-fP)ea=iWO_MY!g{$lP zCMR>wK6~%A_S$Q&^{+JmCzcSPQB1tKaK#(u!apeC3F?FMu9igc!WCsij>ZuED+;*= zwEmlNM}IiDFZ49Pc5g!ae@X#h_R0WF1hByrCQe%>qtTTloD`6_jF8Ha2#ody05EDB zYwg`I0Qd_5mB!4$Kb#!)7Pr zZp%cRtECh9miE%Sz2V+B4IS9E8vtmW;U=^~47O0SnJ^wtGT-U$&6wmY+hg`)EaR+k znSjtWR517LB%GMm-gCyN^`7>AIoSQg&iohS6{)qXm1bkwFkILG0G+m<@fI7h+o)L| zJ*?HR^$dU$w8a;iFYgKW%uF>3XU23R0aSCY<5=mLiq&;LCZq*gtM~PHwf*o2 zrd!sUyj6hQB;v``1|+%x0fp^WYwu5N%o9R5DRLrpMr9aDpq&C64a|A~0QLa$USLQA zLID6;4GA&uVPw{Tv;Yz2Ge8IcMgj0yHfk3uuf+)6I=JWQf$1~>r;yqh#bQ-OW=>=! zQFuNZU4bHW9!clT063^s=Hh|wCtAmkzRA@0$i9oqYbyl7GGx2}NqiJZge)J%=+9+< z?~=wWl0>H?;&Na-6#&Wr@g!!B1caLh zy0`smDk0#+Rf4Lja8Wujcc`arB(|jH3P7;}5mo?-Gf0Sq{_3!K2)*=24(#2&8=ye~ zv?!Pn1*b*sTS;Y28FHxBb4Pa`dSU1DGq6_#P)<;)5qf&;QEHA>?;0mj5lSJ^mjH2% zQkgHtoq?No?b_uYO8}Ol^A@kXoTT$6L|lkO=OW2sHrg`yBrx2oSZzMo_4uFt)`7*I zZq+9v0%**RoBwXk`dcIRFo89bEejZ+nWx*sN7||UK5LxB(#T#`UKg|ZeE*(pzng5n zm{X0C(zD-0GJFFGah`p^Z2AHCF)(<)$NIN}-CMgRdGA;go1T1oF{V9ve#K-APDvBn zBUCp!(BACElXZ6i;8n_u)DCw4dFNyXQQAMnWoMjCBK!j+QD}_1(HQ+;D4lugrI(&M zd_>0$4FXzPa%%T-9F}0kTAEd&GIbF%yr!?S_10;$YBtUiL;HR^KV&cu3K4lPDp8na zesU@)U3tO$s`EIWOnv0YCw4M+k5BXR?1^$K8mp+iE>XE!&97R+iOSTEN-ArLgC_o% zFLw+`l+`RsRIYYo<*R;NR8$?COq(e853{jSUEr%_)dk^kr^Hv$m#6AuV7`i`BLwm?fV$a4kUc7JuD`BkL({b ztiTushC+W&E*|xd7&xI6oEC}Xxu+zO;XjgauJLE8&FujCBBR}IlA#qK#lx}k)O*Lb zL8FK#Yd0n;>;D|DOzn&(>mHe3-H?kerxrv*L%s#Lw9U`}=SY_R10HH@oI;+{}QDfX|`#QILqPJt~gT^(hwQ@I;kcGmLcf~7G z-?EzG3j*mZB4-7mb0#7!H`+V42OHQbFdl5OD@!P=VY`h&?NK~I0Ei-Ibo_|z#F4MY z_9)Yd%DN_j;^!pg0fD(T^aBx0ftDZTWc2ktYDnTtLE^gKYll-)-2BCr0I)b>Ye9 z#^V+B%}CCA>j7)!{Yt{&`U4$X|5RSy5Xml=g6is!{q%OUe#vO>LZjThBU$j2Dq zRAz&KnDYW>IE9A;gUjYllA%%zM_!ZXO$Gvp$4@Pk;kjS%MW)U7gZWrr{^hHDqCB-S z9CliS<6I8Rz*>E^uXD?rU+Q>jzirsv-I5;L>+S8X{{evhuE&34Jd@HYvsuVc0uc*r zfSnsarJbWtr%9=$#kXaK^nFGv7e#oDf4{MDe6q2!Rkw(6 zdyZAqy_5~h zon^hbIV37lpGP6SWV>Z%%>cc99a|RVsqyrS6^y6j$-2$JSku?lT9J(|eSd;L(AhRl z*9G$Kz(Dw}=ke}$|M*EHQ{NQQ`GI9#Yj-Cawbp0eZi|i%-y0JWKJ0`G{+m&L+$A|* zhNE{1Cwiv>eMZ21jpH9>U9C5QQWrAA3rIpBVsWCp?&ITez|k1w5wzcK=SIN{8*MC8 zxcIEV(EBbo0I~6IK_np$sn2H-89d(o?mHgI`iD?DAF~41%H7DM8xHufLpEqCDDVZ$ z0DH8T-Ukfb!a>(4w?`}YMK+lAJ}a{fHpm0rZNFx3WR=nCK_U@B$N5G)S$|haiFKTs zweP!3MHY_;w8^pwQo#J+LHjtLv5Hk+Ua)K}mz14TpSPMbCEuXTudID-qOyMXf@RjL zt5=C+-RH)e9NnL_Jta%eSyEJ5XFW7wL6+F!Ri!o>J$i`o_bsN7Si0KS`6epsch0X^ zb!PSvPW4`_toH5mm!3BiudM%jv~=0x{PtuU^>+<78SU;Ra=sEgv*^!^ndd~Z_EU`V zw|N}i5;Alqlk-R{nYucEF#__pvyPY7eMp8Q_W+n-qtgFA+VlJi*?IXq zW}&Wjmzg^deMcxCQ#%GV1sBGXNrWOsdE1RrYkNB$-yEo=-wz+XC;bIDML3}fgJ!8w zBuO~sLWC*+p=gT1vM5u7wEgRhYaOSDNIFFTd{Qt@Zu?r{Z7_+7x&{#n|EJO36YlWA zs|@Pq$@oM2NmQo(mZ+$EJn!C2Xslhlvi{oQvNKBjFTtL~DL(!x#miS+x}a*!*Gnqu z)-768GbdhAdsAX*>TC0>>#CrsiTpn7by6DB4w*=%-g5NwK=TsGRBNKL{y=f*%J*6j zcXHkugU;f~x}PK}S09YUR^?V;HpXz4ZD(a;rXreReEvj~Myz+HZ zjf=VSFjjHS$AvTJHugrY?CsjNaduFbrAAR&y4M1e&{H&|7xqc#rMU7I4Me374)^qU z3aSPPcJ0a~M4-C5G1T5}vAt$vv4#cpMR9jg^dwZI>;Y}u^x>h;+-vg($3TK6`YnMk z6HlgoD4poLv>I(Q%DuC{VDv{8q0ZlyGMCoUp6(IC`Wa%G|HOx+X2iKKz(tEX!lO>; zQAAu2A1!wE9Eik6A?-y$x(p_fbTX{x6IRae1%_v}hVQnwH*fLZ|3Hm|J6b&T*0)_L z3L>}haK@vk;~+X)0iX^T&^ok}jP1u6N8k1nD+ojQQLpImbA4?W- z$=a_46RF9vgVx=%(=+ypfXC~S``&0@-Rz1e(Zlk>pqrF|Mm5t1T$E>xK(ND)K>OKv3 z++o%7l_1}k_U_rf!)SLO6D|)HeN&(zV81l`DrA?wmf8Gz-@ff5Su1`#F0oZqvMv|< znIH3ei_js%Zbax}ayBVny@LKmWxZ$Of-CkkBzNUvq%?EplZU;rX#E~V8a z{%n;#5vM~Q1*%Srry!BVI_&M0U(J|Ynt(+7yYrXhh<%~y=rkg$t_L2{J=#_c5Q+wZIlh5?_rjj{_EgFG8fgC~)@m=aAta1hFw(YWV|KwSL)PMp!99N(V8izz ziAz}1veMGJoWLQ{K%gZC%pZ;RdH~p>xFKN7SbPde)>|Cj z*GZ#*9k}0rRnQ57-}5O5?AVy~B%O6461}$P(DT2^`9GAKcEL%@D-m%ifO#T-rhLHQ zxWEx+{}5k{gqH(i0|KQ0@hk+a2NM4Rz)_nWClQit_}c@|wmqGF^A!+4V9)xyxBk^; zy<{l*x>!N7(Z?X6$rpexiAc2IY6LtPm=?#9tKR@scRP8m@^KNNh!BZ6NaPU7h`suz zhyN!d9)hV_N0-MDn-TpLTSy1c)0!#(4`qaS2Uny!}N zVWZswKX|e$Y_zw%FWr4U?Aw=Xn*}vsEEpQg)-DQQvZH3rjIgOuz{C3yHcjE0H*PY3 zN7N2YLBacIaL`An5oCFJxyTto|KY%%ZCjwU^vR-3rswT2KMcnB8f4hiW2K9DOk7Z7#cED#S8)@j_7BNhF&Qm?-?G9{AePW zNYK+W>MU6~YEPy$z*VHFxj5A(P6*p3i>RD94o6i z$i`U*2#*BKZ1&X$492C-;2@C=hU_;V9GMj%+w_-ZL;#U5LLT*|5}tw)l7KkV61p+) zBvH2NA{4U1xatqn<=KwJ@zB-_QAla8IWJBy3(Hm|=g+S$=zDg{A!PFuv+iWAUj*iT zvt-{ID8<^i%rL^nr2P+YX7Y~>z1ez_dN$14@SDQX2iWMpYFB-6x=M_E^|eB1HCi2# z6#6%S=5_W#ydrfYJE0p`%C32HS6$lM)powUZv|2?=dkLVPW`Q-uWt|lYYnX5pkaBV zP%pT5V`k(5vMDexG_6Xb1cjou4iI3&l<-})w5M6clc{gYQ1oNkOaJ!p!Tnc{ z_V4PMVWp^LqADCKpZ9GUE_lCjGe2g;B|?T8v^V+-Adw;zCmF47GRoaJ(6im{%c(s{ zi}r4qDfIqwDZn=4<+bZ&WNwqnjQqT>tL+`xjrwZ{Uz@;f~F=rVKeV*Z(l(Lp?fhblYWpW5LgBB zF~gak55Uknpug7h1u!`nIur8U{CO2C%N#LxCo}h@M-S8u?|ZIy0w_8wveno$&U}!8 z3HoV}hhBW5PkY|`0pPHVM4wJ1>wDsr_1`O5a;^nD>|Waj>Ti+eaqx1MWR0HBq@d{k0Ma~@NnaH(pQ~C4h-TSwu3|iU zI*d6UCdeT)(ZJp(o*5bHKT{j^2}Wo|hO3-N^e^$M)QA0Aje@!F!8ze1C=IO5Cg?kK z=D>BE)+*1O1I$lDT4|K@k)m5KuJRq5DYHY<(YVEzPGpU8)1T>oVf)rx7CXNs&A#)T zASP4IKpRF5wEw-oqxJT_jxCMpbf%PzZm~;ZTcDtZY^XFygYGB#H~e}CZoeQrbba$K zJ>qQuW`SWTC4~KoRljU@cH}d^pzNGV67pu{jfF6>_4J`n0B57=@=6vW2CseWh`^qnK$7Yj;Zf*V%# zn?VV=UI4OkGq(=DU`ytop7d0b@XC-6M~_Vy!0mf(>%;yx!(En13dx0;*qjf*x=ptQ zb^ZYO&Bp6ckadF*J+LqtLHmMSVN^Du(X8|O&ZAsuPf+1PNT+I@>4wZu=X5`sknqY= z;7DC#(_h;?lYt+g)F1UvAW7dSxqaPv%;tGST1z2W%Z8a3dYb$FajGtGVAro*X7dc8cn5@wk^rz{s>va|nv{*$ zVz)K^FBU|u;1O5h9LM_ecGZE?o`-Wnjvh+C^WdlMxx4s`b1LOwrvkM5qGI56G zo1Uq*MVqKx?*>kLBOnNQ)KyjxKJyXBF^4kS4qkuH-6cy?Zwie@cMB2vJ;~@_9a-2% zPV`O&-l4p7U0-MGqTbHdh00B*5O@nYk?$oc>)YbxwQFrK)!Wg!S!-_>lJmhs`k)Vg zO}5|&uQp}1VBRf_H;7o|Mjm#R1ycOq0!btoN!RyZ|Ll%#XzE% zjr)bZ&d0wq;pcRDlllLkZ1H03E0?Z&oxoX#o00000NkvXXu0mjf7Zqq~ literal 0 HcmV?d00001 diff --git a/backend/src/test/java/pl/polskalokalnie/send/EmailSenderTest.java b/backend/src/test/java/pl/polskalokalnie/send/EmailSenderTest.java new file mode 100644 index 0000000..a492576 --- /dev/null +++ b/backend/src/test/java/pl/polskalokalnie/send/EmailSenderTest.java @@ -0,0 +1,79 @@ +package pl.polskalokalnie.send; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.mail.BodyPart; +import jakarta.mail.Multipart; +import jakarta.mail.Part; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import org.junit.jupiter.api.Test; + +class EmailSenderTest { + + private final EmailSender sender = new EmailSender(); + + // Regresja: logo bylo linkiem do adresu serwisu, wiec w mailach z lokalnego srodowiska + // (i w programach blokujacych obrazki z zewnatrz) zamiast logo pojawiala sie ikona bledu. + @Test + void templateWithLogoEmbedsItAsInlineImage() throws Exception { + MimeMessage message = newMessage(); + sender.fillMessage(message, new InternetAddress("info@polskalokalnie.pl"), "jan@example.com", "Temat", + "

Tresc

"); + + BodyPart logo = null; + boolean hasHtml = false; + for (BodyPart part : allParts(message)) { + if (part.getHeader("Content-ID") != null) { + logo = part; + } + hasHtml |= part.getContentType().startsWith("text/html"); + } + assertThat(logo).as("czesc z logo").isNotNull(); + assertThat(logo.getHeader("Content-ID")[0]).isEqualTo("<" + EmailSender.LOGO_CID + ">"); + assertThat(logo.getContentType()).startsWith("image/png"); + assertThat(logo.getDisposition()).isEqualTo(Part.INLINE); + assertThat(hasHtml).as("tresc HTML").isTrue(); + } + + @Test + void messageWithoutLogoStaysSinglePart() throws Exception { + MimeMessage message = newMessage(); + sender.fillMessage(message, new InternetAddress("info@polskalokalnie.pl"), "jan@example.com", "Temat", + "

Kampania bez logo

"); + + assertThat(message.getContent()).isInstanceOf(String.class); + } + + private static MimeMessage newMessage() { + return new MimeMessage(Session.getInstance(new Properties())); + } + + // Przebudowa przez zapis, zeby czytac wiadomosc tak, jak zobaczy ja serwer poczty. + private static List allParts(MimeMessage message) throws Exception { + message.saveChanges(); + ByteArrayOutputStream raw = new ByteArrayOutputStream(); + message.writeTo(raw); + MimeMessage parsed = new MimeMessage(Session.getInstance(new Properties()), + new java.io.ByteArrayInputStream(raw.toByteArray())); + List result = new ArrayList<>(); + collect((Multipart) parsed.getContent(), result); + return result; + } + + private static void collect(Multipart multipart, List result) throws Exception { + for (int i = 0; i < multipart.getCount(); i++) { + BodyPart part = multipart.getBodyPart(i); + if (part.getContent() instanceof Multipart nested) { + collect(nested, result); + } else { + result.add(part); + } + } + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c0607b3..fa39f8e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3648,13 +3648,16 @@ function App() { }, [location.pathname, navigationType]); return ( -
+
0} onOpenNotification={handleOpenNotification} />
{/* Trasy publiczne */} } /> - } /> + {/* Logowanie i rejestracja: kazdy krok pod wlasnym adresem, jeden komponent. */} + {Object.values(LOGIN_STEP_PATHS).map((path) => ( + } /> + ))} } /> } /> } /> @@ -12086,12 +12089,32 @@ function AccountPublicProfilePage() { type LoginStep = 'login' | 'register-choice' | 'register-private' | 'register-company' | 'register-pending' | 'forgot'; +// Krok logowania/rejestracji wynika z adresu - kazdy ma wlasna trase. +const LOGIN_STEP_PATHS: Record = { + login: ROUTES.login, + forgot: ROUTES.loginForgot, + 'register-choice': ROUTES.register, + 'register-private': ROUTES.registerPrivate, + 'register-company': ROUTES.registerCompany, + 'register-pending': ROUTES.registerPending, +}; + +function loginStepFromPath(pathname: string): LoginStep { + const entry = Object.entries(LOGIN_STEP_PATHS).find(([, path]) => path === pathname); + return (entry?.[0] as LoginStep | undefined) ?? 'login'; +} + +// Strony logowania i rejestracji maja wspolny uklad (login-shell). +const LOGIN_PAGE_PATHS = new Set(Object.values(LOGIN_STEP_PATHS)); + // Dane firmy z rejestru REGON (GUS) - odpowiedz /api/gus/company. type GusCompany = { name: string | null; nip: string; regon: string | null; address: string | null }; function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) { const { login, register, forgotPassword, resendActivation } = useAuth(); - const [step, setStep] = useState('login'); + const location = useLocation(); + const navigate = useNavigate(); + const step = loginStepFromPath(location.pathname); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); @@ -12153,7 +12176,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) { try { const pending = await action(); setPendingEmail(pending.email); - setStep('register-pending'); + goToStep('register-pending'); // Backend potwierdza, czy SMS naprawde wyszedl - przy awarii bramki nie otwieramy okna na kod. setOtpCodeSent(pending.phoneVerificationRequired); if (pending.phoneVerificationRequired) { @@ -12167,13 +12190,24 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) { } }; + // Przejscie do innego kroku to zmiana adresu. Cel powrotu po zalogowaniu (state.from, + // ustawiany przez ProtectedRoute) idzie dalej, zeby nie zgubic go np. po wizycie w rejestracji. + const goToStep = (nextStep: LoginStep) => { + navigate(LOGIN_STEP_PATHS[nextStep], { state: location.state }); + }; + // 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); + // logowaniem a resetem hasla, gdzie chodzi o to samo konto. Dziala tez dla Wstecz/Dalej. + const previousStepRef = useRef(step); + useEffect(() => { + const previousStep = previousStepRef.current; + previousStepRef.current = step; + if (previousStep === step) { + return; + } + const keepsEmail = (previousStep === 'login' || previousStep === 'forgot') && (step === 'login' || step === 'forgot'); setError(null); setInfo(null); setNotActivated(false); @@ -12192,7 +12226,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) { setCompanyAddress(''); setCompanyPhone(''); setAgreeTerms(false); - }; + }, [step]); const handleLoginSubmit = (event: ReactFormEvent) => { event.preventDefault(); @@ -12749,7 +12783,11 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) { )} - {step === 'register-pending' && ( + {/* Ekran po rejestracji ma sens tylko zaraz po niej - e-mail nie trafia do adresu, + wiec po odswiezeniu strony wracamy do rejestracji. */} + {step === 'register-pending' && !pendingEmail && } + + {step === 'register-pending' && pendingEmail && (

Sprawdź swoją skrzynkę e-mail

@@ -23542,6 +23580,9 @@ function Header({ (isActive ? 'icon-text favorites-shortcut active' : 'icon-text favorites-shortcut')} + // Na wezszych ekranach podpis jest ukryty - nazwa zostaje w dymku i dla czytnikow ekranu. + aria-label="Ulubione" + title="Ulubione" > Ulubione diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index ce246b4..278c2de 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -28,6 +28,12 @@ export const ROUTES = { creditCalculator: '/kalkulator-zdolnosci', map: '/mapa', login: '/logowanie', + // Kroki logowania i rejestracji - kazdy ma wlasny adres (F5, Wstecz/Dalej, nowa karta). + loginForgot: '/logowanie/nie-pamietam-hasla', + register: '/rejestracja', + registerPrivate: '/rejestracja/osoba-prywatna', + registerCompany: '/rejestracja/firma', + registerPending: '/rejestracja/sprawdz-poczte', account: '/konto', accountSearches: '/konto/wyszukiwania', accountPriceAlerts: '/konto/alerty-cenowe', diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 428605e..3d041c0 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -288,6 +288,28 @@ svg { position: relative; } +/* Ponizej 1560 px menu glowne nie miescilo sie miedzy logo a panelem i nachodzilo na oba - + "Ulubione" zostaje samym sercem (jak dzwonek i wiadomosci), podpis jest w dymku. */ +@media (max-width: 1560px) { + .favorites-shortcut span { + display: none; + } + + .header { + gap: 20px; + } + + .nav { + gap: clamp(14px, 1.2vw, 44px); + } + + .post-button { + min-width: 0; + padding-left: 14px; + padding-right: 14px; + } +} + /* Skrot do wiadomosci w pasku gornym - plakietka z liczba nieprzeczytanych. */ .messages-shortcut { position: relative; @@ -12134,7 +12156,13 @@ a.city-card:hover { } .post-button { - min-width: 210px; + min-width: 0; + padding-left: 14px; + padding-right: 14px; + } + + .header-actions { + gap: 16px; } .hero-inner, @@ -12325,6 +12353,33 @@ a.city-card:hover { } } +/* Tuz nad ukladem dwurzedowym (<= 1100 px) menu w jednej linii potrzebuje ciasniejszych odstepow. */ +@media (min-width: 1101px) and (max-width: 1270px) { + .header { + gap: 10px; + } + + .nav { + gap: 10px; + } + + .header-actions { + gap: 10px; + } + + .brand strong { + font-size: 21px; + } + + .brand-logo-img { + height: 40px; + } + + .post-button span { + font-size: 13px; + } +} + @media (max-width: 1100px) { .header { grid-template-columns: 1fr auto; diff --git a/frontend/tests/routing.spec.ts b/frontend/tests/routing.spec.ts index 4063a48..79b3bf6 100644 --- a/frontend/tests/routing.spec.ts +++ b/frontend/tests/routing.spec.ts @@ -22,6 +22,10 @@ const PUBLIC_ROUTES = [ ROUTES.guides, ROUTES.guideBuying, ROUTES.login, + ROUTES.loginForgot, + ROUTES.register, + ROUTES.registerPrivate, + ROUTES.registerCompany, ]; const PROTECTED_ROUTES = [ @@ -397,3 +401,20 @@ test.describe('Wiadomosci', () => { await page.waitForFunction(() => window.location.pathname === '/wiadomosci'); }); }); + +test.describe('Logowanie i rejestracja', () => { + test('kroki maja wlasne adresy, a Wstecz wraca do poprzedniego kroku', async ({ page }) => { + await goto(page, ROUTES.login); + await page.getByRole('button', { name: 'Zarejestruj się' }).first().click(); + await page.waitForFunction(() => window.location.pathname === '/rejestracja'); + await page.locator('.register-choice-option').first().click(); + await page.waitForFunction(() => window.location.pathname === '/rejestracja/osoba-prywatna'); + await page.goBack(); + await page.waitForFunction(() => window.location.pathname === '/rejestracja'); + }); + + test('ekran "sprawdz poczte" bez rejestracji wraca do rejestracji', async ({ page }) => { + await goto(page, ROUTES.registerPending); + await page.waitForFunction(() => window.location.pathname === '/rejestracja'); + }); +});