From 80878679cd6a3ede724d9478cb0908e0010451c8 Mon Sep 17 00:00:00 2001 From: KMKoushik Date: Fri, 19 Apr 2024 20:58:30 +1000 Subject: [PATCH] Add attachment support --- apps/web/package.json | 5 +- apps/web/public/Logo-1.png | Bin 0 -> 8303 bytes apps/web/public/Logo-2.png | Bin 0 -> 12907 bytes .../app/(dashboard)/emails/email-details.tsx | 103 ++++++ .../src/app/(dashboard)/emails/email-list.tsx | 183 +++++++--- .../(dashboard)/emails/email-status-badge.tsx | 83 +++++ apps/web/src/app/(dashboard)/emails/page.tsx | 9 +- apps/web/src/app/(dashboard)/layout.tsx | 25 +- apps/web/src/app/api/ses_callback/route.ts | 8 +- apps/web/src/hooks/useUrlState.ts | 36 ++ apps/web/src/lib/constants/ses-errors.ts | 46 +++ apps/web/src/server/api/routers/email.ts | 63 +++- apps/web/src/server/aws/ses.ts | 58 ++++ .../src/server/public-api/api/send_email.ts | 8 + apps/web/src/server/service/email-service.ts | 43 ++- apps/web/src/types/aws-types.ts | 24 +- apps/web/src/types/index.ts | 4 + packages/ui/package.json | 2 + packages/ui/src/select.tsx | 160 +++++++++ packages/ui/src/separator.tsx | 31 ++ packages/ui/styles/globals.css | 2 +- pnpm-lock.yaml | 326 +++++++++++++----- 22 files changed, 1029 insertions(+), 190 deletions(-) create mode 100644 apps/web/public/Logo-1.png create mode 100644 apps/web/public/Logo-2.png create mode 100644 apps/web/src/app/(dashboard)/emails/email-details.tsx create mode 100644 apps/web/src/app/(dashboard)/emails/email-status-badge.tsx create mode 100644 apps/web/src/hooks/useUrlState.ts create mode 100644 apps/web/src/lib/constants/ses-errors.ts create mode 100644 packages/ui/src/select.tsx create mode 100644 packages/ui/src/separator.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 22edc17..039acd2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,10 +34,12 @@ "hono": "^4.2.2", "install": "^0.13.0", "lucide-react": "^0.359.0", - "next": "^14.1.3", + "mime-types": "^2.1.35", + "next": "^14.2.1", "next-auth": "^4.24.6", "pnpm": "^8.15.5", "prisma": "^5.11.0", + "query-string": "^9.0.0", "react": "18.2.0", "react-dom": "18.2.0", "recharts": "^2.12.5", @@ -48,6 +50,7 @@ }, "devDependencies": { "@types/eslint": "^8.56.2", + "@types/mime-types": "^2.1.4", "@types/node": "^20.11.20", "@types/react": "^18.2.57", "@types/react-dom": "^18.2.19", diff --git a/apps/web/public/Logo-1.png b/apps/web/public/Logo-1.png new file mode 100644 index 0000000000000000000000000000000000000000..a238f961a2107ef07bc7f02271c15e2df4cfb2b7 GIT binary patch literal 8303 zcmd^k`9G9v`2RhNY*|84%5bDeqJt=921zAL){+b@C&pUDDe(*~ib^MmQ;AacC1uSr zB3dnlD9c!~$Jl3op3klG{TseNd_K?XB_**=0stU&VEkCfmbF4)1!927LKABSIvXUuHO04R!= z6nKdMph`Tj*Uaf}+*B{s#i_GYv|mJXVCvklpxFUndbPOUQ}S*x#V_gyzDQ)d&UK%t z43TqgLGK;$rtkgV4Af{z^cjBG_EJYOEBd%|_>?f>q#h+2YyRf}qh!P_IFkrp_y756 z4VCn{-B%M)8FBIA>yj5QW?BUG`GQin>Z?P7P^W5PO?i1aJ0207)Ww0a9rIvjy0@~! zF4c68sfkJWU3A;h(5{bOUb->YM%gST`5>sh^?57UJAiR!hhDX$lF0|8-kbG5R|UXG zKLG^&{Wr&05$!eL#$vmQ)MqY5LD4v&rY_CRo(Qt;av!1U$DoN`j92 zSgyOEtj+J2uC6Y*JdD+M77q%FlQVw8r)gyz(iL0#51{+nXn?n+2yfDjdC0OqD>4xD zOWZ00=Wu-=u5ywLthj9CbwvgD4;)Z8TnA9jP<%;4LxT~wzQlm~?$f7F>_?)2{$-s% zZmW*Y9_x8sbl}nm1rPSo{6y2-gbZt)yJ0xAdm8|k@{2amX^YG)n>TNEOd$jCoVFEw z=6qz9t851S+P8@GzjLTGD;Cm43$XEnLy~al)vH%i?GBQXlKQDu0Qh%0N%nWQdvuEf zj=4HcweV%9gY_`^*u#2p@Yzxw*FQhn;+tLlAdE zF*O_j7xx1|SCIoCE$08+&?;#enX*Bh^X(}_@F<);-8+$Km#TuGf(!g#zkZc9H8m+_ zr$eh;47BIi+1f@PIdWuwW@aX%mw_-|HIT(%@PF$r3oM(bVMn(KiaHocOqbwU%uo^Q zZ9m%#KNPB4Pm^o<~n5j0(f@D9=wH%R&gXCyECqwYV$=DOX`_Op@UkHbJs3oCsT ztJ3{_Zirhx1KWrN8JZ@=p)J@bRjY(T2q4LDs76chEV72p z7fp@dL7~PWvH)m)%R(b<@_4But1&2);gQ9diDE3F{UoHjhKgqlM9qcGowD>;eCNc_ zm56W_ggLZ`1E4T;7me&aj0VO}_f*JVMQzX@B|OAQfNg?`(u$g}f%=$~o=?c`9%kWA z^3NnD~Yy*BJX}NlnrKH2FmsPZMium88zhiImG$cdCRkPe zVQMCaY72m8K;th~X9{WVlVyZ1OK!J^yL*pK(l1#})D<#za>ZFb|NQonz|*IPE%SIo z2O<}rn5tK~2nwMJdiotZuzGh+Sfg;~+4s7P@CTAS)U$(1n$WL9=jP_p-zGvWG6J9x zY}U_(*Ax}a{t>#e5gJbum4Pf5_Ne?W0JTmldHf3vDt3L{4`wnMx1BF3%w4{GnKXC% zjqdndHz34fM0m_P!yaZ!?$UusUV|Eg)`7LnXfmxeQF$1k-h1;IqJR+e_*iTT!s61CZ%nM_MqTRE zD0S2)qJvRfyzoiMBH|hZte&@WkO@3s^uYwH^ATyTyQk;UcFtSR4x~wwc~}9v2KVoA z^`A$mO`{AO1go1-V~8o9EhJ;63(^sJl^F&GPEj1REozpDyJFUAFzlVSdo6o>agI)(jp)J7CtEMiD|t8wibd-!f+Ud3v{6` z1G94=3XN>ysxcB16BQlZpmPvgC>#EB76b8WS7WsHFULhN^ht479*=K0 zI?p%SvBP5}wliS+Z%vxv>NPo~-$^ZLFjNb9)V}I)_V`SCsIt<8{-N1G9F)m&LMXhJ zxp@5aPc_E1Yw+dO3fLML`fjT1Z((0=^7!Yu>~Pad&^lfnBsjGSfRR(J%=p!z7BLfz zq2JXQTKT;CqCw_1JgB|dl+K{)&)uPB?c+A)AlinL#-h>ItLQZmclDtiRIm3HJX zvm$tOV5KPgrb3Kht}?Tt6LZEro?Ets2o!vsJ&QH!j66uUmf>E70hRod{YfEALjf~~ z^jsz@VyIa4F8CLVTKZ7ehM>M^KL8=N4^sm#IU{SM5`Kc`zI*@`N5%vN}bpYI-+CUU)!lovQYt*&S(+~q+nO7Oh`yhG^*j-fWlE~#J zrat?S&<4gOJZSa3j7Dw~bZ1{J*!5HKFTM6r?O4P}3{Z5XB#=+gM|^I}(!xuNNUx6& zj1GcI3z(zQA&Un3caM;an~~Dg;D36ne@Tu&abl4&xO23mih&HghFg~L3%9@uVpr}I z6k&)~i4C8o>M2bmV5a&NeXg0Dumk`?Y;iDEbZ(fFe6@Z;e=8Qqai3*v!hx|XZV0lO zk^g+D@?0d6JPaM1Gst*2wh;$3-L&9&zR(mG3Sj6kBk;ifndY5}{gwl%BSZemhRtdW zy-$a0(lt>bj9*KwYz8%WN`T6C^4BOTQk!V^Semg20;Qc?-Nj_+Eia$^Z#fLrbTjqSeV&k5lt}q}CSr0*+SVFOn@T~0=fVu-}a;o)JcPJebw z@UmQFJkWFee~G~1dn5GA$e#8y$%A&Xfc~#$HG^t2-BEKPer|*1>=_D72N~+neE+43 za3Q}lVrpPuzy)TggE~iHWT)Mp{{Y)b_M@*d^nu6AZ&cuL%8 z+hoDg)i;_cW|6Y4@!l@hxLixi;jlKAUxVcS{L_E%T_)c}V`@+1GQT04 z)BsR?4fFiNpLeL<-OF5#Y|;SWIE&ReCgH*yE%T!dz`qnn6etf+v^_PssNoE<$N z-B@z=w?0N1fYQ>=<1^W5Gj@=NoI3f9uUF~W#gfa^0L5+r#-+SaXf>oVC+Yv&-J_-O zq;lFrj88K~lAzZ&?fI$rWA|_0$it}@kQ92|w?2N3CVNLBnm!%W@2&!$x)b|8p&CPr}LF0=YTERx4lR8+){JIqo&JR`=sw3=o8yTVr z3K%V`g^@652kWI){o`VIhz!NI;SV1^JW1_{b4L2+FfFW_`RqP(iu9DDzuFD7245{% zvB~tO;)#w_#3R>=>tQ~7hV}~@C-J4aY>eRCc0s`1LF&vh4<1U~0B;EX-K)w!s4-;I zFU>R6T!b2?P4=^ACSb8i_5O0Z_7>zJT9)1V1xLYvTqvMxH~CogpWhZ8zey)rM0#;; zsDi(AL+a5N1mNz|XaeP9nMqHPZQY2HF#LZBSOpw?ZMf~mAzv$1X>NXAUe(w@4*8KH zcHr2CAY}+lbsLc=nycDHA>YTGTeA5Y_pKS@F6rDej=mzC)h=7snZ?~$e`$rphTmxJ zF(!bXk5VmSRn1ha*CrNu@;kHst8D@(=>^|cm4DgDkj+KoN#w>t{>DVR*)!8nnOm>@ ztkNDZmp5RZ9fcf=U^wWw+5|s~Zn{c-tI52rfTnTU)`iK{D)c^`NsXUt3e>QjSGJtZ zbS?(;Z`m+a5iThp$#U>Dz9FZwTJ#!HU0xDEvAO6z?(L_XHaDnd#f{vERMUuykK%qN zxO1!@j~K-c+U1Ia+Rswc3#pGDg$##aaNjq182qvMC@c;gzt8gvpK=_DIFS7B?cQ+` ze4!GIvh&MV&)XYjlruPv3UKaq)iCq9i4bz2!aGBXXU>Uk^7%>*Nq6g3K^Dhx9bECi zT@bdbZ$5qy^NfgyAe+H_I`)G>8D!iNyf*<@0{!R_Yn}U4Ne@kQCMg3?$xXeN=&Er{ zJo$BL%v4)j8&fW*;2&^D@)yIQawh@kr}eHF_TB=Dg-5^pUcpx608U>!Q5?yE+FXq7}0wtGG z(cBD6vE--gL8)V$&ep9*gcddlxPHu`GGD`!22&lwM(4gFjf^ zJBBpw19V-(HDDV0=AN6WT$dRo`F1G@tel*j48T!r((MR-;dKFgH4RN?9P?N#d8Lm5 z_~}f^wfo+#zl>ARPSA&*z`2?vWGHQw=mw8DOZ!ZBsX)lPlVo+ z!BNBA+PPW^}s-V z8dKB*-b@R6|MK#W42MqgW$dH%6zg|mJ~2@ZCyepsd1hj8FBzRX~}_~4-YYNJSdre zr}K~GxNIUlXY?B{yexoFdn>WkybX)dKBNJz$XyInktQbOUc>w{@58}d8y)a7zl`aC zv$eH-ywTI|!l>Wo)%^<#3!OxF8VxpMWIZzf+-+OC@nD77t0cPh^7_8+!!#@yS4Zpn z+GAT=0(%c(bCo6SkyahPvOzbpMd)|JaIxNm->susjTx8C!f@4 zlmP6%`qNJ-f^7;r)a%EQLjkyz>s3^0v`*hW^8U$il#V4Y>qP=K8iRd<=@gpzl^<@exo);p z|61IF%QiI7!E8Nx`9qTd*s80`dVWrt@R+RL-$KShRPT6bjom;9XF06X_)UMd0eh(~ zEt;ZgppI#<&YhYpX>@hT{O~ln=kp-~1zPd>hW4XE&TP61sJ1VuhHNyqgv}EV`HkRI zLvaVKe_gO#csmq5{<?Q2cQ60RX zR1Y6ogX_8HMV6+%!SS3bo^HS-`Cox(*`G;3_TZsuH(bbJKjCNx7O>?Isy`{L*ZCYG z+xj9}MczHExT;E!Vzr`8;)-cHQzQGoX=AloH z29pW)Nz}B#t+j4j9Cr`(zgfNnM&G|-fyBkdePQdKe^k;(Hpp9T0thP#-N}$~dC7;4yT&5trerb-5}{7VzoW0@Yqy4Eao&}X zGf}UPZq)=?R50J7ctJ#He4g@)*aL6r+1k+tTDJUmQH%lwtc$mt_wLfOX%S$oZqJAs zAGVfdu7C&jl@qQDKG?co1J8YKy?C)%b9>H}TCZcPybrDXSvttHUwymMLC~@p)k5`@ z^JTyj^;fuwW~)~cxE`lzcA)c61WfEsVJLNz=cZUO@Z^D1(7o3~f`P4%|9JmwkaGBOfJ{A^i-Qw>?%O=&uO-k7HQMLCdJ)7y+O-i?iwv>(n8z7eecj#nD% z@2K?}t)`X{d9WnD@CnOw7gYXWk&JU{!lHI=xhSgYB>L>B)sJl7!|Gj9V=qkec-mjs zShJaHGGmZH$+|XPb#RaD8NvzASjEN86v1JyH1qZIX~#zp4w${g*C7euGp zvVpySX3hB6oiuA7$$M(exy$rFIcUJ}{rjzMv3(|Yj%>Qyk;o7}5QlOfAMElAfw?QeS!>8iw=JGxev!{)1RXK}$AORPRd z=8FrjkB0filJ7k?THf1vF@^9Zu&$Pw^IdK`jpt=Zt)n5t2GYm2Tp)YVSZ5}qxxRLM zeB6WigJ_{0VTrA);8*%Bh$q4dfymP%82X4(X^@HxX}fjL4xcBv}GZ=4CM%HhfL%P)7@V5xMSUBN*E zFS!eihh5C8$TdFuuMa+ET1HWC)jdXf-|(>ysnpz9q1)55vE&d77J(v*ADJ${XksKL zPH^5%I%4B86P(Vm2onQ|WK1xBVB2o8hUYVP0cm&;`xBR<3fi4yWG^P>={?!@c!HfS z%BR4_jYW=?_&Y~pr(BTJo(`5KF6Cu1$GobwLDB=A(KKUBX``*j@wH;J(Pc1a^p3Wq zBDSp-+xs^oGMZ?smmBB|vY^+T{Yv7Q0j2skZ_IYD97*~aq}sd2bvp^B_bOY;s5m%z z&iKyl;4Ybqjce)KcPdg`8yMr9vV4vOqYGCaMf0|QB-tvLcIsUP=f8@a-nR+_?6;=Z zO}W-b+?H###@FgpXi$7kd?FX}Qa7e`66q_P)UOQPuXT%F+LbfHWoe|b*W>*tRcS75 z1jMG*yyS9ZxVY+foVR{5>{5?oh-ztSEk%U0ri#WUVjM|%3ANfJIt6MzaLnt}O5nubLT&GrNXRl08EYLd>4dlO$imp=w z#aC2o4R6oKVx!AlSe}3)Po&>pMO1n^LvVz%_f%WMCCGZtc*-YnywD+K0$W6dd|mlk z1#xgS>#=cp!sk_E8#}r5qw+w)QCV}L<&~Y%M;`Nx)N0)s86=%_(w8+)CC3IV=e_3#aD6|R$p3J?8WIQQ+bda`=-Ebv0hAc zm4eGWA3KOJ&9A@EGY)+5pnx16xVWMjFIuh&e2wLp?X6q)+wUS`^oaHv7w#5V z=H!W~Og9#-0SKe+wP*l=)$7+B5!K0m_S?I$L>U(C0(w47rz@4FUz z8V>x9j5^DTwjLXN6RcVHDwRxLoEsfhAama(0;N@^t;aZaqU?cy-W7M6ePnt1{uU{U zQ~mjTSy|Z{c?kM)dP z#E7Y~JqJM)zis>L<_YLLq4--6`t)8LT(K}lh@3msje#g1J-*QwFV5D3Wk26_ z7drDU>(X42zz02)c%JoLX@wXDTHkSVH?&NkRek9S{O|8Cu_DRH`%vF!wAyr6Yw>oI z(1upvB_HQ=ah?8EStbrjlz$JE_t&0DTnRzzox*VqFD7A!@Tn3oF#dNXRAizs(T|x8yoAld-p$l2Nq8B!(9F$_{ATSgouF;po;Vs&$>&M zQ$Q|9W0{wGdqAkdIxM7ZGF%MpL@eoL>x0O39V6aN3z2DYlc-kh=SkG30-Q4-=KR6ytoE`wJ^s+Z~{dg^KoIQ>`1Y_g! zc{0i&`}_KiSwBPv2M1mKWeUKwijcGL&i;`uRM3Hj7;vi)ZW7v8H{WjBMi6{g2p>{8 z3T)lrT}MTG@?G^&K8)7zlTdDwJ`BFo7Vi@l?~l@iGvV(3wxa3iFFkvT>FCea`%x%U z0WAre8QjHjC9_=H*fUSh=95LJ40SBH3fXN>!7OMWuZqmC8dxS6LwUvMCC1x$*>f&J zTJWwC;Byo!UVaSUXUrAsI*pEYS!sn9FyNHv@uId^RWOg4S1h?duAYjFPT6Mwm5_&c zyx+y)DuO}=%$azR6-M)ghbu?X#I{(GOF!0JwQBg4u5h}3$27nKbY6%e40J|<95o?m zA_z(G=&y@)G=5#mDz!sG zSy4==M)4LV1hccHROAyh0(wH@Z!#Vgj2wy1^*SszN5yJX2U*>=ni2cDH??ZZLr4- z`uYi#{9Roef^^%cF^1Dw@`zQ6q6a?>UQA=KB*||sFV5X%3Hh`yt;XE3tuA1V>s#}v zw2*?2@SI5Lsne`5)btC7%&6lWbVB`>zQ&4T-ZAb(9de)r%cJ7Z{1;bHfj|JJ8FL_C zzS{#Qiv+TbsWJUkoa4(?1U(#ds=UMP1^G&%M0H##047RY!Z<7ZtQxtzf927|whl59 z98P7>%74o=i*oy}my^9gu=C`(cjo(@YYj(=NpOsiolP&UN41deTCWbmb$N}17o{S^ zk)IBW`V@yAiQ2i5OWq8A7*VufF&=V!UxHBcs>lIyJM(!%HyZHAn6y>PM2l&{$8?ol zpBEc;wPoQ&_;kS9mF>o)g^9(55i(|bXM`sjNyvf&8WLkogFC^KL|fjJ`(NG&@nL*i zu65}YhTIc}BF)X$T%^BBMt5onyR1(AQd3Jhw_rovrk%rw?=e_1=lMTWwZpr8lu~Bo55}V zr_=MF)f;KT{w!gC$voIzl2G%fN6k>os9DwP*AeX)$nmapJ~Fc$ee>qcrzp@>nf#5gq)$(I_pX(NpiN}pk^c3e`W=tfIMQLZLXiX00i>HtRLWeVDH;{PU znjbTfF-sd9Oz((8=kk)O>fxHe?{7a4x)DUO2n)1E`3WK=49_T~n-XRirAb3&baMD0 z9j>l6DP!P$Ks+TIxX+MY5sQ{1ZOVdIYlnWlFy@U&pbF;P1|heFVE((q@WK>B zdkXlwj*T;nL#<{K=+U-%E12cs^S?f&;80^S*Z~#kIPAm1i@@XF6aA>dI2ddJAJER# z=ek!GeS|~5257usWW78!Pf`GZrY&7vJI3X{9{3%8f1o2ps3(JrEC}~`leg{abQWbU z#of-r{!h_{J67Yy`uqF!)$pEXk9D}@g)S>^mhkyhDSSob#ZMlVPckxFNgf_k$gqt_ zl?360!_XsJExxIg=sC8LRFR%~M{MFmaH$iG+BPHd7QiuY*bY-kmwW}RhFBvCT5|=K8WS1ZOoIFR`i{oT{%G23C)KR6#Oy4MG}RH~I7-jz7Uh6FhkJ?}0%D^+ zjn2C;$*jqqzDClzDSo)~E_S#gNY%u^Z_{iy7gmzyv+I2U+tcz*K!6=k7U)g$?Rvb) zlB4%tl{Dp(#VkoVa;R_65mDvPuRJ0=j)y`TUV*72;gxAWJkcT z$3I)S4&59z`(*_+JT;M%dFq!s;d%n^+)2K#$?e;>71AX|8yenF4b&wm7~c-p4X?;uf^@Tm z&)Y=+0HB2aa7~o(pM%@!zQvqtXRn|Y(dh8lq24~)9#Kw;$hk@-9Za81y>n+trRq%e zh64i}wJhqN4C7d%1R#o_DG_ zz8X@Q0}?a;IfviHh={zlsET(qRhyP6gTBnyD(fAX9sL}}+zDpKSy71C_6Nc1X3kzi z%k=vujFEeKT)WJAn7ipeL+FM?cHCCO`@ftF*DZN=vJE?Z6Nwq7XO%N(quzg@iSk($ zz|AI;5jd20<2blg#lY8#fDQPAqlU8(Fy={CKy}b~0xThz;=r2&nD{3^#03@k;QD>7 z&TwsLGigqO+&aMN2;p5>fN1bb)lGH$QOVq(E# z&)w0@@CQ1<)eAgb6U1gOMht4c=PYzMzO9kuE5I8X7(7@ec-~M?c3s~Ci|niZvOqt| zN_{8&#Wp~QlAqId1iZQ>hwS4Bb)QX=M1EEK z{2CRGYNlP9OM4+HSkHA2bY=#;!Kw^#IwE%ewQhqnm1oEd7_gf#6+}%js(mpFg(Fw@ zNs>2IGw`eB{=#FYvsDlNorK`QQo-#RzHjY+xRRYhB}rt=rf!KNFOMcmm-5s=+*%(! z?hNnx=s!PTDog}-Y@*2%uOR?8Z#RIOPD{XJO!_HVb={2hYq@r!ans`@0tiLz&C9r< z4`vQ7i6u#7&Zd@$hN4BCi3rOZ5G|#kh=_;*Ec*0u92(Qp2p81DSGR&Khz4T77RDJz zA)`~t?JBFbrhPj1qJ!Jk+}vD-L-EHO8{zjrp&$N$8u9%lQ4+-qKMtP=X(pdNdzMZs zrUx*{XL>@uzfkx%$-f=lPKx*VgL9T2Q~0iBX?(Y$b?@4_cKRTo4tFZizPm z6b2~j1_dutR6CcUaB1w$iXBBDw^t@z^f`L$*oHzzmUIwEI644NcREmGmS1WnU23E~ zw2c0flOAqJnIr)h)plIvEIfRUS)2|0_S|N(z`NR@3WOq^Sm7<-+uPecmxnZyKFTA` zAgiG5C(vd^KoN+pLbaHje;x|NE+@QiV2BM+%GZgtL*tHBCXTE*U-NS>vzW@rx_J^P38Y(!o>?K!=jU?r z%Z*&ueopEm01qOL7SYg6C9%^!{nFKnBR>SGZR>w)_WOCUhqikpU{84P7iJnY4t5fa z$|r-YLV27wS=9hvzz>ThizA8A9=hH)^k?{XU;a8%)8!)GXn`?O zR$5wmq=)!~%12J-Yjpi9&=SGd#*m1JzPj3$7Ak3K{#zNHFxQtB2iLiUtLEh}Xso%> zwkV?@U|~m$hi#E~7IXgSMwfeM;S74_hpd%4U1EI72ACQQUq^X?ZM%a5# zK z1u|7dbo@59Q1{IrulK^{I17JS`w9#jrjvNtCRfNA(d4d7sZ>#guYSbK8BULECQZsA z+c?6gu;Zd)pDst`sd5b;6}<_J&=ULx7PJLk4)B?>^e@YZij3?~1nf09^b2qzW3JsN zfIUwCxu@6yH6}{^a1U+T*WIUXB8pjD!@zHF{gb?2tX#Qru#65*zoqh8On&KwN|GIT zh|Qh9o7j@1MezUh@4r*EmsoyHxYK}m0#`X7>!8N3@K|zroPT@&>K7sy11k?K&X185 z46v1)dhp8x#dm@ek2lhgXaz%V4?AEXhk<|j=ZV^9SGx*lSRh75oXX6_$y&fBI+DQM zj|0;A{11pDQb&(TB{hRw2gZ?j$Z{$w2dMi!r3k>> z0o`4(!{4LOlzST=O|}XB8(7vB;?A(yneA!4?O+(FO)BY($)AU4uy1e9ZE=XW0NPD8 zshq2?-@z^BoG9SA-p7ewWRarN<}9Hu>u*tVBgK%Dj}`)QZR9nQIjcsBgMEQ|AN*H- z96H<+5X-!~Th4?#0H4sWpyR>V_(}8v2<#iT0C?Ow=1gvjxAzrPH4l}>2zjBpW&`bF ztJuQj0%J%TqO2dE%N!XQ@h_&uGXN`v3GI^l+<}$=Qw8oJeyOgZ>?As4Lq$@M&fsx=2>W(#2w_QQPEjYK`{60xU%Jy z_c7Hv_()L4_X8dh;tjHrl9E*#cbcDCUqNF)A5Ud3lk)};;}<0*C7ZrVjeY@zZMMr@ zSK-jpGk_wc0n3UCF*19qKb0NW6xqzFhXHn926*E4luFvs*KhY=-h7G{+CfE1!)JUO zNn(k$HE;Vk_2g@f1Z9x99N+Zi$NaO90CzN!u;YAVcDThivCfN`<$5x?80_5}wSa=7 zuL>W&!<0H^@)(q@7W>ppn!C#u?Y5znjQIGGTwn2zkmGo@!ASh)wXrH?zY{zzlr~YEck{ z2r@`E>`kchG&-kdv)A_zghn-!EToW}1>oA`OJmcLtlsuuHsO&t#7sA0M1nfW5JbNb zMuX`c>l=NKrfkbpc+S8(q2014aSciot)0+t*(maGYA!RyuHl)lK=&#g4`$n^zX`~6 z%QW1LUaAQluysafXJ;?S-tZhcLdpf$J7g2%u)dM-tR4RdD&4TXgdbG%DQAS_2yr$H3v$BRo6;x?s^97SqkH%eB>Kysq&K9%Xo{Xpb+zp z?d1Et@;V|(c7G$chI<`ECKWxIUL{xAeTGX|%1sNX?@~A{#aE@|>A11lfX8`c{7ita zstClU__q=A)UKalG5-EDA7Wl-Fbr(E9PcKIIo`C45+3gcg;Qr7`Ajsqw0K%95m9}5 z*QaXO_3L?YacZ}JRd2zj2tPvj1_>0OD~;^%AAGH0%S?s%$ZWpLLff zzI0;wwh7M}?dg`|-ZkJdu5CG@`(EV3RY&W7p_W$eexHLN+u*>!k|HYY`3gbts5D}v z7|RycOVT87yBQbStj5NUbT=zRzWf*ojqxIx_q_)fm(Lpr#4 z1A_yD*g61)(_rg)BHKOIS@JZ#Lb9QM%jhk!WLal&FS1oE2E_WU+zV>S$$aE3Ju4`L z0~+=p4@teF?39r@kmQM;GZ`W5*ip4>f}$n8jeGqYrvvO)XDBbrr*XIgTcTA9{7(7l zB-y^VWio-hv5{mk!PJQ$LBJ!O`{yRWus0~KK$5IOh~B$gw5KBCTdf4t*2 z!WN&coG0D3i>1suvdAs<67DZ?XeU@n7aYVQNe*7baDSx*RM?h|L2{>JN%Q03mq}gu zH`=)Y_5*w6cX1_$IgB6Uq&LF_(A6N7PIHIMFO=0F}QQJQsXa^BSDs~ z6M?BAElm|_t6*E~l=jM6tBi>~65>)2BtyGTXo{enS+swh#TCuk)rV1$5N3U7(d>vB`>5J7kM;gFTB=Wx_0Y@%04y06@R^= zqN3){oYX|`5AM$;GzDn58pOtEpLPM!{yi3bEGm2(c2C@!-jVjn zu3G{X6|e?eNy55aul|JHssLT6zwF!laG&1B`m+&FESZ;_;oVZy?{=44AdChNr_#t7hH=T(R*J1rCf0T(~bM>QM@~p~V{oI`~tH*8W6Gy>Yq+(xwm9Sxkw|lo287b6H@`7 zG~NxLaB$iqC%$FA0z=ooBvhWroXrYrNP53a;0;oL4_SC^9mFR1cEn%FAu{EiQA_qb zz^%SG@_SHmAI;zOsP1&B31LdJ-yG7cnXUJmsKn&%UMH?{(-S@Q#XlX8ntNko;ojcf zi%qxH6s9|FoqnkMwH_WvcXjl#R#an?r^Td3!x|O?U}4x0W}B&*S*?8!!8~KON*C>^ z1=KV>PxLI*C=prA**ux?N{91MR(x-)qaG+{{Hq3%rhHEibp6({8#YFiyqQUWJ0u8M zEg!_SpG87)IE&*>tN{**Y5Xhy0cc>oR~M{hh)l@$K7~#8IjgC5W2_@9ICSI2r}|NR zYD|Kaub}wYD7kku5HwsL_Qk>P0e#_<*{VG-LBltif?R(SI2c51t@wP(4hb(4nZ70b z0cZ|~?ivKFi5kXl0h(j*?ZQ?r%T(9HNKNEjE0XfONVEa#u)V*1vGm*4&knRMC=6#>T+|80GBA4hLU>G~#Rb zj6rLnYmx6{Kv%y?beAfvcA3B`qjIws};2J-lU&YK%_x@=OM!FVOZ_hCAi5#Y{kmJTlILL z*1dIs=R+EP3@;XeROe6x^$)7Q()Z0D+7tT+d}T#rr9a-)$9_Q+8k5A^KX5npi2?c(=N z`BA%tr=OovY2S#Z;>bxA(1uhIO!rjZe#ApyZWI2V8*fKOQ&ZDz`Qfx&T((|p@-=L$ zSrq2;-X4+x-^72ZK<_@7R-L~h@udqC;%{PVqUxm8Q@= z@O%>Qbi!A_k3-Ash_`?>nGO*^P^aM&WpJtD(tK{}z%+X;-+VA^eS#AINjvw&GJ&!g z1C{V5zY8871us$Jhd==4Kauxq?V&cAvM2BDO|Fn;0j#K!5*+DX6t0zB80t+?3D+9v zov)fG92k zm$Z8}j7Ppe>xI9b<7?DK)>PId7MPemPD?YTPI|IM*WRVI_&c0%L7;sgCm*_PT!4W>D0I7ezp8_?uDO6b}$rj zK7NcNA9mG0*G;0@p?=Fjm$kq|T+daOs6xD;O0>cjSC@o{-KrZ7Vm_wE*lmd};Fai)jFHK_x4)B5G93VjdGM5r2KAvQ_Mt8>%|b9ICq9x?orGO3-$Si8 zbMqPK;#gMA)y-TzuQ#rcC1!z6_rkyTKH6W}uv@bDfFmn>L5s?~^juwV#GF9`nWIFt z46D7m7+bQs6K^u?>tOKP>H7&V)9vlTI{r+J(FrFsR^I@V323 zSp7>m{9Hpo;L^o7^I7QN^TbBN#r0b;$;tRDB>|HI)lSVA_12j7hXz$178MgoAJJ{i zNOKNTT)Sp_#mHMreMtR^FkPqWEPEu0a4_sMi9kgoR5Bs!4_156;2>ZAD4yJZQCc%g zIQ}%Lx()SWIvlXIfhuGaCq1DYRVe+_y-}T%*vbX8I}7>poz+K@c+vFqfjH5?PA>Aj zQ6cElkU8PxsxhUbxXwsaR?TRqqp&5nLR{N)X~`J&xl2Q1W22^|S6-hh1}v{0U6FA< zNo~y`H>`FJL#0RVlKsvQNaCXlChh{~&XWz~DIO~CO&&SohgIZlq-@o=qsbP7-W_|g za;U6?hoo}|v6BRg=j$M)#>lQ^d<^BgXKh63Z%jN)8icIH=5=cURwNcV3fCiA16 z7Rx6X7vzweK!RdmWMnQSJepmyK~deW7q@>m${CL8_^tztS^ODj`)Td3HQDyD9vb55 zD&Hc!aOCa6CHq-g@JD4zL6%BWu2Nevqheq>U%p{QPJ*OMj{{TdRwF?=&kU1nysNtC=N4i%qOZE!oKIrh)+MM9WBL9N@#D#67atNm4`b0g8f z_`MC5V*cvmn6vh3Zc>X9<*1Btdd^pIVj<U%|2*YUk+QBdW$CR94VR{Pybe4ukRsN2BChO~%YPKU2V&XnS|I>h|=`t@st z)`!)wwPfK5nLWDD7E{ywMMzU^7my1Bht0yR>fjxNR??76cb6Yk%!49Llu5%z@z}V6 zo!VQfbv`WFb-llbgAc!Z6YK6G)y@kjYbiB<1Ud3%rK)VX!tLpx&F##H3qc&@V&v>+RV%l18c zcEbOpo!ih6tnKBMiqeJHGgK`rfo;#l1Yy&LJ;l%hh{xkmp*^~mSIk0EQhT7Q2hu!z z!STQfT2szeAfzo40`I;)Sr;p;`k1+?x7;1qtEs5XAFg|@Y8XdeR2N*?^W-Q-(OF66 z2D@Zb>75^BVbMb}q>|;7n`8jf5DPIY8Bw}lC{eNl8h$!iysC$A?dEB~dEBKVe#*S| z-|CpILJ0@ux2wT8Z5`9q?*5TN9x1c|>e7WyIyaZ??#k0l_ERFdL!J@;9U#II@{er$ za4UAVq?{t~$74#&56=s|Z#Jm8V5Us6PrA)iWi?(%4P4`Cd`DO}PFelL9(w-`GkJ%x zGg&Ho3%LF9h*))srr$fdR=Vz_IMEt=u=C+Z_X?b&1W(|Z!)y8Hi34<1hpxkx%(m_? zHOC>Ll6Xg<_evWb$U=#^T?zAZ51q42FLqPPDJ5vNw0eRibpHbuEi_=3H+Fz-Z{zEG zwqOFUol2MlizY3Jd;~vdoN!Sm6_T=PIAbe5HHz>VFg%uQww)7yWm4`aTdyx?MqA84XYfCE_VN{2WK`0%ZN`}qjg#^ zC#LZ09)@7GH_+DXT=Grq;>T;8!LsZ6i49r+nok~X)dP#(^f0k7bFe&BvOwnQO+Ug$ zu*Ub+(3XDrF}z{+=K2tex;kumdAWiS$W=e-hBzUp&s@^H@ZAGRl2^!^mMBroF9lsj z1dGR`B6?1^DHyEg-a6Hw)qm$QIJD}QLO(q1&=@xrt*w=3n0{6Ns+yTM>bIYjboMSJw@1m*pi+A3=A=IV zvEPfi`n}%`#;eXplJATPPxki)oddo+lg)`_nl<*34%`v<66b&SY?Y9xTzTu6cYGB4 zJ@4V}$m#i?+cN4mqI;X?Qht1>ClsewmYWX{Dvnrv2KC9$J=j2rgHlc>X_+6`Hd6bK z#0Ox=O?C2I8G+&TvI8E+e3kcCYIFvvo?t4>X=Bv)rika!s(v}fC)TZB%?-~lV0UB@ z;H+=g-cH>Zo@-0Rr%p#P-W61z-}T^N{t=))*WAU-woLt3$8l-mFOj=y(hcH$YL5ET zE5`fUdS_U-_UNV5iPwD`j=S8394Bb^^^Pr^hR-^e23fO`YOSGl4-DteZllEHYdw(5 zJ3LUO_nvgFsR28F*Evv=ODAYG<`I^UjeD}>`eV0FD?7vd?%EaD`Us)icVJMHQ2|=A$wL^LqV4SEamEAcsaQr#op$qX5+7AFz~l!t_RdFo;L3ATqA*I zuh)Ta*}9Op55bg1ySS}&HXZoL+pQ~ux|MIntgpP%hhe6q2+}^Cp1wcN%QOy2C^nv9 zjO>!wcM_NFW&TM@lDS{c-GmmL`H$Cquay(Vb=+CV)bHH1 zTM2ULTn06NE=?_bQap`2Y>A~W|AuWKTD=-1CymBr@6>@-{iuMLni;WK85u)sm|{(9 zFids6SaMlG2a2oV#|9-s;tfi%s&?XGE5T(5OK=3yNhvv`cz2-699&|-@zDgR_d0R z*^x(krBiC&6jk6hrC0itDMMydb#7l|iZ=$h@4A8rrE`P7^{<=8iJz*ztM*02iYy}`}$CM8j!v)0}zoDkbRWlO`FyHeBE?2(V^2O)nhWh;gCRaIQ^o4yvij_9cfEfT zdP%ij_gGZ=C&^7lO3)W-z(dM2`uU9dQ9(2{3W`hyFVy-oaE4KNr|9ntHg^_#(@y2- z3KHvvtH_LE!@l3MtI}AI3OF{nXQEKOFQ`fhQ>YFlxDOMcs_!3emRfsjtA x^3$pYXp8PRPQfKcU7fcb+sdF{jM;>Wd4|T# +
+
+

{emailQuery.data?.to}

+ +
+
+
+
+
+ From + {emailQuery.data?.from} +
+ +
+ To + {emailQuery.data?.to} +
+ +
+ Subject + {emailQuery.data?.subject} +
+
+
+
+
+
+
+
Events History
+
+
+
+ {emailQuery.data?.emailEvents.map((evt) => ( +
+
+ +
+
+
+ +
+
+ {formatDate(evt.createdAt, "MMM dd, hh:mm a")} +
+
+ +
+
+
+ ))} +
+
+
+
+
+
+ ); +} + +const EmailStatusText = ({ + status, + data, +}: { + status: EmailStatus; + data: JsonValue; +}) => { + if (status === "SENT") { + return ( +
+ We received your request and sent the email to recipient's server. +
+ ); + } else if (status === "DELIVERED") { + return
Mail is successfully delivered to the recipient.
; + } else if (status === "DELIVERY_DELAYED") { + const _errorData = data as unknown as SesDeliveryDelay; + const errorMessage = DELIVERY_DELAY_ERRORS[_errorData.delayType]; + + return
{errorMessage}
; + } + return
{status}
; +}; diff --git a/apps/web/src/app/(dashboard)/emails/email-list.tsx b/apps/web/src/app/(dashboard)/emails/email-list.tsx index 7adb015..a973ebe 100644 --- a/apps/web/src/app/(dashboard)/emails/email-list.tsx +++ b/apps/web/src/app/(dashboard)/emails/email-list.tsx @@ -3,7 +3,6 @@ import Link from "next/link"; import { Table, - TableCaption, TableHeader, TableRow, TableHead, @@ -20,15 +19,85 @@ import { MailWarning, MailX, } from "lucide-react"; -import { formatDistance, formatDistanceToNow } from "date-fns"; +import { formatDistanceToNow } from "date-fns"; import { EmailStatus } from "@prisma/client"; +import { EmailStatusBadge } from "./email-status-badge"; +import { useState } from "react"; +import EmailDetails from "./email-details"; +import { useRouter } from "next/navigation"; +import { useSearchParams } from "next/navigation"; // Adjust the import based on your project setup +import dynamic from "next/dynamic"; +import { useUrlState } from "~/hooks/useUrlState"; +import { Button } from "@unsend/ui/src/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@unsend/ui/src/select"; + +/* Stupid hydrating error. And I so stupid to understand the stupid NextJS docs. Because they stupid change it everyday */ +const DynamicSheetWithNoSSR = dynamic( + () => import("@unsend/ui/src/sheet").then((mod) => mod.Sheet), + { ssr: false } +); + +const DynamicSheetContentWithNoSSR = dynamic( + () => import("@unsend/ui/src/sheet").then((mod) => mod.SheetContent), + { ssr: false } +); export default function EmailsList() { - const emailsQuery = api.email.emails.useQuery(); + const [selectedEmail, setSelectedEmail] = useUrlState("emailId"); + const [page, setPage] = useUrlState("page", "1"); + const [status, setStatus] = useUrlState("status"); + + const pageNumber = Number(page); + + const emailsQuery = api.email.emails.useQuery({ + page: pageNumber, + status: status?.toUpperCase() as EmailStatus, + }); + + const handleSelectEmail = (emailId: string) => { + setSelectedEmail(emailId); + }; + + const handleSheetChange = (isOpen: boolean) => { + if (!isOpen) { + setSelectedEmail(null); + } + }; return ( -
-
+
+
+ +
+
@@ -41,26 +110,62 @@ export default function EmailsList() { - {emailsQuery.data?.map((email) => ( - - - -

{email.to}

-
- - - {/* - {email.latestStatus ?? "Sent"} - */} - - {email.subject} - - {formatDistanceToNow(email.createdAt, { addSuffix: true })} + {emailsQuery.data?.emails.length ? ( + emailsQuery.data?.emails.map((email) => ( + handleSelectEmail(email.id)} + className=" cursor-pointer" + > + +
+ +

{email.to}

+
+
+ + + + {email.subject} + + {formatDistanceToNow(email.createdAt, { addSuffix: true })} + +
+ )) + ) : ( + + + No emails found - ))} + )}
+ + + + {selectedEmail ? : null} + + +
+
+ +
); @@ -113,39 +218,3 @@ const EmailIcon: React.FC<{ status: EmailStatus }> = ({ status }) => { ); } }; - -const EmailStatusBadge: React.FC<{ status: EmailStatus }> = ({ status }) => { - let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color - switch (status) { - case "SENT": - badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; - break; - case "DELIVERED": - badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10"; - break; - case "BOUNCED": - badgeColor = "bg-red-500/10 text-red-800 border-red-600/10"; - break; - case "CLICKED": - badgeColor = "bg-cyan-500/10 text-cyan-600 border-cyan-600/10"; - break; - case "OPENED": - badgeColor = "bg-indigo-500/10 text-indigo-600 border-indigo-600/10"; - break; - case "DELIVERY_DELAYED": - badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; - case "COMPLAINED": - badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; - break; - default: - badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; - } - - return ( -
- {status.toLowerCase().split("_").join(" ")} -
- ); -}; diff --git a/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx b/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx new file mode 100644 index 0000000..4116ea3 --- /dev/null +++ b/apps/web/src/app/(dashboard)/emails/email-status-badge.tsx @@ -0,0 +1,83 @@ +import { EmailStatus } from "@prisma/client"; + +export const EmailStatusBadge: React.FC<{ status: EmailStatus }> = ({ + status, +}) => { + let badgeColor = "bg-gray-400/10 text-gray-400 border-gray-400/10"; // Default color + switch (status) { + case "SENT": + badgeColor = "bg-gray-400/10 text-gray-400 border-gray-400/10"; + break; + case "DELIVERED": + badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10"; + break; + case "BOUNCED": + badgeColor = "bg-red-500/10 text-red-800 border-red-600/10"; + break; + case "CLICKED": + badgeColor = "bg-cyan-500/10 text-cyan-600 border-cyan-600/10"; + break; + case "OPENED": + badgeColor = "bg-indigo-500/10 text-indigo-600 border-indigo-600/10"; + break; + case "DELIVERY_DELAYED": + badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; + case "COMPLAINED": + badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10"; + break; + default: + badgeColor = "bg-gray-400/10 text-gray-400 border-gray-400/10"; + } + + return ( +
+ {status.toLowerCase().split("_").join(" ")} +
+ ); +}; + +export const EmailStatusIcon: React.FC<{ status: EmailStatus }> = ({ + status, +}) => { + let outsideColor = "bg-gray-600"; + let insideColor = "bg-gray-600/50"; + + switch (status) { + case "DELIVERED": + outsideColor = "bg-emerald-500/40"; + insideColor = "bg-emerald-500"; + break; + case "BOUNCED": + outsideColor = "bg-red-500/40"; + insideColor = "bg-red-500"; + break; + case "CLICKED": + outsideColor = "bg-cyan-500/40"; + insideColor = "bg-cyan-500"; + break; + case "OPENED": + outsideColor = "bg-indigo-500/40"; + insideColor = "bg-indigo-500"; + break; + case "DELIVERY_DELAYED": + outsideColor = "bg-yellow-500/40"; + insideColor = "bg-yellow-500"; + case "COMPLAINED": + outsideColor = "bg-yellow-500/40"; + insideColor = "bg-yellow-500"; + break; + default: + outsideColor = "bg-gray-600/40"; + insideColor = "bg-gray-600"; + } + + return ( +
+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/emails/page.tsx b/apps/web/src/app/(dashboard)/emails/page.tsx index 401f972..6fdcd78 100644 --- a/apps/web/src/app/(dashboard)/emails/page.tsx +++ b/apps/web/src/app/(dashboard)/emails/page.tsx @@ -1,13 +1,20 @@ import type { Metadata } from "next"; -import EmailList from "./email-list"; +import { Suspense } from "react"; +import dynamic from "next/dynamic"; +const EmailList = dynamic( + () => import("./email-list").then((mod) => mod.default), + { ssr: false } +); export default async function EmailsPage() { return (

Emails

+ {/* Loading...
}> */} + {/* */}
); } diff --git a/apps/web/src/app/(dashboard)/layout.tsx b/apps/web/src/app/(dashboard)/layout.tsx index 0419194..9124407 100644 --- a/apps/web/src/app/(dashboard)/layout.tsx +++ b/apps/web/src/app/(dashboard)/layout.tsx @@ -3,7 +3,9 @@ import { redirect } from "next/navigation"; import { Bell, BellRing, + BookUser, CircleUser, + Code, Globe, Home, KeyRound, @@ -17,6 +19,7 @@ import { Search, ShoppingCart, Users, + Volume2, } from "lucide-react"; import { Button } from "@unsend/ui/src/button"; @@ -61,12 +64,8 @@ export default async function AuthenticatedDashboardLayout({
- Unsend + Unsend + Unsend
@@ -87,6 +86,16 @@ export default async function AuthenticatedDashboardLayout({ Domains + + + Contacts + + + + + Marketing + + SMS @@ -98,8 +107,8 @@ export default async function AuthenticatedDashboardLayout({ - - API keys + + Developer settings
diff --git a/apps/web/src/app/api/ses_callback/route.ts b/apps/web/src/app/api/ses_callback/route.ts index 1e08af0..fda5ed9 100644 --- a/apps/web/src/app/api/ses_callback/route.ts +++ b/apps/web/src/app/api/ses_callback/route.ts @@ -1,6 +1,3 @@ -import { headers } from "next/headers"; -import { hashToken } from "~/server/auth"; -import { db } from "~/server/db"; import { parseSesHook } from "~/server/service/ses-hook-parser"; export async function GET(req: Request) { @@ -22,14 +19,15 @@ export async function POST(req: Request) { try { message = JSON.parse(data.Message || "{}"); const status = await parseSesHook(message); + console.log("Error is parsing hook", status); if (!status) { - return Response.json({ data: "Error is parsing hook" }, { status: 400 }); + return Response.json({ data: "Error is parsing hook" }); } return Response.json({ data: "Success" }); } catch (e) { console.error(e); - return Response.json({ data: "Error is parsing hook" }, { status: 400 }); + return Response.json({ data: "Error is parsing hook" }); } } diff --git a/apps/web/src/hooks/useUrlState.ts b/apps/web/src/hooks/useUrlState.ts new file mode 100644 index 0000000..a5e0ab9 --- /dev/null +++ b/apps/web/src/hooks/useUrlState.ts @@ -0,0 +1,36 @@ +import { useCallback, useState } from "react"; +import qs from "query-string"; + +/** + * A custom hook to use URL as state + * @param key The query parameter key. + */ +export function useUrlState(key: string, defaultValue: string | null = null) { + const [state, setState] = useState(() => { + if (typeof window === "undefined") return null; + const queryValue = qs.parse(window.location.search)[key]; + if (queryValue !== undefined) { + return (Array.isArray(queryValue) ? queryValue[0] : queryValue) ?? null; + } + return defaultValue; + }); + + // Update URL when state changes + const setUrlState = useCallback( + (newValue: string | null) => { + setState(newValue); + const newQuery = { + ...qs.parse(window.location.search), + [key]: newValue, + }; + const newUrl = qs.stringifyUrl({ + url: window.location.href, + query: newQuery, + }); + window.history.replaceState({}, "", newUrl); + }, + [key] + ); + + return [state, setUrlState] as const; +} diff --git a/apps/web/src/lib/constants/ses-errors.ts b/apps/web/src/lib/constants/ses-errors.ts new file mode 100644 index 0000000..1aced89 --- /dev/null +++ b/apps/web/src/lib/constants/ses-errors.ts @@ -0,0 +1,46 @@ +export const DELIVERY_DELAY_ERRORS = { + InternalFailure: "An internal Unsend issue caused the message to be delayed.", + General: "A generic failure occurred during the SMTP conversation.", + MailboxFull: + "The recipient's mailbox is full and is unable to receive additional messages.", + SpamDetected: + "The recipient's mail server has detected a large amount of unsolicited email from your account.", + RecipientServerError: + "A temporary issue with the recipient's email server is preventing the delivery of the message.", + IPFailure: + "The IP address that's sending the message is being blocked or throttled by the recipient's email provider.", + TransientCommunicationFailure: + "There was a temporary communication failure during the SMTP conversation with the recipient's email provider.", + BYOIPHostNameLookupUnavailable: + "Unsend was unable to look up the DNS hostname for your IP addresses. This type of delay only occurs when you use Bring Your Own IP.", + Undetermined: + "Unsend wasn't able to determine the reason for the delivery delay.", + SendingDeferral: + "Unsend has deemed it appropriate to internally defer the message.", +}; + +export const BOUNCE_ERROR_MESSAGES = { + Undetermined: "Unsend was unable to determine a specific bounce reason.", + Permanent: { + General: + "Unsend received a general hard bounce. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.", + NoEmail: + "Unsend received a permanent hard bounce because the target email address does not exist. If you receive this type of bounce, you should remove the recipient's email address from your mailing list.", + Suppressed: + "Unsend has suppressed sending to this address because it has a recent history of bouncing as an invalid address. To override the global suppression list, see Using the Unsend account-level suppression list.", + OnAccountSuppressionList: + "Unsend has suppressed sending to this address because it is on the account-level suppression list. This does not count toward your bounce rate metric.", + }, + Transient: { + General: + "Unsend received a general bounce. You may be able to successfully send to this recipient in the future.", + MailboxFull: + "Unsend received a mailbox full bounce. You may be able to successfully send to this recipient in the future.", + MessageTooLarge: + "Unsend received a message too large bounce. You may be able to successfully send to this recipient if you reduce the size of the message.", + ContentRejected: + "Unsend received a content rejected bounce. You may be able to successfully send to this recipient if you change the content of the message.", + AttachmentRejected: + "Unsend received an attachment rejected bounce. You may be able to successfully send to this recipient if you remove or change the attachment.", + }, +}; diff --git a/apps/web/src/server/api/routers/email.ts b/apps/web/src/server/api/routers/email.ts index a7d4dca..ad462d8 100644 --- a/apps/web/src/server/api/routers/email.ts +++ b/apps/web/src/server/api/routers/email.ts @@ -1,24 +1,55 @@ +import { EmailStatus } from "@prisma/client"; import { z } from "zod"; -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, - teamProcedure, -} from "~/server/api/trpc"; +import { createTRPCRouter, teamProcedure } from "~/server/api/trpc"; import { db } from "~/server/db"; -import { createDomain, getDomain } from "~/server/service/domain-service"; + +const statuses = Object.values(EmailStatus) as [EmailStatus]; + +const DEFAULT_LIMIT = 30; export const emailRouter = createTRPCRouter({ - emails: teamProcedure.query(async ({ ctx }) => { - const emails = await db.email.findMany({ - where: { - teamId: ctx.team.id, - }, - }); + emails: teamProcedure + .input( + z.object({ + page: z.number().optional(), + status: z.enum(statuses).optional().nullable(), + domain: z.number().optional(), + }) + ) + .query(async ({ ctx, input }) => { + const page = input.page || 1; + const limit = DEFAULT_LIMIT; + const offset = (page - 1) * limit; - return emails; - }), + const whereConditions = { + teamId: ctx.team.id, + ...(input.status ? { latestStatus: input.status } : {}), + ...(input.domain ? { domainId: input.domain } : {}), + }; + + const countP = db.email.count({ where: whereConditions }); + + const emailsP = db.email.findMany({ + where: whereConditions, + select: { + id: true, + createdAt: true, + latestStatus: true, + subject: true, + to: true, + }, + orderBy: { + createdAt: "desc", + }, + skip: offset, + take: limit, + }); + + const [emails, count] = await Promise.all([emailsP, countP]); + + return { emails, totalPage: Math.ceil(count / limit) }; + }), getEmail: teamProcedure .input(z.object({ id: z.string() })) @@ -30,7 +61,7 @@ export const emailRouter = createTRPCRouter({ include: { emailEvents: { orderBy: { - createdAt: "desc", + createdAt: "asc", }, }, }, diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 8b9e6de..d6efcc1 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -10,6 +10,7 @@ import { EventType, } from "@aws-sdk/client-sesv2"; import { generateKeyPairSync } from "crypto"; +import mime from "mime-types"; import { env } from "~/env"; import { EmailContent } from "~/types"; import { APP_SETTINGS } from "~/utils/constants"; @@ -154,6 +155,63 @@ export async function sendEmailThroughSes({ } } +export async function sendEmailWithAttachments({ + to, + from, + subject, + text, + html, + attachments, + region = "us-east-1", + configurationSetName, +}: EmailContent & { + region?: string; + configurationSetName: string; + attachments: { filename: string; content: string }[]; +}) { + const sesClient = getSesClient(region); + const boundary = "NextPart"; + let rawEmail = `From: ${from}\n`; + rawEmail += `To: ${to}\n`; + rawEmail += `Subject: ${subject}\n`; + rawEmail += `MIME-Version: 1.0\n`; + rawEmail += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; + rawEmail += `--${boundary}\n`; + rawEmail += `Content-Type: text/html; charset="UTF-8"\n\n`; + rawEmail += `${html}\n\n`; + + for (const attachment of attachments) { + const content = attachment.content; // Convert buffer to base64 + const mimeType = + mime.lookup(attachment.filename) || "application/octet-stream"; + rawEmail += `--${boundary}\n`; + rawEmail += `Content-Type: ${mimeType}; name="${attachment.filename}"\n`; + rawEmail += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; + rawEmail += `Content-Transfer-Encoding: base64\n\n`; + rawEmail += `${content}\n\n`; + } + + rawEmail += `--${boundary}--`; + + const command = new SendEmailCommand({ + Content: { + Raw: { + Data: Buffer.from(rawEmail), + }, + }, + ConfigurationSetName: configurationSetName, + }); + + try { + const response = await sesClient.send(command); + console.log("Email with attachments sent! Message ID:", response.MessageId); + return response.MessageId; + } catch (error) { + console.error("Failed to send email with attachments", error); + throw new Error("Failed to send email with attachments"); + } +} + export async function addWebhookConfiguration( configName: string, topicArn: string, diff --git a/apps/web/src/server/public-api/api/send_email.ts b/apps/web/src/server/public-api/api/send_email.ts index 0aac629..0ded2da 100644 --- a/apps/web/src/server/public-api/api/send_email.ts +++ b/apps/web/src/server/public-api/api/send_email.ts @@ -19,6 +19,14 @@ const route = createRoute({ subject: z.string(), text: z.string().optional(), html: z.string().optional(), + attachments: z + .array( + z.object({ + filename: z.string(), + content: z.string(), + }) + ) + .optional(), }), }, }, diff --git a/apps/web/src/server/service/email-service.ts b/apps/web/src/server/service/email-service.ts index d68a31f..2fd8070 100644 --- a/apps/web/src/server/service/email-service.ts +++ b/apps/web/src/server/service/email-service.ts @@ -1,12 +1,12 @@ import { EmailContent } from "~/types"; import { db } from "../db"; -import { sendEmailThroughSes } from "../aws/ses"; +import { sendEmailThroughSes, sendEmailWithAttachments } from "../aws/ses"; import { APP_SETTINGS } from "~/utils/constants"; export async function sendEmail( emailContent: EmailContent & { teamId: number } ) { - const { to, from, subject, text, html, teamId } = emailContent; + const { to, from, subject, text, html, teamId, attachments } = emailContent; const fromDomain = from.split("@")[1]; @@ -24,18 +24,33 @@ export async function sendEmail( throw new Error("Domain is not verified"); } - const messageId = await sendEmailThroughSes({ - to, - from, - subject, - text, - html, - region: domain.region, - configurationSetName: getConfigurationSetName( - domain.clickTracking, - domain.openTracking - ), - }); + const messageId = attachments + ? await sendEmailWithAttachments({ + to, + from, + subject, + text, + html, + region: domain.region, + configurationSetName: getConfigurationSetName( + domain.clickTracking, + domain.openTracking + ), + attachments, + }) + : await sendEmailThroughSes({ + to, + from, + subject, + text, + html, + region: domain.region, + configurationSetName: getConfigurationSetName( + domain.clickTracking, + domain.openTracking + ), + attachments, + }); if (messageId) { return await db.email.create({ diff --git a/apps/web/src/types/aws-types.ts b/apps/web/src/types/aws-types.ts index 4549502..ddcbb11 100644 --- a/apps/web/src/types/aws-types.ts +++ b/apps/web/src/types/aws-types.ts @@ -28,8 +28,16 @@ export interface SesMail { } export interface SesBounce { - bounceType: string; - bounceSubType: string; + bounceType: "Transient" | "Permanent" | "Undetermined"; + bounceSubType: + | "General" + | "NoEmail" + | "Suppressed" + | "OnAccountSuppressionList " + | "MailboxFull" + | "MessageTooLarge" + | "ContentRejected" + | "AttachmentRejected"; bouncedRecipients: Array<{ emailAddress: string; action: string; @@ -94,7 +102,17 @@ export interface SesRenderingFailure { } export interface SesDeliveryDelay { - delayType: string; + delayType: + | "InternalFailure" + | "General" + | "MailboxFull" + | "SpamDetected" + | "RecipientServerError" + | "IPFailure" + | "TransientCommunicationFailure" + | "BYOIPHostNameLookupUnavailable" + | "Undetermined" + | "SendingDeferral"; expirationTime: string; delayedRecipients: string[]; timestamp: string; diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index 85ae5b7..141a88e 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -4,4 +4,8 @@ export type EmailContent = { subject: string; text?: string; html?: string; + attachments?: { + filename: string; + content: string; + }[]; }; diff --git a/packages/ui/package.json b/packages/ui/package.json index 1a1169d..5ce22ba 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -31,6 +31,8 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", diff --git a/packages/ui/src/select.tsx b/packages/ui/src/select.tsx new file mode 100644 index 0000000..9c7fd46 --- /dev/null +++ b/packages/ui/src/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; + +import { cn } from "../lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/packages/ui/src/separator.tsx b/packages/ui/src/separator.tsx new file mode 100644 index 0000000..cb18c4f --- /dev/null +++ b/packages/ui/src/separator.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "../lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/packages/ui/styles/globals.css b/packages/ui/styles/globals.css index abc2f19..f21dba8 100644 --- a/packages/ui/styles/globals.css +++ b/packages/ui/styles/globals.css @@ -62,7 +62,7 @@ --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --ring: 217.2 32.6% 17.5%; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c717b6b..c201ecf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,7 +114,7 @@ importers: version: 11.0.0-next-beta.318(@trpc/server@11.0.0-next-beta.318) '@trpc/next': specifier: next - version: 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0) + version: 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.2.1)(react-dom@18.2.0)(react@18.2.0) '@trpc/react-query': specifier: next version: 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(react-dom@18.2.0)(react@18.2.0) @@ -136,18 +136,24 @@ importers: lucide-react: specifier: ^0.359.0 version: 0.359.0(react@18.2.0) + mime-types: + specifier: ^2.1.35 + version: 2.1.35 next: - specifier: ^14.1.3 - version: 14.1.3(react-dom@18.2.0)(react@18.2.0) + specifier: ^14.2.1 + version: 14.2.1(react-dom@18.2.0)(react@18.2.0) next-auth: specifier: ^4.24.6 - version: 4.24.7(next@14.1.3)(react-dom@18.2.0)(react@18.2.0) + version: 4.24.7(next@14.2.1)(react-dom@18.2.0)(react@18.2.0) pnpm: specifier: ^8.15.5 version: 8.15.5 prisma: specifier: ^5.11.0 version: 5.11.0 + query-string: + specifier: ^9.0.0 + version: 9.0.0 react: specifier: 18.2.0 version: 18.2.0 @@ -173,6 +179,9 @@ importers: '@types/eslint': specifier: ^8.56.2 version: 8.56.5 + '@types/mime-types': + specifier: ^2.1.4 + version: 2.1.4 '@types/node': specifier: ^20.11.20 version: 20.11.27 @@ -275,6 +284,12 @@ importers: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-select': + specifier: ^2.0.0 + version: 2.0.0(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.66)(react@18.2.0) @@ -1588,14 +1603,14 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true - /@next/env@14.1.3: - resolution: {integrity: sha512-VhgXTvrgeBRxNPjyfBsDIMvgsKDxjlpw4IAUsHCX8Gjl1vtHUYRT3+xfQ/wwvLPDd/6kqfLqk9Pt4+7gysuCKQ==} - dev: false - /@next/env@14.1.4: resolution: {integrity: sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==} dev: false + /@next/env@14.2.1: + resolution: {integrity: sha512-qsHJle3GU3CmVx7pUoXcghX4sRN+vINkbLdH611T8ZlsP//grzqVW87BSUgOZeSAD4q7ZdZicdwNe/20U2janA==} + dev: false + /@next/eslint-plugin-next@14.1.3: resolution: {integrity: sha512-VCnZI2cy77Yaj3L7Uhs3+44ikMM1VD/fBMwvTBb3hIaTIuqa+DmG4dhUDq+MASu3yx97KhgsVJbsas0XuiKyww==} dependencies: @@ -1608,15 +1623,6 @@ packages: glob: 10.3.10 dev: true - /@next/swc-darwin-arm64@14.1.3: - resolution: {integrity: sha512-LALu0yIBPRiG9ANrD5ncB3pjpO0Gli9ZLhxdOu6ZUNf3x1r3ea1rd9Q+4xxUkGrUXLqKVK9/lDkpYIJaCJ6AHQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@next/swc-darwin-arm64@14.1.4: resolution: {integrity: sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==} engines: {node: '>= 10'} @@ -1626,10 +1632,10 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.1.3: - resolution: {integrity: sha512-E/9WQeXxkqw2dfcn5UcjApFgUq73jqNKaE5bysDm58hEUdUGedVrnRhblhJM7HbCZNhtVl0j+6TXsK0PuzXTCg==} + /@next/swc-darwin-arm64@14.2.1: + resolution: {integrity: sha512-kGjnjcIJehEcd3rT/3NAATJQndAEELk0J9GmGMXHSC75TMnvpOhONcjNHbjtcWE5HUQnIHy5JVkatrnYm1QhVw==} engines: {node: '>= 10'} - cpu: [x64] + cpu: [arm64] os: [darwin] requiresBuild: true dev: false @@ -1644,11 +1650,11 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.1.3: - resolution: {integrity: sha512-USArX9B+3rZSXYLFvgy0NVWQgqh6LHWDmMt38O4lmiJNQcwazeI6xRvSsliDLKt+78KChVacNiwvOMbl6g6BBw==} + /@next/swc-darwin-x64@14.2.1: + resolution: {integrity: sha512-dAdWndgdQi7BK2WSXrx4lae7mYcOYjbHJUhvOUnJjMNYrmYhxbbvJ2xElZpxNxdfA6zkqagIB9He2tQk+l16ew==} engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] + cpu: [x64] + os: [darwin] requiresBuild: true dev: false optional: true @@ -1662,8 +1668,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.1.3: - resolution: {integrity: sha512-esk1RkRBLSIEp1qaQXv1+s6ZdYzuVCnDAZySpa62iFTMGTisCyNQmqyCTL9P+cLJ4N9FKCI3ojtSfsyPHJDQNw==} + /@next/swc-linux-arm64-gnu@14.2.1: + resolution: {integrity: sha512-2ZctfnyFOGvTkoD6L+DtQtO3BfFz4CapoHnyLTXkOxbZkVRgg3TQBUjTD/xKrO1QWeydeo8AWfZRg8539qNKrg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1680,10 +1686,10 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.1.3: - resolution: {integrity: sha512-8uOgRlYEYiKo0L8YGeS+3TudHVDWDjPVDUcST+z+dUzgBbTEwSSIaSgF/vkcC1T/iwl4QX9iuUyUdQEl0Kxalg==} + /@next/swc-linux-arm64-musl@14.2.1: + resolution: {integrity: sha512-jazZXctiaanemy4r+TPIpFP36t1mMwWCKMsmrTRVChRqE6putyAxZA4PDujx0SnfvZHosjdkx9xIq9BzBB5tWg==} engines: {node: '>= 10'} - cpu: [x64] + cpu: [arm64] os: [linux] requiresBuild: true dev: false @@ -1698,8 +1704,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.1.3: - resolution: {integrity: sha512-DX2zqz05ziElLoxskgHasaJBREC5Y9TJcbR2LYqu4r7naff25B4iXkfXWfcp69uD75/0URmmoSgT8JclJtrBoQ==} + /@next/swc-linux-x64-gnu@14.2.1: + resolution: {integrity: sha512-VjCHWCjsAzQAAo8lkBOLEIkBZFdfW+Z18qcQ056kL4KpUYc8o59JhLDCBlhg+hINQRgzQ2UPGma2AURGOH0+Qg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1716,11 +1722,11 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.1.3: - resolution: {integrity: sha512-HjssFsCdsD4GHstXSQxsi2l70F/5FsRTRQp8xNgmQs15SxUfUJRvSI9qKny/jLkY3gLgiCR3+6A7wzzK0DBlfA==} + /@next/swc-linux-x64-musl@14.2.1: + resolution: {integrity: sha512-7HZKYKvAp4nAHiHIbY04finRqjeYvkITOGOurP1aLMexIFG/1+oCnqhGogBdc4lao/lkMW1c+AkwWSzSlLasqw==} engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] + cpu: [x64] + os: [linux] requiresBuild: true dev: false optional: true @@ -1734,10 +1740,10 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.1.3: - resolution: {integrity: sha512-DRuxD5axfDM1/Ue4VahwSxl1O5rn61hX8/sF0HY8y0iCbpqdxw3rB3QasdHn/LJ6Wb2y5DoWzXcz3L1Cr+Thrw==} + /@next/swc-win32-arm64-msvc@14.2.1: + resolution: {integrity: sha512-YGHklaJ/Cj/F0Xd8jxgj2p8po4JTCi6H7Z3Yics3xJhm9CPIqtl8erlpK1CLv+HInDqEWfXilqatF8YsLxxA2Q==} engines: {node: '>= 10'} - cpu: [ia32] + cpu: [arm64] os: [win32] requiresBuild: true dev: false @@ -1752,10 +1758,10 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.1.3: - resolution: {integrity: sha512-uC2DaDoWH7h1P/aJ4Fok3Xiw6P0Lo4ez7NbowW2VGNXw/Xv6tOuLUcxhBYZxsSUJtpeknCi8/fvnSpyCFp4Rcg==} + /@next/swc-win32-ia32-msvc@14.2.1: + resolution: {integrity: sha512-o+ISKOlvU/L43ZhtAAfCjwIfcwuZstiHVXq/BDsZwGqQE0h/81td95MPHliWCnFoikzWcYqh+hz54ZB2FIT8RA==} engines: {node: '>= 10'} - cpu: [x64] + cpu: [ia32] os: [win32] requiresBuild: true dev: false @@ -1770,6 +1776,15 @@ packages: dev: false optional: true + /@next/swc-win32-x64-msvc@14.2.1: + resolution: {integrity: sha512-GmRoTiLcvCLifujlisknv4zu9/C4i9r0ktsA8E51EMqJL4bD4CpO7lDYr7SrUxCR0tS4RVcrqKmCak24T0ohaw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} dependencies: @@ -1854,6 +1869,12 @@ packages: '@prisma/debug': 5.11.0 dev: false + /@radix-ui/number@1.0.1: + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + dependencies: + '@babel/runtime': 7.24.0 + dev: false + /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -2267,6 +2288,68 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-select@2.0.0(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.66)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + aria-hidden: 1.2.4 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.66)(react@18.2.0) + dev: false + + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.66)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -2439,6 +2522,27 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: @@ -2822,12 +2926,23 @@ packages: tslib: 2.6.2 dev: false + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: false + /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: tslib: 2.6.2 dev: false + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + dependencies: + '@swc/counter': 0.1.3 + tslib: 2.6.2 + dev: false + /@t3-oss/env-core@0.9.2(typescript@5.4.2)(zod@3.22.4): resolution: {integrity: sha512-KgWXljUTHgO3o7GMZQPAD5+P+HqpauMNNHowlm7V2b9IeMitSUpNKwG6xQrup/xARWHTdxRVIl0mSI4wCevQhQ==} peerDependencies: @@ -2876,7 +2991,7 @@ packages: '@trpc/server': 11.0.0-next-beta.318 dev: false - /@trpc/next@11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.1.3)(react-dom@18.2.0)(react@18.2.0): + /@trpc/next@11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/react-query@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(next@14.2.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-qeWfJ1vPm7GchLmMZz5Gj+mBka0CRci0bCKEhGoG8RSvI/+9GbbhZHKRRDnlsN81CgexJ2e2nULET9ESO6rt+Q==} peerDependencies: '@tanstack/react-query': ^5.25.0 @@ -2896,7 +3011,7 @@ packages: '@trpc/client': 11.0.0-next-beta.318(@trpc/server@11.0.0-next-beta.318) '@trpc/react-query': 11.0.0-next-beta.318(@tanstack/react-query@5.28.4)(@trpc/client@11.0.0-next-beta.318)(@trpc/server@11.0.0-next-beta.318)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': 11.0.0-next-beta.318 - next: 14.1.3(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -2992,6 +3107,10 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/mime-types@2.1.4: + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + dev: true + /@types/node@20.11.27: resolution: {integrity: sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==} dependencies: @@ -3987,6 +4106,11 @@ packages: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} dev: false + /decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true @@ -4892,6 +5016,11 @@ packages: dependencies: to-regex-range: 5.0.1 + /filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5670,6 +5799,18 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -5719,7 +5860,7 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true - /next-auth@4.24.7(next@14.1.3)(react-dom@18.2.0)(react@18.2.0): + /next-auth@4.24.7(next@14.2.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==} peerDependencies: next: ^12.2.5 || ^13 || ^14 @@ -5734,7 +5875,7 @@ packages: '@panva/hkdf': 1.1.1 cookie: 0.5.0 jose: 4.15.5 - next: 14.1.3(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(react-dom@18.2.0)(react@18.2.0) oauth: 0.9.15 openid-client: 5.6.5 preact: 10.19.6 @@ -5754,45 +5895,6 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /next@14.1.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-oexgMV2MapI0UIWiXKkixF8J8ORxpy64OuJ/J9oVUmIthXOUCcuVEZX+dtpgq7wIfIqtBwQsKEDXejcjTsan9g==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - sass: - optional: true - dependencies: - '@next/env': 14.1.3 - '@swc/helpers': 0.5.2 - busboy: 1.6.0 - caniuse-lite: 1.0.30001597 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(react@18.2.0) - optionalDependencies: - '@next/swc-darwin-arm64': 14.1.3 - '@next/swc-darwin-x64': 14.1.3 - '@next/swc-linux-arm64-gnu': 14.1.3 - '@next/swc-linux-arm64-musl': 14.1.3 - '@next/swc-linux-x64-gnu': 14.1.3 - '@next/swc-linux-x64-musl': 14.1.3 - '@next/swc-win32-arm64-msvc': 14.1.3 - '@next/swc-win32-ia32-msvc': 14.1.3 - '@next/swc-win32-x64-msvc': 14.1.3 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - /next@14.1.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==} engines: {node: '>=18.17.0'} @@ -5832,6 +5934,48 @@ packages: - babel-plugin-macros dev: false + /next@14.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-SF3TJnKdH43PMkCcErLPv+x/DY1YCklslk3ZmwaVoyUfDgHKexuKlf9sEfBQ69w+ue8jQ3msLb+hSj1T19hGag==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.2.1 + '@swc/helpers': 0.5.5 + busboy: 1.6.0 + caniuse-lite: 1.0.30001597 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + optionalDependencies: + '@next/swc-darwin-arm64': 14.2.1 + '@next/swc-darwin-x64': 14.2.1 + '@next/swc-linux-arm64-gnu': 14.2.1 + '@next/swc-linux-arm64-musl': 14.2.1 + '@next/swc-linux-x64-gnu': 14.2.1 + '@next/swc-linux-x64-musl': 14.2.1 + '@next/swc-win32-arm64-msvc': 14.2.1 + '@next/swc-win32-ia32-msvc': 14.2.1 + '@next/swc-win32-x64-msvc': 14.2.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} dev: true @@ -6324,6 +6468,15 @@ packages: engines: {node: '>=6'} dev: true + /query-string@9.0.0: + resolution: {integrity: sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw==} + engines: {node: '>=18'} + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -6753,6 +6906,11 @@ packages: resolution: {integrity: sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==} dev: true + /split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + dev: false + /streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'}