{"version":3,"file":"yotpo-product-reviews-section.min.js","mappings":"wZASA,MAAMA,0BAA0BC,YAC9B,6BAAkBC,GAChB,MAAO,CAAC,cACV,CASO,iBAAAC,GACLC,KAAKC,uBACLD,KAAKE,QACP,CAEO,wBAAAC,CAAyBC,EAAeC,EAAkBC,GAC/DN,KAAKC,uBAEDD,KAAKO,aAAeD,IAAaD,GACnCL,KAAKE,QAET,CAEQ,oBAAAD,GACDD,KAAKQ,aACRR,KAAKS,aAAa,CAAEC,KAAM,SAC1BV,KAAKQ,WAAWG,UAAY,m5DAEhC,CAEQ,MAAAT,GACN,MAAMU,EAASC,SAASb,KAAKc,QAAQF,OAAQ,IAE/BZ,KAAKQ,WAAWO,cAAc,oBACtCC,YAAc,GAAGJ,kBACzB,ECvCI,SAAUK,EAAOC,EAAqBC,GAC1C,IAAKD,EACH,MAAM,IAAIE,eAAeD,EAE7B,CD4CKE,eAAeC,IAAI,wBACtBD,eAAeE,OAAO,sBAAuB3B,mBC3CzC,MAAOwB,uBAAuBI,MAClC,WAAAC,CAAYN,QAAO,IAAPA,IAAAA,EAAU,mBACpBO,MAAMP,GACNnB,KAAK2B,KAAO3B,KAAKyB,YAAYE,IAC/B,ECgBK,SAASC,EAAcC,EAAYC,EAAaC,GACrD,IACEF,EAAMC,GAAOC,CACf,CAAE,MAAOC,GACP,IAAIC,EAEFA,EADED,aAA2BR,MACpBQ,EAAgBb,QAEhB,GAAGa,IAGdE,QAAQC,IAAI,kCAAmC,CAC7CL,MACAH,KAAW,MAALE,OAAK,EAALA,EAAOF,KACbS,IAAKC,OAAOC,UAAUC,SAASC,KAAKX,GACpCI,UAEJ,CACF,CCvCA,MAAMQ,uBAAuB5C,YAC3B,6BAAkBC,GAChB,MAAO,CAAC,mBACV,CA6BQ4C,cAAe,EAKfC,iBAAmB,GAKnBC,cAAgC,GAKhCC,kBAAwC,GAExCC,yBAA2B9C,KAAK+C,oBAAoBC,KAAKhD,MACzDiD,0BAA4BjD,KAAKkD,qBAAqBF,KAAKhD,MAC3DmD,6BAA+BnD,KAAKoD,wBAAwBJ,KAAKhD,MAElE,iBAAAD,GACLC,KAAKC,uBAEL,MAAMoD,EAAarD,KAAKQ,WAAWO,cAA2B,gBACpD,MAAVsC,GAAAA,EAAYC,iBAAiB,QAAStD,KAAK8C,0BAE3C,MAAMS,EAAcvD,KAAKQ,WAAWO,cAAc,gBACvC,MAAXwC,GAAAA,EAAaD,iBAAiB,QAAStD,KAAKiD,2BAE5C,MAAMO,EAAUxD,KAAKQ,WAAWO,cAA2B,aACpD,MAAPyC,GAAAA,EAASF,iBAAiB,QAAStD,KAAKiD,2BAExCjD,KAAKyD,gBAAgBC,MAAM7B,IAIrBA,aAAiBL,OAA2B,gBAAlBK,EAAMV,QAClCe,QAAQC,IAAIN,GAEZK,QAAQyB,KAAK9B,IAGnB,CAEO,oBAAA+B,GACL,MAAMP,EAAarD,KAAKQ,WAAWO,cAA2B,gBACpD,MAAVsC,GAAAA,EAAYQ,oBAAoB,QAAS7D,KAAK8C,0BAE9C,MAAMS,EAAcvD,KAAKQ,WAAWO,cAAc,gBACvC,MAAXwC,GAAAA,EAAaM,oBAAoB,QAAS7D,KAAKiD,2BAE/C,MAAMO,EAAUxD,KAAKQ,WAAWO,cAA2B,aACpD,MAAPyC,GAAAA,EAASK,oBAAoB,QAAS7D,KAAKiD,0BAC7C,CAEO,wBAAA9C,CAAyBwB,EAActB,EAAkBC,GACzDN,KAAKO,aAAeF,IAAaC,GAIzB,qBAATqB,IACe,SAAbrB,EACFN,KAAK8D,aACiB,SAAbzD,GACTL,KAAK+D,cAGX,CAEQ,oBAAA9D,GACDD,KAAKQ,aACRR,KAAKS,aAAa,CAAEC,KAAM,SAC1BV,KAAKQ,WAAWG,UAAY,0hLAEhC,CAMA,mBAAc8C,GAEZ,GAAIzD,KAAK0C,aACP,OAKF,GAFA1C,KAAK0C,cAAe,GAEf1C,KAAKc,QAAQkD,cAAgBhE,KAAKc,QAAQmD,UAC7C,OAGF,MAAMA,EAAYpD,SAASb,KAAKc,QAAQmD,UAAW,IACnD,IAAKA,EACH,OAGF,MAAMC,QCjEHC,eAAmCC,GACxCnD,EAAOmD,EAAMC,QAAS,mBACtBpD,EAAOqD,OAAOC,UAAUH,EAAMI,aAAeJ,EAAMI,WAAa,EAAG,sBAEnE,MAAMC,EAAO,gDAAgDL,EAAMC,oBAC9DD,EAAMI,gDAELN,QAAiBQ,MAAMD,GAE7B,IAAKP,EAASS,GAAI,CAChB,MAAM9C,EAAa,IAAIL,MAAM,mBAAmB0C,EAASU,mCAGzD,MAFA/C,EAAMgD,KAAOX,EAASU,OACtB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CAEA,IAAIkD,EASAC,EARJ,IACED,QAAqBb,EAASe,MAChC,CAAE,MAAOpD,GAGP,MAFAD,EAAcC,EAAO,OAAQqC,EAASU,QACtChD,EAAcC,EAAO,MAAOqC,EAASY,KAC/BjD,CACR,CAGA,IACEmD,EAAOE,KAAKC,MAAMJ,EACpB,CAAE,MAAOlD,GAIP,MAHAD,EAAcC,EAAO,OAAQkD,GAC7BnD,EAAcC,EAAO,OAAQqC,EAASU,QACtChD,EAAcC,EAAO,MAAOqC,EAASY,KAC/BjD,CACR,CAEA,GAoBwB,iBADFE,EAnBFiD,IAoB0B,OAAVjD,GAA0C,iBAAjBA,EAAM6C,QAChD,OAAjB7C,EAAM6C,QAA6C,iBAAnB7C,EAAMmC,UAA4C,OAAnBnC,EAAMmC,SArB5C,CACzB,MAAMrC,EAAa,IAAIL,MAAM,4BAI7B,MAHAK,EAAMmD,KAAOA,EACbnD,EAAMgD,KAAOX,EAASU,OACtB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CAaF,IAAwBE,EAXtB,GAAIiD,EAAKJ,OAAOC,KAAO,IAAK,CAC1B,MAAMhD,EAAa,IAAIL,MAAM,+CAI7B,MAHAK,EAAMmD,KAAOA,EACbnD,EAAMgD,KAAOX,EAASU,OACtB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CAEA,OAAOmD,EAAKd,QACd,CDa2BkB,CAAoB,CACzCf,QAASrE,KAAKc,QAAQkD,YACtBQ,WAAYP,IAGdjE,KAAK2C,kBAA2B,MAARuB,OAAQ,EAARA,EAAUvB,mBAAoB,GACtD3C,KAAK4C,eAAwB,MAARsB,OAAQ,EAARA,EAAUtB,gBAAiB,GAChD5C,KAAK6C,mBAA4B,MAARqB,OAAQ,EAARA,EAAUrB,oBAAqB,GAExD7C,KAAKE,QACP,CAMQ,mBAAA6C,CAAoBsC,GAG1B,KAAMA,EAAMC,kBAAkBC,mBAC5B,OAGFF,EAAMC,OAAOE,aAAa,gBAAiB,QAK3C,MAAMC,EAAa,IAAIC,MAAM,6BAA8B,CACzDC,SAAS,EACTC,UAAU,IAEZP,EAAMC,OAAOO,cAAcJ,GAE3BzF,KAAKc,QAAQgF,WAAa,MAC5B,CAKQ,uBAAA1C,CAAwB2C,GAC9B/F,KAAKc,QAAQgF,WAAa,QAC5B,CAKQ,oBAAA5C,CAAqB6C,GAC3B/F,KAAKc,QAAQgF,WAAa,QAC5B,CAEQ,UAAAhC,GACN,MAAMkC,EAAShG,KAAKQ,WAAWO,cAA2B,WAC1DiF,EAAOC,OAAQ,EAEf3C,iBAAiB,0BAA2BtD,KAAKmD,8BAIjD,MAAM+C,EAAUlG,KAAKQ,WAAWO,cAAc,YAC1CmF,aAAmBrG,cACrBqG,EAAQD,OAAQ,GAGlB,MAAME,EAAiB,IAAIT,MAAM,uBAAwB,CAAEC,SAAS,EAAMC,UAAU,IACpF5F,KAAK6F,cAAcM,GAECH,EAAOjF,cAAc,gBAC7BqF,MAAM,CAAEC,cAAc,GACpC,CAEQ,WAAAtC,GACS/D,KAAKQ,WAAWO,cAA2B,WACnDkF,OAAQ,EAEf,MAAMK,EAAkBtG,KAAKQ,WAAWO,cACtC,qCACa,MAAfuF,GAAAA,EAAiBd,aAAa,gBAAiB,SAI/C,MAAMU,EAAUlG,KAAKQ,WAAWO,cAAc,YAC1CmF,aAAmBrG,cACrBqG,EAAQD,OAAQ,GAGlBpC,oBAAoB,0BAA2B7D,KAAKmD,8BAEpD,MAAMkC,EAAQ,IAAIK,MAAM,wBAAyB,CAAEC,SAAS,EAAMC,UAAU,IAC5E5F,KAAK6F,cAAcR,EACrB,CAEQ,MAAAnF,GAEN,MAAMqG,EAAOvG,KAAKQ,WAAWO,cAA2B,SACpDf,KAAK2C,kBACP4D,EAAKvF,YAAchB,KAAK2C,iBACxB4D,EAAKC,QAAS,IAEdD,EAAKvF,YAAc,GACnBuF,EAAKC,QAAS,GAIhB,MAAMnD,EAAarD,KAAKQ,WAAWO,cAA2B,gBAC9D,IAAK,MAAM0F,KAAezG,KAAK4C,cAAe,CAC5C,MAAM8D,EAAQC,SAASC,cAAc,UACrCF,EAAMG,UAAUC,IAAI,cACpBJ,EAAM1F,YAAc,GAAGyF,EAAYM,iBAAiBN,EAAYO,YAChEN,EAAMlB,aAAa,gBAAiB,SACpCkB,EAAMlB,aAAa,gBAAiB,UACpCkB,EAAMlB,aAAa,gBAAiB,UAEpCnC,EAAW4D,YAAYP,EACzB,CAKA,MAAMQ,EAAQrG,SAASb,KAAKc,QAAQqG,aAAc,IAC5CC,EAAepH,KAAKQ,WAAWO,cAAc,UAC7CsG,EAAY,IAAIC,KAAKC,aAAa,SACxCH,EAAapG,YAAcqG,EAAUG,OAAON,GAG5C,MAAMO,EAASzH,KAAKQ,WAAWO,cAA2B,WAC1D,IAAK,MAAM2F,KAAS1G,KAAK4C,cAAe,CACtC,MAAM8E,EAAQf,SAASC,cAAc,MACrCc,EAAM1G,YAAc0F,EAAMK,aAE1B,MAAMR,EAAOI,SAASC,cAAc,KACpCL,EAAKvF,YAAc0F,EAAMiB,QAEzB,MAAMC,EAAYjB,SAASC,cAAc,KACzCgB,EAAUf,UAAUC,IAAI,aACxBc,EAAU5G,YAAc,gBAAgB0F,EAAMM,sBAE9C,MAAMa,EAAKlB,SAASC,cAAc,MAClCiB,EAAGZ,YAAYS,GACfG,EAAGZ,YAAYV,GACfsB,EAAGZ,YAAYW,GAEfH,EAAOK,OAAOD,EAChB,CAGA,MAAME,EAAiB/H,KAAKQ,WAAWO,cAA2B,kBAClE,IAAK,MAAMiH,KAAahI,KAAK6C,kBAAmB,CAC9C,MAAMoF,EAAatB,SAASC,cAAc,cAC1CqB,EAAWjH,YAAcgH,EAAUA,UAEnC,MAAMH,EAAKlB,SAASC,cAAc,MAClCiB,EAAGZ,YAAYgB,GAEfF,EAAed,YAAYY,EAC7B,CAGA,MAAM3B,EAAUlG,KAAKQ,WAAWO,cAA2B,YACvDf,KAAK6C,kBAAkBqF,OAAS,GAAKlI,KAAK4C,cAAcsF,OAAS,GACnElI,KAAK2C,iBACLuD,EAAQM,QAAS,EAEjBN,EAAQM,QAAS,CAErB,EASGnF,eAAeC,IAAI,qBACtBD,eAAeE,OAAO,mBAAoBkB,wOEtT5C,MAAM0F,qBAAqBtI,YAajBuI,wBAA0BpI,KAAKqI,mBAAmBrF,KAAKhD,MACvDsI,qBAAuBtI,KAAKuI,gBAAgBvF,KAAKhD,MACjDwI,sBAAwBxI,KAAKyI,iBAAiBzF,KAAKhD,MACnD0I,qBAAuB1I,KAAK2I,gBAAgB3F,KAAKhD,MACjD4I,mBAAqB5I,KAAK6I,cAAc7F,KAAKhD,MAC7C8I,oBAAsB9I,KAAK+I,eAAe/F,KAAKhD,MAC/CgJ,0BAA4BhJ,KAAKiJ,qBAAqBjG,KAAKhD,MAC3DkJ,2BAA6BlJ,KAAKmJ,sBAAsBnG,KAAKhD,MAC7DoJ,mBAAqBpJ,KAAKqJ,cAAcrG,KAAKhD,MAC7CsJ,uBAAyBtJ,KAAKuJ,kBAAkBvG,KAAKhD,MACrDwJ,sBAAwBxJ,KAAKyJ,iBAAiBzG,KAAKhD,MACnD0J,qBAAuB1J,KAAK2J,gBAAgB3G,KAAKhD,MACjD4J,uBAAyB5J,KAAK6J,kBAAkB7G,KAAKhD,MAEtD,iBAAAD,GACLC,KAAKC,uBAEL,MAAM6J,EAAc9J,KAAKQ,WAAWO,cAAgC,iBACzD,MAAX+I,GAAAA,EAAaxG,iBAAiB,QAAStD,KAAK4I,oBACjC,MAAXkB,GAAAA,EAAaxG,iBAAiB,SAAUtD,KAAK0I,sBAE7C,MAAMqB,EAAiB/J,KAAKQ,WAAWO,cAAc,4BACvC,MAAdgJ,GAAAA,EAAgBzG,iBAAiB,SAAUtD,KAAKoI,yBAEhD,MAAM4B,EAAchK,KAAKQ,WAAWO,cAAc,wBACvC,MAAXiJ,GAAAA,EAAa1G,iBAAiB,SAAUtD,KAAKsI,sBAE7C,MAAM2B,EAAejK,KAAKQ,WAAWO,cAAc,yBACvC,MAAZkJ,GAAAA,EAAc3G,iBAAiB,SAAUtD,KAAKwI,uBAE9C,MAAM0B,EAAalK,KAAKQ,WAAWO,cAAc,uBACvC,MAAVmJ,GAAAA,EAAY5G,iBAAiB,SAAUtD,KAAK8I,qBAE5CxF,iBAAiB,sBAAuBtD,KAAKgJ,2BAC7C1F,iBAAiB,yBAA0BtD,KAAKkJ,4BAChD5F,iBAAiB,+BAAgCtD,KAAKwJ,uBACtDlG,iBAAiB,+BAAgCtD,KAAK0J,sBACtDpG,iBAAiB,yBAA0BtD,KAAK4J,uBAClD,CAEO,oBAAAhG,GACL,MAAMkG,EAAc9J,KAAKQ,WAAWO,cAAgC,iBACzD,MAAX+I,GAAAA,EAAajG,oBAAoB,QAAS7D,KAAK4I,oBACpC,MAAXkB,GAAAA,EAAajG,oBAAoB,SAAU7D,KAAK0I,sBAEhD,MAAMqB,EAAiB/J,KAAKQ,WAAWO,cAAc,4BACvC,MAAdgJ,GAAAA,EAAgBlG,oBAAoB,SAAU7D,KAAKoI,yBAEnD,MAAM4B,EAAchK,KAAKQ,WAAWO,cAAc,wBACvC,MAAXiJ,GAAAA,EAAanG,oBAAoB,SAAU7D,KAAKsI,sBAEhD,MAAM2B,EAAejK,KAAKQ,WAAWO,cAAc,yBACvC,MAAZkJ,GAAAA,EAAcpG,oBAAoB,SAAU7D,KAAKwI,uBAEjD,MAAM2B,EAAOnK,KAAKQ,WAAWO,cAAc,uBACvC,MAAJoJ,GAAAA,EAAMtG,oBAAoB,SAAU7D,KAAK8I,qBAEzC,MAAMsB,EAAQpK,KAAKQ,WAAW6J,iBAA8B,SAC5D,IAAK,MAAMC,KAAQF,EACjBE,EAAKzG,oBAAoB,QAAS7D,KAAKoJ,oBAGzC,MAAMmB,EAAWvK,KAAKQ,WAAWO,cAA2B,UACpD,MAARwJ,GAAAA,EAAU1G,oBAAoB,QAAS7D,KAAKsJ,wBAE5CzF,oBAAoB,sBAAuB7D,KAAKgJ,2BAChDnF,oBAAoB,yBAA0B7D,KAAKkJ,4BACnDrF,oBAAoB,+BAAgC7D,KAAKwJ,uBACzD3F,oBAAoB,+BAAgC7D,KAAK0J,sBACzD7F,oBAAoB,yBAA0B7D,KAAK4J,uBACrD,CAEQ,oBAAA3J,GACDD,KAAKQ,aACRR,KAAKS,aAAa,CAAEC,KAAM,SAC1BV,KAAKQ,WAAWG,UAAY,okJAEhC,CAEQ,eAAAgI,CAAgBtD,GAClBA,EAAMmF,yBAAyBC,kBAAkD,KAA9BpF,EAAMmF,cAAczI,QACzE2I,aAAa1K,KAAK2K,qBAClB3K,KAAK4K,eAAe,CAAEC,sBAAkBC,IAE5C,CAEQ,aAAAjC,CAAcxD,GAGpB,GAFAA,EAAM0F,oBAEA1F,EAAMmF,yBAAyBC,kBACnC,OAGF,MAAMO,EAAQ3F,EAAMmF,cAAczI,MAIlC,GAFA2I,aAAa1K,KAAK2K,sBAEbK,EAEH,YADAhL,KAAK4K,eAAe,CAAEC,sBAAkBC,IAI1C,IAAIG,EAAU,IACVD,EAAM9C,OAAS,IACjB+C,GAAW,KAGbjL,KAAK2K,oBAAsBO,WAAWlL,KAAK4K,eAAe5H,KAAKhD,MAAOiL,EACpE,CAAEJ,iBAAkBG,GACxB,CAEQ,kBAAA3C,CAAmBhD,GACnBA,EAAMC,kBAAkB6F,oBAIH,KAAvB9F,EAAMC,OAAOvD,MAKjB/B,KAAK4K,eAAe,CAAEQ,UAAW/F,EAAMC,OAAOvD,QAJ5C/B,KAAK4K,eAAe,CAAEQ,eAAWN,IAKrC,CAEQ,gBAAArC,CAAiBpD,GACvB,KAAMA,EAAMC,kBAAkB6F,mBAC5B,OAGF,GAA2B,MAAvB9F,EAAMC,OAAOvD,MAEf,YADA/B,KAAK4K,eAAe,CAAES,OAAQ,KAIhC,MAAMC,EAAQzK,SAASwE,EAAMC,OAAOvD,MAAO,IAC3C/B,KAAK4K,eAAe,CAAES,OAAQ,CAACC,IACjC,CAKQ,oBAAArC,CAAqB5D,GACZrF,KAAKQ,WAAWO,cAAiC,yBACzDgB,MAAQsD,EAAMkG,OAAOC,MAAMjJ,WAClCvC,KAAK4K,eAAe,CAAES,OAAQ,CAAChG,EAAMkG,OAAOC,QAC9C,CAEQ,eAAAjD,CAAgBlD,GAChBA,EAAMC,kBAAkB6F,oBAIH,KAAvB9F,EAAMC,OAAOvD,MAKjB/B,KAAK4K,eAAe,CAAEa,SAAiC,SAAvBpG,EAAMC,OAAOvD,QAJ3C/B,KAAK4K,eAAe,CAAEa,SAAU,OAKpC,CAEQ,cAAA1C,CAAe1D,GACrB,KAAMA,EAAMC,kBAAkB6F,mBAC5B,OAGF,GAA2B,MAAvB9F,EAAMC,OAAOvD,MACf,OAGF,MAAMoI,EAAO9E,EAAMC,OAAOvD,MAE1B,IAAI2J,GAAY,EACZ/J,EAAOwI,EAEX,GAAIA,EAAKwB,SAAS,SAAU,CAC1BhK,EAAO,QACP,MAAM,CAAEiK,GAAazB,EAAK0B,MAAM,KACd,OAAdD,IACFF,GAAY,EAEhB,CAEA1L,KAAK4K,eAAe,CAAC,EAAG,CAAC,CAAEkB,QAASnK,EAAM+J,cAC5C,CAEQ,cAAAd,CAAemB,EAA6BC,GAClD,IAAKhM,KAAKc,QAAQmD,UAChB,OAGF,MACMgI,EAAMC,EAAA,GADKlM,KAAKmM,iBAAmB,CAAC,EACTJ,GAG3B1G,EAAQ,IAAI+G,YAAoB,yBAA0B,CAC9DzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNQ,QAASE,EAKTI,KAAM,EAENpI,UAAWpD,SAASb,KAAKc,QAAQmD,UAAW,IAI5C+H,SAAUA,GAAYhM,KAAKsM,oBAG/BtM,KAAK6F,cAAcR,EACrB,CAEQ,qBAAA8D,CAAsB9D,GAC5BrF,KAAKmM,gBAAkB9G,EAAMkG,OAAOQ,QACpC/L,KAAKsM,iBAAmBjH,EAAMkG,OAAOS,SACrChM,KAAKuM,sBACP,CAEQ,oBAAAA,GACN,MAAMC,EAAYxM,KAAKQ,WAAWO,cAA2B,UAC7D,IAAKyL,EACH,OAIF,MAAMC,EAAgBD,EAAUnC,iBAA8B,SAC9D,IAAK,MAAMC,KAAQmC,EACjBnC,EAAKzG,oBAAoB,QAAS7D,KAAKoJ,oBAGzC,MAAMsD,EAAcF,EAAUzL,cAA2B,UACrD2L,GACFA,EAAY7I,oBAAoB,QAAS7D,KAAKsJ,wBAGhDkD,EAAU7L,UAAY,GAEtB,MAAMoL,EAAU/L,KAAKmM,gBACrB,IAAKJ,EAEH,YADAS,EAAUhG,QAAS,GAKrB,MAAM4D,EAAiD,GAEvD,GAAI2B,EAAQV,QAAUU,EAAQV,OAAOnD,OAAS,EAC5C,IAAK,MAAMoD,KAASS,EAAQV,OAC1BjB,EAAMuC,KAAK,CAAEC,UAAW,SAAUC,MAAO,WAAavB,EAAQ,WAmBlE,IAfyB,IAArBS,EAAQN,SACVrB,EAAMuC,KAAK,CAAEC,UAAW,WAAYC,MAAO,0BACb,IAArBd,EAAQN,UACjBrB,EAAMuC,KAAK,CAAEC,UAAW,WAAYC,MAAO,4BAGzCd,EAAQX,WACVhB,EAAMuC,KAAK,CAAEC,UAAW,YAAaC,MAAO,cAAgBd,EAAQX,YAGlEW,EAAQlB,kBACVT,EAAMuC,KAAK,CAAEC,UAAW,mBAAoBC,MAAO,WAAad,EAAQlB,mBAIrD,IAAjBT,EAAMlC,OAER,YADAsE,EAAUhG,QAAS,GAIrB,IAAK,MAAM8D,KAAQF,EAAO,CACxB,MAAM0C,EAASnG,SAASC,cAAc,UACtCkG,EAAOC,UAAY,OACnBD,EAAOhM,QAAQ8L,UAAYtC,EAAKsC,UAChCE,EAAOxJ,iBAAiB,QAAStD,KAAKoJ,oBAEtC,MAAM4D,EAAOrG,SAASC,cAAc,QACpCoG,EAAKhM,YAAcsJ,EAAKuC,MACxBC,EAAO7F,YAAY+F,GACnBR,EAAUvF,YAAY6F,EACxB,CAEA,MAAMvC,EAAW5D,SAASC,cAAc,UACxC2D,EAASwC,UAAY,QACrBxC,EAASvJ,YAAc,YACvBuJ,EAASjH,iBAAiB,QAAStD,KAAKsJ,wBACxCkD,EAAUvF,YAAYsD,GAEtBiC,EAAUhG,QAAS,CACrB,CAEQ,aAAA6C,CAAchE,GACpB,KAAMA,EAAMmF,yBAAyB3K,aACnC,OAGF,MAAM+M,EAAYvH,EAAMmF,cAAc1J,QAAQ8L,UAC9C,GAAKA,EAIL,GAAkB,WAAdA,EAAwB,CAC1B,MAAMK,EAASjN,KAAKQ,WAAWO,cAAiC,yBAC5DkM,IACFA,EAAOlL,MAAQ,KAGjB/B,KAAK4K,eAAe,CAAES,OAAQ,IAChC,MAAO,GAAkB,aAAduB,EAA0B,CACnC,MAAMK,EAASjN,KAAKQ,WAAWO,cAAiC,wBAC5DkM,IACFA,EAAOlL,MAAQ,IAGjB/B,KAAK4K,eAAe,CAAEa,SAAU,MAClC,MAAO,GAAkB,cAAdmB,EAA2B,CACpC,MAAMK,EAASjN,KAAKQ,WAAWO,cAAiC,4BAC5DkM,IACFA,EAAOlL,MAAQ,IAGjB/B,KAAK4K,eAAe,CAAEQ,eAAWN,GACnC,MAAO,GAAkB,qBAAd8B,EAAkC,CAC3C,MAAMxI,EAAQpE,KAAKQ,WAAWO,cAAgC,iBAC1DqD,IACFA,EAAMrC,MAAQ,IAGhB/B,KAAK4K,eAAe,CAAEC,sBAAkBC,GAC1C,CACF,CAEQ,iBAAAvB,CAAkBxD,GACxB/F,KAAKkN,iBACP,CAEQ,gBAAAzD,CAAiB1D,GACvB/F,KAAKkN,iBACP,CAEQ,eAAAvD,GACN,MAAMwD,EAASnN,KAAKQ,WAAW6J,iBAC7B,iBACF,IAAK,MAAMjG,KAAS+I,EAClB/I,EAAMgJ,UAAW,CAErB,CAEQ,iBAAAvD,GACN,MAAMsD,EAASnN,KAAKQ,WAAW6J,iBAC7B,iBACF,IAAK,MAAMjG,KAAS+I,EAClB/I,EAAMgJ,UAAW,CAErB,CAEQ,eAAAF,GACN,MAAMjD,EAAejK,KAAKQ,WAAWO,cAAiC,yBAClEkJ,IACFA,EAAalI,MAAQ,KAGvB,MAAMiI,EAAchK,KAAKQ,WAAWO,cAAiC,wBACjEiJ,IACFA,EAAYjI,MAAQ,IAGtB,MAAMgI,EAAiB/J,KAAKQ,WAAWO,cACrC,4BACEgJ,IACFA,EAAehI,MAAQ,IAGzB,MAAM+H,EAAc9J,KAAKQ,WAAWO,cAAgC,iBAChE+I,IACFA,EAAY/H,MAAQ,IAGtB/B,KAAK4K,eAAe,CAClBC,sBAAkBC,EAClBM,eAAWN,EACXW,SAAU,KACVJ,OAAQ,IAEZ,EASGhK,eAAeC,IAAI,kBACtBD,eAAeE,OAAO,gBAAiB4G,cC5ZzC,MAAMkF,wBAAwBxN,YAC5B,6BAAkBC,GAChB,MAAO,CAAC,YAAa,cACvB,CA2BQwN,YAAc,EAKdC,SAAW,EAUXC,WAAa,EAObC,6BAA+BzN,KAAK0N,wBAAwB1K,KAAKhD,MACjE2N,yBAA2B3N,KAAK4N,oBAAoB5K,KAAKhD,MACzD6N,mBAAqB7N,KAAK8N,cAAc9K,KAAKhD,MAC7C+N,yBAA2B/N,KAAKgO,oBAAoBhL,KAAKhD,MAE1D,iBAAAD,GACLC,KAAKC,uBAEL,MAAMgO,EAAajO,KAAKQ,WAAWO,cAA2B,oBACpD,MAAVkN,GAAAA,EAAY3K,iBAAiB,QAAStD,KAAKyN,8BAE3C,MAAMS,EAAalO,KAAKQ,WAAWO,cAA2B,oBACpD,MAAVmN,GAAAA,EAAY5K,iBAAiB,QAAStD,KAAK2N,0BAE3C,MAAMQ,EAAQnO,KAAKQ,WAAWO,cAA2B,qBACpD,MAALoN,GAAAA,EAAO7K,iBAAiB,QAAStD,KAAK6N,oBAEtC7N,KAAKoO,WAAaC,WAAW,sBAC7BrO,KAAKoO,WAAW9K,iBAAiB,SAAUtD,KAAK+N,0BAI5C/N,KAAKoO,WAAWE,UAClBtO,KAAKwN,WAAa,GAGpBxN,KAAKE,QACP,CAEO,oBAAA0D,GACL,MAAMqK,EAAajO,KAAKQ,WAAWO,cAA2B,oBACpD,MAAVkN,GAAAA,EAAYpK,oBAAoB,QAAS7D,KAAKyN,8BAE9C,MAAMS,EAAalO,KAAKQ,WAAWO,cAA2B,oBACpD,MAAVmN,GAAAA,EAAYrK,oBAAoB,QAAS7D,KAAK2N,0BAE9C,MAAMQ,EAAQnO,KAAKQ,WAAWO,cAA2B,qBACpD,MAALoN,GAAAA,EAAOtK,oBAAoB,QAAS7D,KAAK6N,oBAErC7N,KAAKoO,YACPpO,KAAKoO,WAAWvK,oBAAoB,SAAU7D,KAAK+N,yBAEvD,CAEO,wBAAA5N,CAAyBwB,EAActB,EAAkBC,GAG9D,GAFAN,KAAKC,uBAEDI,IAAaC,EACf,OAAQqB,GACN,IAAK,YACH3B,KAAKsN,YAAczM,SAASP,EAAU,IACtC,MACF,IAAK,cACHN,KAAKE,SAIb,CAEQ,oBAAAD,GACDD,KAAKQ,aACRR,KAAKS,aAAa,CAAEC,KAAM,SAC1BV,KAAKQ,WAAWG,UAAY,0rDAEhC,CAEQ,uBAAA+M,CAAwB3H,GAC9B,GAAyB,IAArB/F,KAAKsN,YACP,OAQF,MAAMjI,EAAQ,IAAI+G,YAAoB,qBAAsB,CAC1DzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNc,KAAMrM,KAAKsN,YAAc,KAG7BtN,KAAK6F,cAAcR,EACrB,CAEQ,mBAAAuI,CAAoB7H,GAE1B,MAAMV,EAAQ,IAAI+G,YAAoB,qBAAsB,CAC1DzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNc,KAAMrM,KAAKsN,YAAc,EAAItN,KAAKuN,SAAWvN,KAAKuN,SAAWvN,KAAKsN,YAAc,KAGpFtN,KAAK6F,cAAcR,EACrB,CAMQ,aAAAyI,CAAczI,GAIpB,KAAMA,EAAMC,kBAAkBC,mBAC5B,OAKF,IAAKF,EAAMC,OAAOuB,UAAU0H,SAAS,eACnC,OAKF,GAAIlJ,EAAMC,OAAOuB,UAAU0H,SAAS,UAClC,OAIF,MAAMC,EAAc,IAAIpC,YAAoB,qBAAsB,CAChEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNc,KAAMxL,SAASwE,EAAMC,OAAOxE,QAAQ2N,IAAK,OAG7CzO,KAAK6F,cAAc2I,EACrB,CAOQ,mBAAAR,CAAoBU,GAC1B,IAAIC,EAEFA,EADED,EAAeJ,QACD,EAEA,EAIdK,IAAkB3O,KAAKwN,aACzBxN,KAAKwN,WAAamB,EAClB3O,KAAKE,SAET,CAEQ,MAAAA,GACN,MAAM0O,EAAc5O,KAAKQ,WAAWO,cAA2B,qBAK/D6N,EAAYjO,UAAY,GAExB,MAAMkO,EAAa7O,KAAKQ,WAAWO,cAAiC,oBAChE8N,IACFA,EAAWrI,QAAS,GAGtB,MAAMsI,EAAa9O,KAAKQ,WAAWO,cAAiC,oBAChE+N,IACFA,EAAWtI,QAAS,GAGtB,MAAMuI,EAAUlO,SAASb,KAAKc,QAAQiO,QAAS,IACzC7H,EAAQrG,SAASb,KAAKc,QAAQoG,MAAO,KAAO,EAElD,GAAyB,IAArBlH,KAAKsN,aAAiC,IAAZyB,GAA2B,IAAV7H,EAC7C,OAIE2H,IACuB,IAArB7O,KAAKsN,YACPuB,EAAWzB,UAAW,EAEtByB,EAAWzB,UAAW,GAI1BpN,KAAKuN,SAAWyB,KAAKC,KAAK/H,EAAQ6H,GAIlC,MAAMG,EAAkBF,KAAKG,MAAMnP,KAAKwN,WAAa,GAGjDsB,IACE9O,KAAKsN,aAAetN,KAAKuN,SAC3BuB,EAAW1B,UAAW,EAEtB0B,EAAW1B,UAAW,GAKtBpN,KAAKuN,SAAWvN,KAAKwN,aACnBqB,IACFA,EAAWrI,QAAS,GAGlBsI,IACFA,EAAWtI,QAAS,IASxB,IAAI4I,EAAgBpP,KAAKsN,YAAc4B,EAInCE,GAAiB,IACnBA,EAAgB,GAMdA,EAAgBpP,KAAKwN,WAAaxN,KAAKuN,WACzC6B,EAAiBpP,KAAKuN,SAAWvN,KAAKwN,WAAc,GAMtD,IAAI6B,EAAyBrP,KAAKwN,WAC9BxN,KAAKuN,SAAWvN,KAAKwN,aACvB6B,EAAyBrP,KAAKuN,SAC9B6B,EAAgB,GAMlB,MAAME,EAAoBF,EAAgB,EAKpCG,EAAqBH,EAAgBC,EAAyB,EAAIrP,KAAKuN,SAG7E,IAAK,IAAIiC,EAAI,EAAGA,EAAIH,EAAwBG,IAAK,CAG/C,IAAIC,EAAeL,EAAgBI,EAG/BA,EAAI,IAAMH,IACZI,EAAezP,KAAKuN,UAKlB+B,GAA2B,IAANE,IACvBC,EAAe,GAQjB,GAF6BH,GAA2B,IAANE,GAChDD,GAAsBC,IAAMH,EAAyB,EAC7B,CACxB,MAAMK,EAAW/I,SAASC,cAAc,QACxC8I,EAAS1O,YAAc,MACvB0O,EAAS7I,UAAUC,IAAI,eAEvB,MAAMe,EAAKlB,SAASC,cAAc,MAClCiB,EAAGZ,YAAYyI,GACfd,EAAY3H,YAAYY,EAC1B,KAAO,CACL,MAAMiF,EAASnG,SAASC,cAAc,UACtCkG,EAAO6C,KAAO,SACd7C,EAAOjG,UAAUC,IAAI,eACrBgG,EAAOhM,QAAQ2N,IAAM,GAAGgB,IACxB3C,EAAO9L,YAAc,GAAGyO,IACxB3C,EAAOtH,aAAa,aAAc,cAAciK,KAG5CzP,KAAKsN,cAAgBmC,IACvB3C,EAAOjG,UAAUC,IAAI,UACrBgG,EAAOtH,aAAa,eAAgB,SAGtC,MAAMqC,EAAKlB,SAASC,cAAc,MAClCiB,EAAGZ,YAAY6F,GACf8B,EAAY3H,YAAYY,EAC1B,CACF,CACF,EAoBGxG,eAAeC,IAAI,qBACtBD,eAAeE,OAAO,mBAAoB8L,iBC7X5C,MAAMuC,EAAQ,IAAIC,WAAW,ICC7B,MAAMC,EAAY,GAClB,IAAK,IAAIN,EAAI,EAAGA,EAAI,MAAOA,EACvBM,EAAUnD,MAAM6C,EAAI,KAAOjN,SAAS,IAAIwN,MAAM,IAE3C,SAASC,EAAgBC,EAAKC,GACjC,YADuC,IAANA,IAAAA,EAAS,IAClCJ,EAAUG,EAAIC,EAAS,IAC3BJ,EAAUG,EAAIC,EAAS,IACvBJ,EAAUG,EAAIC,EAAS,IACvBJ,EAAUG,EAAIC,EAAS,IACvB,IACAJ,EAAUG,EAAIC,EAAS,IACvBJ,EAAUG,EAAIC,EAAS,IACvB,IACAJ,EAAUG,EAAIC,EAAS,IACvBJ,EAAUG,EAAIC,EAAS,IACvB,IACAJ,EAAUG,EAAIC,EAAS,IACvBJ,EAAUG,EAAIC,EAAS,IACvB,IACAJ,EAAUG,EAAIC,EAAS,KACvBJ,EAAUG,EAAIC,EAAS,KACvBJ,EAAUG,EAAIC,EAAS,KACvBJ,EAAUG,EAAIC,EAAS,KACvBJ,EAAUG,EAAIC,EAAS,KACvBJ,EAAUG,EAAIC,EAAS,MAAMC,aACrC,CCEA,IAAAC,EA1BA,SAAYC,EAASC,EAAKJ,GACtB,OAAKI,GAAQD,IAAWE,OAAOC,WAKnC,SAAaH,EAASC,EAAKJ,GAAQ,IAAAO,EAAAC,EAC/BL,EAAUA,GAAW,CAAC,EACtB,MAAMM,EAAwC,OAApCF,EAAiB,OAAjBC,EAAGL,EAAQO,QAAMF,EAAe,MAAXL,EAAQQ,SAAG,EAAXR,EAAQQ,OAAOJ,EFRvCF,OAAOO,gBAAgBlB,GES9B,GAAIe,EAAKzI,OAAS,GACd,MAAM,IAAI1G,MAAM,qCAIpB,GAFAmP,EAAK,GAAgB,GAAVA,EAAK,GAAa,GAC7BA,EAAK,GAAgB,GAAVA,EAAK,GAAa,IACzBL,EAAK,CAEL,IADAJ,EAASA,GAAU,GACN,GAAKA,EAAS,GAAKI,EAAIpI,OAChC,MAAM,IAAI6I,WAAW,mBAAmBb,KAAUA,EAAS,8BAE/D,IAAK,IAAIV,EAAI,EAAGA,EAAI,KAAMA,EACtBc,EAAIJ,EAASV,GAAKmB,EAAKnB,GAE3B,OAAOc,CACX,CACA,OAAON,EAAgBW,EAC3B,CArBWK,CAAIX,EAASC,EAAKJ,GAFdK,OAAOC,YAGtB,ECPO,IAAIS,ECAAC,GDCX,SAAWD,GAMPA,EAA8B,cAAI,eAKlCA,EAAoC,oBAAI,oBAKxCA,EAAoC,oBAAI,oBAKxCA,EAA8B,cAAI,cACrC,CAtBD,CAsBGA,IAAmBA,EAAiB,CAAC,ICtBxC,SAAWC,GACPA,EAA+B,UAAI,YACnCA,EAA8B,SAAI,WAClCA,EAA6B,QAAI,UACjCA,EAAyB,IAAI,MAC7BA,EAA2B,MAAI,QAC/BA,EAA+B,UAAI,WACtC,CAPD,CAOGA,IAAwBA,EAAsB,CAAC,ICKlD,MAAMC,0BAA0BtR,YAC9B,6BAAkBC,GAChB,MAAO,CAAC,mBACV,CAyCQsR,mBAAqBpR,KAAKqR,cAAcrO,KAAKhD,MAC7CsR,gCAAkCtR,KAAKuR,2BAA2BvO,KAAKhD,MACvEwR,2BAA6BxR,KAAKyR,sBAAsBzO,KAAKhD,MAC7DiD,0BAA4BjD,KAAKkD,qBAAqBF,KAAKhD,MAC3DmD,6BAA+BnD,KAAKoD,wBAAwBJ,KAAKhD,MAElE,iBAAAD,GACLC,KAAKC,uBAEqBD,KAAKQ,WAAWO,cAAiC,iBACzDuC,iBAAiB,QAAStD,KAAKsR,iCAE5BtR,KAAKQ,WAAWO,cAAiC,yBACzDuC,iBAAiB,QAAStD,KAAKwR,4BAExBxR,KAAKQ,WAAWO,cAAc,gBACtCuC,iBAAiB,QAAStD,KAAKiD,2BAEhBjD,KAAKQ,WAAWO,cAA2B,iBACnDuC,iBAAiB,QAAStD,KAAKoR,mBACpD,CAEO,oBAAAxN,GACL,MAAM8N,EAAoB1R,KAAKQ,WAAWO,cAAiC,iBAC1D,MAAjB2Q,GAAAA,EAAmB7N,oBAAoB,QAAS7D,KAAKsR,iCAErD,MAAMK,EAAe3R,KAAKQ,WAAWO,cAAiC,yBAC1D,MAAZ4Q,GAAAA,EAAc9N,oBAAoB,QAAS7D,KAAKwR,4BAEhD,MAAMjO,EAAcvD,KAAKQ,WAAWO,cAAc,gBACvC,MAAXwC,GAAAA,EAAaM,oBAAoB,QAAS7D,KAAKiD,2BAE/C,MAAM2O,EAAqB5R,KAAKQ,WAAWO,cAA2B,iBACpD,MAAlB6Q,GAAAA,EAAoB/N,oBAAoB,QAAS7D,KAAKoR,mBACxD,CAEO,wBAAAjR,CAAyBwB,EAActB,EAAkBC,GACzDN,KAAKO,aAAeF,IAAaC,GAIzB,qBAATqB,IACe,SAAbrB,EACFN,KAAK8D,aACiB,SAAbzD,GACTL,KAAK+D,cAGX,CAEQ,oBAAA9D,GACDD,KAAKQ,aACRR,KAAKS,aAAa,CAAEC,KAAM,SAC1BV,KAAKQ,WAAWG,UAAY,yhLAEhC,CAEQ,qBAAA8Q,CAAsBpM,GAC5BA,EAAMwM,iBACN,MAAMF,EAAetM,EAAMmF,cAC3B,KAAMmH,aAAwBpM,mBAC5B,OAGF,MAAMuM,EAAO9R,KAAKQ,WAAWO,cAAc,QAG3C,IADgB+Q,EAAKC,gBACP,KAAAC,EAEZ,IAAIC,EAA4B,KAChC,IAAK,MAAMC,KAAWJ,EAAKK,UACnBD,aAAmBzH,kBAAoByH,aAAmBE,uBAI5DF,EAAQH,gBACVG,EAAQG,cAAcxL,UAAUyL,OAAO,UAIzCJ,EAAQG,cAAcxL,UAAUC,IAAI,SACpCmL,IAAiBC,IAUfD,aAAwBxH,kBAA0C,UAAtBwH,EAAatQ,OAC3DsQ,EAAejS,KAAKQ,WAAWO,cAAiC,UAGtD,OAAZiR,EAAAC,IAAAD,EAAc5L,MAAM,CAAEC,cAAc,IAMpC,YAHqBrG,KAAKQ,WAAWO,cAA2B,kBACnDyF,QAAS,EAGxB,CAEA,MAAM+L,EAAevS,KAAKQ,WAAWO,cAA2B,kBAChEwR,EAAa/L,QAAS,EAEtB,MAAMgM,EAAW,IAAIC,SAASX,GAGxBY,EAAc,GAAGF,EAASlR,IAAI,iBAAiBkR,EAASlR,IAAI,eAC5DqR,EAAoC,CACxCC,OAAQ5S,KAAKc,QAAQkD,YACrB6O,aAAcH,EACdI,MAAeN,EAASlR,IAAI,SAC5ByR,cAAe/S,KAAKc,QAAQkS,aAC5BC,YAAajT,KAAKc,QAAQoS,WAC1BC,eAAwBX,EAASlR,IAAI,UACrC8R,aAAcvS,SAAiB2R,EAASlR,IAAI,SAAU,IACtD+R,aAAsBb,EAASlR,IAAI,SACnCgS,IAAKtT,KAAKc,QAAQmD,WAGdsP,EAASvT,KAAKQ,WAAWO,cAAc,qBAC7C4Q,EAAavE,UAAW,EACxBmG,EAAO/M,QAAS,ECWbrC,eAA4BC,GACjCnD,EAAOmD,EAAMwO,OAAQ,mBACrB,MAAMnO,EAAO,oCAEb,IAAIP,EAwBAa,EAUAyO,EAjCJ,IACEtP,QAAiBQ,MAAMD,EAAM,CAC3BO,KAAME,KAAKuO,UAAUrP,GACrBsP,QAAS,CACP,eAAgB,oBAElBC,OAAQ,OACRjT,KAAM,QAEV,CAAE,MAAOmB,GAGP,MAFAD,EAAcC,EAAO,MAAO4C,GAC5B7C,EAAcC,EAAO,QAASuC,GACxBvC,CACR,CAEA,IAAKqC,EAASS,GAAI,CAChB,MAAM9C,EAAa,IAAIL,MAAM,mBAAmB0C,EAASU,0BAIzD,MAHA/C,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACrBjD,EAAMuC,MAAQA,EACRvC,CACR,CAGA,IACEkD,QAAqBb,EAASe,MAChC,CAAE,MAAOpD,GAIP,MAHAD,EAAcC,EAAO,SAAUqC,EAASU,QACxChD,EAAcC,EAAO,MAAOqC,EAASY,KACrClD,EAAcC,EAAO,QAASuC,GACxBvC,CACR,CAGA,IACE2R,EAAetO,KAAKC,MAAMJ,EAC5B,CAAE,MAAOlD,GAKP,MAJAD,EAAcC,EAAO,OAAQkD,GAC7BnD,EAAcC,EAAO,SAAUqC,EAASU,QACxChD,EAAcC,EAAO,MAAOqC,EAASY,KACrClD,EAAcC,EAAO,QAASuC,GACxBvC,CACR,CAEA,GC7O0B,iBADkBE,ED8OTyR,IC7Oa,OAAVzR,GAAwC,iBAAfA,EAAM8C,KD6OnB,CAChD,MAAMhD,EAAa,IAAIL,MAAM,4CAK7B,MAJAK,EAAMmD,KAAOwO,EACb3R,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACrBjD,EAAMuC,MAAQA,EACRvC,CACR,CCrPK,IAAuCE,EDuP5C,GAAIyR,EAAa3O,KAAO,IAAK,CAC3B,MAAMhD,EAAa,IAAIL,MAAM,uCAK7B,MAJAK,EAAMmD,KAAOwO,EACb3R,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACrBjD,EAAMuC,MAAQA,EACRvC,CACR,CAEA,OAAO2R,CACT,CD7EII,CAAajB,GAAmBkB,KAAK,KACnClC,EAAavE,UAAW,EACxBmG,EAAO/M,QAAS,EAChBsL,EAAKgC,QACL9T,KAAKc,QAAQgF,WAAa,SAG1B,MAAMiO,EAAiB,IAAI3H,YAAoB,yBAA0B,CACvEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNvG,KAAM2N,EAAkBQ,eACxBa,SAAUhU,KAAKc,QAAQmT,YACvBnB,MAAOH,EAAkBG,MACzBoB,cAAe,GACfC,SAAU/D,IACVgE,WAAoB5B,EAASlR,IAAI,cACjC+S,UAAmB7B,EAASlR,IAAI,aAChCgT,YAAa3B,EAAkBE,aAC/BrO,WAAY3D,SAASb,KAAKc,QAAQmD,UAAW,IAC7CsQ,cAAe1T,SAASb,KAAKc,QAAQ0T,aAAc,IACnDzB,cAAe/S,KAAKc,QAAQkS,aAC5BpS,OAAQ+R,EAAkBS,aAC1BC,aAAcV,EAAkBU,aAChCC,IAAKX,EAAkBW,OAG3BtT,KAAK6F,cAAckO,GAEnB,MAAMU,EAAgB,IAAIrI,YAAY,oBAAqB,CACzDzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNuH,MAAOH,EAAkBG,MACzBsB,WAAoB5B,EAASlR,IAAI,cACjC+S,UAAmB7B,EAASlR,IAAI,gBAGpCtB,KAAK6F,cAAc4O,KAClB/Q,MAAM7B,IACP8P,EAAavE,UAAW,EACxBmG,EAAO/M,QAAS,EAChB+L,EAAavR,YAAc,kEAC3BuR,EAAaF,cAAcxL,UAAUC,IAAI,SACzCyL,EAAa/L,QAAS,EAEtBtE,QAAQL,MAAMA,EAAO8Q,IAEzB,CAKQ,0BAAApB,CAA2BxL,GAEjC,MAAM2O,EAAc1U,KAAKQ,WAAWO,cAAc,iBAClD2T,EAAYlP,aAAa,gBAAiB,QAK1C,MAAMC,EAAa,IAAIC,MAAM,6BAA8B,CACzDC,SAAS,EACTC,UAAU,IAEZ8O,EAAY7O,cAAcJ,GAItBiP,aAAuB7U,cACzB6U,EAAYzO,OAAQ,GAGtBjG,KAAKc,QAAQgF,WAAa,MAC5B,CAKQ,oBAAA5C,CAAqB6C,GAC3B/F,KAAKc,QAAQgF,WAAa,QAC5B,CAMQ,uBAAA1C,CAAwB2C,GAC9B/F,KAAKc,QAAQgF,WAAa,QAC5B,CAEQ,UAAAhC,GACN,MAAMkC,EAAShG,KAAKQ,WAAWO,cAA2B,WAC1DiF,EAAOC,OAAQ,EAGf3C,iBAAiB,0BAA2BtD,KAAKmD,8BAGjD,MAAMkC,EAAQ,IAAIK,MAAM,uBAAwB,CAAEC,SAAS,EAAMC,UAAU,IAC3E5F,KAAK6F,cAAcR,GAECW,EAAOjF,cAAc,gBAC7BqF,MAAM,CAAEC,cAAc,GACpC,CAEQ,WAAAtC,GACS/D,KAAKQ,WAAWO,cAA2B,WACnDkF,OAAQ,EAGf,MAAMyO,EAAc1U,KAAKQ,WAAWO,cAAc,iBAClD2T,EAAYlP,aAAa,gBAAiB,SAItCkP,aAAuB7U,cACzB6U,EAAYzO,OAAQ,GAKtBpC,oBAAoB,0BAA2B7D,KAAKmD,8BAGpD,MAAMkC,EAAQ,IAAIK,MAAM,wBAAyB,CAAEC,SAAS,EAAMC,UAAU,IAC5E5F,KAAK6F,cAAcR,EACrB,CAMQ,aAAAgM,CAAchM,GAChBA,EAAMC,kBAAkBC,mBAAqBF,EAAMC,OAAOuB,UAAU0H,SAAS,SAC/EvO,KAAK2U,WAAWtP,EAAMC,OAE1B,CAMQ,UAAAqP,CAAWrP,GACjB,MAAMgG,EAAQzK,SAASyE,EAAOxE,QAAQ2N,IAAK,IACxBzO,KAAKQ,WAAWO,cAAgC,qBACxDgB,MAAQuJ,EAAM/I,WAEzB,MAAMiJ,EAAQxL,KAAKQ,WAAW6J,iBAA8B,SAC5D,IAAK,MAAMuK,KAAQpJ,EAAO,CACxB,MAAMiD,EAAM5N,SAAS+T,EAAK9T,QAAQ2N,IAAK,IACnCA,IAAQnD,GACVsJ,EAAK5T,YAAc,IACnB4T,EAAKpP,aAAa,eAAgB,SACzBiJ,EAAMnD,GACfsJ,EAAK5T,YAAc,IACnB4T,EAAKpP,aAAa,eAAgB,WAGlCoP,EAAK5T,YAAc,IACnB4T,EAAKpP,aAAa,eAAgB,SAEtC,CACF,EGxVF,IAAIqP,EAKG,SAASC,EAAO1Q,GAChByQ,IACHA,EAAUlO,SAASC,cAAc,aAGnCiO,EAAQlU,UAAYyD,EAEpB,MAAM2Q,EAASF,EAAQ9S,MAKvB,OAFA8S,EAAQlU,UAAY,GAEboU,CACT,CHyZK1T,eAAeC,IAAI,wBACtBD,eAAeE,OAAO,sBAAuB4P,2OI1SxChN,eAAe6Q,EAAqB5Q,GACzC,IAAKA,EAAM6Q,MAAO,KAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAChB,MAAMtJ,EAAW,GACjB,IAAK,MAAMuJ,KAAwB,OAAbC,EAAApR,EAAM2H,cAAO,EAAbyJ,EAAexJ,WAAY,GAAI,KAAAwJ,EACnDxJ,EAASW,KAAK,CACZjB,UAAW6J,EAAQ7J,UACnB+J,OAAQF,EAAQzJ,SAEpB,CAIA,MAAM5H,QCoGHC,eAAoCC,GAAc,IAAAsR,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EACvDhV,EAAOmD,EAAM8R,QAAS,oBACtBjV,EAAOqD,OAAOC,UAAUH,EAAM2K,UAAY3K,EAAM2K,QAAU,EAAG,0BAE7D,MACMtK,GAD6B,OAAnBiR,EAAGtR,EAAM+R,YAAUT,EAAI,IAErC,wBAAwBtR,EAAM8R,mBAAmB9R,EAAMH,oBACnDa,EAAM,IAAIsR,IAAI3R,EAAML,EAAMiS,SAE5BjS,EAAMiI,MACRvH,EAAIwR,aAAaxO,OAAO,OAAQ1D,EAAMiI,KAAK9J,YAGzC6B,EAAM2K,SACRjK,EAAIwR,aAAaxO,OAAO,UAAW1D,EAAM2K,QAAQxM,YAG/C6B,EAAM+F,MACRrF,EAAIwR,aAAaxO,OAAO,OAAQ1D,EAAM+F,MAGpC/F,EAAMwH,WACR9G,EAAIwR,aAAaxO,OAAO,YAAa1D,EAAMwH,WAGzCxH,EAAMmS,MACRzR,EAAIwR,aAAaxO,OAAO,OAAQ1D,EAAMmS,MAGxC,MAAMC,EAA2B,CAAC,EA0ClC,GAxCIpS,EAAMqS,UACRD,EAAYC,QAAUrS,EAAMqS,SAGhB,OAAdd,EAAIvR,EAAMsS,OAANf,EAAYzN,SACdsO,EAAYE,KAAOtS,EAAMsS,MAGvBtS,EAAMuS,iBACRH,EAAYG,eAAiBvS,EAAMuS,gBAGxB,OAAbf,EAAIxR,EAAMwS,MAANhB,EAAW1N,SACbsO,EAAYI,IAAMxS,EAAMwS,KAGR,OAAlBf,EAAIzR,EAAMyS,WAANhB,EAAgB3N,SAClBsO,EAAYK,SAAWzS,EAAMyS,UAGf,OAAhBf,EAAI1R,EAAM0S,SAANhB,EAAc5N,SAChBsO,EAAYM,OAAS1S,EAAM0S,aAGNhM,IAAnB1G,EAAMqH,WACR+K,EAAY/K,SAAWrH,EAAMqH,eAGCX,IAA5B1G,EAAM2S,oBACRP,EAAYO,kBAAoB3S,EAAM2S,mBAGpC3S,EAAM4S,kBACRR,EAAYQ,gBAAkB5S,EAAM4S,iBAGtB,OAAhBjB,EAAI3R,EAAMiH,SAAN0K,EAAc7N,SAChBsO,EAAYnL,OAASjH,EAAMiH,QAGX,OAAlB2K,EAAI5R,EAAM4H,WAANgK,EAAgB9N,OAAQ,CAC1B,MAAM8D,EAAW,GACjB,IAAK,MAAMuJ,KAAWnR,EAAM4H,SAAU,KAAAiL,EACpCjL,EAASW,KAAK,CACZjB,UAA4B,OAAnBuL,EAAE1B,EAAQ7J,YAASuL,EAC5BxB,OAAQF,EAAQE,QAEpB,CAEAe,EAAYxK,SAAWA,CACzB,CAEoB,OAApBiK,EAAI7R,EAAM8S,aAANjB,EAAkB/N,SACpBsO,EAAYU,WAAa9S,EAAM8S,YAGjC,MAAMC,EAAoB,CACxBzD,QAAS,CACP,eAAgB,oBAElB1O,KAAME,KAAKuO,UAAU+C,GACrB7C,OAAQ,QAGV,IAAIzP,EAeAa,EASAyO,EAvBJ,IACEtP,QAAiBQ,MAAMI,EAAKqS,EAC9B,CAAE,MAAOtV,GAEP,MADAD,EAAcC,EAAO,MAAOiD,EAAIsS,MAC1BvV,CACR,CAEA,IAAKqC,EAASS,GAAI,CAChB,MAAM9C,EAAa,IAAIL,MAAM,mBAAmB0C,EAASU,oCAGzD,MAFA/C,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CAGA,IACEkD,QAAqBb,EAASe,MAChC,CAAE,MAAOpD,GAGP,MAFAD,EAAcC,EAAO,SAAUqC,EAASU,QACxChD,EAAcC,EAAO,MAAOqC,EAASY,KAC/BjD,CACR,CAGA,IACE2R,EAAetO,KAAKC,MAAMJ,EAC5B,CAAE,MAAOlD,GAIP,MAHAD,EAAcC,EAAO,OAAQkD,GAC7BnD,EAAcC,EAAO,SAAUqC,EAASU,QACxChD,EAAcC,EAAO,MAAOqC,EAASY,KAC/BjD,CACR,CAEA,GAgBwB,iBADIE,EAfFyR,IAgBoB,OAAVzR,GAAuC,OAArBA,EAAMsV,YACxC,OAAlBtV,EAAMuV,QAjBiC,CACvC,MAAMzV,EAAa,IAAIL,MAAM,4BAI7B,MAHAK,EAAMmD,KAAOwO,EACb3R,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CASF,IAA8BE,EAP5B,OAAOyR,CACT,CD9O2B+D,CAAyB,CAC9ClB,QAAS,wBACTK,KAAmB,OAAfxB,EAAE9Q,EAAM2H,cAAO,EAAbmJ,EAAewB,KACrBC,eAA6B,OAAfxB,EAAE/Q,EAAM2H,cAAO,EAAboJ,EAAetK,iBAC/BwB,KAAMjI,EAAMiI,KACZ8J,WAAY,0BACZpH,QAAS3K,EAAMoT,SACf/L,SAAuB,OAAf2J,EAAEhR,EAAM2H,cAAO,EAAbqJ,EAAe3J,SACzBxH,UAAWG,EAAMI,WACjB6G,OAAqB,OAAfgK,EAAEjR,EAAM2H,cAAO,EAAbsJ,EAAehK,OACvBW,WACAkK,QAAS9R,EAAMC,QACf6S,WAAyB,OAAf5B,EAAElR,EAAM2H,cAAO,EAAbuJ,EAAemC,cAY7B,OATiCC,EAAA,GAC5BxT,EAAQ,CACXmT,WAAY,CACVhL,KAAMnI,EAASmT,WAAWhL,KAC1BmL,SAAUtT,EAASmT,WAAWtI,QAC9B7H,MAAOhD,EAASmT,WAAWnQ,QAKjC,CAEA,MACMzC,EAAoB,mDAA0BL,EAAMI,6BACpDM,EAAM,IAAIsR,IAAI3R,EAAM,yBAE1B,IAAIP,EAeAa,EASAyO,EAvBJ,IACEtP,QAAiBQ,MAAMI,EACzB,CAAE,MAAOjD,GAEP,MADAD,EAAcC,EAAO,MAAOiD,EAAIsS,MAC1BvV,CACR,CAEA,IAAKqC,EAASS,GAAI,CAChB,MAAM9C,EAAa,IAAIL,MAAM,mBAAmB0C,EAASU,mCAGzD,MAFA/C,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CAGA,IACEkD,QAAqBb,EAASe,MAChC,CAAE,MAAOpD,GAGP,MAFAD,EAAcC,EAAO,SAAUqC,EAASU,QACxChD,EAAcC,EAAO,MAAOqC,EAASY,KAC/BjD,CACR,CAGA,IACE2R,EAAetO,KAAKC,MAAMJ,EAC5B,CAAE,MAAOlD,GAIP,MAHAD,EAAcC,EAAO,OAAQkD,GAC7BnD,EAAcC,EAAO,SAAUqC,EAASU,QACxChD,EAAcC,EAAO,MAAOqC,EAASY,KAC/BjD,CACR,CAEA,GA2BwB,iBADIE,EA1BFyR,IA2BoB,OAAVzR,GAA0C,iBAAjBA,EAAM6C,QAChD,OAAjB7C,EAAM6C,OA5BiC,CACvC,MAAM/C,EAAa,IAAIL,MAAM,4BAI7B,MAHAK,EAAMmD,KAAOwO,EACb3R,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CAoBF,IAA8BE,EAlB5B,GAAIyR,EAAa5O,OAAOC,KAAO,IAAK,CAClC,MAAMhD,EAAa,IAAIL,MAAM,+CAI7B,MAHAK,EAAMmD,KAAOwO,EACb3R,EAAM+C,OAASV,EAASU,OACxB/C,EAAMiD,IAAMZ,EAASY,IACfjD,CACR,CAKA,OAFgB2R,EAAatP,QAG/B,yNEzNA,MAAMyT,qBAAqB9X,YA8EjB+X,mBAAqB5X,KAAK6X,cAAc7U,KAAKhD,MAC7CkJ,2BAA6BlJ,KAAKmJ,sBAAsBnG,KAAKhD,MAC7D8X,4BAA8B9X,KAAK+X,uBAAuB/U,KAAKhD,MAC/DgY,0BAA4BhY,KAAKiY,qBAAqBjV,KAAKhD,MAC3DkY,0BAA4BlY,KAAKmY,qBAAqBnV,KAAKhD,MAC3DoY,2BAA6BpY,KAAKqY,sBAAsBrV,KAAKhD,MAQ7DiE,UAAY,EACZqT,QAAqC,GACrCgB,eAAiB,EACjBnM,gBAAuC,CAAC,EAsBxCoM,oBAAqB,EAEtB,iBAAAxY,GACLC,KAAKC,uBAELqD,iBAAiB,yBAA0BtD,KAAKkJ,4BAChD5F,iBAAiB,qBAAsBtD,KAAK4X,oBAC5CtU,iBAAiB,yBAA0BtD,KAAK8X,6BAEhD,MAAMpL,EAAc1M,KAAKQ,WAAWO,cAAiC,kBAC1D,MAAX2L,GAAAA,EAAapJ,iBAAiB,QAAStD,KAAKgY,2BAE5ChY,KAAKQ,WAAW8C,iBAAiB,uBAAwBtD,KAAKkY,2BAC9DlY,KAAKQ,WAAW8C,iBAAiB,wBAAyBtD,KAAKoY,4BAE/DpY,KAAKE,QACP,CAEO,oBAAA0D,GACL8G,aAAa1K,KAAKwY,cAElB3U,oBAAoB,yBAA0B7D,KAAKkJ,4BACnDrF,oBAAoB,qBAAsB7D,KAAK4X,oBAC/C/T,oBAAoB,yBAA0B7D,KAAK8X,6BAEnD,MAAMpL,EAAc1M,KAAKQ,WAAWO,cAAiC,kBAC1D,MAAX2L,GAAAA,EAAa7I,oBAAoB,QAAS7D,KAAKgY,2BAE/ChY,KAAKQ,WAAWqD,oBAAoB,uBAAwB7D,KAAKkY,2BACjElY,KAAKQ,WAAWqD,oBAAoB,wBAAyB7D,KAAKoY,2BACpE,CAEQ,oBAAAnY,GACDD,KAAKQ,aACRR,KAAKS,aAAa,CAAEC,KAAM,SAC1BV,KAAKQ,WAAWG,UAAY,q5DAEhC,CAEQ,MAAAT,GACN,IAAKF,KAAKc,QAAQkD,YAChB,OAIF,GADAhE,KAAKiE,UAAYpD,SAASb,KAAKc,QAAQmD,UAAW,KAC7CjE,KAAKiE,UACR,OAGF,MAAMqU,EAAiBzX,SAASb,KAAKc,QAAQwX,eAAgB,IACzDhU,OAAOC,UAAU+T,IAAmBA,EAAiB,IACvDtY,KAAKsY,eAAiBA,GAMxB,GADqBzX,SAASb,KAAKc,QAAQqG,aAAc,KACrC,EAClB,OAGF,MAAMoM,EAASvT,KAAKQ,WAAWO,cAA2B,yBAC1Df,KAAKyY,YAAYlF,EAAQ,KACzBvT,KAAK6F,cAAc,IAAIH,MAAM,+BAAgC,CAC3DC,SAAS,EACTC,UAAU,KAGZoP,EAAqB,CACnB3Q,QAASrE,KAAKc,QAAQkD,YACtBiR,MAAOjV,KAAKuY,mBACZf,SAAUxX,KAAKsY,eACf9T,WAAYxE,KAAKiE,YAChB4P,KAAK3P,GAAYlE,KAAK0Y,wBAAwBxU,IAC9CR,MAAM7B,GAAS7B,KAAK2Y,4BAA4B9W,IAChD+W,QAAQ,IAAM5Y,KAAK6Y,YAAYtF,GACpC,CAKQ,uBAAAmF,CAAwBxU,GAE9BlE,KAAKuY,oBAAqB,EAE1BvY,KAAKsX,SAAkB,MAARpT,OAAQ,EAARA,EAAUoT,UAAW,GACpCtX,KAAK8Y,kBAA4B,MAAR5U,OAAQ,EAARA,EAAUmT,WAEnCrX,KAAK+Y,sBACL/Y,KAAKgZ,gBACLhZ,KAAKiZ,kBAAiB,GAGtB,MAAMC,EAAc,IAAI9M,YAAoB,yBAA0B,CACpEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNQ,QAAS,CAAC,EACVuL,QAAiB,MAARpT,OAAQ,EAARA,EAAUoT,QACnBD,WAAoB,MAARnT,OAAQ,EAARA,EAAUmT,cAG1BrX,KAAK6F,cAAcqT,EACrB,CAKQ,2BAAAP,CAA4B9W,GAC9BsX,EAAoBtX,GACtBK,QAAQC,IAAIN,GAEZK,QAAQyB,KAAK9B,GAIf7B,KAAKuY,oBAAqB,EAG1B,MAAMW,EAAc,IAAI9M,YAAoB,yBAA0B,CACpEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNQ,QAAS,CAAC,EACVuL,QAAS,GACTD,WAAY,CAAEhL,KAAM,EAAGmL,SAAU,EAAGtQ,MAAO,MAG/ClH,KAAK6F,cAAcqT,EACrB,CAOQ,WAAAT,CAAYlF,EAAqB6F,GACvC1O,aAAa1K,KAAKwY,cAClBxY,KAAKwY,aAAetN,WAAW,KACzBqI,IACFA,EAAO/M,QAAS,IAEjB4S,EACL,CAMQ,WAAAP,CAAYtF,GAClB7I,aAAa1K,KAAKwY,cACdjF,IACFA,EAAO/M,QAAS,EAEpB,CAKQ,SAAA6S,CAAUlY,GAChB,MAAMmY,EAAQ3S,SAASC,cAAc,iBACrC0S,EAAMxY,QAAQyY,cAAgB,QAC9BD,EAAMxY,QAAQ0Y,UAAY,UAC1BF,EAAMxY,QAAQ2Y,UAAY,OAC1BH,EAAMxY,QAAQ4Y,SAAW,OACzBJ,EAAM3Y,UAAY,MAAMQ,QAExBnB,KAAKQ,WAAWyG,YAAYqS,EAC9B,CAOQ,oBAAArB,CAAqBlS,GAC3B,MAAMV,EAAQ,IAAIK,MAAM,+BAAgC,CAAEC,SAAS,EAAMC,UAAU,IACnF5F,KAAK6F,cAAcR,EACrB,CAYQ,oBAAA8S,CAAqB9S,GAE3B,MAAMZ,EAAOY,EAAMsU,eACbC,EAAa5Z,KAAKQ,WAAW6J,iBACjC,4EACF,IAAIwP,EACJ,IAAK,MAAMrN,KAAaoN,EAClBnV,EAAKkH,SAASa,KAChBqN,EAAiBrN,GAIrB,IAAKqN,EACH,OAIF,MAAMC,EAAQ9Z,KAAKQ,WAAW6J,iBAAiB0P,EAAuBF,IACtE,IAAK,MAAMG,KAAQF,EACbE,aAAgBna,cAAgB4E,EAAKkH,SAASqO,KAChDA,EAAK/T,OAAQ,GAIjBjG,KAAK6Z,eAAiBA,CACxB,CAOQ,qBAAAxB,CAAsBtS,GAC5B/F,KAAKia,cACP,CAEQ,YAAAA,GACN,IAAKja,KAAK6Z,eACR,OAGF,MAAMC,EAAQ9Z,KAAKQ,WAAW6J,iBAAiB0P,EAAuB/Z,KAAK6Z,iBAC3E,IAAK,MAAMG,KAAQF,EACbE,aAAgBna,cAClBma,EAAK/T,OAAQ,GAIjBjG,KAAK6Z,oBAAiB/O,CACxB,CAEQ,sBAAAiN,CAAuBhS,GAC7B/F,KAAKqZ,UAAU,6CACjB,CAEQ,qBAAAlQ,CAAsB9D,GAQ5B,MAAM6U,EAAUla,KAAKQ,WAAWO,cAA2B,oBAC3Df,KAAKyY,YAAYyB,EAAS,IAE1Bla,KAAK6F,cAAc,IAAIH,MAAM,+BAAgC,CAC3DC,SAAS,EACTC,UAAU,KAMZ,MAAM8Q,EAAOrR,EAAMkG,OAAOQ,QAAQX,UAChC,CAAC,CAAE+O,QAAS,CAAC9U,EAAMkG,OAAOQ,QAAQX,WAAYgP,WAAY,SAC1D,GAEFpF,EAAqB,CACnB3Q,QAASrE,KAAKc,QAAQkD,YACtBiR,OAAO,EACPlJ,QAAS,CACP2K,OACA7L,iBAAkBxF,EAAMkG,OAAOQ,QAAQlB,iBACvCY,SAAUpG,EAAMkG,OAAOQ,QAAQN,SAC/BJ,OAAQhG,EAAMkG,OAAOQ,QAAQV,OAC7BW,SAAU3G,EAAMkG,OAAOS,SACvByL,YAAapS,EAAMkG,OAAOQ,QAAQ0L,aAEpCpL,KAAMhH,EAAMkG,OAAOc,KACnBmL,SAAUxX,KAAKsY,eACf9T,WAAYa,EAAMkG,OAAOtH,YACxB4P,KAAK3P,IACNlE,KAAKsX,QAAUpT,EAASoT,QACxBtX,KAAKmM,gBAAkB9G,EAAMkG,OAAOQ,QACpC/L,KAAKsM,iBAAmBjH,EAAMkG,OAAOS,SACrChM,KAAK8Y,kBAAoB5U,EAASmT,WAElCrX,KAAKgZ,gBACLhZ,KAAKiZ,kBAAiB,GAgU5B,SAAyB/G,GACvB,IAAKA,EACH,OAGF,MAAMmI,EAAOnI,EAAQoI,wBAMfC,EAAeC,WACnBC,iBAAiB9T,SAAS3B,MAAM0V,iBAAiB,wBAA0B,GAE7E/T,SAASgU,gBAAgBC,UAAYC,QAAUR,EAAKS,IAAMP,CAC5D,CA7UMQ,CAAgB/a,KAAKQ,WAAWO,cAA2B,qBAG3D,MAAMia,EAAgB,IAAI5O,YAAoB,yBAA0B,CACtEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNQ,QAAS1G,EAAMkG,OAAOQ,QACtBuL,QAASpT,EAASoT,QAClBD,WAAYnT,EAASmT,cAGzBrX,KAAK6F,cAAcmV,KAClBtX,MAAM7B,IACHsX,EAAoBtX,GACtBK,QAAQC,IAAIN,GAEZK,QAAQyB,KAAK9B,GAIf,MAAMmZ,EAAgB,IAAI5O,YAAoB,yBAA0B,CACtEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNQ,QAAS1G,EAAMkG,OAAOQ,QACtBuL,QAAS,GACTD,WAAY,CAAEhL,KAAM,EAAGmL,SAAU,EAAGtQ,MAAO,MAG/ClH,KAAK6F,cAAcmV,KAClBpC,QAAQ,KAGT5Y,KAAK6Y,YAAYqB,IAErB,CAMQ,aAAArC,CAAcxS,GAEpB,MAAM4V,EAAgB,IAAI7O,YAAoB,yBAA0B,CACtEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNQ,QAASmP,EAAA,GAAKlb,KAAKmM,iBACnBE,KAAMhH,EAAMkG,OAAOc,KACnBpI,UAAWjE,KAAKiE,UAChB+H,SAAUhM,KAAKsM,oBAGnBtM,KAAK6F,cAAcoV,EACrB,CAEQ,aAAAjC,GACN,MAAMmC,EAAiBnb,KAAKQ,WAAWO,cAAc,oBAKjDf,KAAK6Z,iBAAmBsB,GAC1Bnb,KAAKia,eAGPkB,EAAexa,UAAY,GAE3B,MAAMya,EAAYpb,KAAKQ,WAAWO,cAAoC,eACtE,GAA4B,IAAxBf,KAAKsX,QAAQpP,OAAjB,CAMAkT,EAAU5U,QAAS,EAEnB,IAAK,MAAM6U,KAAUrb,KAAKsX,QACxB6D,EAAelU,YAAYqU,EAAmBD,GALhD,MAHED,EAAU5U,QAAS,CAUvB,CAMQ,gBAAAyS,CAAiBsC,GACvB,MAAMC,EAAoBxb,KAAKQ,WAAWO,cAAc,oBACxD,IAAKya,EACH,OAGF,MAAMnE,EAAarX,KAAK8Y,kBAExB,GAAIzB,GAAcA,EAAWnQ,MAAQmQ,EAAWG,SAAU,CASxD,IAAItQ,EARJsU,EAAkB1a,QAAQuL,KAAOgL,EAAWhL,KAAK9J,WACjDiZ,EAAkB1a,QAAQiO,QAAUsI,EAAWG,SAASjV,WAStD2E,EADEqU,EACM1a,SAASb,KAAKc,QAAQqG,aAAc,IAEpCkQ,EAAWnQ,MAQrB,MAAMuU,EAAiBzM,KAAKG,MAAM,IAAQkI,EAAWG,UAAYH,EAAWG,SACxEtQ,EAAQuU,IACVvU,EAAQuU,GAGVD,EAAkB1a,QAAQoG,MAAQA,EAAM3E,WAGxCiZ,EAAkB1a,QAAQ4a,OAASC,KAAKC,MAAMrZ,WAE9CiZ,EAAkBhV,QAAS,CAC7B,MACEgV,EAAkBhV,QAAS,CAE/B,CASQ,mBAAAuS,GAEN,MAAM8C,EAAuBlV,SAAS5F,cACpC,yCAA2Cf,KAAKiE,UAAY,MAE9D,IAAK4X,EACH,OAGuBhb,SAASgb,EAAqB/a,QAAQgb,MAAO,MACjDjb,SAASb,KAAKc,QAAQqG,aAAc,MAEvD0U,EAAqB/a,QAAQgb,MAAQ9b,KAAKc,QAAQqG,aAEtD,EAUF,SAAS4S,EAAuBvN,GAC9B,OAAIA,EAAU8B,QAAQ,gCACb,iEAGL9B,EAAU8B,QAAQ,4BACb,qEAGF,kKAET,CAQA,SAASgN,EAAmBD,GAC1B,MAAM9P,EAAS5E,SAASC,cAAc,uBACtC2E,EAAOzK,QAAQib,SAAWV,EAAOW,GAAGzZ,WAEpC,MAAM0Z,EA0FkB,iBADFla,EAzFWsZ,IA0Fa,OAAVtZ,GAA4C,iBAAnBA,EAAMma,UACrC,iBAArBna,EAAMoa,WAFjB,IAAwBpa,EAvFtB,IAAIqa,EAAsF,GAEtFH,GAAaZ,EAAOgB,YACtBD,EAAaf,EAAOgB,YAAYC,IAAIC,IAAK,CACvCP,GAAIO,EAAMC,SACVC,SAAUF,EAAMG,UAIhBC,YAAaJ,EAAMK,oBAA8BL,EAAMM,iBAE/CZ,GAAaZ,EAAOe,aAC9BA,EAAaf,EAAOe,YAGtB7Q,EAAOzK,QAAQgc,eAAiBV,EAAWE,IAAIC,GAASA,EAAMI,aAAaI,KAAK,KAChFxR,EAAOzK,QAAQkc,gBAAkBZ,EAAWE,IAAIC,GAASA,EAAME,UAAUM,KAAK,KAE9E,MAAME,EAAUhB,EAAYZ,EAAOa,SAAWb,EAAO4B,QACrD1R,EAAOzK,QAAQmc,SAAWA,GAAW,GAAG1a,WAGtCgJ,EAAOzK,QAAQoc,aADbjB,EAC4BZ,EAAO8B,gBAAkB,OAAS,QAElC9B,EAAO+B,eAAiB,OAAS,QAIjE,MAAMC,EAAS1W,SAASC,cAAc,mBACtCyW,EAAO7X,aAAa,OAAQ,SAC5B6X,EAAOvc,QAAQwc,KAAOrB,EAAYZ,EAAOkC,WAAalC,EAAOmC,UAC7DH,EAAOvc,QAAQa,KAAOsa,EAAYZ,EAAOoC,KAAK5K,aAAewI,EAAOoC,KAAK/K,YAEzE,MAAMgL,EAAWzB,EAAYZ,EAAOsC,eAAiBtC,EAAOuC,cAC5DP,EAAOvc,QAAQ4c,SAAWA,EAAW,OAAS,QAC9CnS,EAAOtE,YAAYoW,GAGnB,MAAMzc,EAAS+F,SAASC,cAAc,uBACtChG,EAAO4E,aAAa,OAAQ,gBAC5B5E,EAAOE,QAAQF,OAASya,EAAO/P,MAAM/I,WACrCgJ,EAAOtE,YAAYrG,GAGnB,MACMid,EAmDR,SAA0BC,GACxB,GAA4B,iBAAjBA,GAA8C,OAAjBA,EACtC,OAGF,MAAMC,EAAQD,EAAa,SAAaA,EAAa,YACrD,GAAqB,iBAAVC,GAAgC,OAAVA,GACR,iBAAhBA,EAAMhc,OAAsBgc,EAAMhc,MACzC,OAAOgc,EAAMhc,KAEjB,CA7DmBic,CADI/B,EAAYZ,EAAO4C,cAAgB5C,EAAOyC,cAE/D,GAAID,EAAU,CACZ,MAAM3L,EAAUvL,SAASC,cAAc,OACvCsL,EAAQ1M,aAAa,OAAQ,aAE7B,MAAMqH,EAAQlG,SAASC,cAAc,UACrCiG,EAAM7L,YAAc,cACpBkR,EAAQjL,YAAY4F,GACpBqF,EAAQjL,YAAYN,SAASuX,eAAeL,IAE5CtS,EAAOtE,YAAYiL,EACrB,CAGA,MAAMhM,EAAUS,SAASC,cAAc,OACvCV,EAAQV,aAAa,OAAQ,WAC7BU,EAAQ6G,UAAY,SAOpB,MAAMrF,EAAQf,SAASC,cAAc,UACrCc,EAAMb,UAAUC,IAAI,yBACpBY,EAAM1G,YAAc8T,EAAOuG,EAAO3T,OAClCxB,EAAQe,YAAYS,GAEpB,MAAMzC,EAAO0B,SAASC,cAAc,KAOpC,OANA3B,EAAK4B,UAAUC,IAAI,wBACnB7B,EAAKjE,YAAc8T,EAAOuG,EAAOnV,SACjCA,EAAQe,YAAYhC,GAEpBsG,EAAOtE,YAAYf,GAEZqF,CACT,CA2BA,SAAS4N,EAAoBtX,GAC3B,KAAMA,aAAiBL,OACrB,OAAO,EAUT,OAAyB,MADFK,EACT+C,QAIP,CACL,cACA,0DACA+G,SAAS9J,EAAMV,QACnB,CAyCKE,eAAeC,IAAI,kBACtBD,eAAeE,OAAO,gBAAiBoW,cC3vBzC,MAAMwG,4BAA4Bte,YAsCxBue,yBAA2Bpe,KAAKqe,oBAAoBrb,KAAKhD,MACzDkY,0BAA4BlY,KAAKmY,qBAAqBnV,KAAKhD,MAC3DoY,2BAA6BpY,KAAKqY,sBAAsBrV,KAAKhD,MAE9D,iBAAAD,GACLC,KAAKC,uBAEL,MAAMqe,EAAete,KAAKQ,WAAW6J,iBAAoC,iBACzE,IAAK,MAAMyC,KAAUwR,EACnBxR,EAAOxJ,iBAAiB,QAAStD,KAAKoe,0BAGxCpe,KAAKQ,WAAW8C,iBAAiB,uBAAwBtD,KAAKkY,2BAC9DlY,KAAKQ,WAAW8C,iBAAiB,wBAAyBtD,KAAKoY,4BAE/DpY,KAAKE,QACP,CAEO,oBAAA0D,GACL,MAAM0a,EAAete,KAAKQ,WAAW6J,iBAAoC,iBACzE,IAAK,MAAMyC,KAAUwR,EACnBxR,EAAOjJ,oBAAoB,QAAS7D,KAAKoe,0BAG3Cpe,KAAKQ,WAAWqD,oBAAoB,uBAAwB7D,KAAKkY,2BACjElY,KAAKQ,WAAWqD,oBAAoB,wBAAyB7D,KAAKoY,2BACpE,CAEQ,oBAAAnY,GACDD,KAAKQ,aACRR,KAAKS,aAAa,CAAEC,KAAM,SAC1BV,KAAKQ,WAAWG,UAAY,k8EAEhC,CAEQ,MAAAT,GACN,MAAMqe,EAAa1d,SAASb,KAAKc,QAAQyd,WAAY,IAC/CC,EAAa3d,SAASb,KAAKc,QAAQ0d,WAAY,IAC/CC,EAAa5d,SAASb,KAAKc,QAAQ2d,WAAY,IAC/CC,EAAa7d,SAASb,KAAKc,QAAQ4d,WAAY,IAC/CC,EAAa9d,SAASb,KAAKc,QAAQ6d,WAAY,IAC/CzX,EAAQrG,SAASb,KAAKc,QAAQqG,aAAc,IAE5CyX,EAAeL,EAAaC,EAAaC,EAAaC,EAAaC,EAEnEE,EAAU7e,KAAKQ,WAAWO,cAA2B,UAG3D,GAAI+d,MAAMF,IAAkC,IAAjBA,GAAsBE,MAAM5X,IAAoB,IAAVA,EAE/D,YADA2X,EAAQrY,QAAS,GAInB,MAAMuY,EAAc/e,KAAKQ,WAAWO,cAAc,wBAClDge,EAAYje,QAAQke,QAAUhf,KAAKc,QAAQme,eAC3CF,EAAYje,QAAQgb,MAAQ9b,KAAKc,QAAQqG,aAElBnH,KAAKQ,WAAWO,cAAc,YACtCC,YAAchB,KAAKc,QAAQme,eAE1C,MAAM7X,EAAepH,KAAKQ,WAAWO,cAAc,UAC7CsG,EAAY,IAAIC,KAAKC,aAAa,SACxCH,EAAapG,YAAcqG,EAAUG,OAAON,GAE5CgY,EAAoBlf,KAAKQ,WAAWO,cAA2B,UAAW4d,EAAYzX,GACtFgY,EAAoBlf,KAAKQ,WAAWO,cAA2B,UAAW2d,EAAYxX,GACtFgY,EAAoBlf,KAAKQ,WAAWO,cAA2B,UAAW0d,EAAYvX,GACtFgY,EAAoBlf,KAAKQ,WAAWO,cAA2B,UAAWyd,EAAYtX,GACtFgY,EAAoBlf,KAAKQ,WAAWO,cAA2B,UAAWwd,EAAYrX,GAEtF2X,EAAQrY,QAAS,CACnB,CAEQ,mBAAA6X,CAAoBhZ,GAC1B,KAAMA,EAAMC,kBAAkBC,mBAC5B,OAGF,MAAMiG,EAAQnG,EAAMC,OAAOxE,QAAQ0K,MACnC,GAAIA,EAAO,CAET,MAAM2T,EAAqB,IAAI/S,YAAoB,sBAAuB,CACxEzG,SAAS,EACTC,UAAU,EACV2F,OAAQ,CACNC,MAAO3K,SAAS2K,EAAO,OAG3BxL,KAAK6F,cAAcsZ,EACrB,CACF,CAUQ,oBAAAhH,CAAqBpS,GAC3B,MAAMqZ,EAAYpf,KAAKQ,WAAWO,cAAc,cAC5Cqe,aAAqBvf,cACvBuf,EAAUnZ,OAAQ,EAEtB,CAOQ,qBAAAoS,CAAsBtS,GAC5B,MAAMqZ,EAAYpf,KAAKQ,WAAWO,cAAc,cAC5Cqe,aAAqBvf,cACvBuf,EAAUnZ,OAAQ,EAEtB,EAGF,SAASiZ,EAAoBhN,EAAsB4J,EAAe5U,GAChE,MAAMmY,EAAWvD,EAAQ5U,EAAS,IACtBgL,EAAQnR,cAA2B,SAC3Cue,MAAMC,MAAQF,EAAU,IAG5B,GAFqBnN,EAAQnR,cAA2B,UAC3CC,YAAcqe,EAAQG,QAAQ,GAAGC,QAAQ,KAAM,IAAM,IACpD,IAAV3D,EAAa,CACA5J,EAAQnR,cAAc,UAC9BqM,UAAW,CACpB,CACF,CAYK/L,eAAeC,IAAI,0BACtBD,eAAeE,OAAO,wBAAyB4c","sources":["webpack://lange/./sections/yotpo-product-reviews/simple-rating-stars/index.ts","webpack://lange/../node_modules/@integrabeauty/assert/src/index.ts","webpack://lange/./lib/decorate-error.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-ai-summary/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-fetch-reviews-summary.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-filters/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-pagination/index.ts","webpack://lange/../node_modules/uuid/dist/rng.js","webpack://lange/../node_modules/uuid/dist/stringify.js","webpack://lange/../node_modules/uuid/dist/v4.js","webpack://lange/../node_modules/@integrabeauty/yotpo-browser-types/dist/swell/swell-popup-type.js","webpack://lange/../node_modules/@integrabeauty/yotpo-browser-types/dist/swell/swell-referral-medium.js","webpack://lange/./sections/yotpo-product-reviews/yotpo-review-create/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-review-create/yotpo-create-review.ts","webpack://lange/../node_modules/@integrabeauty/yotpo-browser-types/dist/reviews/create-review-api-response-body.js","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/decode-html-entities.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/yotpo-fetch-product-reviews.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/yotpo-search-product-reviews.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-reviews/index.ts","webpack://lange/./sections/yotpo-product-reviews/yotpo-stars-breakdown/index.ts"],"sourcesContent":["import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders a simple star rating display (0–5 stars). It doesn't render empty stars, reviews count,\n * or partial stars. It renders a visually hidden label for accessibility. Size is configured via\n * font-size on the element.\n */\nclass SimpleRatingStars extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-rating'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * The numeric rating to display, between 0 and 5. Supports only integer values.\n     */\n    rating: string;\n  };\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n    this.render();\n  }\n\n  public attributeChangedCallback(_name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (this.isConnected && newValue !== oldValue) {\n      this.render();\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    const rating = parseInt(this.dataset.rating, 10);\n\n    const label = this.shadowRoot.querySelector('.visually-hidden');\n    label.textContent = `${rating} out of 5 stars`;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'simple-rating-stars': SimpleRatingStars;\n  }\n}\n\nif (!customElements.get('simple-rating-stars')) {\n  customElements.define('simple-rating-stars', SimpleRatingStars);\n}\n","/**\n * Throws an assertion error if the given condition is falsy.\n *\n * @param expression any expression, although one that evaluates to true or false is preferred by\n * convention\n * @param message optional error text\n */\nexport function assert(expression: unknown, message?: string) {\n  if (!expression) {\n    throw new AssertionError(message);\n  }\n}\n\nexport class AssertionError extends Error {\n  constructor(message = 'Assertion error') {\n    super(message);\n    this.name = this.constructor.name;\n  }\n}\n","/**\n * Copies a single piece of supplementary context onto an error before it is thrown or reported.\n *\n * Use this when decorating a value the calling code did not construct, such as a caught value or an\n * error handed in by another module. An error the caller just built with new is always writable, so\n * plain assignment is fine there. A value from elsewhere may be a foreign object this code does not\n * own. Such a value can carry a non-writable property, such as a DOMException whose code is a read-\n * only accessor, or be frozen outright, and assigning to it then throws. Attaching context must\n * never itself fail, so a rejected assignment is swallowed and the remaining context is still\n * attempted. Guarding each property in isolation means a single read-only slot, the common case,\n * costs only that one property rather than all of them.\n *\n * In practice the value that triggers this is almost always a DOMException that reached the caller\n * from a fetch the browser cut short, such as an abort when an in-flight request is interrupted.\n * The platform defines a DOMException's code, name, and message as read-only attributes, so\n * assigning a code onto one throws under the strict mode that all bundled module code runs in. It\n * surfaces from Safari far more than elsewhere for two reasons. The reported wording, attempted to\n * assign to readonly property, is JavaScriptCore's phrasing for this failure, so the same bug in\n * another engine is worded differently and groups as a separate report, which makes this one look\n * Safari specific even though the read-only rule is not. And iOS Safari abandons in-flight fetches\n * aggressively when a tab is backgrounded or a connection drops, which is when such a DOMException\n * tends to be produced and handed to code that assumed an ordinary writable error.\n *\n * A swallowed failure is logged at a level low enough that it is recorded as a breadcrumb for\n * monitoring rather than escalated into its own captured event. That keeps the lost context visible\n * alongside the eventual report without adding fresh noise of its own. The breadcrumb names the\n * property that could not be attached, the kind of value that rejected it, and why, which is enough\n * to recognize a read-only or frozen caught value after the fact.\n *\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Read-only\n * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMException\n * @see https://webidl.spec.whatwg.org/#dfn-read-only\n */\nexport function decorateError(error: any, key: string, value: unknown) {\n  try {\n    error[key] = value;\n  } catch (assignmentError) {\n    let reason: string;\n    if (assignmentError instanceof Error) {\n      reason = assignmentError.message;\n    } else {\n      reason = `${assignmentError}`;\n    }\n\n    console.log('Failed to assign error property', {\n      key,\n      name: error?.name,\n      tag: Object.prototype.toString.call(error),\n      reason\n    });\n  }\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { fetchReviewsSummary, type SummaryHighlight, type SummaryTopic } from '../yotpo-fetch-reviews-summary.js';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders Yotpo AI summary for a product. Note that not every product will have an AI summary, as\n * it depends on the number of reviews and other factors. So we should gracefully handle the case\n * when there is no summary to display.\n *\n * @see https://support.yotpo.com/docs/adding-the-reviews-summary\n */\nclass YotpoAiSummary extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-modal-state'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Whether the drawer is shown, as \"open\". Anything else, or no value at all, means closed. A\n     * change only takes effect while the element is on the page. For now the element sets this\n     * itself. Later the modal manager sets it instead.\n     */\n    modalState: string;\n\n    /**\n     * The product id, which could be a product sku, with which this element is associated.\n     */\n    productId: string;\n\n    /**\n     * Number of Yotpo customer reviews for this product\n     */\n    reviewsTotal: string;\n\n    /**\n     * The api key used to send requests to the Yotpo reviews API.\n     */\n    yotpoAppKey: string;\n  };\n\n  /**\n   * Whether this element started to fetch results.\n   */\n  private fetchStarted = false;\n\n  /**\n   * One paragraph of summary highlights returned from the Yotpo reviews summary API.\n   */\n  private paragraphSummary = '';\n\n  /**\n   * Topic-level summaries returned from the Yotpo reviews summary API.\n   */\n  private summaryTopics: SummaryTopic[] = [];\n\n  /**\n   * Individual review highlights returned from the Yotpo reviews summary API.\n   */\n  private summaryHighlights: SummaryHighlight[] = [];\n\n  private onTopicLinksClickedBound = this.onTopicLinksClicked.bind(this);\n  private onCloseButtonClickedBound = this.onCloseButtonClicked.bind(this);\n  private onModalDismissRequestedBound = this.onModalDismissRequested.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const topicLinks = this.shadowRoot.querySelector<HTMLElement>('.topic-links');\n    topicLinks?.addEventListener('click', this.onTopicLinksClickedBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.addEventListener('click', this.onCloseButtonClickedBound);\n\n    const readAll = this.shadowRoot.querySelector<HTMLElement>('.read-all');\n    readAll?.addEventListener('click', this.onCloseButtonClickedBound);\n\n    this.startFetching().catch(error => {\n      // The Load failed error is a routinely occurring networking error that we are not interested\n      // in tracing. It has moderate frequency and is outside of our control.\n\n      if (error instanceof Error && error.message === 'Load failed') {\n        console.log(error);\n      } else {\n        console.warn(error);\n      }\n    });\n  }\n\n  public disconnectedCallback() {\n    const topicLinks = this.shadowRoot.querySelector<HTMLElement>('.topic-links');\n    topicLinks?.removeEventListener('click', this.onTopicLinksClickedBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseButtonClickedBound);\n\n    const readAll = this.shadowRoot.querySelector<HTMLElement>('.read-all');\n    readAll?.removeEventListener('click', this.onCloseButtonClickedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    if (!this.isConnected || oldValue === newValue) {\n      return;\n    }\n\n    if (name === 'data-modal-state') {\n      if (newValue === 'open') {\n        this.openDrawer();\n      } else if (oldValue === 'open') {\n        this.closeDrawer();\n      }\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  /**\n   * Fetches the reviews summary data from Yotpo, stores fetched data, and renders the component\n   * accordingly.\n   */\n  private async startFetching() {\n    // Allow this method to be called multiple times but only run once.\n    if (this.fetchStarted) {\n      return;\n    }\n\n    this.fetchStarted = true;\n\n    if (!this.dataset.yotpoAppKey || !this.dataset.productId) {\n      return;\n    }\n\n    const productId = parseInt(this.dataset.productId, 10);\n    if (!productId) {\n      return;\n    }\n\n    const response = await fetchReviewsSummary({\n      app_key: this.dataset.yotpoAppKey,\n      product_id: productId\n    });\n\n    this.paragraphSummary = response?.paragraphSummary || '';\n    this.summaryTopics = response?.summaryTopics || [];\n    this.summaryHighlights = response?.summaryHighlights || [];\n\n    this.render();\n  }\n\n  /**\n   * Handles the click on a topic link, which opens the drawer with the summary for that topic. Any\n   * topic button should just open the drawer no matter which topic is clicked.\n   */\n  private onTopicLinksClicked(event: MouseEvent) {\n    // We listen for clicks on the parent container, but we only want to handle clicks on buttons\n    // inside.\n    if (!(event.target instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    event.target.setAttribute('aria-expanded', 'true');\n\n    // Claim focus return before opening. Without this the modal manager has nothing of ours on its\n    // focus stack, so closing the drawer reaches down to whichever opener an earlier modal left\n    // there, and focusing that one scrolls the page away to wherever it sits.\n    const focusEvent = new Event('focus-stack-push-requested', {\n      bubbles: true,\n      composed: true\n    });\n    event.target.dispatchEvent(focusEvent);\n\n    this.dataset.modalState = 'open';\n  }\n\n  /**\n   * Handles modal's backdrop click and Esc button press\n   */\n  private onModalDismissRequested(_event: Event) {\n    this.dataset.modalState = 'closed';\n  }\n\n  /**\n   * Handles the drawer close button click and the \"Read all\" button click.\n   */\n  private onCloseButtonClicked(_event: MouseEvent) {\n    this.dataset.modalState = 'closed';\n  }\n\n  private openDrawer() {\n    const drawer = this.shadowRoot.querySelector<HTMLElement>('.drawer');\n    drawer.inert = false;\n\n    addEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    // The modal manager leaves the reviews section active while the drawer is open, so the topic\n    // buttons would stay reachable by Tab behind it. The elements around us inert their own parts.\n    const content = this.shadowRoot.querySelector('.content');\n    if (content instanceof HTMLElement) {\n      content.inert = true;\n    }\n\n    const modalShowEvent = new Event('modal-open-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(modalShowEvent);\n\n    const closeButton = drawer.querySelector('close-button');\n    closeButton.focus({ focusVisible: true });\n  }\n\n  private closeDrawer() {\n    const drawer = this.shadowRoot.querySelector<HTMLElement>('.drawer');\n    drawer.inert = true;\n\n    const activeTopicLink = this.shadowRoot.querySelector<HTMLElement>(\n      '.topic-link[aria-expanded=\"true\"]');\n    activeTopicLink?.setAttribute('aria-expanded', 'false');\n\n    // This has to happen before the close request goes out, since the modal manager puts focus back\n    // on the topic button when it hears it, and a focus call into an inert subtree does nothing.\n    const content = this.shadowRoot.querySelector('.content');\n    if (content instanceof HTMLElement) {\n      content.inert = false;\n    }\n\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    const event = new Event('modal-close-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(event);\n  }\n\n  private render() {\n    // Render the paragraph summary if the product has content for it\n    const copy = this.shadowRoot.querySelector<HTMLElement>('.copy');\n    if (this.paragraphSummary) {\n      copy.textContent = this.paragraphSummary;\n      copy.hidden = false;\n    } else {\n      copy.textContent = '';\n      copy.hidden = true;\n    }\n\n    // render topic links\n    const topicLinks = this.shadowRoot.querySelector<HTMLElement>('.topic-links');\n    for (const topicObject of this.summaryTopics) {\n      const topic = document.createElement('button');\n      topic.classList.add('topic-link');\n      topic.textContent = `${topicObject.displayTopic} (${topicObject.coverage})`;\n      topic.setAttribute('aria-expanded', 'false');\n      topic.setAttribute('aria-haspopup', 'dialog');\n      topic.setAttribute('aria-controls', 'drawer');\n\n      topicLinks.appendChild(topic);\n    }\n\n    // render drawer content\n\n    // put total reviews in the drawer description\n    const total = parseInt(this.dataset.reviewsTotal, 10);\n    const totalElement = this.shadowRoot.querySelector('.total');\n    const formatter = new Intl.NumberFormat('en-US');\n    totalElement.textContent = formatter.format(total);\n\n    // render summary by topics\n    const topics = this.shadowRoot.querySelector<HTMLElement>('.topics');\n    for (const topic of this.summaryTopics) {\n      const title = document.createElement('h4');\n      title.textContent = topic.displayTopic;\n\n      const copy = document.createElement('p');\n      copy.textContent = topic.summary;\n\n      const mentioned = document.createElement('p');\n      mentioned.classList.add('mentioned');\n      mentioned.textContent = `Mentioned in ${topic.coverage} of reviews`;\n\n      const li = document.createElement('li');\n      li.appendChild(title);\n      li.appendChild(copy);\n      li.appendChild(mentioned);\n\n      topics.append(li);\n    }\n\n    // render highlights\n    const highlightsList = this.shadowRoot.querySelector<HTMLElement>('.highlights ul');\n    for (const highlight of this.summaryHighlights) {\n      const blockquote = document.createElement('blockquote');\n      blockquote.textContent = highlight.highlight;\n\n      const li = document.createElement('li');\n      li.appendChild(blockquote);\n\n      highlightsList.appendChild(li);\n    }\n\n    // Show the whole content wrapper with a title if there is any content to display\n    const content = this.shadowRoot.querySelector<HTMLElement>('.content');\n    if (this.summaryHighlights.length > 0 || this.summaryTopics.length > 0 ||\n      this.paragraphSummary) {\n      content.hidden = false;\n    } else {\n      content.hidden = true;\n    }\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-ai-summary': YotpoAiSummary;\n  }\n}\n\nif (!customElements.get('yotpo-ai-summary')) {\n  customElements.define('yotpo-ai-summary', YotpoAiSummary);\n}\n","import { assert } from '@integrabeauty/assert';\nimport { decorateError } from '../../lib/decorate-error.js';\n\nexport interface Input {\n  /**\n   * The store ID\n   */\n  app_key: string;\n\n  /**\n   * Shopify product ID\n   */\n  product_id: number;\n}\n\nexport interface SummaryTopic {\n  avgSentiment: number;\n\n  /**\n   * Number of reviews covering this topic\n   */\n  coverage: string;\n\n  displayTopic: string;\n\n  summary: string;\n\n  topic: string;\n}\n\nexport interface SummaryHighlight {\n  highlight: string;\n\n  reviewId: number;\n\n  reviewerName: string;\n}\n\ninterface ResponseData {\n  bundledProductsData: unknown;\n  countries: unknown;\n  customQuestions: unknown;\n  externalId: string;\n  metadata: unknown;\n\n  /**\n   * A paragraph summarizing all reviews for the product\n   */\n  paragraphSummary: string;\n\n  productId: string;\n  productVariants: unknown;\n  storeId: string;\n  summaryHighlights: SummaryHighlight[];\n  summaryTopics: SummaryTopic[];\n  topics: unknown[];\n}\n\nexport interface ResponseBody {\n  response: ResponseData;\n\n  status: {\n    /**\n     * @example 200\n     */\n    code: number;\n\n    /**\n     * @example \"OK\"\n     */\n    message: string;\n  };\n}\n\n/**\n * Fetch AI-generated review summary data for a product from Yotpo.\n */\nexport async function fetchReviewsSummary(input: Input) {\n  assert(input.app_key, 'Invalid app key');\n  assert(Number.isInteger(input.product_id) && input.product_id > 0, 'Invalid product id');\n\n  const path = `/apps/yotpo-ugc-api-cdn/v3/storefront/stores/${input.app_key}/products/` +\n    `${input.product_id}/productData?include=reviewsSummary`;\n\n  const response = await fetch(path);\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} fetching reviews summary`);\n    error.code = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'code', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let body;\n  try {\n    body = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'code', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isResponseBody(body)) {\n    const error: any = new Error('Unexpected response body');\n    error.body = body;\n    error.code = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  if (body.status.code > 399) {\n    const error: any = new Error('Nested response body status code is not 200');\n    error.body = body;\n    error.code = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  return body.response;\n}\n\nfunction isResponseBody(value: any): value is ResponseBody {\n  return typeof value === 'object' && value !== null && typeof value.status === 'object' &&\n    value.status !== null && typeof value.response === 'object' && value.response !== null;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders Yotpo product reviews filters.\n */\nclass YotpoFilters extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * The product id, which could be a product sku, with which this element is associated.\n     */\n    productId: string;\n  };\n\n  private declare selectedFilters: YotpoReviewFilters | null;\n  private declare selectedSortings: YotpoReviewSortings;\n\n  private declare searchDebounceTimer: ReturnType<typeof setTimeout>;\n\n  private onHairTypeSelectedBound = this.onHairTypeSelected.bind(this);\n  private onMediaSelectedBound = this.onMediaSelected.bind(this);\n  private onRatingSelectedBound = this.onRatingSelected.bind(this);\n  private onSearchClearedBound = this.onSearchCleared.bind(this);\n  private onSearchKeyUpBound = this.onSearchKeyUp.bind(this);\n  private onSortSelectedBound = this.onSortSelected.bind(this);\n  private onStarsFilterClickedBound = this.onStarsFilterClicked.bind(this);\n  private onReviewsFilterChangeBound = this.onReviewsFilterChange.bind(this);\n  private onPillClickedBound = this.onPillClicked.bind(this);\n  private onClearAllClickedBound = this.onClearAllClicked.bind(this);\n  private onClearRequestedBound = this.onClearRequested.bind(this);\n  private onSearchStartedBound = this.onSearchStarted.bind(this);\n  private onSearchCompletedBound = this.onSearchCompleted.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const searchInput = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n    searchInput?.addEventListener('keyup', this.onSearchKeyUpBound);\n    searchInput?.addEventListener('search', this.onSearchClearedBound);\n\n    const hairTypeSelect = this.shadowRoot.querySelector('.filter-hair-type select');\n    hairTypeSelect?.addEventListener('change', this.onHairTypeSelectedBound);\n\n    const mediaSelect = this.shadowRoot.querySelector('.filter-media select');\n    mediaSelect?.addEventListener('change', this.onMediaSelectedBound);\n\n    const ratingSelect = this.shadowRoot.querySelector('.filter-rating select');\n    ratingSelect?.addEventListener('change', this.onRatingSelectedBound);\n\n    const sortSelect = this.shadowRoot.querySelector('.filter-sort select');\n    sortSelect?.addEventListener('change', this.onSortSelectedBound);\n\n    addEventListener('star-filter-clicked', this.onStarsFilterClickedBound);\n    addEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    addEventListener('yotpo-filter-clear-requested', this.onClearRequestedBound);\n    addEventListener('yotpo-reviews-search-started', this.onSearchStartedBound);\n    addEventListener('yotpo-reviews-searched', this.onSearchCompletedBound);\n  }\n\n  public disconnectedCallback() {\n    const searchInput = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n    searchInput?.removeEventListener('keyup', this.onSearchKeyUpBound);\n    searchInput?.removeEventListener('search', this.onSearchClearedBound);\n\n    const hairTypeSelect = this.shadowRoot.querySelector('.filter-hair-type select');\n    hairTypeSelect?.removeEventListener('change', this.onHairTypeSelectedBound);\n\n    const mediaSelect = this.shadowRoot.querySelector('.filter-media select');\n    mediaSelect?.removeEventListener('change', this.onMediaSelectedBound);\n\n    const ratingSelect = this.shadowRoot.querySelector('.filter-rating select');\n    ratingSelect?.removeEventListener('change', this.onRatingSelectedBound);\n\n    const sort = this.shadowRoot.querySelector('.filter-sort select');\n    sort?.removeEventListener('change', this.onSortSelectedBound);\n\n    const pills = this.shadowRoot.querySelectorAll<HTMLElement>('.pill');\n    for (const pill of pills) {\n      pill.removeEventListener('click', this.onPillClickedBound);\n    }\n\n    const clearAll = this.shadowRoot.querySelector<HTMLElement>('.clear');\n    clearAll?.removeEventListener('click', this.onClearAllClickedBound);\n\n    removeEventListener('star-filter-clicked', this.onStarsFilterClickedBound);\n    removeEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    removeEventListener('yotpo-filter-clear-requested', this.onClearRequestedBound);\n    removeEventListener('yotpo-reviews-search-started', this.onSearchStartedBound);\n    removeEventListener('yotpo-reviews-searched', this.onSearchCompletedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onSearchCleared(event: Event) {\n    if (event.currentTarget instanceof HTMLInputElement && event.currentTarget.value === '') {\n      clearTimeout(this.searchDebounceTimer);\n      this.dispatchFilter({ free_text_search: undefined });\n    }\n  }\n\n  private onSearchKeyUp(event: KeyboardEvent) {\n    event.stopPropagation();\n\n    if (!(event.currentTarget instanceof HTMLInputElement)) {\n      return;\n    }\n\n    const query = event.currentTarget.value;\n\n    clearTimeout(this.searchDebounceTimer);\n\n    if (!query) {\n      this.dispatchFilter({ free_text_search: undefined });\n      return;\n    }\n\n    let timeout = 500;\n    if (query.length < 4) {\n      timeout += 500;\n    }\n\n    this.searchDebounceTimer = setTimeout(this.dispatchFilter.bind(this), timeout,\n      { free_text_search: query });\n  }\n\n  private onHairTypeSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '') {\n      this.dispatchFilter({ hair_type: undefined });\n      return;\n    }\n\n    this.dispatchFilter({ hair_type: event.target.value });\n  }\n\n  private onRatingSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '0') {\n      this.dispatchFilter({ scores: [] });\n      return;\n    }\n\n    const score = parseInt(event.target.value, 10);\n    this.dispatchFilter({ scores: [score] });\n  }\n\n  /**\n   * Handles stars link click on the rating breakdown component\n   */\n  private onStarsFilterClicked(event: WindowEventMap['star-filter-clicked']) {\n    const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-rating select');\n    select.value = event.detail.stars.toString();\n    this.dispatchFilter({ scores: [event.detail.stars] });\n  }\n\n  private onMediaSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '') {\n      this.dispatchFilter({ pictured: null });\n      return;\n    }\n\n    this.dispatchFilter({ pictured: event.target.value === 'true' });\n  }\n\n  private onSortSelected(event: Event) {\n    if (!(event.target instanceof HTMLSelectElement)) {\n      return;\n    }\n\n    if (event.target.value === '0') {\n      return;\n    }\n\n    const sort = event.target.value;\n\n    let ascending = false;\n    let name = sort;\n\n    if (sort.includes('score')) {\n      name = 'score';\n      const [,direction] = sort.split('_');\n      if (direction === 'up') {\n        ascending = true;\n      }\n    }\n\n    this.dispatchFilter({}, [{ sort_by: name, ascending }]);\n  }\n\n  private dispatchFilter(filters: YotpoReviewFilters, sortings?: YotpoReviewSortings) {\n    if (!this.dataset.productId) {\n      return;\n    }\n\n    const selected = this.selectedFilters || {};\n    const merged = { ...selected, ...filters };\n\n    type Detail = HTMLElementEventMap['yotpo-reviews-filtered']['detail'];\n    const event = new CustomEvent<Detail>('yotpo-reviews-filtered', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: merged,\n\n        // Go back to the first page whenever filters change. Otherwise, if the user is on page 5,\n        // for example, but the new filters only return 2 pages of results, no reviews would be\n        // shown.\n        page: 0,\n\n        productId: parseInt(this.dataset.productId, 10),\n\n        // People don't think of the sort as a filter. It is a preference about how they want to\n        // read the list, so it stays put unless this is the sort itself changing.\n        sortings: sortings || this.selectedSortings\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onReviewsFilterChange(event: HTMLElementEventMap['yotpo-reviews-filtered']) {\n    this.selectedFilters = event.detail.filters;\n    this.selectedSortings = event.detail.sortings;\n    this.renderAppliedFilters();\n  }\n\n  private renderAppliedFilters() {\n    const container = this.shadowRoot.querySelector<HTMLElement>('.pills');\n    if (!container) {\n      return;\n    }\n\n    // Remove all existing pills and their event listeners before rendering new ones.\n    const existingPills = container.querySelectorAll<HTMLElement>('.pill');\n    for (const pill of existingPills) {\n      pill.removeEventListener('click', this.onPillClickedBound);\n    }\n\n    const clearButton = container.querySelector<HTMLElement>('.clear');\n    if (clearButton) {\n      clearButton.removeEventListener('click', this.onClearAllClickedBound);\n    }\n\n    container.innerHTML = '';\n\n    const filters = this.selectedFilters;\n    if (!filters) {\n      container.hidden = true;\n      return;\n    }\n\n    // building the pills array to render the applied filters\n    const pills: { dimension: string; label: string; }[] = [];\n\n    if (filters.scores && filters.scores.length > 0) {\n      for (const score of filters.scores) {\n        pills.push({ dimension: 'scores', label: 'Rating: ' + score + ' Stars' });\n      }\n    }\n\n    if (filters.pictured === true) {\n      pills.push({ dimension: 'pictured', label: 'With Images & Videos' });\n    } else if (filters.pictured === false) {\n      pills.push({ dimension: 'pictured', label: 'Without Images & Videos' });\n    }\n\n    if (filters.hair_type) {\n      pills.push({ dimension: 'hair_type', label: 'Hair Type: ' + filters.hair_type });\n    }\n\n    if (filters.free_text_search) {\n      pills.push({ dimension: 'free_text_search', label: 'Search: ' + filters.free_text_search });\n    }\n\n    // if no filters applied then hide the pills container\n    if (pills.length === 0) {\n      container.hidden = true;\n      return;\n    }\n\n    for (const pill of pills) {\n      const button = document.createElement('button');\n      button.className = 'pill';\n      button.dataset.dimension = pill.dimension;\n      button.addEventListener('click', this.onPillClickedBound);\n\n      const span = document.createElement('span');\n      span.textContent = pill.label;\n      button.appendChild(span);\n      container.appendChild(button);\n    }\n\n    const clearAll = document.createElement('button');\n    clearAll.className = 'clear';\n    clearAll.textContent = 'CLEAR ALL';\n    clearAll.addEventListener('click', this.onClearAllClickedBound);\n    container.appendChild(clearAll);\n\n    container.hidden = false;\n  }\n\n  private onPillClicked(event: MouseEvent) {\n    if (!(event.currentTarget instanceof HTMLElement)) {\n      return;\n    }\n\n    const dimension = event.currentTarget.dataset.dimension;\n    if (!dimension) {\n      return;\n    }\n\n    if (dimension === 'scores') {\n      const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-rating select');\n      if (select) {\n        select.value = '0';\n      }\n\n      this.dispatchFilter({ scores: [] });\n    } else if (dimension === 'pictured') {\n      const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-media select');\n      if (select) {\n        select.value = '';\n      }\n\n      this.dispatchFilter({ pictured: null });\n    } else if (dimension === 'hair_type') {\n      const select = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-hair-type select');\n      if (select) {\n        select.value = '';\n      }\n\n      this.dispatchFilter({ hair_type: undefined });\n    } else if (dimension === 'free_text_search') {\n      const input = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n      if (input) {\n        input.value = '';\n      }\n\n      this.dispatchFilter({ free_text_search: undefined });\n    }\n  }\n\n  private onClearAllClicked(_event: MouseEvent) {\n    this.clearAllFilters();\n  }\n\n  private onClearRequested(_event: WindowEventMap['yotpo-filter-clear-requested']) {\n    this.clearAllFilters();\n  }\n\n  private onSearchStarted() {\n    const inputs = this.shadowRoot.querySelectorAll<HTMLInputElement | HTMLSelectElement>(\n      'input, select');\n    for (const input of inputs) {\n      input.disabled = true;\n    }\n  }\n\n  private onSearchCompleted() {\n    const inputs = this.shadowRoot.querySelectorAll<HTMLInputElement | HTMLSelectElement>(\n      'input, select');\n    for (const input of inputs) {\n      input.disabled = false;\n    }\n  }\n\n  private clearAllFilters() {\n    const ratingSelect = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-rating select');\n    if (ratingSelect) {\n      ratingSelect.value = '0';\n    }\n\n    const mediaSelect = this.shadowRoot.querySelector<HTMLSelectElement>('.filter-media select');\n    if (mediaSelect) {\n      mediaSelect.value = '';\n    }\n\n    const hairTypeSelect = this.shadowRoot.querySelector<HTMLSelectElement>(\n      '.filter-hair-type select');\n    if (hairTypeSelect) {\n      hairTypeSelect.value = '';\n    }\n\n    const searchInput = this.shadowRoot.querySelector<HTMLInputElement>('.search-input');\n    if (searchInput) {\n      searchInput.value = '';\n    }\n\n    this.dispatchFilter({\n      free_text_search: undefined,\n      hair_type: undefined,\n      pictured: null,\n      scores: []\n    });\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-filters': YotpoFilters;\n  }\n}\n\nif (!customElements.get('yotpo-filters')) {\n  customElements.define('yotpo-filters', YotpoFilters);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\n/**\n * Renders pagination links for Yotpo reviews and questions.\n */\nclass YotpoPagination extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-page', 'data-update'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Currently rendered page number.\n     */\n    page: string;\n\n    /**\n     * Number of items to be rendered per page.\n     */\n    perPage: string;\n\n    /**\n     * Number of all items to be rendered.\n     */\n    total: string;\n\n    /**\n     * Setting this attribute triggers rendering.\n     */\n    update: string;\n  };\n\n  /**\n   * Currently selected page.\n   */\n  private currentPage = 0;\n\n  /**\n   * Total number of pages.\n   */\n  private lastPage = 0;\n\n  /**\n   * Max number of page buttons to display. Arrow buttons are not included. We use it to determine\n   * if we need to show the arrow buttons and change it depending on screen size to be sure the\n   * device can fit the pagination. We can't use less than 5 because we need to show the first and\n   * last page buttons, and two ellipsis buttons. If we use 5, we will show the first page, the\n   * current page, the last page, and two ellipses (\"< 1 … 5 … 10 >\"). Use only odd numbers to\n   * ensure the current page is always in the middle of the pagination buttons.\n   */\n  private maxButtons = 5;\n\n  /**\n   * Media query to determine if the screen is larger than 768px. If so, we will show more buttons.\n   */\n  private declare mediaQuery: MediaQueryList;\n\n  private onPreviousButtonClickedBound = this.onPreviousButtonClicked.bind(this);\n  private onNextButtonClickedBound = this.onNextButtonClicked.bind(this);\n  private onPageClickedBound = this.onPageClicked.bind(this);\n  private onMediaQueryChangedBound = this.onMediaQueryChanged.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const prevButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-prev');\n    prevButton?.addEventListener('click', this.onPreviousButtonClickedBound);\n\n    const nextButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-next');\n    nextButton?.addEventListener('click', this.onNextButtonClickedBound);\n\n    const pages = this.shadowRoot.querySelector<HTMLElement>('.pagination-pages');\n    pages?.addEventListener('click', this.onPageClickedBound);\n\n    this.mediaQuery = matchMedia('(min-width: 768px)');\n    this.mediaQuery.addEventListener('change', this.onMediaQueryChangedBound);\n\n    // Check if the media query matches on initial load and update the maxButtons value accordingly\n    // before the first render.\n    if (this.mediaQuery.matches) {\n      this.maxButtons = 7;\n    }\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const prevButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-prev');\n    prevButton?.removeEventListener('click', this.onPreviousButtonClickedBound);\n\n    const nextButton = this.shadowRoot.querySelector<HTMLElement>('.pagination-next');\n    nextButton?.removeEventListener('click', this.onNextButtonClickedBound);\n\n    const pages = this.shadowRoot.querySelector<HTMLElement>('.pagination-pages');\n    pages?.removeEventListener('click', this.onPageClickedBound);\n\n    if (this.mediaQuery) {\n      this.mediaQuery.removeEventListener('change', this.onMediaQueryChangedBound);\n    }\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    this.initializeShadowRoot();\n\n    if (oldValue !== newValue) {\n      switch (name) {\n        case 'data-page':\n          this.currentPage = parseInt(newValue, 10);\n          break;\n        case 'data-update':\n          this.render();\n          break;\n      }\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onPreviousButtonClicked(_event: MouseEvent) {\n    if (this.currentPage === 1) {\n      return;\n    }\n\n    // We leave the current page alone here. It only changes once the reviews element has fetched\n    // the new page and hands the page number back, so the highlight never gets ahead of the reviews\n    // on screen, and stays put if the fetch fails.\n\n    type Detail = WindowEventMap['yotpo-page-changed']['detail'];\n    const event = new CustomEvent<Detail>('yotpo-page-changed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        page: this.currentPage - 1\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  private onNextButtonClicked(_event: MouseEvent) {\n    type Detail = WindowEventMap['yotpo-page-changed']['detail'];\n    const event = new CustomEvent<Detail>('yotpo-page-changed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        page: this.currentPage + 1 > this.lastPage ? this.lastPage : this.currentPage + 1\n      }\n    });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * Handles clicks on page buttons. We are using event delegation and listening for clicks on the\n   * wrapper.\n   */\n  private onPageClicked(event: MouseEvent) {\n    // The ellipsis is a span, not a button, so we need to check that the click target is a button.\n    // It has the same class as the button because the styles are almost the same, so the\n    // classList.contains check below is not enough.\n    if (!(event.target instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    // We are listening for clicks on the wrapper element, so we need to check if the click target\n    // is a page button. If not, we can ignore the click.\n    if (!event.target.classList.contains('page-button')) {\n      return;\n    }\n\n    // Prevent clicking on the active page button. We don't want to trigger a page change event if\n    // the user clicks on the active page button. We can ignore the click in that case.\n    if (event.target.classList.contains('active')) {\n      return;\n    }\n\n    type Detail = WindowEventMap['yotpo-page-changed']['detail'];\n    const changeEvent = new CustomEvent<Detail>('yotpo-page-changed', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        page: parseInt(event.target.dataset.idx, 10)\n      }\n    });\n    this.dispatchEvent(changeEvent);\n  }\n\n  /**\n   * Handles the media query changes. We use this to update the maxButtons property and rerender the\n   * pagination to reflect the new maxButtons value. This is important because we want to show more\n   * buttons on larger screens and less on smaller screens.\n   */\n  private onMediaQueryChanged(mediaQueryList: MediaQueryListEvent) {\n    let newMaxButtons: number;\n    if (mediaQueryList.matches) {\n      newMaxButtons = 7;\n    } else {\n      newMaxButtons = 5;\n    }\n\n    // rerender only if the maxButtons value has changed\n    if (newMaxButtons !== this.maxButtons) {\n      this.maxButtons = newMaxButtons;\n      this.render();\n    }\n  }\n\n  private render() {\n    const listWrapper = this.shadowRoot.querySelector<HTMLElement>('.pagination-pages');\n\n    // Reset on every render. One reason this is important is because on tab switch there might be\n    // zero results. Total gets updated to 0 and this re-renders. We do not want to keep showing old\n    // pagination.\n    listWrapper.innerHTML = '';\n\n    const buttonPrev = this.shadowRoot.querySelector<HTMLButtonElement>('.pagination-prev');\n    if (buttonPrev) {\n      buttonPrev.hidden = true;\n    }\n\n    const buttonNext = this.shadowRoot.querySelector<HTMLButtonElement>('.pagination-next');\n    if (buttonNext) {\n      buttonNext.hidden = true;\n    }\n\n    const perPage = parseInt(this.dataset.perPage, 10);\n    const total = parseInt(this.dataset.total, 10) || 0;\n\n    if (this.currentPage === 0 || perPage === 0 || total === 0) {\n      return;\n    }\n\n    // Disable left arrow on first page.\n    if (buttonPrev) {\n      if (this.currentPage === 1) {\n        buttonPrev.disabled = true;\n      } else {\n        buttonPrev.disabled = false;\n      }\n    }\n\n    this.lastPage = Math.ceil(total / perPage);\n\n    // The middle item index is used to determine the correct starting index. We subtract 1 because\n    // the second-to-last item is reserved for the ellipsis\n    const middleItemIndex = Math.floor(this.maxButtons / 2);\n\n    // Disable right arrow on last page.\n    if (buttonNext) {\n      if (this.currentPage >= this.lastPage) {\n        buttonNext.disabled = true;\n      } else {\n        buttonNext.disabled = false;\n      }\n    }\n\n    // if all pages can't be displayed, then show the prev and next buttons\n    if (this.lastPage > this.maxButtons) {\n      if (buttonPrev) {\n        buttonPrev.hidden = false;\n      }\n\n      if (buttonNext) {\n        buttonNext.hidden = false;\n      }\n    }\n\n    // Set the starting index. It's an index of the first button to display. We want to display the\n    // current page in the middle of the pagination buttons, so we subtract the middle item index\n    // from the current page to get the starting index. This will ensure that the current page is\n    // always in the middle of the pagination buttons, unless we are at the beginning or end of the\n    // pagination.\n    let startingIndex = this.currentPage - middleItemIndex;\n\n    // If the current page is less than or equal to the middle item index, we want to start\n    // displaying the buttons from the first page.\n    if (startingIndex <= 0) {\n      startingIndex = 1;\n    }\n\n    // If the starting index plus the max buttons is greater than the last page, that means we are\n    // at the end of pagination. In that case, we want to start displaying the buttons from the last\n    // page minus the max buttons.\n    if (startingIndex + this.maxButtons > this.lastPage) {\n      startingIndex = (this.lastPage - this.maxButtons) + 1;\n    }\n\n    // Set the number of items to display. Usually, we show the maximum number of buttons, set by\n    // this.maxButtons. But if there are fewer pages than maxButtons, we show all pages, so the\n    // starting index should be 1 and the number of items to display should be the last page.\n    let numberOfItemsToDisplay = this.maxButtons;\n    if (this.lastPage < this.maxButtons) {\n      numberOfItemsToDisplay = this.lastPage;\n      startingIndex = 1;\n    }\n\n    // The first ellipsis (second item after the first page) should be rendered if the starting\n    // index is greater than 1, which means there are more pages before the starting index that are\n    // not being displayed.\n    const showFirstEllipsis = startingIndex > 1;\n\n    // Check if we need to show the ellipsis at the end of the pagination before the last page. We\n    // need to subtract 1 because of the ellipsis at the beginning of the pagination right after the\n    // first page.\n    const showSecondEllipsis = startingIndex + numberOfItemsToDisplay - 1 < this.lastPage;\n\n    // Rendering pagination buttons\n    for (let i = 0; i < numberOfItemsToDisplay; i++) {\n      // The current index is the starting index plus the loop index. This is used to determine\n      // which page number to display on the button\n      let currentIndex = startingIndex + i;\n\n      // If it's the last button to render we need to always display the last page number\n      if (i + 1 === numberOfItemsToDisplay) {\n        currentIndex = this.lastPage;\n      }\n\n      // If it's the first button to render and we need to show the first ellipsis, we need to\n      // always display the first page number\n      if (showFirstEllipsis && i === 0) {\n        currentIndex = 1;\n      }\n\n      // Check if we need to render an ellipsis instead of page button. It happens in two cases:\n      // 1) when the first ellipsis should be rendered and we are rendering the second item\n      // 2) when the second ellipsis should be rendered and we are rendering the second-to-last item\n      const shouldRenderEllipsis = showFirstEllipsis && i === 1 ||\n        showSecondEllipsis && i === numberOfItemsToDisplay - 2;\n      if (shouldRenderEllipsis) {\n        const ellipsis = document.createElement('span');\n        ellipsis.textContent = '...';\n        ellipsis.classList.add('page-button');\n\n        const li = document.createElement('li');\n        li.appendChild(ellipsis);\n        listWrapper.appendChild(li);\n      } else {\n        const button = document.createElement('button');\n        button.type = 'button';\n        button.classList.add('page-button');\n        button.dataset.idx = `${currentIndex}`;\n        button.textContent = `${currentIndex}`;\n        button.setAttribute('aria-label', `Go to page ${currentIndex}`);\n\n        // set the active pagination button\n        if (this.currentPage === currentIndex) {\n          button.classList.add('active');\n          button.setAttribute('aria-current', 'page');\n        }\n\n        const li = document.createElement('li');\n        li.appendChild(button);\n        listWrapper.appendChild(li);\n      }\n    }\n  }\n}\n\n/**\n * Fired when a user interacts with the pagination element.\n */\ntype PageChangedEvent = CustomEvent<{\n  page: number;\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'yotpo-page-changed': PageChangedEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'yotpo-pagination': YotpoPagination;\n  }\n}\n\nif (!customElements.get('yotpo-pagination')) {\n  customElements.define('yotpo-pagination', YotpoPagination);\n}\n","const rnds8 = new Uint8Array(16);\nexport default function rng() {\n    return crypto.getRandomValues(rnds8);\n}\n","import validate from './validate.js';\nconst byteToHex = [];\nfor (let i = 0; i < 256; ++i) {\n    byteToHex.push((i + 0x100).toString(16).slice(1));\n}\nexport function unsafeStringify(arr, offset = 0) {\n    return (byteToHex[arr[offset + 0]] +\n        byteToHex[arr[offset + 1]] +\n        byteToHex[arr[offset + 2]] +\n        byteToHex[arr[offset + 3]] +\n        '-' +\n        byteToHex[arr[offset + 4]] +\n        byteToHex[arr[offset + 5]] +\n        '-' +\n        byteToHex[arr[offset + 6]] +\n        byteToHex[arr[offset + 7]] +\n        '-' +\n        byteToHex[arr[offset + 8]] +\n        byteToHex[arr[offset + 9]] +\n        '-' +\n        byteToHex[arr[offset + 10]] +\n        byteToHex[arr[offset + 11]] +\n        byteToHex[arr[offset + 12]] +\n        byteToHex[arr[offset + 13]] +\n        byteToHex[arr[offset + 14]] +\n        byteToHex[arr[offset + 15]]).toLowerCase();\n}\nfunction stringify(arr, offset = 0) {\n    const uuid = unsafeStringify(arr, offset);\n    if (!validate(uuid)) {\n        throw TypeError('Stringified UUID is invalid');\n    }\n    return uuid;\n}\nexport default stringify;\n","import rng from './rng.js';\nimport { unsafeStringify } from './stringify.js';\nfunction v4(options, buf, offset) {\n    if (!buf && !options && crypto.randomUUID) {\n        return crypto.randomUUID();\n    }\n    return _v4(options, buf, offset);\n}\nfunction _v4(options, buf, offset) {\n    options = options || {};\n    const rnds = options.random ?? options.rng?.() ?? rng();\n    if (rnds.length < 16) {\n        throw new Error('Random bytes length must be >= 16');\n    }\n    rnds[6] = (rnds[6] & 0x0f) | 0x40;\n    rnds[8] = (rnds[8] & 0x3f) | 0x80;\n    if (buf) {\n        offset = offset || 0;\n        if (offset < 0 || offset + 16 > buf.length) {\n            throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);\n        }\n        for (let i = 0; i < 16; ++i) {\n            buf[offset + i] = rnds[i];\n        }\n        return buf;\n    }\n    return unsafeStringify(rnds);\n}\nexport default v4;\n","export var SwellPopupType;\n(function (SwellPopupType) {\n    /**\n     * The standard rewards popup that customers use to earn points, redeem points, manage referral\n     * program, view history, and read faqs. You can configure it in the Rewards Popup section of\n     * the Swell Admin.\n     */\n    SwellPopupType[\"REWARDS_POPUP\"] = \"RewardsPopup\";\n    /**\n     * This popup is used to collect email addresses for your newsletter. You can configure and\n     * design it in the Newsletter Signup section of the Swell Admin.\n     */\n    SwellPopupType[\"EMAIL_CAPTURE_POPUP\"] = \"EmailCapturePopup\";\n    /**\n     * This popup is used by customers to view and share their referral link via social media and\n     * email. You can configure and design it in the Referral Program section of the Swell Admin.\n     */\n    SwellPopupType[\"REFERRAL_LINK_POPUP\"] = \"ReferralLinkPopup\";\n    /**\n     * This popup is used to view all point earning and redemption activity. It will also display\n     * any coupon codes earned by the customer.\n     */\n    SwellPopupType[\"HISTORY_POPUP\"] = \"HistoryPopup\";\n})(SwellPopupType || (SwellPopupType = {}));\n","export var SwellReferralMedium;\n(function (SwellReferralMedium) {\n    SwellReferralMedium[\"UNDEFINED\"] = \"undefined\";\n    SwellReferralMedium[\"FACEBOOK\"] = \"facebook\";\n    SwellReferralMedium[\"TWITTER\"] = \"twitter\";\n    SwellReferralMedium[\"SMS\"] = \"sms\";\n    SwellReferralMedium[\"EMAIL\"] = \"email\";\n    SwellReferralMedium[\"COPY_LINK\"] = \"copy_link\";\n})(SwellReferralMedium || (SwellReferralMedium = {}));\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport { v4 as uuidv4 } from 'uuid';\nimport html from './index.html';\nimport styles from './index.scss';\nimport { createReview, type Input } from './yotpo-create-review.js';\n\n/**\n * Renders Yotpo reviews \"Write a review\" button and review form in a drawer.\n *\n * This has to observe changes to data-yotpo-app-key because of the way this element is initially\n * included in the parent element template. It is already connected and then the attribute is set\n * later.\n */\nclass YotpoReviewCreate extends HTMLElement implements CustomElement {\n  public static get observedAttributes() {\n    return ['data-modal-state'];\n  }\n\n  declare readonly dataset: {\n    /**\n     * Whether the drawer is shown, as \"open\". Anything else, or no value at all, means closed. A\n     * change only takes effect while the element is on the page. For now the element sets this\n     * itself. Later the modal manager sets it instead.\n     */\n    modalState: string;\n\n    /**\n     * Shopify product id to which the newly created review will pertain\n     */\n    productId: string;\n\n    /**\n     * Shopify product price to which the newly created review will pertain\n     */\n    productPrice: string;\n\n    /**\n     * Shopify product title to which the newly created review will pertain\n     */\n    productTitle: string;\n\n    /**\n     * Shopify product type to which the newly created review will pertain\n     */\n    productType: string;\n\n    /**\n     * Shopify product url to which the newly created review will pertain\n     */\n    productUrl: string;\n\n    /**\n     * Yotpo API app key\n     */\n    yotpoAppKey: string;\n  };\n\n  private onStarClickedBound = this.onStarClicked.bind(this);\n  private onWriteReviewButtonClickedBound = this.onWriteReviewButtonClicked.bind(this);\n  private onSubmitButtonClickedBound = this.onSubmitButtonClicked.bind(this);\n  private onCloseButtonClickedBound = this.onCloseButtonClicked.bind(this);\n  private onModalDismissRequestedBound = this.onModalDismissRequested.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const writeReviewButton = this.shadowRoot.querySelector<HTMLButtonElement>('.write-review');\n    writeReviewButton.addEventListener('click', this.onWriteReviewButtonClickedBound);\n\n    const submitButton = this.shadowRoot.querySelector<HTMLButtonElement>('button[type=\"submit\"]');\n    submitButton.addEventListener('click', this.onSubmitButtonClickedBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton.addEventListener('click', this.onCloseButtonClickedBound);\n\n    const ratingStarsElement = this.shadowRoot.querySelector<HTMLElement>('.rating-stars');\n    ratingStarsElement.addEventListener('click', this.onStarClickedBound);\n  }\n\n  public disconnectedCallback() {\n    const writeReviewButton = this.shadowRoot.querySelector<HTMLButtonElement>('.write-review');\n    writeReviewButton?.removeEventListener('click', this.onWriteReviewButtonClickedBound);\n\n    const submitButton = this.shadowRoot.querySelector<HTMLButtonElement>('button[type=\"submit\"]');\n    submitButton?.removeEventListener('click', this.onSubmitButtonClickedBound);\n\n    const closeButton = this.shadowRoot.querySelector('close-button');\n    closeButton?.removeEventListener('click', this.onCloseButtonClickedBound);\n\n    const ratingStarsElement = this.shadowRoot.querySelector<HTMLElement>('.rating-stars');\n    ratingStarsElement?.removeEventListener('click', this.onStarClickedBound);\n  }\n\n  public attributeChangedCallback(name: string, oldValue: string, newValue: string) {\n    if (!this.isConnected || oldValue === newValue) {\n      return;\n    }\n\n    if (name === 'data-modal-state') {\n      if (newValue === 'open') {\n        this.openDrawer();\n      } else if (oldValue === 'open') {\n        this.closeDrawer();\n      }\n    }\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private onSubmitButtonClicked(event: MouseEvent) {\n    event.preventDefault();\n    const submitButton = event.currentTarget;\n    if (!(submitButton instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    const form = this.shadowRoot.querySelector('form');\n\n    const isValid = form.checkValidity();\n    if (!isValid) {\n      // Show fields' error message\n      let firstInvalid: HTMLElement = null;\n      for (const element of form.elements) {\n        if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {\n          continue;\n        }\n\n        if (element.checkValidity()) {\n          element.parentElement.classList.remove('error');\n          continue;\n        }\n\n        element.parentElement.classList.add('error');\n        firstInvalid ??= element;\n      }\n\n      // We want to drop the user into the first thing they still have to fill in. Usually that is\n      // just the field itself, but the score is a special case. The score input holds the number of\n      // stars the user picked and we keep it hidden, because the five star buttons above it are the\n      // real thing people click. Nothing can move focus into a hidden input, so if we asked for it\n      // we would get no focus move at all and the user would be left wondering where to go. That\n      // happens on an empty form, which is the most common way to fail this. So when the score is\n      // the first thing missing we send them to the first star instead.\n      if (firstInvalid instanceof HTMLInputElement && firstInvalid.name === 'score') {\n        firstInvalid = this.shadowRoot.querySelector<HTMLButtonElement>('.star');\n      }\n\n      firstInvalid?.focus({ focusVisible: true });\n\n      // Show a general error message\n      const errorSummary = this.shadowRoot.querySelector<HTMLElement>('.error-summary');\n      errorSummary.hidden = false;\n\n      return;\n    }\n\n    const errorSummary = this.shadowRoot.querySelector<HTMLElement>('.error-summary');\n    errorSummary.hidden = true;\n\n    const formData = new FormData(form);\n\n    // Product ids are sent in the sku property to Yotpo.\n    const displayName = `${formData.get('first_name')} ${formData.get('last_name')}`;\n    const createReviewInput: Partial<Input> = {\n      appkey: this.dataset.yotpoAppKey,\n      display_name: displayName,\n      email: <string>formData.get('email'),\n      product_title: this.dataset.productTitle,\n      product_url: this.dataset.productUrl,\n      review_content: <string>formData.get('review'),\n      review_score: parseInt(<string>formData.get('score'), 10),\n      review_title: <string>formData.get('title'),\n      sku: this.dataset.productId\n    };\n\n    const loader = this.shadowRoot.querySelector('loading-indicator');\n    submitButton.disabled = true;\n    loader.hidden = false;\n\n    createReview(createReviewInput).then(() => {\n      submitButton.disabled = false;\n      loader.hidden = true;\n      form.reset();\n      this.dataset.modalState = 'closed';\n\n      type Detail = WindowEventMap['yotpo-review-submitted']['detail'];\n      const submittedEvent = new CustomEvent<Detail>('yotpo-review-submitted', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          body: createReviewInput.review_content,\n          category: this.dataset.productType,\n          email: createReviewInput.email,\n          error_message: '',\n          event_id: uuidv4(),\n          first_name: <string>formData.get('first_name'),\n          last_name: <string>formData.get('last_name'),\n          person_name: createReviewInput.display_name,\n          product_id: parseInt(this.dataset.productId, 10),\n          product_price: parseInt(this.dataset.productPrice, 10),\n          product_title: this.dataset.productTitle,\n          rating: createReviewInput.review_score,\n          review_title: createReviewInput.review_title,\n          sku: createReviewInput.sku\n        }\n      });\n      this.dispatchEvent(submittedEvent);\n\n      const identityEvent = new CustomEvent('identity-captured', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          email: createReviewInput.email,\n          first_name: <string>formData.get('first_name'),\n          last_name: <string>formData.get('last_name')\n        }\n      });\n      this.dispatchEvent(identityEvent);\n    }).catch(error => {\n      submitButton.disabled = false;\n      loader.hidden = true;\n      errorSummary.textContent = 'Sorry we are having temporary problems, please try again later.';\n      errorSummary.parentElement.classList.add('error');\n      errorSummary.hidden = false;\n\n      console.error(error, createReviewInput);\n    });\n  }\n\n  /**\n   * Opens the write a review form drawer when the button is clicked\n   */\n  private onWriteReviewButtonClicked(_event: MouseEvent) {\n    // handle write a review button accessibility\n    const writeReview = this.shadowRoot.querySelector('.write-review');\n    writeReview.setAttribute('aria-expanded', 'true');\n\n    // Claim focus return before opening. Without this the modal manager has nothing of ours on its\n    // focus stack, so closing the drawer reaches down to whichever opener an earlier modal left\n    // there, and focusing that one scrolls the page away to wherever it sits.\n    const focusEvent = new Event('focus-stack-push-requested', {\n      bubbles: true,\n      composed: true\n    });\n    writeReview.dispatchEvent(focusEvent);\n\n    // The modal manager leaves the reviews section active while the drawer is open, so the button\n    // would stay reachable by Tab behind it. The reviews element inerts the rest of the section.\n    if (writeReview instanceof HTMLElement) {\n      writeReview.inert = true;\n    }\n\n    this.dataset.modalState = 'open';\n  }\n\n  /**\n   * Handles the `click` event on the close button of the drawer.\n   */\n  private onCloseButtonClicked(_event: MouseEvent) {\n    this.dataset.modalState = 'closed';\n  }\n\n  /**\n   * Handles the event sent when clicking on the modal backdrop or pressing the Escape key. The\n   * Event is sent by modal-manager.\n   */\n  private onModalDismissRequested(_event: Event) {\n    this.dataset.modalState = 'closed';\n  }\n\n  private openDrawer() {\n    const drawer = this.shadowRoot.querySelector<HTMLElement>('#drawer');\n    drawer.inert = false;\n\n    // listen for clicks outside the drawer and for Escape key presses\n    addEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    // notify modal-manager that the drawer is open\n    const event = new Event('modal-open-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(event);\n\n    const closeButton = drawer.querySelector('close-button');\n    closeButton.focus({ focusVisible: true });\n  }\n\n  private closeDrawer() {\n    const drawer = this.shadowRoot.querySelector<HTMLElement>('#drawer');\n    drawer.inert = true;\n\n    // handle write a review button accessibility\n    const writeReview = this.shadowRoot.querySelector('.write-review');\n    writeReview.setAttribute('aria-expanded', 'false');\n\n    // This has to happen before the close request goes out, since the modal manager puts focus back\n    // on this button when it hears it, and a focus call into an inert subtree does nothing.\n    if (writeReview instanceof HTMLElement) {\n      writeReview.inert = false;\n    }\n\n    // we need to remove listeners for clicks outside the drawer and for Escape key presses to not\n    // interfere with other modals\n    removeEventListener('modal-dismiss-requested', this.onModalDismissRequestedBound);\n\n    // notify modal-manager that the drawer is closed\n    const event = new Event('modal-close-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * Handles the `click` event on the rating stars. Since we're listening on the parent element, we\n   * need to check whether the click was on a star, not on the parent or another child element.\n   */\n  private onStarClicked(event: MouseEvent) {\n    if (event.target instanceof HTMLButtonElement && event.target.classList.contains('star')) {\n      this.selectStar(event.target);\n    }\n  }\n\n  /**\n   * Visually selects a star and all stars before it, and sets the value of the hidden input field\n   * to the selected star's value.\n   */\n  private selectStar(target: HTMLButtonElement) {\n    const score = parseInt(target.dataset.idx, 10);\n    const scoreInput = this.shadowRoot.querySelector<HTMLInputElement>('input[name=score]');\n    scoreInput.value = score.toString();\n\n    const stars = this.shadowRoot.querySelectorAll<HTMLElement>('.star');\n    for (const star of stars) {\n      const idx = parseInt(star.dataset.idx, 10);\n      if (idx === score) {\n        star.textContent = '★';\n        star.setAttribute('aria-checked', 'true');\n      } else if (idx < score) {\n        star.textContent = '★';\n        star.setAttribute('aria-checked', 'false');\n      } else {\n        // idx > score\n        star.textContent = '☆';\n        star.setAttribute('aria-checked', 'false');\n      }\n    }\n  }\n}\n\ninterface ReviewSubmittedDetail {\n  /**\n   * The body content of the review.\n   */\n  body: string;\n\n  /**\n   * Product type.\n   *\n   * @example \"hair-dryer\"\n   */\n  category: string;\n\n  /**\n   * An email address provided by the reviewer.\n   */\n  email: string;\n\n  /**\n   * Error message if review submission failed\n   */\n  error_message: string;\n\n  event_id?: string;\n\n  first_name: string;\n\n  last_name: string;\n\n  /**\n   * The name provided by the visitor submitting the review.\n   *\n   * @example \"Jane Doe\"\n   */\n  person_name: string;\n\n  product_id: number;\n\n  product_price: number;\n\n  /**\n   * The name of the product that was reviewed.\n   *\n   * @example \"Le Volume Blush 75mm\"\n   */\n  product_title: string;\n\n  /**\n   * The number of stars specified by the person submitting the review. May be 0.\n   *\n   * @example 1\n   */\n  rating: number;\n\n  review_title: string;\n\n  /**\n   * The SKU of the product that was reviewed\n   */\n  sku: string;\n}\n\n/**\n * This event is emitted when a user submits a product review.\n *\n * Technically this event is emitted after a successful request to the Yotpo API to create the\n * product review. There is no event emitted on error.\n */\ntype ReviewSubmittedEvent = CustomEvent<Partial<ReviewSubmittedDetail>>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'yotpo-review-submitted': ReviewSubmittedEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'yotpo-review-create': YotpoReviewCreate;\n  }\n}\n\nif (!customElements.get('yotpo-review-create')) {\n  customElements.define('yotpo-review-create', YotpoReviewCreate);\n}\n","import { assert } from '@integrabeauty/assert';\nimport { isCreateReviewAPIResponseBody } from '@integrabeauty/yotpo-browser-types';\nimport { decorateError } from '../../../lib/decorate-error.js';\n\nexport interface Input {\n  /**\n   * The store ID\n   */\n  appkey: string;\n\n  customer_metadata?: {\n    /**\n     * The customer's address\n     */\n    address?: string;\n\n    /**\n     * Two-letter uppercase country code indicating the customer country. To comply with Google\n     * Seller Ratings conventions, the country code value must be from this list of compatible\n     * codes. Default value: US.\n     */\n    country?: string;\n\n    custom_properties?: {\n      name?: string;\n      value?: string;\n    };\n\n    /**\n     * The customer's phone number\n     */\n    phone_number?: string;\n\n    /**\n     * The state the customer is located in\n     */\n    state?: string;\n  };\n\n  /**\n   * The type of delivery designated for this purchase e.g. express, expedited, standard.\n   */\n  delivery_type?: string;\n\n  /**\n   * The reviewer's name\n   */\n  display_name: string;\n\n  /**\n   * The account domain\n   */\n  domain?: string;\n\n  /**\n   * The email of the reviewer\n   */\n  email: string;\n\n  /**\n   * The type of incentive provided for the review\n   */\n  incentive_type?:\n    'coupon' |\n    'employee_review' |\n    'free_product' |\n    'loyalty_points' |\n    'other' |\n    'paid_promotion';\n\n  /**\n   * Indicates if the review comes from an incentivized source (for example, a product sampling\n   * company).\n   */\n  is_incentivized?: boolean;\n\n  order_metadata?: {\n    /**\n     * Part of order_metadata, indicates whether a coupon was used at purchase\n     */\n    coupon_used?: boolean;\n\n    /**\n     * The name of an order-related custom property e.g. order history\n     */\n    name?: string;\n\n    /**\n     * The order-related custom_properties value\n     */\n    value?: string;\n  };\n\n  /**\n   * The url of the product image\n   *\n   * @example \"http://www.shop.com/phone.jpg\"\n   */\n  product_image_url?: string;\n\n  product_metadata?: {\n    color?: string;\n    coupon_used?: boolean;\n    custom_properties?: {\n      name?: string;\n      value?: string;\n    };\n    material?: string;\n    model?: string;\n    size?: string;\n    vendor?: string;\n  };\n\n  /**\n   * The title of the product\n   */\n  product_title: string;\n\n  /**\n   * The url of the product\n   */\n  product_url: string;\n\n  /**\n   * The content of the review\n   */\n  review_content: string;\n\n  /**\n   * The review score. Yotpo documents this as an int32, so it is a whole number.\n   */\n  review_score: number;\n\n  /**\n   * The title of the review\n   */\n  review_title: string;\n\n  /**\n   * Required for customers using the Trusted Vendors feature. Possible values: verified_buyer or\n   * verified_reviewer.\n   */\n  reviewer_type?: string;\n\n  /**\n   * Relevant only for customers using the Trusted Vendors feature. The signature is the hexadecimal\n   * representation of a computed Hash-based Message Authentication Code, using SHA256 as the\n   * cryptographic function (HMAC-SHA256). The secret for the function is the account secret. Note:\n   * The time_stamp and reviewer_type parameters are mandatory when using the signature parameter.\n   */\n  signature?: string;\n\n  /**\n   * The unique ID of the product on your site (doesn't have to be the SKU). To create a site\n   * review, use the value: yotpo_site_reviews. This parameter supports alphanumeric (a...z, A...Z,\n   * 0...9), \"_\" and \"-\" characters.\n   */\n  sku: string;\n\n  /**\n   * Relevant only for customers using the Trusted Vendors feature. The date the review was created\n   * (if not today). Can be up to 7 days in the past. If you want to add a creation date that is\n   * more than 7 days in the past, please contact support.\n   */\n  submission_time_stamp?: string;\n\n  /**\n   * Required for customers using the Trusted Vendors feature. Unix timestamp.\n   */\n  time_stamp?: string;\n}\n\n/**\n * Send a request to Yotpo to create a product review.\n *\n * If successful, the output is the response body from Yotpo.\n *\n * If there is an error, the output is undefined.\n *\n * This is the asynchronous method and is recommended for general use.\n *\n * This call will create reviews only as anonymous and will send out review verification emails to\n * all the customers you've created reviews for.\n *\n * The following parameters are relevant only for customers using the Trusted Vendors feature:\n * signature, time_stamp, submission_time_stamp, and reviewer_type.\n *\n * Note that custom fields should carry values which are in line with the type of question being\n * asked. For example, a rating question type would carry numerical values e.g. 1-5 while a textual\n * question type would carry values such as \"good\" \"okay\" \"poor\".\n *\n * @see https://apidocs.yotpo.com/reference/create-review\n */\nexport async function createReview(input: Partial<Input>) {\n  assert(input.appkey, 'Invalid app key');\n  const path = '/apps/yotpo-ugc/v1/widget/reviews';\n\n  let response;\n  try {\n    response = await fetch(path, {\n      body: JSON.stringify(input),\n      headers: {\n        'Content-Type': 'application/json'\n      },\n      method: 'POST',\n      mode: 'cors'\n    });\n  } catch (error) {\n    decorateError(error, 'url', path);\n    decorateError(error, 'input', input);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} creating review`);\n    error.status = response.status;\n    error.url = response.url;\n    error.input = input;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    decorateError(error, 'input', input);\n    throw error;\n  }\n\n  let responseBody;\n  try {\n    responseBody = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    decorateError(error, 'input', input);\n    throw error;\n  }\n\n  if (!isCreateReviewAPIResponseBody(responseBody)) {\n    const error: any = new Error('Malformed response body in create review');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    error.input = input;\n    throw error;\n  }\n\n  if (responseBody.code > 399) {\n    const error: any = new Error('Response body status code is not ok');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    error.input = input;\n    throw error;\n  }\n\n  return responseBody;\n}\n","/**\n * Returns whether the given value looks sufficiently like the expected parsed response body for\n * creating a review.\n */\nexport function isCreateReviewAPIResponseBody(value) {\n    return typeof value === 'object' && value !== null && typeof value.code === 'number';\n}\n","/**\n * Initialized on first call to encodeEntities, then re-used\n */\nlet element: HTMLTextAreaElement;\n\n/**\n * Encodes HTML entities in a mostly safe manner but at the expense of touching the dom.\n */\nexport function decode(input: string) {\n  if (!element) {\n    element = document.createElement('textarea');\n  }\n\n  element.innerHTML = input;\n\n  const output = element.value;\n\n  // avoid leaving around stuff in dom indefinitely, even when detached\n  element.innerHTML = '';\n\n  return output;\n}\n","import type { LegacyReview } from '@integrabeauty/yotpo-browser-types';\nimport { decorateError } from '../../../lib/decorate-error.js';\nimport { type CustomReviewField, type SearchResponseBody, searchProductReviews as liveSearchProductReviews } from './yotpo-search-product-reviews.js';\n\n/**\n * Where in the result set a response sits. The cached shape and the live shape disagree on the\n * spelling of the page size, so this module settles on the snake_case one the components use and\n * translates the live response into it.\n */\nexport interface Pagination {\n  page: number;\n  per_page: number;\n  total: number;\n}\n\n/**\n * One ordering our components ask for, under the snake_case name they use for it.\n */\nexport interface FilterSorting {\n  ascending?: boolean;\n  sort_by?: string;\n}\n\n/**\n * The filters our own components speak, in the snake_case names they have always used. The endpoint\n * reads camelCase, so this module translates on the way down.\n */\nexport interface Filters {\n  crfs: CustomReviewField[];\n  free_text_search: string;\n  pictured: boolean;\n  scores: number[];\n  sortings: FilterSorting[];\n  topic_names: string[];\n}\n\nexport interface Input {\n  /**\n   * The Yotpo store, which Yotpo's admin calls the app key.\n   */\n  app_key: string;\n\n  /**\n   * Read the product's first page of reviews out of our own cache instead of asking Yotpo. Only the\n   * unfiltered first page is cached, so this cannot be combined with a filter or a page number.\n   */\n  cache?: boolean;\n\n  /**\n   * Which reviews to narrow the results down to. Every filter is optional, so leaving this out asks\n   * for all of the product's reviews.\n   */\n  filters?: Partial<Filters>;\n\n  /**\n   * Which page of results to return, counting from 1. Leave it out for the first page.\n   */\n  page?: number;\n\n  /**\n   * The number of results to return per page\n   */\n  per_page: number;\n\n  /**\n   * The Shopify id of the product the reviews are about.\n   */\n  product_id: number;\n}\n\n/**\n * What sits under the cached body's `response` key, carrying the older snake_case field names. This\n * is the payload rather than a body of its own, and it is what the cached branch hands back. The\n * reviews component decides per review at runtime which of the two shapes it got.\n */\ninterface LegacyPayload {\n  /**\n   * The product's review count and average score. The live search does not return this, so it is\n   * one of the ways the two shapes differ.\n   */\n  bottomline: Record<string, unknown>;\n\n  found_filtered_reviews: boolean;\n  grouping_data: Record<string, unknown>;\n  pagination: Pagination;\n  product_tags: string[];\n  products: Record<string, unknown>[];\n  reviews: LegacyReview[];\n  syndication_data: Record<string, unknown>;\n}\n\n/**\n * The body Yotpo's live search gives back, with its pagination rewritten into the snake_case shape\n * the cached response already uses. Everything else comes through untouched.\n */\ninterface LiveResponseBody extends Omit<SearchResponseBody, 'pagination'> {\n  pagination: Pagination;\n}\n\n/**\n * The whole body the cache file holds, where the payload sits under a status wrapper. Yotpo's live\n * search has no such wrapper, which is the other way the two shapes differ.\n */\nexport interface CachedResponseBody {\n  response: LegacyPayload;\n\n  status: {\n    /**\n     * @example 200\n     */\n    code: number;\n\n    /**\n     * @example \"OK\"\n     */\n    message: string;\n  };\n}\n\n/**\n * Fetch a product's reviews, from our cache when the caller asks for it and from Yotpo otherwise.\n *\n * The cache is a JSON file we generate per product holding its unfiltered first page of reviews. It\n * exists because the first paint of the reviews list should not wait on Yotpo, so the initial load\n * reads the file and everything after it goes to Yotpo. A cached read answers with the older\n * snake_case shape, so callers get one of two response shapes and have to handle both.\n *\n * The cache also runs behind, so a cached review count comes in lower than the live one. Treat it\n * as approximate.\n *\n * This is the module to call. It holds what we know about how this store uses reviews, and\n * searchProductReviews underneath it stays a plain client for the Yotpo endpoint.\n */\nexport async function searchProductReviews(input: Input) {\n  if (!input.cache) {\n    const sortings = [];\n    for (const sorting of input.filters?.sortings || []) {\n      sortings.push({\n        ascending: sorting.ascending,\n        sortBy: sorting.sort_by\n      });\n    }\n\n    // Reaching api-cdn.yotpo.com from the browser would be a cross origin request, so we go through\n    // the Shopify app proxy that sits on our own domain.\n    const response = await liveSearchProductReviews({\n      baseUrl: 'https://langehair.com',\n      crfs: input.filters?.crfs,\n      freeTextSearch: input.filters?.free_text_search,\n      page: input.page,\n      pathPrefix: '/apps/yotpo-ugc-api-cdn',\n      perPage: input.per_page,\n      pictured: input.filters?.pictured,\n      productId: input.product_id,\n      scores: input.filters?.scores,\n      sortings,\n      storeId: input.app_key,\n      topicNames: input.filters?.topic_names\n    });\n\n    const output: LiveResponseBody = {\n      ...response,\n      pagination: {\n        page: response.pagination.page,\n        per_page: response.pagination.perPage,\n        total: response.pagination.total\n      }\n    };\n\n    return output;\n  }\n\n  const pathPrefix = '/apps/yotpo-reviews-cache';\n  const path = pathPrefix + `/lange-product-reviews-${input.product_id}-first-page.json`;\n  const url = new URL(path, 'https://langehair.com');\n\n  let response;\n  try {\n    response = await fetch(url);\n  } catch (error) {\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} fetching product reviews`);\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let responseBody;\n  try {\n    responseBody = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isCachedResponseBody(responseBody)) {\n    const error: any = new Error('Unexpected response body');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  if (responseBody.status.code > 399) {\n    const error: any = new Error('Nested response body status code is not 200');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  // This unwraps the response body to return just the useful bits.\n  const payload = responseBody.response;\n\n  return payload;\n}\n\n/**\n * Returns whether the given value satisfies the type's schema. This does not fully validate the\n * values, this just provides runtime type safety.\n */\nfunction isCachedResponseBody(value: any): value is CachedResponseBody {\n  return typeof value === 'object' && value !== null && typeof value.status === 'object' &&\n    value.status !== null;\n}\n","import { assert } from '@integrabeauty/assert';\nimport type { Review } from '@integrabeauty/yotpo-browser-types';\nimport { decorateError } from '../../../lib/decorate-error.js';\n\n/**\n * One of the custom questions set up in the Yotpo admin, such as the hair type a customer picks\n * when writing a review. The question id is the Yotpo id of that question and the answers are the\n * values to match against what the customer answered.\n *\n * displayAnswers does not affect which reviews match, so it reads as a display hint.\n */\nexport interface CustomReviewField {\n  answers: string[];\n  displayAnswers?: string[];\n  questionId: number;\n}\n\n/**\n * One entry of the order metadata a review came in with, which Yotpo describes as covering\n * customer, product, and order properties. The question is the field's label and the answers are\n * the values to match against it.\n */\nexport interface OrderMetadataField {\n  answers: string[];\n  question: string;\n\n  /**\n   * A string here, unlike the number a custom review field uses for the same idea. That is what the\n   * example in Yotpo's OpenAPI definition shows.\n   */\n  questionId: string;\n\n  subType: string;\n}\n\n/**\n * One entry of the custom metadata collected alongside a review. The question is the field's label\n * and the answers are the values to match against it.\n */\nexport interface ReviewMetadataField {\n  answers: string[];\n  fieldType: string;\n  question: string;\n}\n\n/**\n * One ordering to apply to the results. Several can be sent at once.\n */\nexport interface Sorting {\n  /**\n   * True sorts ascending and false sorts descending.\n   */\n  ascending?: boolean;\n\n  /**\n   * What to sort on. `score`, `rating`, `date`, `votes_up`, `images`, `smart`, and `_score` all\n   * work, and `score` and `rating` behave identically. `image_count` and `reviewer_type` return\n   * nothing, so do not carry those two over from Yotpo's older docs.\n   */\n  sortBy?: string;\n}\n\n/**\n * How to search Yotpo for product reviews.\n *\n * @see https://apidocs.yotpo.com/v3.0/reference/storefront-product-reviews-search.md\n */\nexport interface Input {\n  /**\n   * What the path is resolved against, handed straight to the URL constructor as its base. Leaving\n   * it out means the path has to stand on its own as a full URL, which is what pathPrefix is for.\n   *\n   * @example 'https://langehair.com'\n   */\n  baseUrl?: string;\n\n  /**\n   * The country the reviewer ordered from, as a two letter code.\n   */\n  country?: string;\n\n  /**\n   * The custom review fields to filter on, which is what Yotpo abbreviates as crfs.\n   */\n  crfs?: CustomReviewField[];\n\n  /**\n   * Which way to sort. `asc` and `desc` both work. Yotpo documents the parameter without saying\n   * which values it takes, so those two are the ones we know of.\n   */\n  direction?: string;\n\n  /**\n   * Free text search terms. Supported languages are English, French, German, Italian, Norwegian,\n   * Portuguese, Spanish, Russian, Swedish, and Turkish.\n   */\n  freeTextSearch?: string;\n\n  /**\n   * Return only these reviews, by Yotpo review id.\n   */\n  ids?: number[];\n\n  /**\n   * A language code, which Yotpo documents as localizing the review content. It changed nothing in\n   * anything we tried, so we do not know what makes it do something. Unverified.\n   */\n  lang?: string;\n\n  /**\n   * Custom metadata collected with the review. Unverified, because we could not find a review\n   * carrying any metadata to match against.\n   */\n  metadata?: ReviewMetadataField[];\n\n  /**\n   * Order metadata the review came in with. Unverified for the same reason as metadata.\n   */\n  orders?: OrderMetadataField[];\n\n  /**\n   * Which page of results to return, counting from 1. Leave it out for the first page.\n   */\n  page?: number;\n\n  /**\n   * Goes in front of the endpoint's own path, with no trailing slash. Leave it out to request the\n   * path as Yotpo documents it. A caller that reaches the endpoint through a proxy passes the\n   * proxy's prefix here.\n   *\n   * @example '/apps/yotpo-ugc-api-cdn'\n   */\n  pathPrefix?: string;\n\n  /**\n   * How many reviews to return per page.\n   */\n  perPage: number;\n\n  /**\n   * True returns only reviews with media, meaning pictures and videos, and false returns only\n   * reviews without it. Leave it out to get reviews either way.\n   */\n  pictured?: boolean;\n\n  /**\n   * How many pictures to return per review. It capped nothing, so it looks ignored here.\n   * Unverified.\n   */\n  picturesPerReview?: number;\n\n  /**\n   * The product to return reviews for. Yotpo matches this against the product's Shopify id.\n   */\n  productId: number;\n\n  /**\n   * The variants the reviewer bought, as option name to option value, such as\n   * `{ Size: 'Grande', Color: 'Blush' }`.\n   */\n  productVariants?: Record<string, string>;\n\n  /**\n   * The star ratings to return, so 4 returns the four star reviews.\n   */\n  scores?: number[];\n\n  /**\n   * How to order the results. Each of these gives a different ordering, and a value outside the\n   * list is ignored rather than rejected, so a typo silently returns the default order.\n   *\n   * This overlaps with the sortings filter in the request body, which reaches the same orderings\n   * through its own names. We have not checked which one wins when both are set.\n   */\n  sort?: 'badge' | 'date' | 'images' | 'rating' | 'smart';\n\n  /**\n   * How to order the results, under the endpoint's own sorting names. See sort for how the two\n   * overlap.\n   */\n  sortings?: Sorting[];\n\n  /**\n   * Which Yotpo store to search. This is the value Yotpo's admin calls the app key.\n   */\n  storeId: string;\n\n  /**\n   * The topics mentioned in the review, matching the names the trending topics endpoint returns.\n   */\n  topicNames?: string[];\n}\n\n/**\n * Which of the parameters travel in the JSON body. Everything else is a path param, a query param,\n * or one of ours for saying where to send the request.\n */\ntype RequestBody = Pick<Input, 'country' | 'crfs' | 'freeTextSearch' | 'ids' | 'metadata' |\n  'orders' | 'pictured' | 'picturesPerReview' | 'productVariants' | 'scores' | 'sortings' |\n  'topicNames'>;\n\n/**\n * Part of the response body\n */\nexport interface Pagination {\n  /**\n   * @example 1\n   */\n  page: number;\n\n  /**\n   * @example 1\n   */\n  perPage: number;\n\n  /**\n   * @example 2\n   */\n  total: number;\n}\n\n/**\n * The whole body this endpoint returns, with no status wrapper around it. Every other Yotpo\n * endpoint we call wraps its payload, and this one is the outlier. These seven fields are the whole\n * top level.\n */\nexport interface SearchResponseBody {\n  foundFilteredReviews: boolean;\n  groupingData: Record<string, unknown>;\n  pagination: Pagination;\n  productTags: string[];\n  products: Record<string, unknown>[];\n  reviews: Review[];\n\n  /**\n   * Null for everything we have seen, so we do not know the shape when it is populated.\n   */\n  syndicationData: Record<string, unknown>;\n}\n\n/**\n * Search Yotpo for a product's public reviews.\n *\n * @see https://apidocs.yotpo.com/v3.0/reference/storefront-product-reviews-search\n */\nexport async function searchProductReviews(input: Input) {\n  assert(input.storeId, 'Invalid store id');\n  assert(Number.isInteger(input.perPage) && input.perPage > 0, 'Invalid per page input');\n\n  const pathPrefix = input.pathPrefix ?? '';\n  const path = pathPrefix +\n    `/v3/storefront/store/${input.storeId}/product/${input.productId}/reviews`;\n  const url = new URL(path, input.baseUrl);\n\n  if (input.page) {\n    url.searchParams.append('page', input.page.toString());\n  }\n\n  if (input.perPage) {\n    url.searchParams.append('perPage', input.perPage.toString());\n  }\n\n  if (input.sort) {\n    url.searchParams.append('sort', input.sort);\n  }\n\n  if (input.direction) {\n    url.searchParams.append('direction', input.direction);\n  }\n\n  if (input.lang) {\n    url.searchParams.append('lang', input.lang);\n  }\n\n  const requestBody: RequestBody = {};\n\n  if (input.country) {\n    requestBody.country = input.country;\n  }\n\n  if (input.crfs?.length) {\n    requestBody.crfs = input.crfs;\n  }\n\n  if (input.freeTextSearch) {\n    requestBody.freeTextSearch = input.freeTextSearch;\n  }\n\n  if (input.ids?.length) {\n    requestBody.ids = input.ids;\n  }\n\n  if (input.metadata?.length) {\n    requestBody.metadata = input.metadata;\n  }\n\n  if (input.orders?.length) {\n    requestBody.orders = input.orders;\n  }\n\n  if (input.pictured !== undefined) {\n    requestBody.pictured = input.pictured;\n  }\n\n  if (input.picturesPerReview !== undefined) {\n    requestBody.picturesPerReview = input.picturesPerReview;\n  }\n\n  if (input.productVariants) {\n    requestBody.productVariants = input.productVariants;\n  }\n\n  if (input.scores?.length) {\n    requestBody.scores = input.scores;\n  }\n\n  if (input.sortings?.length) {\n    const sortings = [];\n    for (const sorting of input.sortings) {\n      sortings.push({\n        ascending: sorting.ascending ?? false,\n        sortBy: sorting.sortBy\n      });\n    }\n\n    requestBody.sortings = sortings;\n  }\n\n  if (input.topicNames?.length) {\n    requestBody.topicNames = input.topicNames;\n  }\n\n  const init: RequestInit = {\n    headers: {\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify(requestBody),\n    method: 'POST'\n  };\n\n  let response;\n  try {\n    response = await fetch(url, init);\n  } catch (error) {\n    decorateError(error, 'url', url.href);\n    throw error;\n  }\n\n  if (!response.ok) {\n    const error: any = new Error(`Received status ${response.status} searching product reviews`);\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  let responseText;\n  try {\n    responseText = await response.text();\n  } catch (error) {\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  let responseBody;\n  try {\n    responseBody = JSON.parse(responseText);\n  } catch (error) {\n    decorateError(error, 'body', responseText);\n    decorateError(error, 'status', response.status);\n    decorateError(error, 'url', response.url);\n    throw error;\n  }\n\n  if (!isSearchResponseBody(responseBody)) {\n    const error: any = new Error('Unexpected response body');\n    error.body = responseBody;\n    error.status = response.status;\n    error.url = response.url;\n    throw error;\n  }\n\n  return responseBody;\n}\n\n/**\n * Returns whether the given value satisfies the type's schema. This does not fully validate the\n * values, this just provides runtime type safety.\n */\nfunction isSearchResponseBody(value: any): value is SearchResponseBody {\n  return typeof value === 'object' && value !== null && value.pagination !== null &&\n    value.reviews !== null;\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport type { LegacyReview, Review } from '@integrabeauty/yotpo-browser-types';\nimport { decode } from './decode-html-entities.js';\nimport html from './index.html';\nimport styles from './index.scss';\nimport { type Pagination, searchProductReviews } from './yotpo-fetch-product-reviews.js';\n\ntype SearchProductReviewsResponse = Awaited<ReturnType<typeof searchProductReviews>>;\n\n/**\n * Renders product reviews element feed with reviews fetched from Yotpo API. You can access this\n * element from the PDP page.\n */\nclass YotpoReviews extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Shopify customer id of currently logged in user\n     */\n    customerId: string;\n\n    /**\n     * Shopify product id to which the reviews in this element pertain\n     */\n    productId: string;\n\n    /**\n     * Shopify product price to which the reviews in this element pertain\n     */\n    productPrice: string;\n\n    /**\n     * Shopify product title to which the reviews in this element pertain\n     */\n    productTitle: string;\n\n    /**\n     * Shopify product type to which the reviews in this element pertain\n     */\n    productType: string;\n\n    /**\n     * Shopify product url to which the reviews in this element pertain\n     */\n    productUrl: string;\n\n    /**\n     * Average review score from the automatically inserted metafield value via the Yotpo App\n     */\n    reviewsAverage: string;\n\n    /**\n     * Number of reviews fetched per page from Yotpo API\n     */\n    reviewsPerPage: string;\n\n    /**\n     * Total reviews value from the automatically inserted metafield value via the Yotpo App\n     */\n    reviewsTotal: string;\n\n    /**\n     * Number of reviews that gave 1 star.\n     */\n    star1Count: string;\n\n    /**\n     * Number of reviews that gave 2 stars.\n     */\n    star2Count: string;\n\n    /**\n     * Number of reviews that gave 3 stars.\n     */\n    star3Count: string;\n\n    /**\n     * Number of reviews that gave 4 stars.\n     */\n    star4Count: string;\n\n    /**\n     * Number of reviews that gave 5 stars.\n     */\n    star5Count: string;\n\n    /**\n     * Yotpo API app key.\n     */\n    yotpoAppKey: string;\n  };\n\n  private onPageChangedBound = this.onPageChanged.bind(this);\n  private onReviewsFilterChangeBound = this.onReviewsFilterChange.bind(this);\n  private onYotpoReviewSubmittedBound = this.onYotpoReviewSubmitted.bind(this);\n  private onClearButtonClickedBound = this.onClearButtonClicked.bind(this);\n  private onModalOpenRequestedBound = this.onModalOpenRequested.bind(this);\n  private onModalCloseRequestedBound = this.onModalCloseRequested.bind(this);\n  private declare loadingTimer: ReturnType<typeof setTimeout>;\n\n  /**\n   * The container of our template holding the drawer or photo popup open in this section, if any.\n   */\n  private declare modalContainer?: Element;\n  private declare paginationReviews?: Pagination;\n  private productId = 0;\n  private reviews: LegacyReview[] | Review[] = [];\n  private reviewsPerPage = 5;\n  private selectedFilters: Record<string, any> = {};\n  private declare selectedSortings: YotpoReviewSortings;\n\n  /**\n   * This controls whether to fetch reviews from Integra's Yotpo cache service instead of fetching\n   * reviews from Yotpo. Not everyone reads reviews, but this fetch has to run on every page load.\n   * The reviews are dynamic and derived from request parameters, leading to several permutations,\n   * so it is infeasible to cache all the variations. Therefore, we only cache the first page based\n   * on the same criteria we would normally use to fetch the first page of reviews from the API and\n   * require an API call for all other situations.\n   *\n   * Initially this is true. If this is true, the cache is used. After the first fetch, this is set\n   * to false. That way all future calls use the API.\n   *\n   * This is not a perfect solution. It is possible someone loads the page, the cache is used\n   * initially, then the user changes the review filters, where this is false, and then resets the\n   * filters back to the original state, where this could become true again, but we are not\n   * bothering to ever transition back. We could use the cache again if we detected a revert to\n   * original state. The thing is, detecting that at the moment is a little complex, and we are at\n   * that point where the user is already \"interacting\" with the reviews, which happens far less\n   * frequently than the number of page loads, so skipping the cache is ok.\n   */\n  private useCacheForReviews = true;\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    addEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    addEventListener('yotpo-page-changed', this.onPageChangedBound);\n    addEventListener('yotpo-review-submitted', this.onYotpoReviewSubmittedBound);\n\n    const clearButton = this.shadowRoot.querySelector<HTMLButtonElement>('.clear-filters');\n    clearButton?.addEventListener('click', this.onClearButtonClickedBound);\n\n    this.shadowRoot.addEventListener('modal-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.addEventListener('modal-close-requested', this.onModalCloseRequestedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    clearTimeout(this.loadingTimer);\n\n    removeEventListener('yotpo-reviews-filtered', this.onReviewsFilterChangeBound);\n    removeEventListener('yotpo-page-changed', this.onPageChangedBound);\n    removeEventListener('yotpo-review-submitted', this.onYotpoReviewSubmittedBound);\n\n    const clearButton = this.shadowRoot.querySelector<HTMLButtonElement>('.clear-filters');\n    clearButton?.removeEventListener('click', this.onClearButtonClickedBound);\n\n    this.shadowRoot.removeEventListener('modal-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.removeEventListener('modal-close-requested', this.onModalCloseRequestedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    if (!this.dataset.yotpoAppKey) {\n      return;\n    }\n\n    this.productId = parseInt(this.dataset.productId, 10);\n    if (!this.productId) {\n      return;\n    }\n\n    const reviewsPerPage = parseInt(this.dataset.reviewsPerPage, 10);\n    if (Number.isInteger(reviewsPerPage) && reviewsPerPage > 0) {\n      this.reviewsPerPage = reviewsPerPage;\n    }\n\n    // A product with no reviews has nothing to fetch, and fetching anyway flashes the loading\n    // indicator under the section's no-reviews message.\n    const reviewsTotal = parseInt(this.dataset.reviewsTotal, 10);\n    if (reviewsTotal <= 0) {\n      return;\n    }\n\n    const loader = this.shadowRoot.querySelector<HTMLElement>('#first-load-indicator');\n    this.showLoading(loader, 300);\n    this.dispatchEvent(new Event('yotpo-reviews-search-started', {\n      bubbles: true,\n      composed: true\n    }));\n\n    searchProductReviews({\n      app_key: this.dataset.yotpoAppKey,\n      cache: this.useCacheForReviews,\n      per_page: this.reviewsPerPage,\n      product_id: this.productId\n    }).then(response => this.onInitialReviewsFetched(response))\n      .catch(error => this.onInitialReviewsFetchFailed(error))\n      .finally(() => this.hideLoading(loader));\n  }\n\n  /**\n   * Renders the first page of reviews once the initial fetch comes back.\n   */\n  private onInitialReviewsFetched(response: SearchProductReviewsResponse) {\n    // Prevent future calls from using the cache\n    this.useCacheForReviews = false;\n\n    this.reviews = response?.reviews || [];\n    this.paginationReviews = response?.pagination;\n\n    this.correctReviewsCount();\n    this.renderReviews();\n    this.renderPagination(true);\n\n    type Detail = YotpoReviewsSearchedEvent['detail'];\n    const searchEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: {},\n        reviews: response?.reviews,\n        pagination: response?.pagination\n      }\n    });\n    this.dispatchEvent(searchEvent);\n  }\n\n  /**\n   * Logs the failed initial fetch and tells listeners the search came back empty.\n   */\n  private onInitialReviewsFetchFailed(error: any) {\n    if (isRoutineYotpoError(error)) {\n      console.log(error);\n    } else {\n      console.warn(error);\n    }\n\n    // Prevent future calls from using the cache\n    this.useCacheForReviews = false;\n\n    type Detail = YotpoReviewsSearchedEvent['detail'];\n    const searchEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: {},\n        reviews: [],\n        pagination: { page: 0, per_page: 0, total: 0 }\n      }\n    });\n    this.dispatchEvent(searchEvent);\n  }\n\n  /**\n   * Delays showing the loading indicator so a fast API response never flashes it. If the fetch\n   * resolves before the delay in milliseconds runs out, the timer is cleared before the indicator\n   * ever appears.\n   */\n  private showLoading(loader: HTMLElement, delay: number) {\n    clearTimeout(this.loadingTimer);\n    this.loadingTimer = setTimeout(() => {\n      if (loader) {\n        loader.hidden = false;\n      }\n    }, delay);\n  }\n\n  /**\n   * Cancels a pending show-loading timer if the fetch beat it, or hides the indicator if it was\n   * already visible.\n   */\n  private hideLoading(loader: HTMLElement) {\n    clearTimeout(this.loadingTimer);\n    if (loader) {\n      loader.hidden = true;\n    }\n  }\n\n  /**\n   * Creates and displays a success alert message when the review is submitted successfully.\n   */\n  private showAlert(message: string) {\n    const alert = document.createElement('alert-element');\n    alert.dataset.alertPosition = 'right';\n    alert.dataset.alertType = 'success';\n    alert.dataset.fadeDelay = '3000';\n    alert.dataset.closable = 'true';\n    alert.innerHTML = `<p>${message}</p>`;\n\n    this.shadowRoot.appendChild(alert);\n  }\n\n  /**\n   * On click of the clear filters button in no results element, dispatch a custom event to notify\n   * the yotpo-filters component to clear all the filters and reset the reviews list. We don't do it\n   * right here, because the yotpo-filters component handles all the logic for clearing the filters.\n   */\n  private onClearButtonClicked(_event: MouseEvent) {\n    const event = new Event('yotpo-filter-clear-requested', { bubbles: true, composed: true });\n    this.dispatchEvent(event);\n  }\n\n  /**\n   * The modal manager inerts every section but the one holding the open modal, so while a drawer or\n   * a review's photo popup in this section is open, the rest of the section stays reachable by Tab.\n   * The element that owns the modal inerts its own parts beside it, and we inert the parts of our\n   * shadow root beside that element.\n   *\n   * Only the modal manager is meant to act on this request. We listen for it here only because it\n   * comes from one of our own child elements, and we run before the manager's listener on the\n   * window.\n   */\n  private onModalOpenRequested(event: HTMLElementEventMap['modal-open-requested']) {\n    // The event's path tells us which container of our template the modal sits in\n    const path = event.composedPath();\n    const containers = this.shadowRoot.querySelectorAll(\n      'slot[name=\"stars-breakdown\"], slot[name=\"create-form\"], .items-container');\n    let modalContainer: Element;\n    for (const container of containers) {\n      if (path.includes(container)) {\n        modalContainer = container;\n      }\n    }\n\n    if (!modalContainer) {\n      return;\n    }\n\n    // Anything on the path is skipped, so the review holding a photo popup stays out of inert\n    const parts = this.shadowRoot.querySelectorAll(getPartsBesideSelector(modalContainer));\n    for (const part of parts) {\n      if (part instanceof HTMLElement && !path.includes(part)) {\n        part.inert = true;\n      }\n    }\n\n    this.modalContainer = modalContainer;\n  }\n\n  /**\n   * Takes our parts back out of inert. We run before the modal manager's listener on the window\n   * puts focus back on the element that opened the modal, and a focus call into an inert subtree\n   * does nothing.\n   */\n  private onModalCloseRequested(_event: HTMLElementEventMap['modal-close-requested']) {\n    this.releaseParts();\n  }\n\n  private releaseParts() {\n    if (!this.modalContainer) {\n      return;\n    }\n\n    const parts = this.shadowRoot.querySelectorAll(getPartsBesideSelector(this.modalContainer));\n    for (const part of parts) {\n      if (part instanceof HTMLElement) {\n        part.inert = false;\n      }\n    }\n\n    this.modalContainer = undefined;\n  }\n\n  private onYotpoReviewSubmitted(_event: WindowEventMap['yotpo-review-submitted']) {\n    this.showAlert('Thank you! Your review has been submitted.');\n  }\n\n  private onReviewsFilterChange(event: HTMLElementEventMap['yotpo-reviews-filtered']) {\n    // Changes happen after the initial render so always make a live API call and never use the\n    // cache. This guarantees that if someone changes which reviews should be displayed, we attempt\n    // to fetch those reviews from Yotpo instead.\n\n    // The overlay is the only sign a click registered, since the page highlight waits for the\n    // fetch, so it gets a much shorter delay than the first load. It still skips the rare instant\n    // response.\n    const overlay = this.shadowRoot.querySelector<HTMLElement>('#loading-overlay');\n    this.showLoading(overlay, 50);\n\n    this.dispatchEvent(new Event('yotpo-reviews-search-started', {\n      bubbles: true,\n      composed: true\n    }));\n\n    // Hair Type is one of the custom questions customers answer when they write a review, and the\n    // fetch takes those as a list of question ids rather than knowing about hair type, so we look\n    // up the id here. Yotpo gives the question id 103765 and we do not expect that to change.\n    const crfs = event.detail.filters.hair_type ?\n      [{ answers: [event.detail.filters.hair_type], questionId: 103765 }] :\n      [];\n\n    searchProductReviews({\n      app_key: this.dataset.yotpoAppKey,\n      cache: false,\n      filters: {\n        crfs,\n        free_text_search: event.detail.filters.free_text_search,\n        pictured: event.detail.filters.pictured,\n        scores: event.detail.filters.scores,\n        sortings: event.detail.sortings,\n        topic_names: event.detail.filters.topic_names\n      },\n      page: event.detail.page,\n      per_page: this.reviewsPerPage,\n      product_id: event.detail.productId\n    }).then(response => {\n      this.reviews = response.reviews;\n      this.selectedFilters = event.detail.filters;\n      this.selectedSortings = event.detail.sortings;\n      this.paginationReviews = response.pagination;\n\n      this.renderReviews();\n      this.renderPagination(false);\n\n      scrollToElement(this.shadowRoot.querySelector<HTMLElement>('.items-container'));\n\n      type Detail = WindowEventMap['yotpo-reviews-searched']['detail'];\n      const searchedEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          filters: event.detail.filters,\n          reviews: response.reviews,\n          pagination: response.pagination\n        }\n      });\n      this.dispatchEvent(searchedEvent);\n    }).catch(error => {\n      if (isRoutineYotpoError(error)) {\n        console.log(error);\n      } else {\n        console.warn(error);\n      }\n\n      type Detail = WindowEventMap['yotpo-reviews-searched']['detail'];\n      const searchedEvent = new CustomEvent<Detail>('yotpo-reviews-searched', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          filters: event.detail.filters,\n          reviews: [],\n          pagination: { page: 0, per_page: 0, total: 0 }\n        }\n      });\n      this.dispatchEvent(searchedEvent);\n    }).finally(() => {\n      // We hide the overlay here rather than in the success and failure handlers above, so it comes\n      // down even when one of those handlers throws.\n      this.hideLoading(overlay);\n    });\n  }\n\n  /**\n   * When the user clicks on one of the page links in the pagination element we want to scroll to\n   * the top of the reviews element.\n   */\n  private onPageChanged(event: WindowEventMap['yotpo-page-changed']) {\n    type Detail = WindowEventMap['yotpo-reviews-filtered']['detail'];\n    const filteredEvent = new CustomEvent<Detail>('yotpo-reviews-filtered', {\n      bubbles: true,\n      composed: true,\n      detail: {\n        filters: { ...this.selectedFilters },\n        page: event.detail.page,\n        productId: this.productId,\n        sortings: this.selectedSortings\n      }\n    });\n    this.dispatchEvent(filteredEvent);\n  }\n\n  private renderReviews() {\n    const itemsContainer = this.shadowRoot.querySelector('.items-container');\n\n    // Clearing the list removes any review whose photo popup is open. The close request that review\n    // sends on its way out goes straight to the window rather than through us, and the modal\n    // manager puts focus back during the clearing, so we take our parts out of inert first.\n    if (this.modalContainer === itemsContainer) {\n      this.releaseParts();\n    }\n\n    itemsContainer.innerHTML = '';\n\n    const noResults = this.shadowRoot.querySelector<HTMLParagraphElement>('.no-results');\n    if (this.reviews.length === 0) {\n      noResults.hidden = false;\n\n      return;\n    }\n\n    noResults.hidden = true;\n\n    for (const review of this.reviews) {\n      itemsContainer.appendChild(createReviewDetail(review));\n    }\n  }\n\n  /**\n   * Renders the pagination element based on the current reviews and pagination data. There's a flag\n   * if data\n   */\n  private renderPagination(fromCache: boolean) {\n    const paginationElement = this.shadowRoot.querySelector('yotpo-pagination');\n    if (!paginationElement) {\n      return;\n    }\n\n    const pagination = this.paginationReviews;\n\n    if (pagination && pagination.total > pagination.per_page) {\n      paginationElement.dataset.page = pagination.page.toString();\n      paginationElement.dataset.perPage = pagination.per_page.toString();\n\n      // The total reviews number comes from different sources: metafields and yotpo api where 2\n      // cached and uncached calls return different results. The cached total reviews value looks\n      // inaccurate while the metafield value (this.dataset.reviewsTotal) and value from uncached\n      // response looks accurate. Therefore, if the reviews are fetched from cache, we use the\n      // metafield value instead of the cached value.\n      let total: number;\n      if (fromCache) {\n        total = parseInt(this.dataset.reviewsTotal, 10);\n      } else {\n        total = pagination.total;\n      }\n\n      // Yotpo only pages through the first 10000 reviews. Ask for a page past that and it answers\n      // with the last page it will serve rather than the page we asked for, so on a product with\n      // more reviews than that the later page buttons all land on the same reviews. We hand the\n      // pagination element a total that stops at the last whole page inside the window, so it only\n      // offers pages Yotpo will serve. Yotpo does not document the limit, we measured it.\n      const reachableTotal = Math.floor(10000 / pagination.per_page) * pagination.per_page;\n      if (total > reachableTotal) {\n        total = reachableTotal;\n      }\n\n      paginationElement.dataset.total = total.toString();\n\n      // it triggers the pagination element to rerender\n      paginationElement.dataset.update = Date.now().toString();\n\n      paginationElement.hidden = false;\n    } else {\n      paginationElement.hidden = true;\n    }\n  }\n\n  /**\n   * numbers come from different sources:\n   * * metafields\n   * * yotpo api (where 2 methods return different results)\n   *\n   * therefore this is a workaround for correcting values\n   */\n  private correctReviewsCount() {\n    type ProductRatingStarsElement = HTMLElementTagNameMap['product-rating-stars'];\n    const ratingStarsComponent = document.querySelector<ProductRatingStarsElement>(\n      'product-rating-stars[data-product-id=\"' + this.productId + '\"]');\n\n    if (!ratingStarsComponent) {\n      return;\n    }\n\n    const ratingStarsCount = parseInt(ratingStarsComponent.dataset.count, 10);\n    const reviewsTotal = parseInt(this.dataset.reviewsTotal, 10);\n    if (ratingStarsCount !== reviewsTotal) {\n      ratingStarsComponent.dataset.count = this.dataset.reviewsTotal;\n    }\n  }\n}\n\n/**\n * Returns a selector for the parts of the reviews template beside a container that can hold an open\n * modal. It leaves out the container's own ancestors, since inerting one of those would inert the\n * modal too.\n *\n * @param container The stars breakdown slot, the create form slot, or the review list\n */\nfunction getPartsBesideSelector(container: Element) {\n  if (container.matches('slot[name=\"stars-breakdown\"]')) {\n    return 'slot[name=\"featured\"], slot[name=\"create-form\"], #reviews-list';\n  }\n\n  if (container.matches('slot[name=\"create-form\"]')) {\n    return 'slot[name=\"stars-breakdown\"], slot[name=\"featured\"], #reviews-list';\n  }\n\n  return 'slot[name=\"stars-breakdown\"], slot[name=\"featured\"], slot[name=\"create-form\"], ' +\n    'slot[name=\"filters\"], .reviews-pagination, .items-container > yotpo-review-detail';\n}\n\n/**\n * Builds the element that displays one review, filled in and ready to append.\n *\n * Yotpo hands us reviews in two shapes, an older one with snake case properties and a newer one\n * with camel case, so we work out which shape this review is and read from it accordingly.\n */\nfunction createReviewDetail(review: LegacyReview | Review) {\n  const detail = document.createElement('yotpo-review-detail');\n  detail.dataset.reviewId = review.id.toString();\n\n  const useLegacy = isLegacyReview(review);\n\n  let imagesData: Pick<Review['imagesData'][number], 'id' | 'originalUrl' | 'thumbUrl'>[] = [];\n\n  if (useLegacy && review.images_data) {\n    imagesData = review.images_data.map(image => ({\n      id: image.image_id,\n      thumbUrl: image.thumb_url,\n      // @ts-expect-error: original_url is not in the type but exists in legacy data; it is not\n      // documented and used only in some API responses, so instead of adding it to the interface,\n      // we just ignore the type error here.\n      originalUrl: image.original_image_url || <string>image.original_url\n    }));\n  } else if (!useLegacy && review.imagesData) {\n    imagesData = review.imagesData;\n  }\n\n  detail.dataset.originalImages = imagesData.map(image => image.originalUrl).join(',');\n  detail.dataset.thumbnailImages = imagesData.map(image => image.thumbUrl).join(',');\n\n  const votesUp = useLegacy ? review.votes_up : review.votesUp;\n  detail.dataset.votesUp = (votesUp || 0).toString();\n\n  if (useLegacy) {\n    detail.dataset.incentivized = review.is_incentivized ? 'true' : 'false';\n  } else {\n    detail.dataset.incentivized = review.isIncentivized ? 'true' : 'false';\n  }\n\n  // create profile badge element for slot\n  const avatar = document.createElement('yotpo-avatar-v2');\n  avatar.setAttribute('slot', 'badge');\n  avatar.dataset.date = useLegacy ? review.created_at : review.createdAt;\n  avatar.dataset.name = useLegacy ? review.user.display_name : review.user.displayName;\n\n  const verified = useLegacy ? review.verified_buyer : review.verifiedBuyer;\n  avatar.dataset.verified = verified ? 'true' : 'false';\n  detail.appendChild(avatar);\n\n  // create rating stars for slot\n  const rating = document.createElement('simple-rating-stars');\n  rating.setAttribute('slot', 'rating-stars');\n  rating.dataset.rating = review.score.toString();\n  detail.appendChild(rating);\n\n  // create hair type for slot\n  const customFields = useLegacy ? review.custom_fields : review.customFields;\n  const hairType = getHairTypeValue(customFields);\n  if (hairType) {\n    const element = document.createElement('div');\n    element.setAttribute('slot', 'hair-type');\n\n    const label = document.createElement('strong');\n    label.textContent = 'Hair Type: ';\n    element.appendChild(label);\n    element.appendChild(document.createTextNode(hairType));\n\n    detail.appendChild(element);\n  }\n\n  // create content for slot\n  const content = document.createElement('div');\n  content.setAttribute('slot', 'content');\n  content.className = 'review';\n\n  // Review title and content can contain HTML tags and HTML entities. Review title and content are\n  // user-generated content and fundamentally unsafe. Therefore, we never want to set inner HTML.\n  // However, we do want reviews to render properly as many reviews use entities. So we manually\n  // decode the entities for beauty but only set text content for safety.\n\n  const title = document.createElement('strong');\n  title.classList.add('customer-review-title');\n  title.textContent = decode(review.title);\n  content.appendChild(title);\n\n  const text = document.createElement('p');\n  text.classList.add('customer-review-text');\n  text.textContent = decode(review.content);\n  content.appendChild(text);\n\n  detail.appendChild(content);\n\n  return detail;\n}\n\n/**\n * Validation and type guard to check for legacy reviews types when building out the <yotpo-review-\n * detail> custom elements.\n */\nfunction isLegacyReview(value: any): value is LegacyReview {\n  return typeof value === 'object' && value !== null && typeof value.votes_up === 'number' &&\n    typeof value.votes_down === 'number';\n}\n\n/**\n * Returns the Hair Type custom field value from a review's custom fields object. The field appears\n * under key \"103765\" in v3 API responses and \"--103765\" in legacy responses.\n */\nfunction getHairTypeValue(customFields: Record<string, any>): string | undefined {\n  if (typeof customFields !== 'object' || customFields === null) {\n    return;\n  }\n\n  const field = customFields['103765'] || customFields['--103765'];\n  if (typeof field === 'object' && field !== null &&\n    typeof field.value === 'string' && field.value) {\n    return field.value;\n  }\n}\n\nfunction isRoutineYotpoError(error: any): error is Error {\n  if (!(error instanceof Error)) {\n    return false;\n  }\n\n  // A 429 here is not really about the reviews. Shopify throttles the whole storefront domain at\n  // once, so when we get one the cart and the recommendations are usually failing alongside it, and\n  // there is nothing this element can do about that.\n  //\n  // The status gets attached to the error where that is possible, but attaching it can quietly fail\n  // on an error we did not create, so we look for the property rather than count on it.\n  const decorated: any = error;\n  if (decorated.status === 429) {\n    return true;\n  }\n\n  return [\n    'Load failed',\n    'Failed to get response body finding reviews by product'\n  ].includes(error.message);\n}\n\nfunction scrollToElement(element?: HTMLElement) {\n  if (!element) {\n    return;\n  }\n\n  const rect = element.getBoundingClientRect();\n\n  // The header script measures the sticky site header and publishes the viewport y of its bottom\n  // edge as --content-position, so reading it puts the element just below the site header at\n  // whatever height that header currently has. The fallback covers a page that renders no header\n  // section, where the variable is absent and parseFloat returns NaN.\n  const headerOffset = parseFloat(\n    getComputedStyle(document.body).getPropertyValue('--content-position')) || 80;\n\n  document.documentElement.scrollTop = scrollY + rect.top - headerOffset;\n}\n\n/**\n * Fired when a call to the Yotpo API returns search results.\n */\ntype YotpoReviewsSearchedEvent = CustomEvent<{\n  filters: YotpoReviewFilters | Record<string, unknown>;\n  pagination: Pagination;\n  productId?: number;\n  reviews: LegacyReview[] | Review[];\n}>;\n\ndeclare global {\n  interface GlobalEventHandlersEventMap {\n    'yotpo-filter-clear-requested': Event;\n    'yotpo-reviews-search-started': Event;\n    'yotpo-reviews-searched': YotpoReviewsSearchedEvent;\n  }\n\n  interface HTMLElementTagNameMap {\n    'yotpo-reviews': YotpoReviews;\n  }\n}\n\nif (!customElements.get('yotpo-reviews')) {\n  customElements.define('yotpo-reviews', YotpoReviews);\n}\n","import type { CustomElement } from '@integrabeauty/custom-elements';\nimport html from './index.html';\nimport styles from './index.scss';\n\ntype StarFilterClickedEvent = CustomEvent<{\n  stars: number;\n}>;\n\n/**\n * The rating summary for a product's reviews, showing the average score, the total number of\n * reviews, and a bar for each star level with the share of reviews that gave that rating.\n *\n * Each star level bar is also a filter. Clicking one emits an event so the reviews list can narrow\n * to reviews with that rating.\n */\nclass YotpoStarsBreakdown extends HTMLElement implements CustomElement {\n  declare readonly dataset: {\n    /**\n     * Value of Yotpo product score on scale 1 - 5\n     */\n    reviewsAverage: string;\n\n    /**\n     * Number of Yotpo customer reviews for this product\n     */\n    reviewsTotal: string;\n\n    /**\n     * Number of reviews that gave 1 star.\n     */\n    star1Count: string;\n\n    /**\n     * Number of reviews that gave 2 stars.\n     */\n    star2Count: string;\n\n    /**\n     * Number of reviews that gave 3 stars.\n     */\n    star3Count: string;\n\n    /**\n     * Number of reviews that gave 4 stars.\n     */\n    star4Count: string;\n\n    /**\n     * Number of reviews that gave 5 stars.\n     */\n    star5Count: string;\n  };\n\n  private onStarFilterClickedBound = this.onStarFilterClicked.bind(this);\n  private onModalOpenRequestedBound = this.onModalOpenRequested.bind(this);\n  private onModalCloseRequestedBound = this.onModalCloseRequested.bind(this);\n\n  public connectedCallback() {\n    this.initializeShadowRoot();\n\n    const titleButtons = this.shadowRoot.querySelectorAll<HTMLButtonElement>('.title button');\n    for (const button of titleButtons) {\n      button.addEventListener('click', this.onStarFilterClickedBound);\n    }\n\n    this.shadowRoot.addEventListener('modal-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.addEventListener('modal-close-requested', this.onModalCloseRequestedBound);\n\n    this.render();\n  }\n\n  public disconnectedCallback() {\n    const titleButtons = this.shadowRoot.querySelectorAll<HTMLButtonElement>('.title button');\n    for (const button of titleButtons) {\n      button.removeEventListener('click', this.onStarFilterClickedBound);\n    }\n\n    this.shadowRoot.removeEventListener('modal-open-requested', this.onModalOpenRequestedBound);\n    this.shadowRoot.removeEventListener('modal-close-requested', this.onModalCloseRequestedBound);\n  }\n\n  private initializeShadowRoot() {\n    if (!this.shadowRoot) {\n      this.attachShadow({ mode: 'open' });\n      this.shadowRoot.innerHTML = `<style>${styles}</style>${html}`;\n    }\n  }\n\n  private render() {\n    const star1Count = parseInt(this.dataset.star1Count, 10);\n    const star2Count = parseInt(this.dataset.star2Count, 10);\n    const star3Count = parseInt(this.dataset.star3Count, 10);\n    const star4Count = parseInt(this.dataset.star4Count, 10);\n    const star5Count = parseInt(this.dataset.star5Count, 10);\n    const total = parseInt(this.dataset.reviewsTotal, 10);\n    // the actual sum may differ from the total - Yotpo provides different values\n    const starCountSum = star1Count + star2Count + star3Count + star4Count + star5Count;\n\n    const wrapper = this.shadowRoot.querySelector<HTMLElement>('.stats');\n\n    // if any of the star counts is NaN - their sum will be NaN as well\n    if (isNaN(starCountSum) || starCountSum === 0 || isNaN(total) || total === 0) {\n      wrapper.hidden = true;\n      return;\n    }\n\n    const ratingStars = this.shadowRoot.querySelector('product-rating-stars');\n    ratingStars.dataset.average = this.dataset.reviewsAverage;\n    ratingStars.dataset.count = this.dataset.reviewsTotal;\n\n    const averageElement = this.shadowRoot.querySelector('.average');\n    averageElement.textContent = this.dataset.reviewsAverage;\n\n    const totalElement = this.shadowRoot.querySelector('.total');\n    const formatter = new Intl.NumberFormat('en-US');\n    totalElement.textContent = formatter.format(total);\n\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star5'), star5Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star4'), star4Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star3'), star3Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star2'), star2Count, total);\n    updateStarRatingBar(this.shadowRoot.querySelector<HTMLElement>('.star1'), star1Count, total);\n\n    wrapper.hidden = false;\n  }\n\n  private onStarFilterClicked(event: MouseEvent) {\n    if (!(event.target instanceof HTMLButtonElement)) {\n      return;\n    }\n\n    const stars = event.target.dataset.stars;\n    if (stars) {\n      type Detail = StarFilterClickedEvent['detail'];\n      const filterClickedEvent = new CustomEvent<Detail>('star-filter-clicked', {\n        bubbles: true,\n        composed: true,\n        detail: {\n          stars: parseInt(stars, 10)\n        }\n      });\n      this.dispatchEvent(filterClickedEvent);\n    }\n  }\n\n  /**\n   * The modal manager leaves the reviews section active while the AI summary drawer slotted into us\n   * is open, so the star filter buttons would stay reachable by Tab behind it. We inert them, and\n   * the elements around us inert their own parts.\n   *\n   * Only the modal manager is meant to act on this request. We listen for it here only because it\n   * comes from our own slotted child, and we run before the manager's listener on the window.\n   */\n  private onModalOpenRequested(_event: HTMLElementEventMap['modal-open-requested']) {\n    const breakdown = this.shadowRoot.querySelector('.breakdown');\n    if (breakdown instanceof HTMLElement) {\n      breakdown.inert = true;\n    }\n  }\n\n  /**\n   * Takes the star filter buttons back out of inert. We run before the modal manager's listener on\n   * the window puts focus back on the element that opened the drawer, and a focus call into an\n   * inert subtree does nothing.\n   */\n  private onModalCloseRequested(_event: HTMLElementEventMap['modal-close-requested']) {\n    const breakdown = this.shadowRoot.querySelector('.breakdown');\n    if (breakdown instanceof HTMLElement) {\n      breakdown.inert = false;\n    }\n  }\n}\n\nfunction updateStarRatingBar(element: HTMLElement, count: number, total: number) {\n  const percent = (count / total) * 100;\n  const bar = element.querySelector<HTMLElement>('.fill');\n  bar.style.width = percent + '%';\n  const countElement = element.querySelector<HTMLElement>('.count');\n  countElement.textContent = percent.toFixed(1).replace('.0', '') + '%';\n  if (count === 0) {\n    const button = element.querySelector('button');\n    button.disabled = true;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'yotpo-stars-breakdown': YotpoStarsBreakdown;\n  }\n\n  interface GlobalEventHandlersEventMap {\n    'star-filter-clicked': StarFilterClickedEvent;\n  }\n}\n\nif (!customElements.get('yotpo-stars-breakdown')) {\n  customElements.define('yotpo-stars-breakdown', YotpoStarsBreakdown);\n}\n"],"names":["SimpleRatingStars","HTMLElement","observedAttributes","connectedCallback","this","initializeShadowRoot","render","attributeChangedCallback","_name","oldValue","newValue","isConnected","shadowRoot","attachShadow","mode","innerHTML","rating","parseInt","dataset","querySelector","textContent","assert","expression","message","AssertionError","customElements","get","define","Error","constructor","super","name","decorateError","error","key","value","assignmentError","reason","console","log","tag","Object","prototype","toString","call","YotpoAiSummary","fetchStarted","paragraphSummary","summaryTopics","summaryHighlights","onTopicLinksClickedBound","onTopicLinksClicked","bind","onCloseButtonClickedBound","onCloseButtonClicked","onModalDismissRequestedBound","onModalDismissRequested","topicLinks","addEventListener","closeButton","readAll","startFetching","catch","warn","disconnectedCallback","removeEventListener","openDrawer","closeDrawer","yotpoAppKey","productId","response","async","input","app_key","Number","isInteger","product_id","path","fetch","ok","status","code","url","responseText","body","text","JSON","parse","fetchReviewsSummary","event","target","HTMLButtonElement","setAttribute","focusEvent","Event","bubbles","composed","dispatchEvent","modalState","_event","drawer","inert","content","modalShowEvent","focus","focusVisible","activeTopicLink","copy","hidden","topicObject","topic","document","createElement","classList","add","displayTopic","coverage","appendChild","total","reviewsTotal","totalElement","formatter","Intl","NumberFormat","format","topics","title","summary","mentioned","li","append","highlightsList","highlight","blockquote","length","YotpoFilters","onHairTypeSelectedBound","onHairTypeSelected","onMediaSelectedBound","onMediaSelected","onRatingSelectedBound","onRatingSelected","onSearchClearedBound","onSearchCleared","onSearchKeyUpBound","onSearchKeyUp","onSortSelectedBound","onSortSelected","onStarsFilterClickedBound","onStarsFilterClicked","onReviewsFilterChangeBound","onReviewsFilterChange","onPillClickedBound","onPillClicked","onClearAllClickedBound","onClearAllClicked","onClearRequestedBound","onClearRequested","onSearchStartedBound","onSearchStarted","onSearchCompletedBound","onSearchCompleted","searchInput","hairTypeSelect","mediaSelect","ratingSelect","sortSelect","sort","pills","querySelectorAll","pill","clearAll","currentTarget","HTMLInputElement","clearTimeout","searchDebounceTimer","dispatchFilter","free_text_search","undefined","stopPropagation","query","timeout","setTimeout","HTMLSelectElement","hair_type","scores","score","detail","stars","pictured","ascending","includes","direction","split","sort_by","filters","sortings","merged","_extends","selectedFilters","CustomEvent","page","selectedSortings","renderAppliedFilters","container","existingPills","clearButton","push","dimension","label","button","className","span","select","clearAllFilters","inputs","disabled","YotpoPagination","currentPage","lastPage","maxButtons","onPreviousButtonClickedBound","onPreviousButtonClicked","onNextButtonClickedBound","onNextButtonClicked","onPageClickedBound","onPageClicked","onMediaQueryChangedBound","onMediaQueryChanged","prevButton","nextButton","pages","mediaQuery","matchMedia","matches","contains","changeEvent","idx","mediaQueryList","newMaxButtons","listWrapper","buttonPrev","buttonNext","perPage","Math","ceil","middleItemIndex","floor","startingIndex","numberOfItemsToDisplay","showFirstEllipsis","showSecondEllipsis","i","currentIndex","ellipsis","type","rnds8","Uint8Array","byteToHex","slice","unsafeStringify","arr","offset","toLowerCase","dist_v4","options","buf","crypto","randomUUID","_ref","_options$random","rnds","random","rng","getRandomValues","RangeError","_v4","SwellPopupType","SwellReferralMedium","YotpoReviewCreate","onStarClickedBound","onStarClicked","onWriteReviewButtonClickedBound","onWriteReviewButtonClicked","onSubmitButtonClickedBound","onSubmitButtonClicked","writeReviewButton","submitButton","ratingStarsElement","preventDefault","form","checkValidity","_firstInvalid","firstInvalid","element","elements","HTMLTextAreaElement","parentElement","remove","errorSummary","formData","FormData","displayName","createReviewInput","appkey","display_name","email","product_title","productTitle","product_url","productUrl","review_content","review_score","review_title","sku","loader","responseBody","stringify","headers","method","createReview","then","reset","submittedEvent","category","productType","error_message","event_id","first_name","last_name","person_name","product_price","productPrice","identityEvent","writeReview","selectStar","star","decode_html_entities_element","decode","output","yotpo_fetch_product_reviews_searchProductReviews","cache","_input$filters2","_input$filters3","_input$filters4","_input$filters5","_input$filters6","sorting","_input$filters","sortBy","_input$pathPrefix","_input$crfs","_input$ids","_input$metadata","_input$orders","_input$scores","_input$sortings","_input$topicNames","storeId","pathPrefix","URL","baseUrl","searchParams","lang","requestBody","country","crfs","freeTextSearch","ids","metadata","orders","picturesPerReview","productVariants","_sorting$ascending","topicNames","init","href","pagination","reviews","searchProductReviews","per_page","topic_names","yotpo_fetch_product_reviews_extends","YotpoReviews","onPageChangedBound","onPageChanged","onYotpoReviewSubmittedBound","onYotpoReviewSubmitted","onClearButtonClickedBound","onClearButtonClicked","onModalOpenRequestedBound","onModalOpenRequested","onModalCloseRequestedBound","onModalCloseRequested","reviewsPerPage","useCacheForReviews","loadingTimer","showLoading","onInitialReviewsFetched","onInitialReviewsFetchFailed","finally","hideLoading","paginationReviews","correctReviewsCount","renderReviews","renderPagination","searchEvent","isRoutineYotpoError","delay","showAlert","alert","alertPosition","alertType","fadeDelay","closable","composedPath","containers","modalContainer","parts","getPartsBesideSelector","part","releaseParts","overlay","answers","questionId","rect","getBoundingClientRect","headerOffset","parseFloat","getComputedStyle","getPropertyValue","documentElement","scrollTop","scrollY","top","scrollToElement","searchedEvent","filteredEvent","yotpo_reviews_extends","itemsContainer","noResults","review","createReviewDetail","fromCache","paginationElement","reachableTotal","update","Date","now","ratingStarsComponent","count","reviewId","id","useLegacy","votes_up","votes_down","imagesData","images_data","map","image","image_id","thumbUrl","thumb_url","originalUrl","original_image_url","original_url","originalImages","join","thumbnailImages","votesUp","incentivized","is_incentivized","isIncentivized","avatar","date","created_at","createdAt","user","verified","verified_buyer","verifiedBuyer","hairType","customFields","field","getHairTypeValue","custom_fields","createTextNode","YotpoStarsBreakdown","onStarFilterClickedBound","onStarFilterClicked","titleButtons","star1Count","star2Count","star3Count","star4Count","star5Count","starCountSum","wrapper","isNaN","ratingStars","average","reviewsAverage","updateStarRatingBar","filterClickedEvent","breakdown","percent","style","width","toFixed","replace"],"sourceRoot":""}