From 013560f0c13a84c513388bf39a279b71248f27ae Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 19 Mar 2026 06:13:01 +0000 Subject: [PATCH] Autosave: 20260319-061301 --- .../vm-shot-2026-03-19T06-12-36-229Z.jpg | Bin 0 -> 31627 bytes frontend/src/pages/constructor.tsx | 196 +++-- .../page_elements/page_elements-list.tsx | 667 +++--------------- .../page_elements-project-edit.tsx | 3 + 4 files changed, 231 insertions(+), 635 deletions(-) create mode 100644 frontend/public/assets/vm-shot-2026-03-19T06-12-36-229Z.jpg create mode 100644 frontend/src/pages/page_elements/page_elements-project-edit.tsx diff --git a/frontend/public/assets/vm-shot-2026-03-19T06-12-36-229Z.jpg b/frontend/public/assets/vm-shot-2026-03-19T06-12-36-229Z.jpg new file mode 100644 index 0000000000000000000000000000000000000000..706c29cd7505677baf97e8db8be9325cbf2bf779 GIT binary patch literal 31627 zcmeFZ2Ut_vwkRCCqUc7NQY=&nO-ewjh29M$^s;FJ0fHtJ>FO4RAi;p47Yz`SK!TJ2 z0YL?&ML-~-gl3^vr5BMG_daF6ch5fGx%ZxT-u?gY%zRmE%rWL1bF8_>nrqH6$5^}l zyB`2Q8S5MA1NQ6z0QRtdfZb1f`i*pST&`PM=o^{p{YGd3u+#pF0DwO{5MgO>?VPQh z{kcOQeyj0==IV~T`Q!WtiOqMd_Xl?Xphxx(%KSa_5f4wKJG;Oa_FoLaZk#PF4;$w3 z`VGGD19tlj*8Bm71l|l}m%07}MnEie*sup1zTovc*zI?)`%T1;@{ibMH2wX8ezf%? z{gC*WCmd?UJ|AZPg#bu^CBOi1?Z@@A^XwFW0{||q0s#9S{}tz!4ggfe005_0f5nMC z1prRm2LLKN|BCx-pWJjsxc zmMeh$^8)w*JOSqbMgTa#9U#Ys6ag0j3ILVeL4Xe6;Qj*#59~j9@W8=ChYlX*_=$t# z$Po^n<0p>&#Lsi;6h99ipMbFVSph*&AwIryGUr4kB&C2-r_ah>l)Z3K{DRblA58Wf zI&_HRFb6jW2loX5K7kAW>9qR_z;$@<`vZV|dqeZtb`9=N5nC zdGHXM2;$lU*vl4R@1cVS_x(oS!?kzc`TZvqz*jNcJiM;Ao_#rRkk9gNsfeP|>#mR2 zy1(wS0LS*Rt8wk)0$c`cJ=*Ir2Dx_RPTAvoKg;BwOFJrcSYmJ6Ka6C%qEBD2KboGS!Wl5`ZN2L<$>OE#utKMas-jTs;uWcq1Mn)Bb6ai&v z+k+G4c|Yp4>}~u1P!*^Ua{=PQGPhHVmRL;k;CcR+JM&Km((b1j0RSjAUjr*mNW~HZ zV%YyQ4z+n^wE;v(heE&T?!6NFAZ5PXbjDxD2v*rO%0(?x2O3X%Lo1laVL(csrwxt2 zj{DT)FChbGXx~0g%j3VJw={MEu}QhVG=GjXMGjNzHWywAKUE`(6N(l?%&wZa6wZDQ`tSUD@9qO2)|aUkH7~a~YSO%?^9dv@ z=Bi+b)}o{6b%QnHy3H;?XP;G17jwX?kOnD`)g<+P#ZC=VhEUA5rLC4bF1~fwoy3yV zrLv|)b&+Wsxq{E@=nqAJh3%YiuZQLP!Xsr_2~=h3a!*%*ZqQ>TSQBamwixgtU6-CL zRL30i=+X9tSy(|4Xz7sBdL!PdL%k+Bw%FJ;M3YVJ1)8}YZa7VI$-a%Go>?bz(#b8s zuLr6+2;^Ec5~wzyg4@W83**DB#|q25?Fv8SOgcXv>I;JEka&3=F>pVBmtBCF5N)d( z%DPbHuH~=E8(2}+S{a5#=nj)=EAckD2*g*#1MjAe1Y(L0Bq{@Ry+>#)b~ zrN=Iyrr>SaPV)DeyQctzzg6mwaD7AaJbW{4KzmloTd@5qIva(m9ZDN-RSPMMMhWhF z2cHHLDEu?1aL*ts+c&r>#+6ckwBazVb3G@1Eq25^`<)78J!D3G_LvfcalM-XrfU{I zfW~wZ+%x(NXuSNx^wh(|ETr}WM^1Nc?w5(RzSCa@qfez~fY#GW9?s1Xe2GO#K?TXN zkKav+twUAniXKW{K5^#rg{4`=I;OJnlhsw{c5np6BS}v^ufR=z)s&{Ootzu}R&7}# z!??`>G(k(1{-uyQ)SZ)k_WF3h(YUiUD;;NBXVQahYSW814nNv-mnUZIw0u&bC6cP1 zcK7u_A5p_1Epd+O9?<1PhHr8|KaJn(sDS#f&%RNH$20 zpB*m_qQL5_b^*uJ0Jn!<{pAMi%Vw-^l2?^02h9|%M_^sn7aZq{6XHh2;=jP6J88#b zKg}!Zcl1z2NkdI7EMU;x4o9~$;k$qvb7yh{k0G+k+T>H@Y8Sb7=IJ(4s94rGZtKS8 zN@v$-N^k&P)o<;DoAdEqfIR=p%f~{#pkuVtjkvfbOZ-%Rd0By?E;<+47&){f_qBxkiybaFs*0y zw0D}=_^m3Btt@MF4wz2g1)R<#P4j=`>MJmN=U>}ekOQvUcoS6rvUumV#`Qg4a<3fB zyH4)C;7Tf&7LPb?V`8|XyY?W$9{t@nxla4^qChCTtPupA^svZonIfKwt?%gk1v=V+ zSTAYRYIf24CbWKbls==iUM{o?cuO~{28{K)4*@?_y@&KdfceT_Yivj%^DxioDUVZq z0+%R!TN&z4p!aa9x+{7O^9a}I8DD`3G(1k#L5(Ch&U$I0QlB9Js7})ZXcBbwC|m$&>B3vZ7uf<3(4fyp_Eb zD}7EnC)JO-oY2@Xp$F#Z;}DKO-262C{phEBMrUQWO&M<9x-|+@ug2xTohg`=5XYaS z{}$%;fsk8t*6a9DVIR88t?#F3i4S+&J8pb_(Tob3{+@zzBse~{+4fSJ$)rwFs^)adp-Px z_T1!JghiC5oThn1b;qTkuerz9m39G%nll|Ks;XLN8V(byM>_c{zHDrB$2E2g_fI}r zZZfh+*UCkKO*Z~zZ7#F(RvAYuF=paU zMCrgnVQ^Bfy@7?dtVc$Pi4#m%kX&x%7Iv74pxC(bDx_*0y}xcVypeS#r=o|YA=P8w zG%5~J-B>m8anOLBrIyN#TZ<}HaS+nSv6DFl1HJ94?iIZzHooN!-lPdrBM?Yhl>K}; zWW*M28zvh~Gb)?mCI_C$er=zZDKNgYVV1fJ7}ED|(8$>CZ#gUK>xr<}xNfcW4&iHi zb3%@nKX9naie^@c?@9@Ca$yyu9PCx;aW9XQ4k%b_#Xe@rD9}777DES?y&v-36s;&Q|!d>OEa*GdwSJr_YvSk41?!t!dJt zvTn2`b;-eE!M*+-)f4MH#9X_y!$P~zCFLv;7U3_GkozVm0|ez5oX<)@eV+pu>RzsG z>jDk8x0OW1VP~?tU?zN zuKe0k$<@_9+n}a;;A{+T*v8>#a5Gp-NemPB0S#%B1T84euRu~*7YOr==a!b>rYT z?p9IIR_2s>2O_3 zddU2_R5M$AWcWncpjc32K&3R%9ZDQry`5F}EkY@4&yrZGw0mGeoD^AMV0vz=1553` zdNa~VfTRF*5key?t2m*MwsqWOt{l1?GCdU(;Li^uWn|3C>RdSQmx9VJ!9uyJ{Rq{% zLnu^Te;cxce>!EFF6!4&q0;VeVpThpgv~lInOoj8vJ1fQgjt!t;Jd&~th+&Un_quOPb1L_rJsP;0eLmvMyQyxJEG0g8K>$DEKinA*9GZ#p zK3{-G<+$|plfazoArGEonK$M=K;LDiSt$qCEsIu^n|{uOXYcB zXcs56xWHBuDBHJh;&KmMbCVW8jT&6##y!g~#3#^G6;H9=KsRycg+cgXb% zHba1?N}lG0kF0lF(K<0@G;3ujaxj77?8v;B=DgByvOrVHa^^ltnSeYAN`=9#ail!t z=p)k7Ch>w$&kzA{5c1dVP~(iuJJ0;-t+J)c(fdapXtj`99kRZR=}wT_q8dvMISKo% zC?%Mw5g#Zu3Y&%^rj`Tp&SKPkPc@&79XS{g>^}rwIsWLONux|z%fJFR{;Vh% zjHy!l*c9I`h}|i4JfUAGOw8df6x3ZU8tw2Jn@l``R@1k7@d}`6{^3_ijvMD0$tZ`| zbXhdl40-5gkjOTgyoejh12MY#o?HS%Y;#D1;PGE72jsfR;=E!h)5H|DlQ)W!iq=Iw zLfeQ{r^jM*HJ3ClhSh~Qq}-ZdMb$eCXGI?5=XT=L;kl{X^32^mE;hmURGL06L`3rS z(N~&vPImzN4sHjSkW*?#rumdfF<%ySJBC|`S*Kt1s+lXDHn25{O(WiVlH%lKWW=ed z{$K>t(9|^a-t%VM{qMI1TvbJ%r|ULIT@PgT(yaq7BzB8jzHzhR;uPSx>>a@VH)0Kj zfc-ygXn#BHKerc$N?(*Jt%poG0;=32i0}OZuU;>QYwR6C~eZBt_EcZ{zN6rDbuKXdhpYCwn`9tErhOkHB$i*bU z9^R(l0;srFSx4`XS7jqO@ZOP`^O^T2n#O_`!#`x950WUKD*6tSGf-ue2T4nqucNoo zr^RA?Fu|2x;YEv-o3VAx(X2e*(ZXp5zx*Jplrm*38RzMcc}AGlDo6ts%Ae@H$y1Ta zi?5<1d#KnzQ z0&4D2Vrt_2D^gfS1CR=arM4GDD?UXuXQGNzui+yX^8SRa`14GtRnM?n9v_HNZJ1|>EA%3IkQ&9|q(gM4K^S^EI*v29^HWav_jjB)shO8N3m+p8v2flf zNe!F)MdoTn(vY-BI-}2U_9T>?>HEo}78AD@bc(6J)Zv6RU+J(?$Z(?tS;23;hF2+w zVVCI!%CR%OG>Wf-OgNZ8gCJ_<9IH|kft>HX6JT5PGuc}=a_S_joBF%!4Fy^}b73Uv zMFKIqD2A2bh_KW+QyrDN~iw(0Yc#<0?+Jytk>H%=q%O^7m7*Ni~XFKI_61Cx#V zbP6hBsNFp>Yhf9og$T7lM+~*4R%?i~-TDZ3O1@ilpt$Pjv9duy-)b8Ru=0FFypl+g zXR^Cp700-##2jAnMe1pXGWe97eSH#&QhAC12bI2eACxSJqo8w#haY;jP#`cINkNE! zvR*9mAv5NQ=zHT%dUMm(TO?a>m!i0y(_j{{Hpz>#4Hu!7F%y4a2^BtPttQ)%SKBr{ zv{k4jhR3|~JwxcFTI#NY!MqNgDW(EDr-L2B$vjk|ctOh46cNKr>L~{HN;^&}s_X)! zb4(tpaeurAk(=Mqr{w$3XgG-rM+TXEKoR*eREjQHs(mFN07i(BX z(*&{7p1!ga-vSWOriM?38|39HraunthYZDz!zAym3p%M5!sb&eiU~=FbW<|yk14bj zy@_+CID6+TzDV2n_PFP2e!%m7e`^W4zx`3qK<>BKOQQK~^U}{!9jZ8k8H;obRQq(B zNW#bifa24?j1z~i9JsP~qbKDGC`WulcLSiu%N2D1?REF|-k)4V5UsA)4=J!ByUSV! zM)GKb5+7_VuhW$k=Y1=+$WXmT3^*Eg#^9M$?a#~%?LmT1_|>Ng)Y9Td_#EbO?&FXK zTJdJYPbZ$f9v8-}-3_muaTrPHU3tRT%6ZSgc3V!yfKSe^ck$QGmPUDxjitSw`M@^W zzaCi`Ar;M=w?{l>_?qE;h`s7XH=Mo|?JR^x6_uwfJNr~&Mt+F@F|e|nR2TfLxMC1j zXe=lyb=y)w_)Ykj8*MxFQiHr$gCt`8?(knO^j`q{BSZIHMZ~p}9uxPje643`xD<6& zp-il?mCX?)G3cV2H%&$&T|M|A>B>kb1nO9VVB0QVJal2r24z z*(iozFZl@Pr)FDvLJ$jtZnrQW5uNdZG+KE@35=T!EG#TG$TgNp`l|XItgY~fk&jnm zp42{LDI+mFbU7qABqT!NtFi!hsjo`nJl`T|TGEeh^JdK+ROoA`S2vhliriwQ={7>G z(r51QXZ(~bFLoYi5n&{6D8w-n~~9##0Suk z@pY$KVAi)2Zoez;`*L9N`-F1I_tT@FE*-Zji&P`P)dwMK@kRc0Z$gdbwzzQJY`SO+ zSJp`Gfj55U-l!tl&8$R?In{E5wbX%j$#Jcj&j%jd3w6akeTFGEP;;tVGNc%YP+abt zA_kWySAzm}(9z!qUryZ~-4=-+TDiP2y$hJR<1-wPN_A$IY{Bd6D|7XyZ9n1dfj%jh zVd-EiMIaKg?wtEw>XD}*C=y%u5GJb>)*XZ`ovw{R`mYN*F4x%BjX@2{LzEFBYRFG< zU!XdQBvMpMxHF@QPCnqVWnfvUmhEK1L7ywHQZwRG!+CbPu+lpCPlYwCj0^=GPcQQU z8V>}HA3(zS1@qslU%&{UVw>O%MOfa}L|%(~z`L~G=p7sH6B|wz8$pL;S!YU0XUzK+ zGQu5Ur{~qL(YbA(Jg(OdkGzJHt}5cPIfAw5Pg}Gi{;twN&{9}^n}%B*3`Gy}bjC2Q zH_wB3Ff#E58eH7+y5C{jY+KzV9hi1~d`=+Qa9X=ZnR?l?YI>%)FD%!!+Fn?!+d!c9 zh*0q-2{70u7qsvNst#;3+IE?b2KO&uS0DRuDq|Y9fOgjMu2YXzrtS8F4;LvZ5%8T1Pi4 zBv%>D-?~<&J}ax_VW-7DJ%qfRV_VvkhnO81VQzMnvf$2~5+P)o=4Pa@fP&+xR>{)VL1G7@; z*I6XGZvh@ro6FhWK+@tqtrsAZ<>jivveU3+@(-WBY6E{y<$3gg&$wAnSWka~veCZ~ z4(8eg6ooWiy0ZS|UoZmx*2G@7ldfVHCPl*;gc_?uqteYO8pX~|dl zHu*Ow04eUP4DuVmkd6J*T>6A~_tqgObS>M03|ejLl7$|SCVCV7O);V{4TN-7iRt8t z5V^>mZ_mTt|I51Z&kY@kHM;*86+MAD+4-&eoSJs8G|YcE0|sk5GFDi0Z&Vp$B3Ma7 z>r;67KNQkku&l&s?Gxh%79Y+J3BSbtv}18&G@-w_ck*TJX0Gj4DisCHOuUhifxdSH z2%M~H7Xs@KcZ6e$?(1$4m-)xe5I;pqt(XOrMM|O%jSV?>C3+6N`55VxSvO2;id^`j zoju34$9*2arx%yx7^7(OUs`3#G>|#tFi&U{FzBJ=fEKS=ZMm6}fy@3Ut2Z4!qHXix zeF`m%HedwLEMOTvLggJa^AU_RLw!nNWyBX~NzYhc7#k2=hmWmTf8&aFZF|2Kf3th5 zZ5&=RbBUUel+r2Q9Y#|3wgUl6`}<#j^yRERR6F|&e_haU?V^T_nh(xbnCF>uwsFuh zt&2*(D%c=HK?qX(5b+mL$W++r6~3pXKE6RDCa4YjrRtY`KPNa62&nmlg~<<3ebZUY z^tHCIUe@A9ar*PYXZA8Cw`^2{rvxYy4D)pjSO{|BL7`o6CJ*cQqLU}7+D@*rtt~99 z8t8pm+)TYy6wKA7HpMg$Swg6VdI_~BY+oC`Q8g7ds0BR=I-Z(vS`ZdOR-T%25F$f$ zhtgRnbhG^Nh!>x!vZ@aLJvH}8J2nR3B5F{>-N)|-S*wYj8vU{T>k^Yd5MmN2`RauA z%iv7!3P(6(q%nl@fDu~-tO0r?ame5oBL^{1@C&8@x*;UhNyC_GIU?*=>9EMpN_&dN z=S^jJWLJD`Nq4J*wzjvnbJ3s{cbi8wB#YtXddGR|VbLIbZ5vi@D>!{IB}vaz;|9ZN z9y&>dzAX9V|G6tH(`<%L?b9>+dCm6?&pTov#iO(`$I!0P7pLzfGx+T-WanomBbInz zUGY%G_FNwi)v~K$H91D$X4a&dwHK+5SJawqftIGpU7u36uFdN`?w3M`PN*L_HGQsx zfY-mOVLx9)Z5f9+u8U$w)h_!WHCABoS{|6_-@_8eRbSULauB{|*jL&$Ig-GAfj~1i z#@CjQbi!+*?M*_{Q^L&zr?Yt~_Z35kcZFto+ReatI=er(uhlS9jK ze;EXRhyQ;9{+Jz}=h+3cizqrWLc(}HR=qlQ)8ppghfDrn_6eO9QrS2z7y)QLXuw&& z@4+ZQ;Wef);Lbm_(9v`3@lc;CI22<@_%ZFD>Sww`+x4KP%U5LgXQ_k=zP~vz7I4M# zqEVw4rtT1Dj{eF4(SH;E-6jA3Rt6_gfkz0oF+DEFknJ}J_^a2?9}E+0HdApgGFh|OCh0>S|3!Lq!6G`xrtvJFbtx1xDahnb z^7@!O&gsdTAkv)9umqE*h$0w16|1+eq0N~<$|ex_G2#R+C||WznyhG-nPDpk6~9Jp zNi@>Ro_~1ct67J$vxY?$wdqlv6G#lS7A}k^`^pXL<$E<|C=nuMI}TaDxq?`37T2L< z8N{D<`DJ=uI3nKCC*FVLmI-BuBdxe(3|D23I9X+`>-0=&%bYgTS(NjBfTFD^p1!Kku!_-_*U>^sb zp#X#BC(OO^zrlM_QgRtNBJw~7v_FM>H5#?(xGCGq;!xwFbB9Qelu_vqa6npHFP=Lk z9HZ!IQtad7LpkZ_zb}mx8L&3wlFy=kVH~SW)5a0z?owtQr0F?niE}+K^j2XG`=dRG}2weQO3h_1u}Iwi8$nQ#D&spX%@=HA-q(P_RaF*7#tv zPFYI5F{&m`O3<_=IZo&ujP@?f>qx7{GTNKgtmH%hy?AU31o~I97?i@2dnjBEB<}3K zoQ>~QFR3A*X{tRb9pQBZYc##`*2-6TUdP9uhx2Js2LAR1+u|E%kA60*pMi(GNQde# z>6}^4hw~;dItnu|Wkb?LX&>-HF!w!7cu51It*LHcjPUY&X>EWU(O23|HzhtF+qw@X zS(qS#B=%2A+m}F+k78jcwcwYE{OUUf_U4;bsI?Ej6|O9{o%*_BklcZ&-LF1pGQgZWnRNqB_*KZ~ z#PkbQ&OCujx~{^auQ&aj94FOA!m6UN8Hp=9quXP-6KDSlD&JY0*>XwIeAe0RHR8Z_ zA~4vBTHIN(SU&MrP~eJ|i^)>+lQYTMOT*u-VV8T>qZgB+)1v+gcJKL>%hQ$g^_0b> za`5-u+Fd|I^0&s{!A*}(*mQp@9Y2)c`Qi_y39C;XPrRTd5jkG_t(XyAxUdTt725?| z{C!3te;;$@rA+A6X{ANk-}OSAPf$()K0DbUl16*yh%>TWr$#$ClH#|`BTn9qJk7ev zW-J#uz9yGTj%;i6r(g(uOe;N&jO-SQ8?f-(JK7N0OK#OsAe-y^c>m>(E4zS?6)eV1 zMpXGGx>5m+2MdpH6-#`Ldm=bI5dy9o@d}bXq>>sCl0K}z9-7Ul*c1HY^doO_h5x^d zmN|iFi6xQZKY64*jw~`Q`?H$-rY!%QnNpH<`n{eROUg%XoSjbp3NxdYQ#JXMG+u|UOHD$OYf?x8v0bE% zoB;v!0#vV4$=!(HJGCmh_=MsCO-%1m6NoPj0TyKtbNn4i?pn>qMRhcnCL2Ox*)3SD zmgCueP|b2q!&>m1f(-m@V6~d_043-ry`#S8@h(9FNNo4|j)$p86xCc`v;r%ulgUe| zWjWK6eZ;XPzG)c4;djZY!3rW88F+hG9)o#w-mf?hM=m51g6v;wI8~|%k4#8AqAiYj z9lhV``O?zbVXQ_yN!@$!X3YDRrsfAt@pKtoXX98>?2B#2xPYU%LdBx6Mx?H)+O+8+ zCwA2QdY9hqVpQ0}!93KV_gZbS(@v@?d9(STl{3p??lTWNdyUEP0g?4uMIL0 z7Rai>8$=4>8B%P7C>^Zem@_++fWVRD!wNyKJL&$VmJU8}Py($cr{3sf#;JnDh+Cy# z(j?8{A&_RnscUXD;v*ZPMlPoKaVE6dn9kU#;wS>+?1hPZbj<^&+Uq7JSA}`?czGk0 zEoGjU5z!$>=#S>DX=-xZY_D9WrduCZmDif!@`Ywq(}U3|boI3H7frhWm%)jt;X;J6+k% zlnCsd4Gcn()M3@15QeKA(5T8@Qb`zzWTjY*(D+s1Ywjy zK5rVi`eu~Naj9wmi8(IEBtr*HOe~ceWfr|NN1Oe>3!0psOfYp{__9I0vXAv)YgRX! z7JOnO^74ki3ia}h++wD8;=0`HUBHRLT>vM(w{Wg!HJ5%Vac2B$?(o*|<*~$HP5}@; z6D(4CMlMxdRw*P2TH3>MLIjD5S*w&mGatlB@bb31Nd&KZwq~ViJ~v6N3;FGu@#q=43mV`;zIpaAi?9L?Fo+7&l&otL{?kD4^g{V>C5rYFsx7 zWJd}6F^J1P2UV{XY%pp|FnePArakjvoR*oD<3v6}EJ2?tfT_wrLT*6$UsfOEsIwp} z@Y*N&t8o@oZXfHwM+A{E-ORR@LD4RXFGj>vO8`aqMzJ}pr&nB}p0sn-0N*`mV&vdf z_@WFp<$;~EJ2Tc_?8}O873`EkM>||Hdpmly%Tb!5^cqp?fTMk$=733Mw2)F^Am{*$ zMk4FNig-zcxMa_(1@pbz-$Lw_llYtA=sqD%h++ECGZZ-Y=}yg%&xf<_d?0R{RXY-8 zcvc{#J&r}hLLv!!0VbUnb3XGPq5oOqFm1DegcFejK_NOXx@^J*nF&Dn6U8oQna_Eo z%dyA#v3)?X1i#osecD9=nmyg?JPrD3EyTazX=Nk4g!Y~}n0>jt@3rX3K)-I8B!vOD z(smml!kVH|*qqL=o6=5ricW{k%nC7~fvboB%uErs4JQ&2&fcW_a3|t3{8@#KkH(m_ z18>CE{JK3OuLSorLsd_*;-&jABtc))fvy&fa6eRtFJ=7X2=c)CQ42CnT~{p(N2X`& zpFB!$cZ2IwLx9`#ClH9)3#?5=H!CXDY|s<;EGbkZ2@fl<%RMwlyto}?SN~~!#*P814? zHnn;;QIy`KH+bK%9I?!*rMZTMwey=f_~P+JHBNYP0dcJ+Jh?ES;;kGn4VD)y;tw^; z3?>1WWUHsr;3{pvuv+H$>XV>2C(g!%v%*R9)`)c#(O{YtOi5_F{M~Wfe$4Zx?hvbjE^_CO zUi<$Iylm=R?mFaAl1o|iu%%-DKjvig)QEpTY2n#vF&B@{nmdjFk$U|;A!@-c;Br3j z@|#=v+L?^uG+WStZj+d6y;cyD4m~`EY*2#A%XqO1u;#RwuriM}V^2PP&wl9~@{auQ zQK6?vu{tgGON6jRIkiwKt+4aT7XPwT;br{rE`XZ2;r=_=%jJ#c+0Z5Tftv4=f2WxH z*As_EA`)-RZs%(4n{s)F&-$Jj^1b)+$Hd=(zjD&bELn(HIq@u7F$1~fKe}zZ3wUt( z_wY|&A}3qc;4f**@3mvTwfS^TS4Gdee?RqmQ0Mh}uIPVNq@7Vr$x3qJ=sbt#cmRt0?@c5x547kCVQZBNu z)#2FemKa9{!ZkdxVaBU7Ykb5d`c$#osVl7?RNpq<8Rb81eNpi?=hvI;>?`+ul5EWJ zeG!fa|0_tHAL=OMVdgfjl$##?+1CgWS$mg41~M;q62L>8Ochb%bLCbwu~ zQ`EP@ZHtJ)DoPfvrvGzc65a^t{I2Giz;_`(J%lvW=5#Rc#W}=8Xw%4mR#n1^Z0#@Q zF>`}Hmm)4pgun;UwnG7@P&aNkmplu?mfH{BAXkX%oIdtKvMgBCG5)^e64VqZAJ;A9 z{4lSYB?skO=?Szh68wP1t4Za&E4W5|VK@>yZUdCNIVa*;J=rpUtkWq)sJEKN96ar2 zf4bVlDi~joI&1(>Sb!iyai6uN-uWKQcB+4A+H9c8iA~k->Bt0!G?g08AVphCol(ik=QUYOSb zSyov_87@WY0-VRM*ge-s*3{n@#zh{gy*UnXqB&{_RO^kmbjigL$<@8;IG~`Du#=Xa zmZbw86IqTLCQLSw^{K5`$L+SFuZqeYDa6WlQ&y&adYli=w!1Qk*J#|S7Tn*T1#(apg{1?c5kOHD@y6I#?6InM&?xc$6&J?YUB1lgn_Bg;s9gLHWjI8v( zkz_!J8R56qq2OqHU{*U{CaGXjaV58s&-xlPg7ew?hU`q#DpZ@T@rEC5&j*=qj}^5dib zYJN15AGo|gJZ*Ue@aqOJ;4jvS|H$Tt^qe-Ji%ipq_W$$}OyBp(*Im z7TqXZ@3r7cj7*V*Mn;;^#aP0(2*r3N&*E{x()qJ;=x$0OSzkN|LI*3Is!fu?i74=Y zzHhz@;I*PDV4d3y3}jj2kwKhgtU*ursfi+5cenZtg+!#CSZtr`8B}`1hJX({1~v8N z5Mg0mN}h+Ol38s8)DiT$3!WN|FZZ=U07;%)kxbRCS&NsJ4g~KmsnKIWE34dP5%WnB zm*wFXX;4!~_X;{Z5KTLz`eiVbgn9#Ulz+pJORP3# zkcc@LIEbGVUSO=?h$+R0+G9#@rONddf^^$R>xc7);}J|y1yvW|z zuz)T7`cFHS&)ba|HT7}t7m>-ZYh)XEW2o2BM_y}O*4{K+ofFVUHY{wMvmwCSX6O?% zDSL+Di@Dew5K#NXw2GtXU9Dhk%Sfv`I|@pu8K*|kWBaxvQu!JM^p1-9+V_?c7{2VC zvfTX^_xMkF|8E+;hwXE9@>h$m{;x~3r~H7w6PEtJRs9c{|6b7ge_+Q({6lWKzpf?k zd;Ds>-g6Ux+SdWnudHYI+6AKTRb7WV`JD6AYbg6HI%sCtx9rY)dQok`{I)4#r+UN( zIB&x($JrNHY3x1SRf4J+z5bnt?6^P|n`o~#B~kbx?&@qLgocQaMVVXM(#0BM_l~Zat@$;d?_iIG)PG|d-@yhMIQNeTfjAurDhFCP` zouUJxmKG)V$FT?;O-;VJ|8Mo{-^KklM|5=fF-IhD{r5vy{uF}p$BVtcrGq6rBwBg- z@qUorB>1XjJBWzGV^S|Y3oOQG@JaKjso;u`Scv28h_Bg2+ZNBA1GSd$%o~~4S<}4- zjl`Wq2XsX0bxn)1hG?ZE(d-8$jjM%}JK8Syo|XG*d5Mel+Ii0?%i0&G7#s9-x2#=0 z5`)BwA;B9XBeoDz5nOs|f(HEQ+@Y)1-K;zlpcZ^Ea!rFEm>6#Dd(iB84APTBQJrJ-W<|+NXzq{RITis zf_PkF`TeN$v&&Rh{;7`MnZMg-iN#&yF@UiqsY zWT&uWI2)$_erSdSU*C>7)}>RA_G?wTRGXaKpFZ@~tcBQXqvBO1 zOHXSr5s1`!IxJ0AvGsb2eC9jjj=~e4%g{|YCIf4=FR|kh za9)@933Nt1K;KIWi=T0smM%JNbih$TH%n4WO$_x`cdI3?l|Z2ALN+pIKRNB>NZMxW zH2W@X%yP912*!+|15_wCX$|WkLK#zSGqvoMbeXY0IC;Q5NyT#E!)r04v}_ufmrmn? z_M4B5x0FIu<$Hruj0kDt>Ds>}h!LHL5AjTwvCZuv7I`DQ^wQ-A2bGuyz5V{3FQJm$fMeuGacT_O4Xs=ARk# z?5@eVr{4(Gk`}c#VEg|itdB6Eje}VtIIoQO-pWF1v*Rk&36^w^uhU^}E-o@%Xt+Ok znj09`9=!0n(PhkV^-NxIkX{|tjOhEcyjKM-Zr?1ZqE_sY;JdD-#kAJ4O4YVCS(P5t zE_lHh0?Mv!-oB_HOPyBSzR|zFKKfO|DomMI9{Sq9k+!KUO!|J;ZZM51FNg=>I-Khy7(SwzJ-^+x*uQ4eMKTg8)D~Y~L>i2H`26Arq{rL6D52woG?>BgBSU(&#`%5c)ti!0e54b>wPI8}E$ta)fBXma?j3H*_OU4UY{eVYSR0AlpYUpHQ3c&`E) zs*mR-)u#H=fRZ5~Q5x(()zI|x%=tn?e99$oB=OQf7)~Vr_5=gkKtX{2h9Vi=BIuXr*&e`cjDzvyi6XwVBR6pCyOm6!P-+f*vgOD-{TN7#V%`=62!rJ2be zYOQ2lP_BwnvaDY^3XAJZ4mgq$^m1lc;z;;ORbNX8ya}hoskdNLSl13&5wKQQ_qJVW z%y^?`F47v)oE}=cS_0zcvA^=`=kNZ35&8epz<%KmoBmsdEBmnRV?jNJ<-L8fG86+N zqc?hS8_jY)H11SoJ>5;7WD6_WLJ|;_=4D@Ktzs=5qOfq|tu7sUkvtxwx2SrqQVlk& zxTc=X4x)TkoMAWS$F#V+fSHP;i6tneHP#KehD1DO*^#Nrm@CvECbyA*A*w@!mWC{g zC5;V@!(6%AXp3J}p4ypl2BE{~t7dLCW+oQ0Vu5W>7Ied(Z}K8(MfV$fIMSTIf0j#X z)-&EhbMwb&DMQdPHZq2v4Mf-*WjVCT4}PyoJIxAH1U&71CW>+{2p^CJj_Z2JB1GhP zi`yvL(%218MUsg{VfQJiOCYK;lgrA$Ozg?F6{?r*HkC29LE*jlV!QnVpB1FiTBU9k zvWk7Okm}aaGBjK+4d&t0~W z>GX#b5OwVV)Vja}3WZ=bwU3VOts1=n1(VRQLUCvVo>%e3J3HlJr3uXU9`;5HUcE{m z4d>IBhVt6L$1QHDd7U|>dZ!X}_KM}s$tOp|=H0!5OvDW&)(p#J()F6ufF)re2uDnn zxm9szhJ6MNAMu5uwTMJa>aB*dz2P;%T$f#I_cj$y2{>~!zsZF4ZbyfC#l4KusHa2+ zyc+~u`9sC{KM4OL6w^_-H*)LV4mbV<+0-rg9DQ;FV>3N1~f(jG{2 zh(}`j$%!{;e^Snj-@wAsIK5edax$$i^6HAGpG*ocs1WnDRET;ZiLx#Vb~vMk11?X* zkG2|qV0@*)5fHs&7vERfv~;MzxK0(dyRp*rn!pI!m>C#P#HT!?gCQ+5;^Z zqx5I{Slk0@nFtOjLR}bK&=|4cS!`HR=_^X?s(6HXUfg3MeI6wiJlu(3uU9_g^1AFl zkLp;nGTA!}m-9_;bjQva=;p{mV4k6DZxfGU*{_a7%hTDSyd{2*l0dr1Gt4$MMO1nx z>SsT-`wU*b!Gz&H357Bl#upt9#snYj9kgRrmo+3pqKrO7WO4|bx1lm_XCHHLxn5Cb za%@`0KiN$y)9L;Yd$&8()#{YuWl5PaIj8*$T4G&H*trj_Uh1hqGUxsyJhYNlr- zF$YdN_+;FxETcq;V!pBcO)zLZJw13YPJ);FgHyr)pYOx&Kwn6I_01ZCO7Y_nFW_+RI!!Ku5qr^{b)_%Q!>$!I< zVtC_;`^4fKT>QVQ{I_~c?bj~`GV*PI^lUSV^qtxwd;K3!mcL)*AKD)J`z-$NQ~zXm zz4M1Hu|F5|hqQkY;UDYB&51e~sPZMcFR=Rcwbnce(FjQVL^SN?D7?~kJ1?D2g(ff} zI!tw}t?eSDop^S4!kVt7(>%bJ0g^G;CLH*fSheM+WJq zK6p<{RjfT_s^+j~6e>r>ACS~rH%d|j+SWjhZ{XvQ@bpKt)~$NY#;NBK{VDcNQhDJj zy}>>+FE1=V+X?(k5n4YL|&ie+-N z&%2SRgBgs;^24aQ*`csC6oV(M$k;(XxPMew+(SxKG|biRi#0_J0fCHvI5>A!Gq}sd zuV}uR$5{h(>gK|tsPk{YZPZ3agc`7LJ zZ25)IIcCPa@$Vf>Y#*u3IOBh{ckSU&uJ2m4N~@9_hSiKi%Q5G|kTj~*AV~}}hB=U9 zj8lzM3~E*65M>y08Z{1MzA+(x!hpubyKi2ox zdtKjqc;5Sauj~EZ=e?i%ab?eh`Mnv+P~E5<5>CPpSn3%3v}VJxFZYye!+UE=xg*Tt zu1x0;+EQ(S;%_*)Gf>@#w{Z7)NIHlsa!s!cO4SL4l^KMLISDL-G`ZYrm9Anqvo=swsZCG!w| zB;LHwDxE=lIF$?bTbk8n1@)lSD28ehZV)L2KEXp~sFhbC81IO*E;p#T@?9+C&OMV} zYkSwT^g1;`rHxUMtJ_!~r^0vanJLvtVSVwMq_y~M%<}A{gQI`6Eeu+pJ0G455ON5J zg{Nm)qNFvMnB-nF-12FSv1gIDR>Gr$5{8*T$dpb=L}^*lcQNbE4#(tgvH_XiL80l7 zvPD8vHq@Exz|{mMzn9cVw>8OF0Y^L!Nf<$(Lg|#&BucY_zqDCez`Z7IW_Gq(Bhk|S zk^t1dQ1#qaF&9jVg@WvG3npmZMWzbu2AmibaHy zptI!>esU~jYS?Xbad6H=qv>q8f_oiLKVW=2XAO7 zW_DbGzJLg?Ox=$AyrrNbYB-Q=X(Q}60XV4v-3cl2s}%PeqdSMh z?;-6#NA$C3&XzJ!ypKdEb?BEEOYxi!>Q}kV>6? z*<*qxNt`-}*5=2g*Y5r7JrTA6OdT(uAk%=c4~KiWU_UnKOj!)sgc@mMgo-OttYcZx z4KAR6z6IO&gSSQTn-zq}BkcWtm7m%_ZZu7vncCQvQuKJ$G??51Fh9eNuQ`1e^D^wT z^R8eAnU@>uio`$0JPn9(McnKm_eyAJ>X8F^1vR#=8dec7dm{X71Lv_H)Jor#58yV^M4{&Qvn~i@9Z43imr&QH{1=ut zqK|xWnUwD^5tGcF`MYkJo=*?GngLT8%b+0gUbSc&ZNo!8Uc=Yy)-_URtv#ebZEzw@ zMVq~6PD~-DuCTxRWUv1ha9(`9_5>%R$FZe{&tqJBW`-$C?azy#si;P&{L1RA%SKG0 zk1OgI@hMqvJY#+iEN7%HrFrF+l~r<0)+Qf6TG}UG@Mc}@UcY|Umn>&D?I}HSdCz({ z-m}@(YPnqzaxlLX4WXPrYg!4MtT2VJ^s9QpIK4j~q8L8K%YeRZXFhA^&%x*5u+y8K zPVasd6q!Tp%<)6459zdX0HI*dNr8p)hZM?FsqJMyisMZdMMehLf;R7O9II|7g zi~@x8OHoY$L6Wg4m@en37D&ExjXBbJTKTU29w!U601B##8-{e=!09+0;f<}l6{q^M zcB0UVS3a_tis$*6&Y#~Cq+!$!BXv{@%%pVOYJ_)MWdyCzTppX_(C1%LeE0FR@4yBX z6CG{&&|r6Zl}f4CGnH}PDyn6c*|vLW6RR5IMeF{Ai7jQ9?q_u6-9%_gpQD)uPns(X zE3I8+q*4?o^_YMr*mD#o0MC{G?{GyR5UIuG`$?Ud9d_sFsOq`sdMh)92CrOz(Tr^+#6;y4Xq7UI zUxDU3?a@c8QT5z&0-G1amlGVtj-Q(&XrmL4cd3MkDG1h2b`KgH9GnU^)v;^4M>3k; zuk^yzvxWD|5=lV-N1-emopj5B>%yRYXNkZ^kFscsML8t|4o0z*z&|U+DE1^j_7_TD5Ueg z|6@OPkQ&9lJa9IhVN7KG##ha)y)ai7`<)Mk7J8JeRhl@^eW zk|I^53|=oU-O-o|bnVbbWJ^UHx&b*kHq+j%zH5b{ zGso_}e{#y*cND5sH5r1mw3{MMZBqpK{3+ZH?>{A^J$ZP!&9KAXYl9EyU{5haS=Xg*v4P9gDMra_=7b;Hc zkSgaV0{tIQWTmHnj-^%E7+h#lbfT^Rr7s=ToYx;wFgdu%!V^Veg#9xI5L3WTy(7!J z=(ObR9@%=X2jvCEpD1hm?=8_R^zcGOVALs(>J;K@E;5-5$tt&~6HeuYj{ z{K31WC`Cj3J@>OU_>p3AH@va6cF0Nv@Br$SRhu}IA%bXY9DZhH!zhrn|C|fND4#{T zmqfw+x|EL$yVv|8IakhRBG4~|-`nF)+8TbE0_B?HE#WEIt$J!9r0fYG$EnkT4$fbD z|H3k6J)K(O?1~*boF-&(vc-GQ5dp>1+TIySYF$pyE?D0LFirp}+Uz+JJHsM!0@B;M zx!y#5gkyrPD6nX3SRxh&)gfz>_lYTm|4sYme|2q{x<~vpxyo-KYy*rAM^qK0`f-!xnauKa{z^wVGU}0m@=5pWkSjV7TaT8X>i7 z-Qq55!Y~!D04ce%q!r{jJ&eF+Noz7IkMW8}$i>^{Adcfjp z3B@bJ0Y0gz+wfDBrgNpk)y0uJNA2S#5nm6$jFo&oe|oJ!I;2`4+1 zVDCzpQm7FAt@HGt6MB~3oeAoia z-dv_@$t&#_tSLjOli&FKDh>J_CSItuzbKXQ3FS>@K7J8`JaQm;mSjD%GJ2POwV!^4 zOZIU#DJ?X|szf%9xlMZQyqRq9q4CPK^_9^oo8az!g$0X+;0ZIj*7hqUtLP|_* z*>s-z9GubB>+fab4lb&r!jlsF69pD}ZeJtmX$PXHoV&~igI&F@YN6^7=|jFr#O9(F zrkmgs(&69(Ru|9_TU>r8uH|;IbtCUtuw~1+ajNaRB*411sJ>Sv5kAm3HUa2l9;7j0gr@TNbdkm?|j+Fk?R zVKxmE2cAFHWTQGL?4kLfE_o_TN6`C0&+%@>WE-{QZW)1=zfm(|BEnqwcwz3u6nh9L zq0iKRdk;d)C8e{Eu+v$QUn%w}$HYQ1LS7TvWtCU-^}oqP&aJR`gQ`##u5&L&(+Fkt6NCDI%EIQ?NrK8cw}@xNuuUUzinIB6Tu^^K8yHN zMG==z?&K$OV4j4J{cJ`X(%rc2A{UM#d4|eZIKP~z{&w--3dqya}Tuv#%&Aka^uib*_ee8o(H~)2^}tJWysd zI&mb*fJj?Fu4#SQHrm0N^yz`Q>Qc^FBXI7^^du>)%INlra}6XOjkX}-=u5dnM1(Tq z`4`Q_p#w4zm5LpNi%NYLzKfk3xbF+E76vU^QiF4?Up!ndYx*w6ANFs5;(s=WJj5of z&|{3-CJVof)+>M8U%%sW)Uao+Qji_PiwwxeLQajvI%Si+>Qpy^=a6frI<0ygy{*E4 z@+<(NUZcGeZ=}WWv=6^A@3xF`BLI&C=-i5^%424!0e;D!Ju0|-Vlp5PXTw{nwVItK zQaV!y+7iM8g7%cR=R-|h?9idT+RRaCmslaKz)=(3zn z3)$FZ|4OZ}M^gBhzonVgPyQT83s>YWh87hQnFU*6$n;7SNq|XC>e%ksT~-m{H_BD4kq~%3Z-E&C{f-LSis`Xn#)&L4VJ|xr0qY5&$&-;8j~Jk-^ckrv<+p)#=LSF zV}yNlbrS>&0t)hhpw;ut!Iv*e%W|`5W>Y@oy`w}kvekG%E<;$xXp>#3_|qcg+crnb zm3f!S#M4{L42+N+2tAQ5Nzw==$|u}b&;_rX5y_+ulNSu9!chZL+ckMOeb6WF$ze6a zo|b&L=Qx9>nAwQlQP9FW!M}8Xe97e~@w=EqoQK-CdvW96?kHU-gT~{Z0mIOrs=??^ z=-0zZ%mSz_b-6Wv3IX}_+-ZA$#=B4fCmbqpMD`->oSam~7hWxsgpZ+a1d{cibCvwl z+$}FQ&3+gA)8?c9IiTbolQ5J7Nh%EoGmE?5`(H2OJkj%RP(T zf+D3@q=o!m`1>_k?3VWvYm!BIZ_kZv+k*)Fqx82|*M2!}c~4Zj{Kc$(TihOxt#VI9 zr9Ud~T|552H0wLYcN^8#4htd5pDm+x(atiq%VN$xN<2n;@?&QFFZ81Pz3<-u{DiG) literal 0 HcmV?d00001 diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index 1304809..b24a498 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -510,16 +510,10 @@ const getElementButtonTitle = (element: CanvasElement) => { return `${element.label} (${element.carouselSlides?.length || 0})`; } - if (element.type === 'tooltip' && element.tooltipTitle) - return element.tooltipTitle; - if (element.type === 'description' && element.descriptionTitle) - return element.descriptionTitle; - if ( - (element.type === 'navigation_next' || - element.type === 'navigation_prev') && - element.navLabel - ) - return element.navLabel; + if (element.type === 'tooltip') return element.tooltipTitle ?? ''; + if (element.type === 'description') return element.descriptionTitle ?? ''; + if (element.type === 'navigation_next' || element.type === 'navigation_prev') + return element.navLabel ?? ''; if ( (element.type === 'video_player' || element.type === 'audio_player') && element.mediaUrl @@ -547,6 +541,11 @@ const ConstructorPage = () => { if (Array.isArray(value)) return value[0] || ''; return String(value || ''); }, [router.query.pageId]); + const elementIdFromRoute = useMemo(() => { + const value = router.query.elementId; + if (Array.isArray(value)) return value[0] || ''; + return String(value || ''); + }, [router.query.elementId]); const [pages, setPages] = useState([]); const [assets, setAssets] = useState([]); @@ -577,6 +576,7 @@ const ConstructorPage = () => { const [resolvedDurationBySource, setResolvedDurationBySource] = useState< Record >({}); + const [canvasElapsedSec, setCanvasElapsedSec] = useState(0); const [menuPosition, setMenuPosition] = useState({ x: 0, y: 110 }); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -596,22 +596,15 @@ const ConstructorPage = () => { const reverseAnimationFrame = useRef(null); const didSetInitialCanvasFocus = useRef(false); const durationProbeInFlightRef = useRef>(new Set()); + const pagePlaybackStartedAtRef = useRef(Date.now()); const activePage = useMemo( () => pages.find((item) => item.id === activePageId) || null, [activePageId, pages], ); - const activePageIndex = useMemo( - () => pages.findIndex((item) => item.id === activePageId), - [activePageId, pages], - ); const allowedNavigationTypes = useMemo(() => { - if (pages.length <= 1) return ['navigation_next']; - if (activePageIndex < 0) return ['navigation_next', 'navigation_prev']; - if (activePageIndex <= 0) return ['navigation_next']; - if (activePageIndex >= pages.length - 1) return ['navigation_prev']; return ['navigation_next', 'navigation_prev']; - }, [activePageIndex, pages.length]); + }, []); const pageNameById = useMemo(() => { const acc: Record = {}; pages.forEach((page, index) => { @@ -970,6 +963,25 @@ const ConstructorPage = () => { }); }, [isAuthReady, isLoading, router.isReady]); + useEffect(() => { + if (typeof window === 'undefined') return; + if (isLoading || !activePageId) { + setCanvasElapsedSec(0); + return; + } + + pagePlaybackStartedAtRef.current = Date.now(); + setCanvasElapsedSec(0); + + const intervalId = window.setInterval(() => { + const elapsed = + (Date.now() - pagePlaybackStartedAtRef.current) / 1000; + setCanvasElapsedSec(elapsed > 0 ? elapsed : 0); + }, 100); + + return () => window.clearInterval(intervalId); + }, [activePageId, isLoading]); + useEffect(() => { if (!activePage) { setElements([]); @@ -1076,6 +1088,12 @@ const ConstructorPage = () => { setSelectedMenuItem('none'); setSelectedElementId((current) => { if (!normalizedElements.length) return ''; + if ( + elementIdFromRoute && + normalizedElements.some((element) => element.id === elementIdFromRoute) + ) { + return elementIdFromRoute; + } if (normalizedElements.some((element) => element.id === current)) return current; return ''; @@ -1083,7 +1101,7 @@ const ConstructorPage = () => { setBackgroundImageUrl(activePage.background_image_url || ''); setBackgroundVideoUrl(activePage.background_video_url || ''); setBackgroundAudioUrl(activePage.background_audio_url || ''); - }, [activePage]); + }, [activePage, elementIdFromRoute]); useEffect(() => { if (allowedNavigationTypes.length !== 1) return; @@ -1573,24 +1591,24 @@ const ConstructorPage = () => { element.type === 'navigation_next' || element.type === 'navigation_prev' ) { + if (element.iconUrl) { + return ( + // eslint-disable-next-line @next/next/no-img-element + Navigation icon + ); + } + const targetPageName = element.targetPageId ? pageNameById[element.targetPageId] : ''; return (
- {element.iconUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - Navigation icon - ) : null} - - {element.navLabel || - (element.type === 'navigation_next' ? 'Forward' : 'Back')} - + {element.navLabel}
{targetPageName ? ( @@ -1602,18 +1620,21 @@ const ConstructorPage = () => { } if (element.type === 'tooltip') { + if (element.iconUrl) { + return ( + // eslint-disable-next-line @next/next/no-img-element + Tooltip icon + ); + } + return (
- {element.iconUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - Tooltip icon - ) : null}

- {element.tooltipTitle || 'Tooltip title'} + {element.tooltipTitle}

{element.tooltipText || 'Tooltip text'} @@ -1623,18 +1644,21 @@ const ConstructorPage = () => { } if (element.type === 'description') { + if (element.iconUrl) { + return ( + // eslint-disable-next-line @next/next/no-img-element + Description icon + ); + } + return (

- {element.iconUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - Description icon - ) : null}

- {element.descriptionTitle || 'Description title'} + {element.descriptionTitle}

{element.descriptionText || 'Description text'} @@ -1770,6 +1794,20 @@ const ConstructorPage = () => { return getElementButtonTitle(element); }; + const isElementVisibleOnCanvas = (element: CanvasElement) => { + const delay = Number(element.appearDelaySec || 0); + if (canvasElapsedSec < delay) return false; + + if (element.appearDurationSec === null || element.appearDurationSec === undefined) { + return true; + } + + const duration = Number(element.appearDurationSec); + if (!Number.isFinite(duration) || duration <= 0) return true; + + return canvasElapsedSec <= delay + duration; + }; + const canvasBackgroundStyle: React.CSSProperties = {}; const backgroundImageSrc = resolveAssetPlaybackUrl(backgroundImageUrl); const backgroundVideoSrc = resolveAssetPlaybackUrl(backgroundVideoUrl); @@ -1999,27 +2037,43 @@ const ConstructorPage = () => { />

) : ( - elements.map((element) => ( - - )) + elements.map((element) => { + const shouldRender = + selectedElementId === element.id || + isElementVisibleOnCanvas(element); + if (!shouldRender) return null; + + const hasIconDrivenSize = + Boolean(element.iconUrl) && + (element.type === 'navigation_next' || + element.type === 'navigation_prev' || + element.type === 'tooltip' || + element.type === 'description'); + + return ( + + ); + }) )}
diff --git a/frontend/src/pages/page_elements/page_elements-list.tsx b/frontend/src/pages/page_elements/page_elements-list.tsx index 5236e50..5516acd 100644 --- a/frontend/src/pages/page_elements/page_elements-list.tsx +++ b/frontend/src/pages/page_elements/page_elements-list.tsx @@ -1,128 +1,59 @@ -import { mdiChartTimelineVariant, mdiClose, mdiViewDashboard } from '@mdi/js'; +import { mdiChartTimelineVariant, mdiViewDashboard } from '@mdi/js'; import Head from 'next/head'; import { useRouter } from 'next/router'; -import React, { - ReactElement, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; import axios from 'axios'; -import { toast } from 'react-toastify'; import BaseButton from '../../components/BaseButton'; import CardBox from '../../components/CardBox'; -import CardBoxModal from '../../components/CardBoxModal'; import LayoutAuthenticated from '../../layouts/Authenticated'; import SectionMain from '../../components/SectionMain'; import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; import { getPageTitle } from '../../config'; -import { hasPermission } from '../../helpers/userPermissions'; -import { useAppSelector } from '../../stores/hooks'; -type ElementSettings = { - color: string; - backgroundColor: string; - border: string; - icon: string; +type TourPage = { + id: string; + name?: string; + sort_order?: number; + ui_schema_json?: string; +}; + +type ConstructorElement = { + id?: string; + type?: string; + label?: string; + navLabel?: string; + tooltipTitle?: string; + descriptionTitle?: string; +}; + +type ConstructorSchema = { + elements?: ConstructorElement[]; }; type ProjectElementItem = { id: string; + pageId: string; + pageName: string; elementType: string; name: string; - settings: ElementSettings; }; -type PlatformElementOption = { - elementType: string; - name: string; - defaults: ElementSettings; -}; - -type PageElementRecord = { - id: string; - element_type?: string; - name?: string; - style_json?: string; - content_json?: string; -}; - -const FALLBACK_ELEMENT_TYPES = [ - 'nav_button', - 'spot', - 'description', - 'tooltip', - 'gallery', - 'carousel', - 'logo', - 'video_player', - 'popup', -]; - -const FALLBACK_DEFAULTS_BY_TYPE: Record = { - nav_button: { - color: '#111827', - backgroundColor: '#ffffff', - border: '1px solid #d1d5db', - icon: 'mdiArrowRight', - }, - spot: { - color: '#111827', - backgroundColor: '#fde68a', - border: '1px solid #f59e0b', - icon: 'mdiMapMarker', - }, - description: { - color: '#111827', - backgroundColor: '#f3f4f6', - border: '1px solid #d1d5db', - icon: 'mdiTextBox', - }, - tooltip: { - color: '#ffffff', - backgroundColor: '#1f2937', - border: '1px solid #1f2937', - icon: 'mdiTooltipText', - }, - gallery: { - color: '#111827', - backgroundColor: '#ffffff', - border: '1px solid #d1d5db', - icon: 'mdiImageMultiple', - }, - carousel: { - color: '#111827', - backgroundColor: '#ffffff', - border: '1px solid #d1d5db', - icon: 'mdiViewCarousel', - }, - logo: { - color: '#111827', - backgroundColor: '#ffffff', - border: '1px solid #d1d5db', - icon: 'mdiImage', - }, - video_player: { - color: '#ffffff', - backgroundColor: '#111827', - border: '1px solid #111827', - icon: 'mdiPlayCircle', - }, - popup: { - color: '#111827', - backgroundColor: '#ffffff', - border: '1px solid #d1d5db', - icon: 'mdiOpenInNew', - }, -}; - -const parseJsonObject = (value?: string): Record => { +const parseJsonObject = (value?: unknown): Record => { if (!value) return {}; + try { - const parsed = JSON.parse(value); - return typeof parsed === 'object' && parsed !== null ? parsed : {}; - } catch { + if (typeof value === 'string') { + const parsed = JSON.parse(value); + return typeof parsed === 'object' && parsed !== null ? parsed : {}; + } + + if (typeof value === 'object') { + return value as Record; + } + + return {}; + } catch (error) { + console.error('Failed to parse page schema JSON on pages elements list:', error); return {}; } }; @@ -133,23 +64,21 @@ const toElementLabel = (value: string) => .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); -const createLocalId = () => { - if (typeof window !== 'undefined' && window.crypto?.randomUUID) { - return window.crypto.randomUUID(); +const getElementName = (element: ConstructorElement) => { + if (element.type === 'navigation_next' || element.type === 'navigation_prev') { + return String(element.navLabel || '').trim(); } - return `pe_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; -}; + if (element.type === 'tooltip') { + return String(element.tooltipTitle || '').trim(); + } -const normalizeSettings = ( - settings: Partial | undefined, - fallback: ElementSettings, -): ElementSettings => ({ - color: settings?.color || fallback.color, - backgroundColor: settings?.backgroundColor || fallback.backgroundColor, - border: settings?.border || fallback.border, - icon: settings?.icon || fallback.icon, -}); + if (element.type === 'description') { + return String(element.descriptionTitle || '').trim(); + } + + return String(element.label || '').trim(); +}; const PagesElementsListPage = () => { const router = useRouter(); @@ -159,75 +88,16 @@ const PagesElementsListPage = () => { return String(value || ''); }, [router.query.projectId]); - const { currentUser } = useAppSelector((state) => state.auth); - const hasCreatePermission = hasPermission( - currentUser, - 'CREATE_PAGE_ELEMENTS', - ); - const hasUpdatePermission = hasPermission( - currentUser, - 'UPDATE_PAGE_ELEMENTS', - ); - const hasDeletePermission = hasPermission( - currentUser, - 'DELETE_PAGE_ELEMENTS', - ); - const [projectName, setProjectName] = useState(''); const [isLoadingProject, setIsLoadingProject] = useState(false); const [isLoadingElements, setIsLoadingElements] = useState(false); const [errorMessage, setErrorMessage] = useState(''); - - const [themeConfig, setThemeConfig] = useState>({}); - const [selectedElements, setSelectedElements] = useState< - ProjectElementItem[] - >([]); - const [platformElements, setPlatformElements] = useState< - PlatformElementOption[] - >([]); - - const [isAddDropdownOpen, setIsAddDropdownOpen] = useState(false); - const [newElementType, setNewElementType] = useState(''); - - const [isSettingsModalActive, setIsSettingsModalActive] = useState(false); - const [activeElement, setActiveElement] = useState( - null, - ); - const [elementName, setElementName] = useState(''); - const [color, setColor] = useState(''); - const [backgroundColor, setBackgroundColor] = useState(''); - const [border, setBorder] = useState(''); - const [icon, setIcon] = useState(''); - - const [isSaving, setIsSaving] = useState(false); - - // Check if projects exist and redirect if not - useEffect(() => { - const checkProjects = async () => { - try { - const response = await axios.get('/projects/autocomplete?limit=1'); - const projects = Array.isArray(response?.data) ? response.data : []; - if (projects.length === 0) { - toast('Please create a project first', { - type: 'info', - position: 'bottom-center', - }); - router.replace('/projects/projects-new'); - } - } catch (error) { - console.error('Failed to check projects:', error); - } - }; - checkProjects(); - }, [router]); + const [projectElements, setProjectElements] = useState([]); const loadData = useCallback(async () => { if (!routeProjectId) { setProjectName(''); - setThemeConfig({}); - setSelectedElements([]); - setPlatformElements([]); - setNewElementType(''); + setProjectElements([]); return; } @@ -236,92 +106,51 @@ const PagesElementsListPage = () => { setErrorMessage(''); try { - const [projectResponse, pageElementsResponse] = await Promise.all([ + const [projectResponse, pagesResponse] = await Promise.all([ axios.get(`/projects/${routeProjectId}`), - axios.get('/page_elements?limit=1000&page=0&sort=desc&field=updatedAt'), + axios.get( + `/tour_pages?limit=500&page=0&sort=asc&field=sort_order&project=${routeProjectId}`, + ), ]); const project = projectResponse?.data || {}; setProjectName(project?.name || ''); - const parsedThemeConfig = parseJsonObject(project?.theme_config_json); - const rawProjectElements = Array.isArray(parsedThemeConfig?.pageElements) - ? parsedThemeConfig.pageElements + const pageRows: TourPage[] = Array.isArray(pagesResponse?.data?.rows) + ? pagesResponse.data.rows : []; - const normalizedProjectElements: ProjectElementItem[] = rawProjectElements - .filter((item: any) => item && item.elementType) - .map((item: any) => { - const elementType = String(item.elementType); - const defaults = FALLBACK_DEFAULTS_BY_TYPE[elementType] || { - color: '#111827', - backgroundColor: '#ffffff', - border: '1px solid #d1d5db', - icon: '', - }; + const items: ProjectElementItem[] = []; - return { - id: String(item.id || createLocalId()), + pageRows.forEach((page, pageIndex) => { + const schema = parseJsonObject(page.ui_schema_json) as ConstructorSchema; + const elements = Array.isArray(schema.elements) ? schema.elements : []; + + elements.forEach((element) => { + const elementType = String(element?.type || '').trim(); + const elementId = String(element?.id || '').trim(); + if (!elementType || !elementId) return; + + items.push({ + id: elementId, + pageId: String(page.id), + pageName: String(page.name || `Page ${pageIndex + 1}`), elementType, - name: String(item.name || toElementLabel(elementType)), - settings: normalizeSettings(item.settings || {}, defaults), - }; - }); - - const rows = Array.isArray(pageElementsResponse?.data?.rows) - ? pageElementsResponse.data.rows - : []; - const optionsMap = new Map(); - - FALLBACK_ELEMENT_TYPES.forEach((type) => { - optionsMap.set(type, { - elementType: type, - name: toElementLabel(type), - defaults: FALLBACK_DEFAULTS_BY_TYPE[type], + name: getElementName(element), + }); }); }); - rows.forEach((row: PageElementRecord) => { - const elementType = String(row.element_type || '').trim(); - if (!elementType || optionsMap.has(elementType)) return; - - const rowStyle = parseJsonObject(row.style_json); - const rowContent = parseJsonObject(row.content_json); - const fallback = FALLBACK_DEFAULTS_BY_TYPE[elementType] || { - color: '#111827', - backgroundColor: '#ffffff', - border: '1px solid #d1d5db', - icon: '', - }; - - optionsMap.set(elementType, { - elementType, - name: row.name || toElementLabel(elementType), - defaults: { - color: String(rowStyle.color || fallback.color), - backgroundColor: String( - rowStyle.backgroundColor || fallback.backgroundColor, - ), - border: String(rowStyle.border || fallback.border), - icon: String(rowContent.icon || fallback.icon), - }, - }); - }); - - const normalizedOptions = Array.from(optionsMap.values()); - setThemeConfig(parsedThemeConfig); - setSelectedElements(normalizedProjectElements); - setPlatformElements(normalizedOptions); + setProjectElements(items); } catch (error: any) { const message = error?.response?.data?.message || error?.message || 'Failed to load pages elements.'; setErrorMessage(message); - console.error('Failed to load pages elements list:', error); - setThemeConfig({}); - setSelectedElements([]); - setPlatformElements([]); + console.error('Failed to load project elements from constructor pages:', error); + setProjectName(''); + setProjectElements([]); } finally { setIsLoadingProject(false); setIsLoadingElements(false); @@ -332,190 +161,23 @@ const PagesElementsListPage = () => { loadData(); }, [loadData]); - const selectedTypes = useMemo( - () => new Set(selectedElements.map((item) => item.elementType)), - [selectedElements], - ); - - const availableToAdd = useMemo( - () => - platformElements.filter((item) => !selectedTypes.has(item.elementType)), - [platformElements, selectedTypes], - ); - - useEffect(() => { - if (!availableToAdd.length) { - setNewElementType(''); - return; - } - - if (!availableToAdd.some((item) => item.elementType === newElementType)) { - setNewElementType(availableToAdd[0].elementType); - } - }, [availableToAdd, newElementType]); - - const saveProjectElements = useCallback( - async (elements: ProjectElementItem[]) => { - if (!routeProjectId) return; - - const nextThemeConfig = { - ...(themeConfig || {}), - pageElements: elements.map((item) => ({ - id: item.id, - elementType: item.elementType, - name: item.name, - settings: item.settings, - })), - }; - - await axios.put(`/projects/${routeProjectId}`, { - id: routeProjectId, - data: { - theme_config_json: JSON.stringify(nextThemeConfig), - }, - }); - - setThemeConfig(nextThemeConfig); - }, - [routeProjectId, themeConfig], - ); - - const handleAddElement = async () => { - if (!hasCreatePermission) return; - - if (!routeProjectId) { - setErrorMessage('Please select a project first.'); - return; - } - - if (!newElementType || selectedTypes.has(newElementType)) { - return; - } - - const selectedOption = platformElements.find( - (item) => item.elementType === newElementType, - ); - if (!selectedOption) { - setErrorMessage('Selected element type is not available.'); - return; - } - - setIsSaving(true); - setErrorMessage(''); - - try { - const updatedElements = [ - ...selectedElements, - { - id: createLocalId(), - elementType: selectedOption.elementType, - name: selectedOption.name, - settings: { ...selectedOption.defaults }, - }, - ]; - - await saveProjectElements(updatedElements); - setSelectedElements(updatedElements); - setIsAddDropdownOpen(false); - } catch (error: any) { - const message = - error?.response?.data?.message || - error?.message || - 'Failed to add element.'; - setErrorMessage(message); - console.error('Failed to add project element:', error); - } finally { - setIsSaving(false); - } - }; - - const handleRemoveElement = async (item: ProjectElementItem) => { - if (!hasDeletePermission) return; - - const confirmed = window.confirm( - `Remove ${item.name || toElementLabel(item.elementType)} from this project?`, - ); - if (!confirmed) return; - - setIsSaving(true); - setErrorMessage(''); - - try { - const updatedElements = selectedElements.filter( - (existing) => existing.id !== item.id, - ); - await saveProjectElements(updatedElements); - setSelectedElements(updatedElements); - } catch (error: any) { - const message = - error?.response?.data?.message || - error?.message || - 'Failed to remove element.'; - setErrorMessage(message); - console.error('Failed to remove project element:', error); - } finally { - setIsSaving(false); - } - }; - - const openSettings = (item: ProjectElementItem) => { - if (!hasUpdatePermission) return; - - setActiveElement(item); - setElementName(item.name || toElementLabel(item.elementType)); - setColor(item.settings.color); - setBackgroundColor(item.settings.backgroundColor); - setBorder(item.settings.border); - setIcon(item.settings.icon); - setIsSettingsModalActive(true); - }; - - const closeSettings = () => { - setIsSettingsModalActive(false); - setActiveElement(null); - }; - - const saveSettings = async () => { - if (!activeElement || !hasUpdatePermission) return; - - setIsSaving(true); - setErrorMessage(''); - - try { - const updatedElements = selectedElements.map((item) => { - if (item.id !== activeElement.id) return item; - - return { - ...item, - name: elementName, - settings: { - color, - backgroundColor, - border, - icon, - }, - }; - }); - - await saveProjectElements(updatedElements); - setSelectedElements(updatedElements); - closeSettings(); - } catch (error: any) { - const message = - error?.response?.data?.message || - error?.message || - 'Failed to save element settings.'; - setErrorMessage(message); - console.error('Failed to update project element settings:', error); - } finally { - setIsSaving(false); - } - }; - const constructorHref = routeProjectId ? `/constructor?projectId=${routeProjectId}` : '/constructor'; + const openElementInEditor = (item: ProjectElementItem) => { + if (!routeProjectId) return; + + router.push({ + pathname: '/page_elements/page_elements-project-edit', + query: { + projectId: routeProjectId, + pageId: item.pageId, + elementId: item.id, + }, + }); + }; + return ( <> @@ -540,51 +202,6 @@ const PagesElementsListPage = () => { className='mb-6' cardBoxClassName='flex flex-wrap items-start gap-3' > - {hasCreatePermission && ( -
- setIsAddDropdownOpen((prev) => !prev)} - disabled={isSaving} - /> - {isAddDropdownOpen && ( -
- - -
- )} -
- )} - {

- Selected pages elements + Project elements from constructor pages

{isLoadingElements ? (

Loading elements...

- ) : selectedElements.length === 0 ? ( -

No elements selected yet.

+ ) : projectElements.length === 0 ? ( +

No constructor elements found yet.

) : (
- {selectedElements.map((item) => ( -
( + - - {hasDeletePermission ? ( - handleRemoveElement(item)} - disabled={isSaving} - /> - ) : null} -
+

{item.name}

+

+ {item.pageName} • {toElementLabel(item.elementType)} +

+ ))}
)}
- - -
- - setElementName(event.target.value)} - /> -
-
- - setColor(event.target.value)} - placeholder='#111827' - /> -
-
- - setBackgroundColor(event.target.value)} - placeholder='#ffffff' - /> -
-
- - setBorder(event.target.value)} - placeholder='1px solid #d1d5db' - /> -
-
- - setIcon(event.target.value)} - placeholder='mdiStar' - /> -
-
); }; diff --git a/frontend/src/pages/page_elements/page_elements-project-edit.tsx b/frontend/src/pages/page_elements/page_elements-project-edit.tsx new file mode 100644 index 0000000..3c0ebff --- /dev/null +++ b/frontend/src/pages/page_elements/page_elements-project-edit.tsx @@ -0,0 +1,3 @@ +import ConstructorPage from '../constructor'; + +export default ConstructorPage;