Initial commit: Atomaste website

This commit is contained in:
2025-12-10 12:17:30 -05:00
commit 0b9e5d1605
19260 changed files with 5206382 additions and 0 deletions

View File

@@ -0,0 +1 @@
.base-modal__title-container[data-v-2402c4b1]{align-items:center;display:flex;margin-bottom:8px}.base-modal__title[data-v-2402c4b1]{color:var(--dark);font-size:20px;font-weight:700;margin:0}.base-modal__subtitle[data-v-2402c4b1]{color:var(--gray);font-size:14px;margin-bottom:24px;margin-top:4px}

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[400],{400:function(t,e,l){l.r(e),l.d(e,{default:function(){return d}});var s=l(641),a=l(33),i=l(352);const n={class:"base-modal"},o={class:"base-modal__title-container"},c={key:1,class:"base-modal__title text-heading-2"},u={key:0,class:"base-modal__subtitle"};var r=(0,s.pM)({__name:"BaseModal",props:{title:{},subtitle:{},titleIcon:{}},setup(t){return(e,l)=>((0,s.uX)(),(0,s.CE)("div",n,[(0,s.Lk)("span",o,[t.titleIcon?((0,s.uX)(),(0,s.Wv)(i.A,{key:0,class:"h-mr-12",name:t.titleIcon.name,color:t.titleIcon.color},null,8,["name","color"])):(0,s.Q3)("v-if",!0),t.title?((0,s.uX)(),(0,s.CE)("h2",c,(0,a.v_)(t.title),1)):(0,s.Q3)("v-if",!0)]),t.subtitle?((0,s.uX)(),(0,s.CE)("p",u,(0,a.v_)(t.subtitle),1)):(0,s.Q3)("v-if",!0),(0,s.RG)(e.$slots,"default")]))}});var d=(0,l(262).A)(r,[["__scopeId","data-v-2402c4b1"]])}}]);

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[415],{415:function(e,l,n){n.r(l),n.d(l,{default:function(){return u}});var C=n(641);const r={width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg"};const t={};var u=(0,n(262).A)(t,[["render",function(e,l){return(0,C.uX)(),(0,C.CE)("svg",r,[...l[0]||(l[0]=[(0,C.Lk)("path",{"fill-rule":"evenodd","clip-rule":"evenodd",d:"M7.99998 6.23374C7.02266 6.23374 6.2304 7.02601 6.2304 8.00332C6.2304 8.98063 7.02266 9.7729 7.99998 9.7729C8.97729 9.7729 9.76956 8.98063 9.76956 8.00332C9.76956 7.02601 8.97729 6.23374 7.99998 6.23374ZM4.69123 8.00332C4.69123 6.17595 6.17261 4.69457 7.99998 4.69457C9.82735 4.69457 11.3087 6.17595 11.3087 8.00332C11.3087 9.83069 9.82735 11.3121 7.99998 11.3121C6.17261 11.3121 4.69123 9.83069 4.69123 8.00332Z",fill:"currentColor"},null,-1),(0,C.Lk)("path",{"fill-rule":"evenodd","clip-rule":"evenodd",d:"M7.99508 3.92088C6.23615 3.92088 3.12832 4.78339 1.55391 7.93872C1.5343 7.97801 1.53407 8.02604 1.55433 8.06651C3.12861 11.2094 6.1426 12.0858 8.00495 12.0858C9.76389 12.0858 12.8717 11.2233 14.4461 8.06799C14.4657 8.0287 14.466 7.98067 14.4457 7.9402C12.8714 4.79726 9.85743 3.92088 7.99508 3.92088ZM0.176668 7.25152C2.09445 3.40802 5.86487 2.38171 7.99508 2.38171C10.2434 2.38171 13.909 3.43186 15.8219 7.25088C16.0585 7.72326 16.0598 8.28126 15.8234 8.75519C13.9056 12.5987 10.1352 13.625 8.00495 13.625C5.7566 13.625 2.09108 12.5749 0.178153 8.75583C-0.058457 8.28345 -0.0598124 7.72545 0.176668 7.25152Z",fill:"currentColor"},null,-1)])])}]])}}]);

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[433],{433:function(n,t,e){e.r(t),e.d(t,{default:function(){return s}});var r=e(641);const u={xmlns:"http://www.w3.org/2000/svg",width:"24",height:"24",viewBox:"0 0 24 24",fill:"none"};const o={};var s=(0,e(262).A)(o,[["render",function(n,t){return(0,r.uX)(),(0,r.CE)("svg",u,[...t[0]||(t[0]=[(0,r.Lk)("path",{d:"M4.5 21C4.1 21 3.75 20.85 3.45 20.55C3.15 20.25 3 19.9 3 19.5V4.5C3 4.1 3.15 3.75 3.45 3.45C3.75 3.15 4.1 3 4.5 3H11.475V4.5H4.5V19.5H19.5V12.525H21V19.5C21 19.9 20.85 20.25 20.55 20.55C20.25 20.85 19.9 21 19.5 21H4.5ZM9.55 15.525L8.5 14.45L18.45 4.5H12.975V3H21V11.025H19.5V5.575L9.55 15.525Z",fill:"currentColor"},null,-1)])])}]])}}]);

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[52],{52:function(e,n,C){C.r(n),C.d(n,{default:function(){return u}});var r=C(641);const t={width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg"};const l={};var u=(0,C(262).A)(l,[["render",function(e,n){return(0,r.uX)(),(0,r.CE)("svg",t,[...n[0]||(n[0]=[(0,r.Lk)("path",{"fill-rule":"evenodd","clip-rule":"evenodd",d:"M2.5 5.2478C2.5 3.72902 3.73122 2.4978 5.25 2.4978H6C6.41421 2.4978 6.75 2.83359 6.75 3.2478C6.75 3.66202 6.41421 3.9978 6 3.9978H5.25C4.55964 3.9978 4 4.55745 4 5.2478V10.6859C4 11.3763 4.55964 11.9359 5.25 11.9359H10.7508C11.4411 11.9359 12.0008 11.3763 12.0008 10.6859V10C12.0008 9.58579 12.3366 9.25 12.7508 9.25C13.165 9.25 13.5008 9.58579 13.5008 10V10.6859C13.5008 12.2047 12.2696 13.4359 10.7508 13.4359H5.25C3.73122 13.4359 2.5 12.2047 2.5 10.6859V5.2478ZM12 5.06077L8.03033 9.03044C7.73744 9.32333 7.26256 9.32333 6.96967 9.03044C6.67678 8.73754 6.67678 8.26267 6.96967 7.96977L10.9393 4.00011H9C8.58579 4.00011 8.25 3.66432 8.25 3.2501C8.25 2.83589 8.58579 2.50011 9 2.50011L12.25 2.50011C12.9404 2.50011 13.5 3.05975 13.5 3.75011V7.0001C13.5 7.41432 13.1642 7.7501 12.75 7.7501C12.3358 7.7501 12 7.41432 12 7.0001V5.06077Z",fill:"currentColor"},null,-1)])])}]])}}]);

View File

@@ -0,0 +1 @@
.base-modal__title-container[data-v-2402c4b1]{align-items:center;display:flex;margin-bottom:8px}.base-modal__title[data-v-2402c4b1]{color:var(--dark);font-size:20px;font-weight:700;margin:0}.base-modal__subtitle[data-v-2402c4b1]{color:var(--gray);font-size:14px;margin-bottom:24px;margin-top:4px}

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[400,609],{400:function(t,l,e){e.r(l),e.d(l,{default:function(){return d}});var o=e(641),a=e(33),s=e(352);const n={class:"base-modal"},i={class:"base-modal__title-container"},c={key:1,class:"base-modal__title text-heading-2"},r={key:0,class:"base-modal__subtitle"};var _=(0,o.pM)({__name:"BaseModal",props:{title:{},subtitle:{},titleIcon:{}},setup(t){return(l,e)=>((0,o.uX)(),(0,o.CE)("div",n,[(0,o.Lk)("span",i,[t.titleIcon?((0,o.uX)(),(0,o.Wv)(s.A,{key:0,class:"h-mr-12",name:t.titleIcon.name,color:t.titleIcon.color},null,8,["name","color"])):(0,o.Q3)("v-if",!0),t.title?((0,o.uX)(),(0,o.CE)("h2",c,(0,a.v_)(t.title),1)):(0,o.Q3)("v-if",!0)]),t.subtitle?((0,o.uX)(),(0,o.CE)("p",r,(0,a.v_)(t.subtitle),1)):(0,o.Q3)("v-if",!0),(0,o.RG)(l.$slots,"default")]))}});var d=(0,e(262).A)(_,[["__scopeId","data-v-2402c4b1"]])},609:function(t,l,e){e.r(l),e.d(l,{default:function(){return u}});var o=e(641),a=e(953),s=e(33),n=e(843),i=e(400),c=e(283),r=e(203);const _={class:"h-mb-24 text-gray"},d={class:"d-flex justify-content-end"};var u=(0,o.pM)({__name:"EnableLlmsTxtModal",props:{data:{}},setup(t){const l=t,{closeModal:e}=(0,c.hS)(),u=()=>{l.data.onConfirm&&l.data.onConfirm(),e()};return(t,l)=>((0,o.uX)(),(0,o.Wv)(i.default,{"title-icon":{name:"icon-info",color:"danger"},title:(0,a.R1)(r.Tl)("hostinger_tools_llms_txt_modal_title")},{default:(0,o.k6)(()=>[(0,o.Lk)("p",_,(0,s.v_)((0,a.R1)(r.Tl)("hostinger_tools_llms_txt_modal_description")),1),(0,o.Lk)("div",d,[(0,o.bF)(n.A,{color:"danger",variant:"text",class:"h-mr-16",onClick:(0,a.R1)(e)},{default:(0,o.k6)(()=>[(0,o.eW)((0,s.v_)((0,a.R1)(r.Tl)("hostinger_tools_llms_txt_modal_cancel")),1)]),_:1},8,["onClick"]),(0,o.bF)(n.A,{color:"danger",onClick:u},{default:(0,o.k6)(()=>[(0,o.eW)((0,s.v_)((0,a.R1)(r.Tl)("hostinger_tools_llms_txt_modal_create_file")),1)]),_:1})])]),_:1},8,["title"]))}})}}]);

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[614],{614:function(e,n,r){r.r(n),r.d(n,{default:function(){return o}});var t=r(641);const u={"fill-rule":"evenodd","clip-rule":"evenodd",d:"M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z"};const c={};var o=(0,r(262).A)(c,[["render",function(e,n){return(0,t.uX)(),(0,t.CE)("path",u)}]])}}]);

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[736],{736:function(C,n,t){t.r(n),t.d(n,{default:function(){return s}});var e=t(641);const r={d:"M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16V12C13 11.7167 12.9042 11.4792 12.7125 11.2875C12.5208 11.0958 12.2833 11 12 11C11.7167 11 11.4792 11.0958 11.2875 11.2875C11.0958 11.4792 11 11.7167 11 12V16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 9C12.2833 9 12.5208 8.90417 12.7125 8.7125C12.9042 8.52083 13 8.28333 13 8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8C11 8.28333 11.0958 8.52083 11.2875 8.7125C11.4792 8.90417 11.7167 9 12 9ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22ZM12 20C14.2333 20 16.125 19.225 17.675 17.675C19.225 16.125 20 14.2333 20 12C20 9.76667 19.225 7.875 17.675 6.325C16.125 4.775 14.2333 4 12 4C9.76667 4 7.875 4.775 6.325 6.325C4.775 7.875 4 9.76667 4 12C4 14.2333 4.775 16.125 6.325 17.675C7.875 19.225 9.76667 20 12 20Z"};const u={};var s=(0,t(262).A)(u,[["render",function(C,n){return(0,e.uX)(),(0,e.CE)("path",r)}]])}}]);

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[79],{79:function(C,e,l){l.r(e),l.d(e,{default:function(){return t}});var n=l(641);const r={width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg"};const L={};var t=(0,l(262).A)(L,[["render",function(C,e){return(0,n.uX)(),(0,n.CE)("svg",r,[...e[0]||(e[0]=[(0,n.Lk)("path",{"fill-rule":"evenodd","clip-rule":"evenodd",d:"M6.85691 14.1816C6.85691 13.7674 7.14483 13.4316 7.5 13.4316H13.6069C13.9621 13.4316 14.25 13.7674 14.25 14.1816C14.25 14.5958 13.9621 14.9316 13.6069 14.9316H7.5C7.14483 14.9316 6.85691 14.5958 6.85691 14.1816Z",fill:"currentColor"},null,-1),(0,n.Lk)("path",{"fill-rule":"evenodd","clip-rule":"evenodd",d:"M4.29084 12.6658L13.0587 3.80312C13.0835 3.77806 13.1068 3.75446 13.1289 3.7321C13.1066 3.70997 13.083 3.68661 13.0579 3.66183L12.2098 2.82257C12.1847 2.79775 12.1611 2.77438 12.1387 2.75227C12.1165 2.77459 12.0932 2.7982 12.0683 2.82325L6.68596 8.25301L3.29957 11.6881C3.29328 11.6945 3.28738 11.7004 3.28183 11.7061C3.27962 11.7137 3.27728 11.7217 3.27478 11.7303L2.87798 13.0951L4.24799 12.6914C4.25676 12.6888 4.26497 12.6864 4.27271 12.6841C4.27839 12.6784 4.28441 12.6723 4.29084 12.6658ZM5.61974 7.19793L2.23046 10.6359C2.12466 10.7432 2.07176 10.7969 2.02795 10.857C1.98904 10.9104 1.95553 10.9675 1.9279 11.0275C1.89679 11.0951 1.87576 11.1674 1.8337 11.3121L1.24307 13.3436C1.05693 13.9838 0.963865 14.3039 1.04577 14.5217C1.11715 14.7115 1.26745 14.8611 1.45773 14.9316C1.67608 15.0125 1.99607 14.9182 2.63606 14.7296L4.67252 14.1295C4.81992 14.086 4.89362 14.0643 4.96229 14.0322C5.02327 14.0036 5.0812 13.969 5.13522 13.9288C5.19605 13.8836 5.25007 13.829 5.3581 13.7198L14.1259 4.85713C14.5195 4.45935 14.7162 4.26046 14.7891 4.03197C14.8533 3.83099 14.8521 3.61488 14.7859 3.41459C14.7105 3.1869 14.5117 2.99011 14.1139 2.59653L13.2658 1.75727C12.8677 1.36333 12.6687 1.16637 12.4399 1.09331C12.2386 1.02905 12.0222 1.03009 11.8216 1.09629C11.5935 1.17155 11.3964 1.37042 11.0021 1.76817L5.61974 7.19793ZM12.4708 2.43329L12.4689 2.43476L12.4708 2.43329ZM11.8055 2.43797L11.8035 2.43651L11.8055 2.43797ZM13.4478 4.06357L13.4463 4.06157L13.4478 4.06357ZM13.4428 3.39923L13.4443 3.39725L13.4428 3.39923Z",fill:"currentColor"},null,-1)])])}]])}}]);

View File

@@ -0,0 +1 @@
.base-modal__title-container[data-v-2402c4b1]{align-items:center;display:flex;margin-bottom:8px}.base-modal__title[data-v-2402c4b1]{color:var(--dark);font-size:20px;font-weight:700;margin:0}.base-modal__subtitle[data-v-2402c4b1]{color:var(--gray);font-size:14px;margin-bottom:24px;margin-top:4px}

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[400,869],{400:function(t,e,l){l.r(e),l.d(e,{default:function(){return d}});var a=l(641),s=l(33),n=l(352);const o={class:"base-modal"},i={class:"base-modal__title-container"},r={key:1,class:"base-modal__title text-heading-2"},c={key:0,class:"base-modal__subtitle"};var _=(0,a.pM)({__name:"BaseModal",props:{title:{},subtitle:{},titleIcon:{}},setup(t){return(e,l)=>((0,a.uX)(),(0,a.CE)("div",o,[(0,a.Lk)("span",i,[t.titleIcon?((0,a.uX)(),(0,a.Wv)(n.A,{key:0,class:"h-mr-12",name:t.titleIcon.name,color:t.titleIcon.color},null,8,["name","color"])):(0,a.Q3)("v-if",!0),t.title?((0,a.uX)(),(0,a.CE)("h2",r,(0,s.v_)(t.title),1)):(0,a.Q3)("v-if",!0)]),t.subtitle?((0,a.uX)(),(0,a.CE)("p",c,(0,s.v_)(t.subtitle),1)):(0,a.Q3)("v-if",!0),(0,a.RG)(e.$slots,"default")]))}});var d=(0,l(262).A)(_,[["__scopeId","data-v-2402c4b1"]])},869:function(t,e,l){l.r(e),l.d(e,{default:function(){return u}});var a=l(641),s=l(953),n=l(33),o=l(843),i=l(400),r=l(283),c=l(203);const _={class:"h-mb-24 text-gray"},d={class:"d-flex justify-content-end"};var u=(0,a.pM)({__name:"ByPassLinkResetModal",props:{data:{}},setup(t){const e=t,{closeModal:l}=(0,r.hS)(),u=()=>{e.data.onConfirm&&e.data.onConfirm(),l()};return(t,e)=>((0,a.uX)(),(0,a.Wv)(i.default,{"title-icon":{name:"icon-info",color:"danger"},title:(0,s.R1)(c.Tl)("bypass_link_reset_modal_title")},{default:(0,a.k6)(()=>[(0,a.Lk)("p",_,(0,n.v_)((0,s.R1)(c.Tl)("bypass_link_reset_modal_description")),1),(0,a.Lk)("div",d,[(0,a.bF)(o.A,{color:"danger",variant:"text",class:"h-mr-16",onClick:(0,s.R1)(l)},{default:(0,a.k6)(()=>[(0,a.eW)((0,n.v_)((0,s.R1)(c.Tl)("bypass_link_reset_modal_cancel")),1)]),_:1},8,["onClick"]),(0,a.bF)(o.A,{color:"danger",onClick:u},{default:(0,a.k6)(()=>[(0,a.eW)((0,n.v_)((0,s.R1)(c.Tl)("bypass_link_reset_modal_reset_link")),1)]),_:1})])]),_:1},8,["title"]))}})}}]);

View File

@@ -0,0 +1 @@
.base-modal__title-container[data-v-2402c4b1]{align-items:center;display:flex;margin-bottom:8px}.base-modal__title[data-v-2402c4b1]{color:var(--dark);font-size:20px;font-weight:700;margin:0}.base-modal__subtitle[data-v-2402c4b1]{color:var(--gray);font-size:14px;margin-bottom:24px;margin-top:4px}

View File

@@ -0,0 +1 @@
"use strict";(self.webpackChunkhostinger=self.webpackChunkhostinger||[]).push([[98,400],{98:function(t,e,l){l.r(e),l.d(e,{default:function(){return _}});var a=l(641),o=l(953),n=l(33),s=l(843),i=l(400),c=l(283),r=l(203);const u={class:"h-mb-24 text-gray"},d={class:"d-flex justify-content-end"};var _=(0,a.pM)({__name:"XmlSecurityModal",props:{data:{}},setup(t){const e=t,{closeModal:l}=(0,c.hS)(),_=()=>{e.data.onConfirm&&e.data.onConfirm(),l()};return(t,e)=>((0,a.uX)(),(0,a.Wv)(i.default,{"title-icon":{name:"icon-info",color:"danger"},title:(0,o.R1)(r.Tl)("xml_security_modal_title")},{default:(0,a.k6)(()=>[(0,a.Lk)("p",u,(0,n.v_)((0,o.R1)(r.Tl)("xml_security_modal_description")),1),(0,a.Lk)("div",d,[(0,a.bF)(s.A,{color:"danger",variant:"text",class:"h-mr-16",onClick:(0,o.R1)(l)},{default:(0,a.k6)(()=>[(0,a.eW)((0,n.v_)((0,o.R1)(r.Tl)("xml_security_modal_cancel")),1)]),_:1},8,["onClick"]),(0,a.bF)(s.A,{color:"danger",onClick:_},{default:(0,a.k6)(()=>[(0,a.eW)((0,n.v_)((0,o.R1)(r.Tl)("xml_security_modal_proceed_anyway")),1)]),_:1})])]),_:1},8,["title"]))}})},400:function(t,e,l){l.r(e),l.d(e,{default:function(){return d}});var a=l(641),o=l(33),n=l(352);const s={class:"base-modal"},i={class:"base-modal__title-container"},c={key:1,class:"base-modal__title text-heading-2"},r={key:0,class:"base-modal__subtitle"};var u=(0,a.pM)({__name:"BaseModal",props:{title:{},subtitle:{},titleIcon:{}},setup(t){return(e,l)=>((0,a.uX)(),(0,a.CE)("div",s,[(0,a.Lk)("span",i,[t.titleIcon?((0,a.uX)(),(0,a.Wv)(n.A,{key:0,class:"h-mr-12",name:t.titleIcon.name,color:t.titleIcon.color},null,8,["name","color"])):(0,a.Q3)("v-if",!0),t.title?((0,a.uX)(),(0,a.CE)("h2",c,(0,o.v_)(t.title),1)):(0,a.Q3)("v-if",!0)]),t.subtitle?((0,a.uX)(),(0,a.CE)("p",r,(0,o.v_)(t.subtitle),1)):(0,a.Q3)("v-if",!0),(0,a.RG)(e.$slots,"default")]))}});var d=(0,l(262).A)(u,[["__scopeId","data-v-2402c4b1"]])}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,42 @@
/*!
* vue-router v4.5.1
* (c) 2025 Eduardo San Martin Morote
* @license MIT
*/
/*!
* pinia v2.3.1
* (c) 2025 Eduardo San Martin Morote
* @license MIT
*/
/*! #__NO_SIDE_EFFECTS__ */
/*! https://mths.be/punycode v1.4.1 by @mathias */
/**
* @license
* Lodash <https://lodash.com/>
* Copyright OpenJS Foundation and other contributors <https://openjsf.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/**
* @vue/reactivity v3.5.22
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/
/**
* @vue/runtime-dom v3.5.22
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/
/**
* @vue/shared v3.5.22
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/

View File

@@ -0,0 +1,57 @@
<script setup lang="ts">
import { computed } from "vue";
import { RouterView, useRoute } from "vue-router";
import Modals from "@/components/Modals/Base/Modals.vue";
import Wrapper from "@/layouts/Wrapper.vue";
import { EditSiteButton, HeaderButton, PreviewSiteButton } from "@/types";
const route = useRoute();
const headerTitle = computed(() => route.meta.title as string);
const headerButton = computed(() => route.meta.headerButton as HeaderButton);
const previewSiteButton = computed(
() => route.meta.previewSiteButton as PreviewSiteButton
);
const editSiteButton = computed(
() => route.meta.editSiteButton as EditSiteButton
);
</script>
<template>
<div>
<div id="overhead-button" />
<Wrapper
:title="headerTitle"
:header-button="headerButton"
:preview-site-button="previewSiteButton"
:edit-site-button="editSiteButton"
>
<RouterView v-slot="{ Component }">
<Component :is="Component" />
</RouterView>
</Wrapper>
<Modals />
</div>
</template>
<style lang="scss" scoped>
:deep(.h-button-v2) {
&:hover {
cursor: pointer;
}
}
#overhead-button {
position: absolute;
right: 0;
padding: 40px;
z-index: 2;
@media (max-width: 576px) {
padding: 16px;
}
}
</style>

View File

@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_814_31010)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.6249C5.37269 18.6249 0 15.7987 0 12.3124C0 8.82625 5.37269 6 12 6C18.6273 6 24 8.82625 24 12.3124C24 15.7987 18.6273 18.6249 12 18.6249ZM13.0236 14.2957C12.9847 14.2957 12.9478 14.2783 12.9231 14.2483C12.8983 14.2183 12.8883 14.1788 12.8957 14.1406L13.4396 11.3416C13.4913 11.0754 13.4786 10.8844 13.4037 10.8037C13.3579 10.7543 13.2202 10.6715 12.8131 10.6715H11.8278L11.144 14.1903C11.1321 14.2515 11.0785 14.2957 11.0162 14.2957H9.64899C9.6101 14.2957 9.57325 14.2783 9.54851 14.2483C9.52377 14.2183 9.51375 14.1788 9.52117 14.1406L10.7507 7.81464C10.7626 7.75349 10.8162 7.7093 10.8785 7.7093H12.2457C12.2846 7.7093 12.3214 7.72666 12.3462 7.75665C12.3709 7.78669 12.3809 7.82614 12.3735 7.86434L12.0768 9.39116H13.1368C13.9443 9.39116 14.4919 9.53352 14.8108 9.8264C15.1359 10.1252 15.2373 10.6029 15.1123 11.2465L14.5403 14.1903C14.5284 14.2515 14.4748 14.2957 14.4124 14.2957H13.0236ZM6.51972 13.037C7.04337 13.037 7.4343 12.9404 7.68165 12.75C7.92635 12.5616 8.09536 12.2352 8.18395 11.7799C8.26655 11.3543 8.23508 11.0571 8.09046 10.8967C7.94259 10.7329 7.62288 10.6498 7.14024 10.6498H6.3034L5.83947 13.037H6.51972ZM3.78261 15.9775C3.74377 15.9775 3.70687 15.9602 3.68218 15.9301C3.65744 15.9001 3.64741 15.8606 3.65483 15.8225L4.88435 9.4965C4.89624 9.43534 4.94984 9.39116 5.01217 9.39116H7.66208C8.49489 9.39116 9.11476 9.61729 9.50439 10.0633C9.8961 10.5117 10.0171 11.1385 9.8639 11.9262C9.80157 12.2471 9.69436 12.5452 9.54528 12.812C9.39601 13.0792 9.19875 13.3264 8.95886 13.5468C8.67171 13.8156 8.34671 14.0105 7.99376 14.1251C7.64641 14.2383 7.20031 14.2957 6.66789 14.2957H5.59489L5.28847 15.8721C5.27658 15.9333 5.22302 15.9775 5.16069 15.9775H3.78261ZM18.4375 12.75C18.1902 12.9404 17.7992 13.037 17.2756 13.037H16.5953L17.0593 10.6498H17.8961C18.3787 10.6498 18.6984 10.7329 18.8463 10.8967C18.991 11.0571 19.0224 11.3543 18.9399 11.7799C18.8512 12.2352 18.6822 12.5617 18.4375 12.75ZM14.4381 15.9301C14.4628 15.9602 14.4997 15.9775 14.5385 15.9775H15.9165C15.9789 15.9775 16.0325 15.9333 16.0444 15.8721L16.3507 14.2957H17.4238C17.9562 14.2957 18.4023 14.2383 18.7496 14.1251C19.1026 14.0105 19.4276 13.8156 19.7147 13.5468C19.9546 13.3264 20.1519 13.0792 20.3011 12.812C20.4502 12.5452 20.5574 12.2471 20.6198 11.9262C20.7729 11.1385 20.652 10.5117 20.2603 10.0633C19.8706 9.61729 19.2508 9.39116 18.4179 9.39116H15.7681C15.7057 9.39116 15.6521 9.43534 15.6402 9.4965L14.4107 15.8225C14.4033 15.8606 14.4133 15.9001 14.4381 15.9301Z" fill="#727586"/>
</g>
<defs>
<clipPath id="clip0_814_31010">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.4879 2 2 6.48387 2 12C2 17.5121 6.4879 22 12 22C17.5121 22 22 17.5121 22 12C22 6.48387 17.5121 2 12 2ZM3.00806 12C3.00806 10.6976 3.28629 9.45968 3.78629 8.34274L8.07661 20.0927C5.07661 18.6331 3.00806 15.5565 3.00806 12ZM12 20.9919C11.1169 20.9919 10.2661 20.8629 9.45968 20.625L12.1573 12.7863L14.9194 20.3589C14.9395 20.4032 14.9597 20.4435 14.9839 20.4839C14.0524 20.8105 13.0484 20.9919 12 20.9919ZM13.2379 7.78629C13.7782 7.75806 14.2661 7.70161 14.2661 7.70161C14.75 7.64516 14.6935 6.93145 14.2097 6.95968C14.2097 6.95968 12.754 7.07258 11.8145 7.07258C10.9315 7.07258 9.44758 6.95968 9.44758 6.95968C8.96371 6.93145 8.90726 7.67339 9.39113 7.70161C9.39113 7.70161 9.85081 7.75806 10.3347 7.78629L11.7339 11.625L9.76613 17.5242L6.49194 7.78629C7.03226 7.75806 7.52016 7.70161 7.52016 7.70161C8.00403 7.64516 7.94758 6.93145 7.46371 6.95968C7.46371 6.95968 6.00806 7.07258 5.06855 7.07258C4.89919 7.07258 4.70161 7.06855 4.4879 7.06048C6.09677 4.62097 8.85887 3.00806 12 3.00806C14.3387 3.00806 16.4718 3.90323 18.0726 5.36694C18.0323 5.3629 17.996 5.35887 17.9556 5.35887C17.0726 5.35887 16.4476 6.12903 16.4476 6.95564C16.4476 7.69758 16.875 8.32258 17.3306 9.06452C17.6734 9.66129 18.0726 10.4315 18.0726 11.5444C18.0726 12.3145 17.7782 13.2056 17.3871 14.4516L16.4919 17.4476L13.2379 7.78629ZM16.5202 19.7702L19.2661 11.8306C19.7782 10.5484 19.9516 9.52419 19.9516 8.60887C19.9516 8.27823 19.9315 7.97177 19.8911 7.68548C20.5927 8.96774 20.9919 10.4355 20.9919 12C20.9919 15.3185 19.1935 18.2137 16.5202 19.7702Z" fill="#727586"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,81 @@
<template>
<div
class="accordion-card"
:class="{
'accordion-card--is-toggleable': props.togglable,
'accordion-card--is-hidden': !cardVisible,
}"
>
<div
class="accordion-card__header"
@click.prevent="toggleCard"
>
<div class="accordion-card__title">
<slot name="title" />
</div>
<div
v-if="slots['title-right']"
class="accordion-card__title accordion-card__title--align-right"
>
<slot name="title-right" />
</div>
<div
v-if="props.togglable"
class="accordion-card__arrow"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M11.9998 14.95C11.8665 14.95 11.7415 14.9292 11.6248 14.8875C11.5081 14.8458 11.3998 14.775 11.2998 14.675L6.6998 10.075C6.51647 9.89166 6.4248 9.65833 6.4248 9.37499C6.4248 9.09166 6.51647 8.85833 6.6998 8.67499C6.88314 8.49166 7.11647 8.39999 7.3998 8.39999C7.68314 8.39999 7.91647 8.49166 8.0998 8.67499L11.9998 12.575L15.8998 8.67499C16.0831 8.49166 16.3165 8.39999 16.5998 8.39999C16.8831 8.39999 17.1165 8.49166 17.2998 8.67499C17.4831 8.85833 17.5748 9.09166 17.5748 9.37499C17.5748 9.65833 17.4831 9.89166 17.2998 10.075L12.6998 14.675C12.5998 14.775 12.4915 14.8458 12.3748 14.8875C12.2581 14.9292 12.1331 14.95 11.9998 14.95Z"
fill="#673DE6"
/>
</svg>
</div>
</div>
<Vue3SlideUpDown
v-model="cardVisible"
:duration="400"
>
<div class="accordion-card__body">
<slot name="body" />
</div>
</Vue3SlideUpDown>
</div>
</template>
<script lang="ts" setup>
import { ref, useSlots } from "vue";
import { Vue3SlideUpDown } from "vue3-slide-up-down";
const props = defineProps({
togglable: {
type: Boolean,
default: false,
required: false
},
isVisible: {
type: Boolean,
default: true,
required: false
}
});
const slots = useSlots();
const cardVisible = ref(true);
if (!props.isVisible) {
cardVisible.value = false;
}
const toggleCard = () => {
if (props.togglable) {
cardVisible.value = !cardVisible.value;
}
};
</script>

View File

@@ -0,0 +1,161 @@
<script lang="ts" setup>
import { ref } from "vue";
import Icon from "@/components/Icon/Icon.vue";
import CircleLoader from "@/components/Loaders/CircleLoader.vue";
import { useButton } from "@/composables/useButton";
import type { IButtonProps } from "@/types";
type Emit = {
click: [event: Event];
};
const props = withDefaults(defineProps<IButtonProps>(), {
size: "medium",
variant: "contain",
color: "primary",
isDisabled: null,
isLoading: false
});
const emit = defineEmits<Emit>();
const buttonTextRef = ref<HTMLElement>();
const { style, tag } = useButton(props);
const handleClick = (event: Event) => {
if (props.isDisabled) {
event.preventDefault();
event.stopPropagation();
return;
}
emit("click", event);
};
</script>
<template>
<Component
:is="tag"
:to="isDisabled ? undefined : to"
:target="isDisabled ? undefined : target"
:href="isDisabled ? undefined : to"
class="button-v2"
:class="{
'button-v2--disabled': isDisabled,
'button-v2--hovered': isHovered,
'button-v2--loading': isLoading,
}"
:disabled="isDisabled || null"
@click="handleClick"
>
<Icon
v-if="iconPrepend && !isLoading"
class="button-v2__icon"
:name="iconPrepend"
:color="isDisabled ? 'gray' : style.icon.color"
:dimensions="style.icon.size"
/>
<div class="button-v2__loader">
<CircleLoader
v-show="isLoading"
:dimensions="style.loader.size"
:border-color="style.loader.borderColor"
:border-size="style.loader.border"
:color="props.color"
/>
</div>
<span
v-if="$slots.default"
ref="buttonTextRef"
class="button-v2__text"
>
<slot />
</span>
<Icon
v-if="iconAppend && !isLoading"
class="button-v2__icon"
:name="iconAppend"
:color="isDisabled ? 'gray' : style.icon.color"
:dimensions="style.icon.size"
/>
</Component>
</template>
<style lang="scss">
.button-v2 {
$this: &;
padding: v-bind("style.padding");
color: v-bind("style.color");
background-color: v-bind("style.backgroundColor");
border: v-bind("style.border");
border-radius: 8px;
display: inline-flex;
align-items: center;
gap: 8px;
position: relative;
transition: background-color 0.1s ease-in-out;
text-decoration: none;
font-size: 12px;
line-height: 24px;
font-weight: 700;
width: fit-content;
flex-wrap: nowrap;
justify-content: center;
text-wrap: nowrap;
&--disabled,
&[disabled] {
color: v-bind("style.colorDisabled") !important;
background-color: v-bind("style.backgroundColorDisabled");
font-size: 12px !important;
pointer-events: none;
cursor: not-allowed;
}
&--loading {
pointer-events: none;
#{$this}__text {
opacity: 0;
}
#{$this}__loader {
opacity: 1;
}
}
&__text {
opacity: 1;
transition: opacity 0.2s ease-in-out;
}
&__loader {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
&--hovered:not(&--disabled):not([disabled]) {
background-color: v-bind("style.backgroundHoverColor");
}
&:hover:not(&--disabled):not([disabled]) {
background-color: v-bind("style.backgroundHoverColor");
cursor: pointer;
}
@media (max-width: 576px) {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,163 @@
import type { ButtonSize, IButtonVariantConfiguration } from "@/types";
export const BUTTON_SIZE_CONFIGURATION: Record<
ButtonSize,
{ padding: string }
> = {
small: {
padding: "6px 16px"
},
medium: {
padding: "8px 24px"
},
large: {
padding: "12px 32px"
}
};
export const BUTTON_ICON_CONFIGURATION = {
small: {
size: 16
},
medium: {
size: 24
},
large: {
size: 24
}
};
export const BUTTON_LOADER_DIMENSION_CONFIGURATION = {
small: { size: "20px", border: "3px" },
medium: { size: "24px", border: "4px" },
large: { size: "32px", border: "5px" }
};
export const BUTTON_VARIANT_CONFIGURATION: IButtonVariantConfiguration = {
contain: {
primary: {
backgroundColor: "primary",
hoverBackgroundColor: "primary-dark",
border: "none",
color: "white",
disabled: {
color: "white",
backgroundColor: "gray"
}
},
danger: {
backgroundColor: "danger",
hoverBackgroundColor: "danger-dark",
border: "none",
color: "white",
disabled: {
color: "white",
backgroundColor: "gray"
}
},
dark: {
backgroundColor: "dark",
hoverBackgroundColor: "meteorite-gray",
border: "1px solid var(--light)",
color: "white",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
},
warning: {
backgroundColor: "warning",
hoverBackgroundColor: "warning-dark",
border: "none",
color: "dark",
disabled: {
color: "white",
backgroundColor: "gray"
}
}
},
outline: {
primary: {
backgroundColor: "transparent",
hoverBackgroundColor: "primary-light",
border: "1px solid var(--gray-border)",
color: "primary",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
},
dark: {
backgroundColor: "dark",
hoverBackgroundColor: "meteorite-gray",
border: "1px solid var(--light)",
color: "white",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
},
danger: {
backgroundColor: "transparent",
hoverBackgroundColor: "danger-light",
border: "1px solid var(--gray-border)",
color: "danger",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
},
warning: {
backgroundColor: "transparent",
hoverBackgroundColor: "warning-light",
border: "1px solid var(--gray-border)",
color: "warning",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
}
},
text: {
primary: {
backgroundColor: "transparent",
hoverBackgroundColor: "primary-light",
border: "none",
color: "primary",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
},
danger: {
backgroundColor: "transparent",
hoverBackgroundColor: "danger-light",
border: "none",
color: "danger",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
},
dark: {
backgroundColor: "dark",
hoverBackgroundColor: "meteorite-gray",
border: "1px solid var(--light)",
color: "white",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
},
warning: {
backgroundColor: "transparent",
hoverBackgroundColor: "warning-light",
border: "none",
color: "warning",
disabled: {
color: "gray",
backgroundColor: "transparent"
}
}
}
};

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { defineEmits, defineProps, useSlots } from "vue";
interface Props {
header?: string;
}
type Emits = {
(eventName: "click"): void;
};
defineProps<Props>();
const emit = defineEmits<Emits>();
const slots = useSlots();
</script>
<template>
<div
class="card"
@click="emit('click')"
>
<div class="card__header">
<slot
v-if="slots.header"
name="header"
/>
<h2
v-else-if="header"
class="h-m-0"
>
{{ header }}
</h2>
</div>
<div class="card__body">
<slot v-if="slots.default" />
</div>
<div
v-if="slots.footer"
class="card__footer"
>
<slot name="footer" />
</div>
</div>
</template>
<style lang="scss" scoped>
.card {
border-radius: 16px;
border: 1px solid var(--gray-border);
background: var(--light);
display: flex;
padding: 24px;
flex-direction: column;
align-items: center;
gap: 16px;
align-self: stretch;
text-align: left;
max-width: unset;
width: 100%;
&__header,
&__body,
&__footer {
display: flex;
width: 100%;
}
&__body {
display: flex;
flex-direction: column;
flex-grow: 1;
}
&__footer {
padding: 16px;
cursor: pointer;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
import { computed } from "vue";
import { Color } from "@/types";
import { wrapInCssVar } from "@/utils/helpers";
const props = withDefaults(defineProps<Props>(), {
color: "primary",
size: "medium",
dimensions: undefined,
borderSize: undefined,
borderColor: undefined
});
const DIMENSION_MAP = {
small: "24px",
medium: "40px",
large: "72px"
} as const;
type HCircleLoaderSize = keyof typeof DIMENSION_MAP;
interface Props {
color?: Color;
size?: HCircleLoaderSize;
dimensions?: string;
borderSize?: string;
borderColor?: Color;
}
const getDimensions = () => {
if (props.dimensions) {
return props.dimensions;
}
return DIMENSION_MAP[props.size];
};
const getBorder = () => props.borderSize || "4px";
const getBorderColor = (): string => {
if (props.borderColor) {
return wrapInCssVar(props.borderColor);
}
return wrapInCssVar(`${props.color}-light`);
};
const style = computed(() => ({
color: wrapInCssVar(props.color),
borderColor: getBorderColor(),
width: getDimensions(),
borderSize: getBorder(),
height: getDimensions()
}));
</script>
<template>
<div class="loader" />
</template>
<style lang="scss" scoped>
.loader {
border: v-bind("style.borderSize") solid v-bind("style.borderColor");
border-top: v-bind("style.borderSize") solid v-bind("style.color");
width: v-bind("style.width");
height: v-bind("style.height");
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import { defineProps, withDefaults } from "vue";
import Icon from "@/components/Icon/Icon.vue";
import { copyString } from "@/utils/helpers";
interface Props {
link: string;
}
withDefaults(defineProps<Props>(), {});
</script>
<template>
<div
class="copy-field"
@click="copyString(link)"
>
<span class="copy-field__link">{{ link }}</span>
<Icon
name="icon-content-copy"
color="primary"
/>
</div>
</template>
<style lang="scss" scoped>
.copy-field {
display: flex;
cursor: pointer;
justify-content: space-between;
padding: 12px 16px;
border-radius: 8px;
word-wrap: break-word;
align-items: center;
background: var(--gray-light);
&__link {
flex: 1 1 auto;
color: var(--gray);
overflow-wrap: anywhere;
}
}
</style>

View File

@@ -0,0 +1,246 @@
<script lang="ts" setup>
import Button from "@/components/Button/Button.vue";
import Card from "@/components/Card.vue";
import CopyField from "@/components/CopyField.vue";
import Icon from "@/components/Icon/Icon.vue";
import SkeletonLoader from "@/components/Loaders/SkeletonLoader.vue";
import Toggle from "@/components/Toggle.vue";
import { SectionHeaderButton, SectionItem } from "@/types";
import { translate } from "@/utils/helpers";
type SectionHeaderToggle = {
value: boolean;
onToggle?: (value: boolean) => void;
};
type Props = {
title: string;
isLoading?: boolean;
sectionItems: SectionItem[];
headerButtons?: SectionHeaderButton[];
headerToggle?: SectionHeaderToggle;
warning?: string;
isDisabled?: boolean;
};
type Emits = {
"save-section": [value: boolean, item: SectionItem];
};
defineProps<Props>();
const emit = defineEmits<Emits>();
</script>
<template>
<div class="home-section">
<Card v-if="isLoading">
<SkeletonLoader
class="h-mb-24"
width="50%"
:height="24"
rounded
/>
<SkeletonLoader
v-for="(item, index) in sectionItems"
:key="`skeleton-${index}`"
:class="{ 'h-mb-24': index !== sectionItems.length - 1 }"
width="100%"
:height="item.copyLink ? 48 : 24"
rounded
/>
</Card>
<Card v-else>
<template #header>
<div class="w-100">
<slot name="snackbar" />
<div
class="d-flex align-items-center justify-content-between w-100"
>
<div class="d-flex align-items-center">
<Toggle
v-if="headerToggle"
:model-value="headerToggle.value"
:bind="false"
:is-disabled="isDisabled"
class="h-mr-16"
@click="
headerToggle.onToggle &&
headerToggle.onToggle(!headerToggle.value)
"
/>
<h2 class="h-m-0">
{{ title }}
</h2>
</div>
<div
v-if="headerButtons?.length"
class="d-flex align-items-center"
>
<Button
v-for="button in headerButtons"
:key="button.id"
size="small"
:to="button.to"
:is-disabled="isDisabled"
:variant="button.variant"
:target="button.to ? '_blank' : undefined"
:icon-append="
button.to ? 'icon-launch' : undefined
"
>
{{ button.text }}
</Button>
</div>
</div>
<div
v-if="warning"
class="hostinger-notice d-flex align-items-center w-100 mt-3"
>
<Icon
name="icon-info"
color="gray-dark"
/>
<p class="text-body-3">
{{ warning }}
</p>
</div>
</div>
</template>
<div
v-for="item in sectionItems"
:key="item.title"
class="home-section__section-item"
:class="{
'home-section__section-item--disabled':
headerToggle && !headerToggle.value,
}"
>
<div class="d-flex flex-direction-column">
<div class="d-flex align-items-center w-100">
<Toggle
v-if="item.toggleValue !== undefined"
class="h-mr-16"
:model-value="Boolean(item.toggleValue)"
:bind="false"
:is-disabled="
(headerToggle && !headerToggle.value) ||
isDisabled
"
@click="
emit(
'save-section',
Boolean(!item.toggleValue),
item,
)
"
/>
<div class="d-flex flex-column flex-grow-1">
<h3 class="h-m-0">
{{ item.title }}
</h3>
<p class="h-m-0 text-body-2">
{{ item.description }}
<template v-if="item.learnMoreLink">
<a
:href="item.learnMoreLink"
target="_blank"
rel="noopener"
class="text-link-2 additional-link"
>
{{
translate(
"hostinger_tools_llms_txt_learn_more",
)
}}
</a>
</template>
</p>
</div>
<div
v-if="
item.sideButtons?.length ||
item.sideButton?.text
"
class="d-flex align-items-center h-ml-16 section-buttons"
>
<Button
v-for="(button, index) in item.sideButtons"
:key="`${item.title}-${button.id || index}`"
size="small"
:variant="button.variant || 'text'"
:to="button.to"
:target="button.to ? '_blank' : undefined"
:icon-append="button.icon || 'icon-launch'"
:is-disabled="button.isDisabled || isDisabled"
color="primary"
@click="button.onClick"
>
{{ button.text }}
</Button>
<Button
v-if="
item.sideButton?.text &&
!item.sideButtons?.length
"
size="small"
variant="text"
:is-disabled="isDisabled"
@click="item.sideButton?.onClick"
>
{{ item.sideButton?.text }}
</Button>
</div>
</div>
</div>
<CopyField
v-if="item.copyLink"
class="h-mt-16"
:link="item.copyLink"
/>
</div>
</Card>
</div>
</template>
<style lang="scss">
.home-section {
&__section-item {
display: flex;
flex-direction: column;
margin-top: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--gray-border);
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
&--disabled {
opacity: 0.5;
pointer-events: none;
}
.additional-link {
text-decoration: none !important;
}
}
.section-buttons {
gap: 8px;
}
}
.hostinger-notice {
background: var(--gray-light);
color: var(--gray-dark);
border: 1px solid var(--gray-border);
border-radius: 12px;
padding: 12px 16px;
font-size: var(--font-size-sm);
gap: 1em;
}
</style>

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import Button from "@/components/Button/Button.vue";
import Card from "@/components/Card.vue";
import SkeletonLoader from "@/components/Loaders/SkeletonLoader.vue";
import { translate } from "@/utils/helpers";
type Props = {
title: string;
toolImageSrc: string;
version?: string;
actionButton?: {
text: string;
onClick?: () => void;
};
isLoading?: boolean;
};
defineProps<Props>();
</script>
<template>
<Card v-if="isLoading">
<template #header>
<SkeletonLoader
width="50%"
:height="24"
rounded
/>
</template>
<SkeletonLoader
width="100%"
:height="24"
rounded
/>
</Card>
<Card
v-else
class="tool-version-card"
>
<template #header>
<div class="d-flex justify-content-between w-100">
<div class="d-flex">
<img
class="h-mr-8"
height="24"
width="24"
:src="toolImageSrc"
alt="Tool icon"
>
<div>
<h3 class="h-m-0">
{{ title }}
</h3>
<p class="text-body-2">
{{ version }}
</p>
</div>
</div>
</div>
</template>
<Button
v-if="actionButton"
@click="actionButton?.onClick"
>
{{
translate("hostinger_tools_update")
}}
</Button>
</Card>
</template>
<style lang="scss" scoped>
.tool-version-card {
gap: 0;
display: flex;
flex-direction: row;
@media (max-width: 768px) {
flex-direction: column;
gap: 16px;
}
::v-deep(.card__body) {
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<script lang="ts" setup>
import { computed, defineAsyncComponent } from "vue";
import type { Color } from "@/types";
import type { IconUnion } from "@/types/enums/iconModels";
import { toTitleCase, wrapInCssVar } from "@/utils/helpers";
import { kebabToCamel } from "@/utils/services/snakeCamelService";
interface Props {
dimensions?: number;
color?: Color;
name: IconUnion;
}
const props = withDefaults(defineProps<Props>(), {
dimensions: 24,
color: "white"
});
const iconColor = computed(() => {
if (!props.color) return "";
return wrapInCssVar(props.color);
});
const selectedIcon = computed(() =>
defineAsyncComponent(
() =>
import(
`@/components/Icon/Icons/${kebabToCamel(toTitleCase(props.name))}.vue`
)
)
);
</script>
<template>
<svg
class="icon"
aria-hidden="true"
>
<g>
<Component :is="selectedIcon" />
</g>
</svg>
</template>
<style lang="scss" scoped>
.icon {
transition: 0.3s ease transform;
fill: currentColor;
color: v-bind(iconColor);
width: v-bind("dimensions + 'px'");
height: v-bind("dimensions + 'px'");
min-width: v-bind("dimensions + 'px'");
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z"
/>
</template>

View File

@@ -0,0 +1,24 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.85691 14.1816C6.85691 13.7674 7.14483 13.4316 7.5 13.4316H13.6069C13.9621 13.4316 14.25 13.7674 14.25 14.1816C14.25 14.5958 13.9621 14.9316 13.6069 14.9316H7.5C7.14483 14.9316 6.85691 14.5958 6.85691 14.1816Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.29084 12.6658L13.0587 3.80312C13.0835 3.77806 13.1068 3.75446 13.1289 3.7321C13.1066 3.70997 13.083 3.68661 13.0579 3.66183L12.2098 2.82257C12.1847 2.79775 12.1611 2.77438 12.1387 2.75227C12.1165 2.77459 12.0932 2.7982 12.0683 2.82325L6.68596 8.25301L3.29957 11.6881C3.29328 11.6945 3.28738 11.7004 3.28183 11.7061C3.27962 11.7137 3.27728 11.7217 3.27478 11.7303L2.87798 13.0951L4.24799 12.6914C4.25676 12.6888 4.26497 12.6864 4.27271 12.6841C4.27839 12.6784 4.28441 12.6723 4.29084 12.6658ZM5.61974 7.19793L2.23046 10.6359C2.12466 10.7432 2.07176 10.7969 2.02795 10.857C1.98904 10.9104 1.95553 10.9675 1.9279 11.0275C1.89679 11.0951 1.87576 11.1674 1.8337 11.3121L1.24307 13.3436C1.05693 13.9838 0.963865 14.3039 1.04577 14.5217C1.11715 14.7115 1.26745 14.8611 1.45773 14.9316C1.67608 15.0125 1.99607 14.9182 2.63606 14.7296L4.67252 14.1295C4.81992 14.086 4.89362 14.0643 4.96229 14.0322C5.02327 14.0036 5.0812 13.969 5.13522 13.9288C5.19605 13.8836 5.25007 13.829 5.3581 13.7198L14.1259 4.85713C14.5195 4.45935 14.7162 4.26046 14.7891 4.03197C14.8533 3.83099 14.8521 3.61488 14.7859 3.41459C14.7105 3.1869 14.5117 2.99011 14.1139 2.59653L13.2658 1.75727C12.8677 1.36333 12.6687 1.16637 12.4399 1.09331C12.2386 1.02905 12.0222 1.03009 11.8216 1.09629C11.5935 1.17155 11.3964 1.37042 11.0021 1.76817L5.61974 7.19793ZM12.4708 2.43329L12.4689 2.43476L12.4708 2.43329ZM11.8055 2.43797L11.8035 2.43651L11.8055 2.43797ZM13.4478 4.06357L13.4463 4.06157L13.4478 4.06357ZM13.4428 3.39923L13.4443 3.39725L13.4428 3.39923Z"
fill="currentColor"
/>
</svg>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,5 @@
<template>
<path
d="M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16V12C13 11.7167 12.9042 11.4792 12.7125 11.2875C12.5208 11.0958 12.2833 11 12 11C11.7167 11 11.4792 11.0958 11.2875 11.2875C11.0958 11.4792 11 11.7167 11 12V16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 9C12.2833 9 12.5208 8.90417 12.7125 8.7125C12.9042 8.52083 13 8.28333 13 8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8C11 8.28333 11.0958 8.52083 11.2875 8.7125C11.4792 8.90417 11.7167 9 12 9ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22ZM12 20C14.2333 20 16.125 19.225 17.675 17.675C19.225 16.125 20 14.2333 20 12C20 9.76667 19.225 7.875 17.675 6.325C16.125 4.775 14.2333 4 12 4C9.76667 4 7.875 4.775 6.325 6.325C4.775 7.875 4 9.76667 4 12C4 14.2333 4.775 16.125 6.325 17.675C7.875 19.225 9.76667 20 12 20Z"
/>
</template>

View File

@@ -0,0 +1,16 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.5 5.2478C2.5 3.72902 3.73122 2.4978 5.25 2.4978H6C6.41421 2.4978 6.75 2.83359 6.75 3.2478C6.75 3.66202 6.41421 3.9978 6 3.9978H5.25C4.55964 3.9978 4 4.55745 4 5.2478V10.6859C4 11.3763 4.55964 11.9359 5.25 11.9359H10.7508C11.4411 11.9359 12.0008 11.3763 12.0008 10.6859V10C12.0008 9.58579 12.3366 9.25 12.7508 9.25C13.165 9.25 13.5008 9.58579 13.5008 10V10.6859C13.5008 12.2047 12.2696 13.4359 10.7508 13.4359H5.25C3.73122 13.4359 2.5 12.2047 2.5 10.6859V5.2478ZM12 5.06077L8.03033 9.03044C7.73744 9.32333 7.26256 9.32333 6.96967 9.03044C6.67678 8.73754 6.67678 8.26267 6.96967 7.96977L10.9393 4.00011H9C8.58579 4.00011 8.25 3.66432 8.25 3.2501C8.25 2.83589 8.58579 2.50011 9 2.50011L12.25 2.50011C12.9404 2.50011 13.5 3.05975 13.5 3.75011V7.0001C13.5 7.41432 13.1642 7.7501 12.75 7.7501C12.3358 7.7501 12 7.41432 12 7.0001V5.06077Z"
fill="currentColor"
/>
</svg>
</template>

View File

@@ -0,0 +1,14 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M4.5 21C4.1 21 3.75 20.85 3.45 20.55C3.15 20.25 3 19.9 3 19.5V4.5C3 4.1 3.15 3.75 3.45 3.45C3.75 3.15 4.1 3 4.5 3H11.475V4.5H4.5V19.5H19.5V12.525H21V19.5C21 19.9 20.85 20.25 20.55 20.55C20.25 20.85 19.9 21 19.5 21H4.5ZM9.55 15.525L8.5 14.45L18.45 4.5H12.975V3H21V11.025H19.5V5.575L9.55 15.525Z"
fill="currentColor"
/>
</svg>
</template>

View File

@@ -0,0 +1,24 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.99998 6.23374C7.02266 6.23374 6.2304 7.02601 6.2304 8.00332C6.2304 8.98063 7.02266 9.7729 7.99998 9.7729C8.97729 9.7729 9.76956 8.98063 9.76956 8.00332C9.76956 7.02601 8.97729 6.23374 7.99998 6.23374ZM4.69123 8.00332C4.69123 6.17595 6.17261 4.69457 7.99998 4.69457C9.82735 4.69457 11.3087 6.17595 11.3087 8.00332C11.3087 9.83069 9.82735 11.3121 7.99998 11.3121C6.17261 11.3121 4.69123 9.83069 4.69123 8.00332Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.99508 3.92088C6.23615 3.92088 3.12832 4.78339 1.55391 7.93872C1.5343 7.97801 1.53407 8.02604 1.55433 8.06651C3.12861 11.2094 6.1426 12.0858 8.00495 12.0858C9.76389 12.0858 12.8717 11.2233 14.4461 8.06799C14.4657 8.0287 14.466 7.98067 14.4457 7.9402C12.8714 4.79726 9.85743 3.92088 7.99508 3.92088ZM0.176668 7.25152C2.09445 3.40802 5.86487 2.38171 7.99508 2.38171C10.2434 2.38171 13.909 3.43186 15.8219 7.25088C16.0585 7.72326 16.0598 8.28126 15.8234 8.75519C13.9056 12.5987 10.1352 13.625 8.00495 13.625C5.7566 13.625 2.09108 12.5749 0.178153 8.75583C-0.058457 8.28345 -0.0598124 7.72545 0.176668 7.25152Z"
fill="currentColor"
/>
</svg>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
import { computed } from "vue";
import { H_LABEL_THEME_CONFIGURATION, HLabelTheme } from "@/types";
import { wrapInCssVar } from "@/utils/helpers";
interface Props {
theme?: HLabelTheme;
}
const props = withDefaults(defineProps<Props>(), {
theme: "primary"
});
const configuration = computed(() => H_LABEL_THEME_CONFIGURATION[props.theme]);
const style = computed(() => ({
backgroundColor: wrapInCssVar(configuration.value.backgroundColor),
color: wrapInCssVar(configuration.value.color)
}));
</script>
<template>
<div class="label text-overline">
<slot />
</div>
</template>
<style lang="scss" scoped>
.label {
display: flex;
padding: 4px 8px;
align-items: center;
gap: 10px;
border-radius: 6px;
background-color: v-bind("style.backgroundColor");
color: v-bind("style.color");
}
</style>

View File

@@ -0,0 +1,80 @@
<script lang="ts" setup>
import { computed } from "vue";
import { Color } from "@/types";
import { wrapInCssVar } from "@/utils/helpers";
const props = withDefaults(defineProps<Props>(), {
color: "primary",
size: "medium",
dimensions: undefined,
borderSize: undefined,
borderColor: undefined
});
const DIMENSION_MAP = {
small: "24px",
medium: "40px",
large: "72px"
} as const;
type HCircleLoaderSize = keyof typeof DIMENSION_MAP;
interface Props {
color?: Color;
size?: HCircleLoaderSize;
dimensions?: string;
borderSize?: string;
borderColor?: Color;
}
const getDimensions = () => {
if (props.dimensions) {
return props.dimensions;
}
return DIMENSION_MAP[props.size];
};
const getBorder = () => props.borderSize || "4px";
const getBorderColor = (): string => {
if (props.borderColor) {
return wrapInCssVar(props.borderColor);
}
return wrapInCssVar(`${props.color}-light`);
};
const style = computed(() => ({
color: wrapInCssVar(props.color),
borderColor: getBorderColor(),
width: getDimensions(),
borderSize: getBorder(),
height: getDimensions()
}));
</script>
<template>
<div class="loader" />
</template>
<style lang="scss" scoped>
.loader {
border: v-bind("style.borderSize") solid v-bind("style.borderColor");
border-top: v-bind("style.borderSize") solid v-bind("style.color");
width: v-bind("style.width");
height: v-bind("style.height");
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,97 @@
<script lang="ts" setup>
import { computed } from "vue";
type DimensionValue = number | string;
type Props = {
circle?: boolean;
rounded?: boolean;
roundedXs?: boolean;
roundedXl?: boolean;
width?: DimensionValue;
height?: DimensionValue;
isInline?: boolean;
};
const props = defineProps<Props>();
const classes = computed(() => ({
"skeleton-loader--circle": props.circle,
"skeleton-loader--rounded": props.rounded,
"skeleton-loader--rounded-xs": props.roundedXs,
"skeleton-loader--rounded-xl": props.roundedXl,
"skeleton-loader--inline": props.isInline
}));
const getSkeletonSize = (value?: DimensionValue) => {
if (Number.isInteger(value)) {
return `${value}px`;
}
return value;
};
</script>
<template>
<div
class="skeleton-loader"
:class="{ ...classes }"
:style="{
'max-width': getSkeletonSize(width),
height: getSkeletonSize(height),
width: props.isInline ? getSkeletonSize(width) : undefined,
}"
/>
</template>
<style lang="scss" scoped>
.skeleton-loader {
position: relative;
overflow: hidden;
background-color: var(--gray-1);
width: 100%;
&::after {
top: 0;
left: 0;
right: 0;
bottom: 0;
content: "";
position: absolute;
animation: HSkeletonLoader-keyframes-wave 1.6s linear 0.5s infinite;
transform: translateX(-100%);
background: linear-gradient(90deg, transparent, var(--gray-2));
}
&--circle {
border-radius: 50%;
}
&--rounded {
border-radius: 8px;
}
&--rounded-xs {
border-radius: 4px;
}
&--rounded-xl {
border-radius: 24px;
}
&--inline {
display: inline-block;
vertical-align: middle;
}
}
@keyframes HSkeletonLoader-keyframes-wave {
0% {
transform: translateX(-100%);
}
60% {
transform: translateX(100%);
}
100% {
transform: translateX(100%);
}
}
</style>

View File

@@ -0,0 +1,64 @@
<script lang="ts" setup>
import Icon from "@/components/Icon/Icon.vue";
import { Color, IconUnion } from "@/types";
interface Props {
title?: string;
subtitle?: string;
titleIcon?: {
name: IconUnion;
color: Color;
};
}
defineProps<Props>();
</script>
<template>
<div class="base-modal">
<span class="base-modal__title-container">
<Icon
v-if="titleIcon"
class="h-mr-12"
:name="titleIcon.name"
:color="titleIcon.color"
/>
<h2
v-if="title"
class="base-modal__title text-heading-2"
>
{{ title }}
</h2>
</span>
<p
v-if="subtitle"
class="base-modal__subtitle"
>
{{ subtitle }}
</p>
<slot />
</div>
</template>
<style lang="scss" scoped>
.base-modal {
&__title-container {
display: flex;
align-items: center;
margin-bottom: 8px;
}
&__title {
font-size: 20px;
color: var(--dark);
margin: 0;
font-weight: 700;
}
&__subtitle {
font-size: 14px;
margin-top: 4px;
margin-bottom: 24px;
color: var(--gray);
}
}
</style>

View File

@@ -0,0 +1,134 @@
<script lang="ts" setup>
import { computed, defineAsyncComponent } from "vue";
import Icon from "@/components/Icon/Icon.vue";
import { useModal } from "@/composables";
import { useModalStore } from "@/stores";
const { closeModal } = useModal();
const modalStore = useModalStore();
const activeModal = computed(() => modalStore.activeModal);
const modalComponent = computed(
() =>
activeModal.value &&
defineAsyncComponent(
() => import(`@/components/Modals/${activeModal.value?.name}.vue`)
)
);
</script>
<template>
<Transition name="fade">
<div
v-if="activeModal"
class="modal__wrapper"
>
<div
class="modal__container"
:class="{
'modal__container--xxl': activeModal.settings?.isXXL,
'modal__container--xl': activeModal.settings?.isXL,
'modal__container--lg': activeModal.settings?.isLG,
}"
>
<Icon
v-if="activeModal.settings?.hasCloseButton"
name="icon-close"
class="modal__icon"
color="gray"
@click="closeModal"
/>
<div
class="modal__content"
:class="{
'modal__content--no-content-padding':
activeModal.settings?.noContentPadding,
'modal__content--no-border':
activeModal.settings?.noBorder,
}"
>
<component
:is="modalComponent"
v-bind="activeModal.data"
/>
</div>
</div>
</div>
</Transition>
</template>
<style lang="scss" scoped>
.modal {
&__wrapper {
--modal-backdrop: rgba(0, 0, 0, 0.3);
overflow: auto;
position: fixed;
z-index: var(--z-index-modal);
height: 100%;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: var(--modal-backdrop);
display: flex;
align-items: center;
justify-content: center;
&.is-above-intercom {
z-index: var(--z-index-intercom-2);
}
}
&__icon {
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
}
&__container {
position: relative;
max-height: calc(100vh - 6rem);
width: 100%;
max-width: 500px;
&--auto-width {
max-width: none !important;
width: auto;
}
&--lg {
max-width: 600px;
}
&--xl {
max-width: 800px;
}
&--xxl {
max-width: 964px;
}
}
&__content {
border: 1px solid var(--gray-border);
border-radius: 16px;
background-color: var(--light);
padding: 32px 40px 40px;
@media screen and (max-width: 600px) {
padding: 32px 40px 40px;
}
&--no-content-padding {
padding: 0;
}
&--no-border {
border: none;
}
}
}
</style>

View File

@@ -0,0 +1,52 @@
<script lang="ts" setup>
import Button from "@/components/Button/Button.vue";
import BaseModal from "@/components/Modals/Base/BaseModal.vue";
import { useModal } from "@/composables";
import { translate } from "@/utils/helpers";
interface Props {
data: {
onConfirm: () => void;
};
}
const props = defineProps<Props>();
const { closeModal } = useModal();
const onProceed = () => {
if (props.data.onConfirm) props.data.onConfirm();
closeModal();
};
</script>
<template>
<BaseModal
:title-icon="{
name: 'icon-info',
color: 'danger',
}"
:title="translate('bypass_link_reset_modal_title')"
>
<p class="h-mb-24 text-gray">
{{ translate("bypass_link_reset_modal_description") }}
</p>
<div class="d-flex justify-content-end">
<Button
color="danger"
variant="text"
class="h-mr-16"
@click="closeModal"
>
{{ translate("bypass_link_reset_modal_cancel") }}
</Button>
<Button
color="danger"
@click="onProceed"
>
{{ translate("bypass_link_reset_modal_reset_link") }}
</Button>
</div>
</BaseModal>
</template>

View File

@@ -0,0 +1,52 @@
<script lang="ts" setup>
import Button from "@/components/Button/Button.vue";
import BaseModal from "@/components/Modals/Base/BaseModal.vue";
import { useModal } from "@/composables";
import { translate } from "@/utils/helpers";
interface Props {
data: {
onConfirm: () => void;
};
}
const props = defineProps<Props>();
const { closeModal } = useModal();
const onProceed = () => {
if (props.data.onConfirm) props.data.onConfirm();
closeModal();
};
</script>
<template>
<BaseModal
:title-icon="{
name: 'icon-info',
color: 'danger',
}"
:title="translate('hostinger_tools_llms_txt_modal_title')"
>
<p class="h-mb-24 text-gray">
{{ translate("hostinger_tools_llms_txt_modal_description") }}
</p>
<div class="d-flex justify-content-end">
<Button
color="danger"
variant="text"
class="h-mr-16"
@click="closeModal"
>
{{ translate("hostinger_tools_llms_txt_modal_cancel") }}
</Button>
<Button
color="danger"
@click="onProceed"
>
{{ translate("hostinger_tools_llms_txt_modal_create_file") }}
</Button>
</div>
</BaseModal>
</template>

View File

@@ -0,0 +1,52 @@
<script lang="ts" setup>
import Button from "@/components/Button/Button.vue";
import BaseModal from "@/components/Modals/Base/BaseModal.vue";
import { useModal } from "@/composables";
import { translate } from "@/utils/helpers";
interface Props {
data: {
onConfirm: () => void;
};
}
const props = defineProps<Props>();
const { closeModal } = useModal();
const onProceed = () => {
if (props.data.onConfirm) props.data.onConfirm();
closeModal();
};
</script>
<template>
<BaseModal
:title-icon="{
name: 'icon-info',
color: 'danger',
}"
:title="translate('xml_security_modal_title')"
>
<p class="h-mb-24 text-gray">
{{ translate("xml_security_modal_description") }}
</p>
<div class="d-flex justify-content-end">
<Button
color="danger"
variant="text"
class="h-mr-16"
@click="closeModal"
>
{{ translate("xml_security_modal_cancel") }}
</Button>
<Button
color="danger"
@click="onProceed"
>
{{ translate("xml_security_modal_proceed_anyway") }}
</Button>
</div>
</BaseModal>
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang="ts">
interface Props {
title?: string;
subtitle?: string;
imageSrc?: string;
hasImage?: boolean;
}
withDefaults(defineProps<Props>(), {
title: hst_affiliate_data.translations.nothing_found,
subtitle: hst_affiliate_data.translations.try_other_results,
imageSrc: hst_affiliate_data.asset_url + "assets/img/no-search-results.svg",
hasImage: true
});
</script>
<template>
<div class="d-flex justify-content-center">
<div class="text-center">
<img
v-if="hasImage"
:src="imageSrc"
alt="nothing-found"
class="h-mb-8"
>
<h3
v-if="title"
v-trans
>
{{ title }}
</h3>
<p
v-if="subtitle"
v-trans
>
{{ subtitle }}
</p>
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { computed } from "vue";
import { type RouteLocationNamedRaw, useRouter } from "vue-router";
type Props = {
text: string;
route?: RouteLocationNamedRaw;
to?: string;
action?: () => void;
};
type Emits = {
click: [];
};
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const router = useRouter();
const buttonText = computed(() => props?.text || "");
const onButtonClick = () => {
emit("click");
(props?.action || redirectToRoute)();
};
const tag = computed(() => {
if (typeof props.to === "string") {
return "a";
}
if (props.to) {
return "router-link";
}
return "button";
});
const redirectToRoute = () => {
const { name, query } = props?.route || {};
if (!name) return;
router.push({
name,
query
});
};
</script>
<template>
<Teleport
:key="buttonText"
to="#overhead-button"
>
<component
:is="tag"
class="overhead-button text-button-2"
@click="onButtonClick"
>
{{ buttonText }}
</component>
<slot />
</Teleport>
</template>
<style scoped lang="scss">
.overhead-button {
display: inline-flex;
padding: 12px 32px;
align-items: flex-start;
gap: 12px;
color: var(--primary);
border-radius: 8px;
border: 1px solid var(--gray-border);
background: var(--light);
&:hover {
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
}
}
</style>

View File

@@ -0,0 +1,67 @@
<script setup lang="ts">
interface Props {
stepsCount?: number;
isClickable?: boolean;
wide?: boolean;
step?: number;
}
type Emits = {
"on-click": [index: number];
};
const props = withDefaults(defineProps<Props>(), {
stepsCount: 0,
isClickable: false,
wide: false,
step: 0
});
const emit = defineEmits<Emits>();
const getIsActive = (index: number, step: number) =>
(!props.isClickable && index <= step) ||
(props.isClickable && index === step);
</script>
<template>
<div class="stepper">
<div
v-for="(_, index) in stepsCount"
:key="index"
class="stepper__indicator"
:class="{
'stepper__indicator--active': getIsActive(index, step),
'stepper__indicator--clickable': isClickable,
'stepper__indicator--wide': wide,
}"
@click="emit('on-click', index)"
/>
</div>
</template>
<style lang="scss" scoped>
.stepper {
display: flex;
justify-content: center;
margin-top: 24px;
&__indicator {
width: 8px;
height: 8px;
margin-left: 4px;
border-radius: 50%;
background-color: rgba(114, 117, 134, 0.3);
&--wide {
margin: 0 8px;
}
&--active {
background-color: var(--primary);
}
&--clickable {
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,228 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
interface Props {
modelValue: boolean;
bind?: boolean;
isDisabled?: boolean;
}
type Emits = {
"update:modelValue": [value: boolean];
click: [];
};
const props = withDefaults(defineProps<Props>(), {
bind: true
});
const emit = defineEmits<Emits>();
const inputValue = ref(props.modelValue);
const displayValue = computed(() =>
props.bind ? inputValue.value : props.modelValue
);
const onChange = (event: Event) => {
inputValue.value = (event.target as HTMLInputElement).checked;
emit("update:modelValue", inputValue.value);
};
const onClick = () => {
if (!props.bind && !props.isDisabled) {
emit("click");
}
};
watch(
() => props.modelValue,
(value) => {
inputValue.value = value;
}
);
</script>
<template>
<div class="toggle__element-container">
<label
class="toggle h-mb-0"
:class="{ active: displayValue, 'toggle--disabled': isDisabled }"
@click="onClick"
>
<input
type="checkbox"
:checked="displayValue"
:disabled="!bind || isDisabled"
@change="onChange"
>
<span />
</label>
</div>
</template>
<style lang="scss" scoped>
$toggle-width: 40px;
$toggle-height: 22px;
$toggle-border-radius: 1.25em;
$toggle-border-size: 1px;
.toggle {
width: $toggle-width;
height: $toggle-height;
background: var(--gray-border);
border: 1px solid var(--gray-border);
border-radius: $toggle-border-radius;
position: relative;
overflow: hidden;
transition: all 0.5s;
cursor: pointer;
display: flex;
align-items: center;
color: var(--grayscale-000-gray-000-38);
&.active {
background: var(--primary);
}
&.toggle > span {
width: 20px;
height: 20px;
transform: translate(0%, 0);
border: 0;
box-shadow: var(--shadow);
opacity: 1;
}
input {
margin-left: -999px;
height: 0;
width: 0;
overflow: hidden;
position: absolute;
opacity: 0;
}
input:empty ~ span &--on {
display: none;
}
input:empty ~ span &--off {
display: inline;
}
input:checked ~ span {
opacity: 1;
background: var(--light);
transform: translate(100%, 0);
}
input:checked ~ span > &--on {
display: inline;
}
input:checked ~ span &--off {
display: none;
}
> span {
width: $toggle-height;
height: $toggle-height;
opacity: 0.4;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--gray-light);
border-radius: 50%;
transform: translate(0, 0);
margin: -1px;
transition: all 0.15s;
overflow: hidden;
background: var(--light);
}
&--disabled {
opacity: 0.38;
pointer-events: none;
}
&--disabled,
&--unavailable {
color: var(--gray-border);
}
}
.toggle--primary {
color: var(--primary);
}
.toggle--secondary {
color: var(--secondary);
}
.toggle--success {
color: var(--success);
}
.toggle--info {
color: var(--primary-hostinger);
}
.toggle--warning {
color: var(--warning);
}
.toggle--warning-regular {
color: var(--warning-regular);
}
.toggle--danger {
color: var(--danger);
}
.toggle--light {
color: var(--light);
}
.toggle--dark {
color: var(--dark);
}
.toggle--black {
color: var(--dark);
}
.toggle--gray {
color: var(--gray);
}
.toggle--gray-light {
color: var(--gray-light);
}
.toggle--header-bg {
color: var(--header-bg);
}
.toggle--danger-light {
color: var(--danger-light);
}
.toggle--success-dark {
color: var(--success-dark);
}
.toggle--success-light {
color: var(--success-light);
}
.toggle--warning-light {
color: var(--warning-light);
}
.toggle--warning-dark {
color: var(--warning-dark);
}
</style>

View File

@@ -0,0 +1,3 @@
export * from './useButton';
export * from './useModal';
export * from './useToggle';

View File

@@ -0,0 +1,84 @@
import { computed } from "vue";
import {
BUTTON_ICON_CONFIGURATION,
BUTTON_LOADER_DIMENSION_CONFIGURATION,
BUTTON_SIZE_CONFIGURATION,
BUTTON_VARIANT_CONFIGURATION
} from "@/components/Button/configuration";
import type { Color, IButtonPropsMandatory } from "@/types";
import { wrapInCssVar } from "@/utils/helpers";
export const useButton = (props: IButtonPropsMandatory) => {
if (!BUTTON_VARIANT_CONFIGURATION[props.variant][props.color]) {
throw new Error(
`Invalid variant and color combination: ${props.variant} ${props.color}`
);
}
const tag = computed(() => {
if (typeof props.to === "string") {
return "a";
}
if (props.to) {
return "router-link";
}
return "button";
});
const configuration = computed(
() => BUTTON_VARIANT_CONFIGURATION[props.variant][props.color]
);
const iconConfiguration = computed(() => {
const color = props.isDisabled
? configuration.value.disabled.color
: configuration.value.color;
return {
size: BUTTON_ICON_CONFIGURATION[props.size].size,
color
};
});
const getColorConfiguration = () => wrapInCssVar(configuration.value.color);
const getBackgroundConfiguration = () =>
wrapInCssVar(configuration.value.backgroundColor);
const getLoaderBorderColor = (): Color | undefined => {
if (props.variant === "contain") {
return "white";
}
return undefined;
};
const style = computed(() => ({
border: configuration.value.border,
padding: BUTTON_SIZE_CONFIGURATION[props.size].padding,
backgroundColor: getBackgroundConfiguration(),
color: getColorConfiguration(),
colorDisabled: wrapInCssVar(configuration.value.disabled.color),
backgroundColorDisabled: wrapInCssVar(
configuration.value.disabled.backgroundColor
),
backgroundHoverColor: wrapInCssVar(
configuration.value.hoverBackgroundColor
),
icon: iconConfiguration.value,
loader: {
borderColor: getLoaderBorderColor(),
size: BUTTON_LOADER_DIMENSION_CONFIGURATION[props.size].size,
border: BUTTON_LOADER_DIMENSION_CONFIGURATION[props.size].border
}
}));
return {
style: style.value,
tag,
configuration
};
};

View File

@@ -0,0 +1,19 @@
import { useModalStore } from "@/stores";
import { ModalName } from "@/types/enums";
import { ModalContent, ModalSettings } from "@/types/models";
export const useModal = () => {
const modalStore = useModalStore();
const openModal = (
name: ModalName,
data?: ModalContent,
settings?: ModalSettings
) => {
modalStore.openModal(name, data, settings);
};
const closeModal = () => modalStore.closeModal();
return { openModal, closeModal };
};

View File

@@ -0,0 +1,18 @@
import { ref } from "vue";
export const useToggle = (isDefaultToggled = false) => {
const isToggled = ref(isDefaultToggled);
const toggle = () => (isToggled.value = !isToggled.value);
const toggleOn = () => (isToggled.value = true);
const toggleOff = () => (isToggled.value = false);
return {
isToggled,
toggle,
toggleOff,
toggleOn
};
};

View File

@@ -0,0 +1,218 @@
import js from "@eslint/js";
import typescript from "@typescript-eslint/eslint-plugin";
import typescriptParser from "@typescript-eslint/parser";
import vueTypeScript from "@vue/eslint-config-typescript";
import importPlugin from "eslint-plugin-import";
import modulesNewlines from "eslint-plugin-modules-newlines";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import vue from "eslint-plugin-vue";
import vueParser from "vue-eslint-parser";
export default [
js.configs.recommended,
...vue.configs["flat/recommended"],
...vueTypeScript(),
{
files: ["webpack.config.js"],
languageOptions: {
globals: {
require: "readonly",
module: "readonly",
__dirname: "readonly",
process: "readonly"
}
},
rules: {
"@typescript-eslint/no-require-imports": "off",
"no-undef": "off"
}
},
{
files: ["src/**/*.{js,jsx,ts,tsx,vue}"],
ignores: ["types/**/*", "src/styles/**/*"],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: typescriptParser,
ecmaVersion: "latest",
sourceType: "module",
},
globals: {
browser: true,
node: true,
es6: true,
},
},
plugins: {
"@typescript-eslint": typescript,
vue,
import: importPlugin,
"modules-newlines": modulesNewlines,
"unused-imports": unusedImports,
"simple-import-sort": simpleImportSort,
},
rules: {
"no-console": "off",
"no-debugger": "off",
"comma-dangle": ["error", "never"],
"vue/no-multiple-template-root": "off",
"vue/require-default-prop": "off",
"vue/no-deprecated-slot-attribute": "off",
"vue/no-v-html": "off",
"vue/require-explicit-emits": "error",
"vue/prop-name-casing": "off",
"vue/component-definition-name-casing": ["error", "PascalCase"],
"vue/max-attributes-per-line": [
"error",
{
singleline: {
max: 7,
},
multiline: {
max: 1,
},
},
],
"vue/html-self-closing": [
"error",
{
html: { normal: "always", void: "always", component: "always" },
},
],
"vue/component-name-in-template-casing": [
"error",
"PascalCase",
{
ignores: ["/^hp-/"],
registeredComponentsOnly: true,
globals: [],
},
],
"vue/html-indent": "off",
"vue/singleline-html-element-content-newline": "off",
"vue/valid-v-for": "error",
"vue/component-options-name-casing": ["error", "PascalCase"],
"vue/custom-event-name-casing": ["error", "kebab-case"],
"vue/define-macros-order": ["error", {
order: ['defineProps', 'defineEmits'],
}],
"vue/html-comment-content-spacing": ["error", "always"],
"vue/no-unused-refs": "error",
"vue/padding-line-between-blocks": ["error", "always"],
"vue/prefer-separate-static-class": "error",
"arrow-parens": ["error", "always"],
"no-nested-ternary": "error",
"vue/attribute-hyphenation": [
"off",
"error",
"never",
{
ignore: ["custom-prop"],
},
],
"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }],
"no-trailing-spaces": ["error"],
"quote-props": ["error", "as-needed"],
semi: ["error", "always"],
"prefer-const": "error",
"no-const-assign": "error",
"no-array-constructor": "error",
"no-new-object": "error",
"newline-before-return": ["error"],
"simple-import-sort/imports": [
"error",
{
groups: [
[
"^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)",
],
["^@?\\w"],
["^(@|@company|@ui|components|utils|config|vendored-lib)(/.*|$)"],
["^\\u0000"],
["^\\.\\.(?!/?$)", "^\\.\\./?$"],
["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"],
],
},
],
"simple-import-sort/exports": "error",
"import/extensions": [
"error",
"ignorePackages",
{
"": "never",
js: "never",
ts: "never",
vue: "ignorePackages",
},
],
"@typescript-eslint/no-unused-vars": "off",
"no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"error",
{
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-explicit-any": "warn",
"camelcase": ["error", {
properties: 'always',
allow: ['^[A-Z][a-zA-Z0-9]*$', '^[A-Z_][A-Z0-9_]*$', '^hostinger_tools_data$', '^hst_affiliate_data$'],
ignoreDestructuring: false,
ignoreImports: true,
ignoreGlobals: true
}],
"func-style": "error",
"wrap-iife": "error",
"no-loop-func": "error",
"prefer-rest-params": "error",
"no-new-func": "error",
"no-duplicate-imports": "error",
"prefer-promise-reject-errors": "error",
"no-param-reassign": [
"error",
{
props: false,
},
],
"prefer-spread": "error",
"vue/order-in-components": "off",
"vue/multi-word-component-names": "off",
"arrow-spacing": "error",
"prefer-arrow-callback": "error",
"arrow-body-style": ["error", "as-needed"],
"lines-around-comment": [
"error",
{
beforeBlockComment: true,
allowBlockStart: true,
allowClassStart: true,
allowObjectStart: true,
allowArrayStart: true,
},
],
"padding-line-between-statements": [
"error",
{ blankLine: "always", prev: "import", next: "*" },
{ blankLine: "any", prev: "import", next: "import" },
{ blankLine: "always", prev: "*", next: ["const", "let", "var"] },
{
blankLine: "any",
prev: ["const", "let", "var"],
next: ["const", "let", "var"],
},
],
},
},
{
files: ["**/*.vue"],
rules: {
"vue/multi-word-component-names": "off"
}
}
];

View File

@@ -0,0 +1,119 @@
<script lang="ts" setup>
import Button from "@/components/Button/Button.vue";
import { useGeneralStoreData } from "@/stores";
import { EditSiteButton, HeaderButton, PreviewSiteButton } from "@/types";
const props = defineProps<Props>();
const { siteUrl, editSiteUrl } = useGeneralStoreData();
type Props = {
title?: string;
headerButton?: HeaderButton;
previewSiteButton?: PreviewSiteButton;
editSiteButton?: EditSiteButton;
};
</script>
<template>
<div class="wrapper">
<div class="wrapper__content">
<div class="wrapper__header">
<h1
v-if="props.title"
class="text-heading-1"
>
{{ props.title }}
</h1>
<div class="wrapper__buttons-wrapper">
<Button
v-if="headerButton"
class="wrapper__button"
:to="headerButton?.href"
size="small"
variant="outline"
:target="headerButton.href ? '_blank' : undefined"
icon-append="icon-launch"
@click="headerButton?.onClick"
>
{{ headerButton.text }}
</Button>
<Button
v-if="previewSiteButton && siteUrl"
class="wrapper__button"
:to="siteUrl"
size="small"
variant="outline"
:target="siteUrl ? '_blank' : undefined"
icon-prepend="icon-visibility"
@click="previewSiteButton?.onClick"
>
{{ previewSiteButton.text }}
</Button>
<Button
v-if="editSiteButton && editSiteUrl"
class="wrapper__button"
:to="editSiteUrl"
size="small"
variant="outline"
:target="editSiteUrl ? '_blank' : undefined"
icon-prepend="icon-launch"
@click="editSiteButton?.onClick"
>
{{ editSiteButton.text }}
</Button>
</div>
</div>
<slot />
</div>
</div>
</template>
<style lang="scss" scoped>
.wrapper {
padding: 48px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: left;
min-height: calc(100vh - var(--header-height));
@media (max-width: 768px) {
padding-right: 10px;
padding-left: 0px;
}
&__buttons-wrapper {
display: flex;
@media (max-width: 500px) {
width: 100%;
flex-wrap: wrap;
}
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
&__button {
background-color: var(--white);
margin-left: 10px;
display: flex;
flex-wrap: nowrap;
@media (max-width: 500px) {
margin: 5px 0;
}
}
&__content {
max-width: 740px;
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,27 @@
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
import { createApp } from "vue";
import router from "@/router";
import "@/scss/main.scss";
import "vue3-toastify/dist/index.css";
import App from "./App.vue";
const initializeVueApp = () => {
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
const app = createApp(App);
app.use(pinia);
app.use(router);
app.config.globalProperties.window = window;
app.mount("#hostinger-tools-vue-app");
};
document.addEventListener("DOMContentLoaded", () => {
initializeVueApp();
});

View File

@@ -0,0 +1,20 @@
import { Header, SettingsData } from "@/types";
import http from "@/utils/services/httpService";
const URL = `${hostinger_tools_data.rest_base_url}hostinger-tools-plugin/v1`;
export const generalDataRepo = {
getSettings: () =>
http.get<{ data: SettingsData }>(`${URL}/get-settings`, {
headers: { [Header.WP_NONCE]: hostinger_tools_data.nonce }
}),
postSettings: (data: SettingsData) =>
http.post<{ data: SettingsData }>(`${URL}/update-settings`, data, {
headers: { [Header.WP_NONCE]: hostinger_tools_data.nonce }
}),
getRegenerateByPassCode: () =>
http.get<{ data: SettingsData }>(`${URL}/regenerate-bypass-code`, {
headers: { [Header.WP_NONCE]: hostinger_tools_data.nonce }
})
};

View File

@@ -0,0 +1 @@
export * from './generalDataRepo';

View File

@@ -0,0 +1,24 @@
import { RouteBase } from "@/types/enums";
import { translate } from "@/utils/helpers";
import Home from "@/views/HostingerTools.vue";
export default [
{
path: "/",
name: RouteBase.HOSTINGER_TOOLS,
meta: {
title: translate("routes_tools"),
headerButton: {
text: translate("hostinger_tools_open_guide"),
href: "https://www.hostinger.com/tutorials/how-to-use-hostinger-tools-plugin"
},
previewSiteButton: {
text: translate("hostinger_tools_preview_site")
},
editSiteButton: {
text: translate("hostinger_tools_edit_site")
}
},
component: Home
}
];

View File

@@ -0,0 +1,10 @@
import { createMemoryHistory, createRouter } from "vue-router";
import baseRoutes from "@/router/baseRoutes";
const router = createRouter({
history: createMemoryHistory(),
routes: baseRoutes
});
export default router;

View File

@@ -0,0 +1,19 @@
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-fast-enter-active,
.fade-fast-leave-active {
transition: opacity 0.15s ease;
}
.fade-fast-enter-from,
.fade-fast-leave-to {
opacity: 0;
}

View File

@@ -0,0 +1,35 @@
* {
--primary: #673de6;
--primary-light: #ebe4ff;
--primary-lightest: #673de614;
--primary-dark: #5025d1;
--primary-charts: #b39ef3;
--white: #ffffff;
--gray: #727586;
--gray-light: #f2f3f6;
--gray-dark: #36344d;
--gray-border: #dad9da;
--success: #00b090;
--success-light: #def4f0;
--success-dark: #008361;
--info-light: #e0f8ff;
--danger: #fc5185;
--danger-light: #ffe8ef;
--danger-dark: #d63163;
--warning: #ffcd35;
--warning-light: #fff8e2;
--warning-dark: #fea419;
--warning-dark-2: #9f6000;
--meteorite: #8c85ff;
--meteorite-light: #d5dfff;
--meteorite-gray: #dadce03d;
--meteorite-dark: #2f1c6a;
--light: #ffffff;
--dark: #1d1e20;
--white-blue: #f4f5ff;
--primary-timer: #8564eb;
--black-timer: #343434;
--periwinkle: #c5cde9;
--gray-1: rgba(201, 201, 201, 0.5);
--gray-2: rgba(227, 227, 277, 0.5);
}

View File

@@ -0,0 +1,5 @@
@mixin base-card {
border: 1px solid var(--gray-border);
border-radius: 8px;
background-color: var(--light);
}

View File

@@ -0,0 +1,72 @@
:root {
--tooltip-rotation: 0deg;
--tooltip-width: 250px;
}
@mixin tooltipBase {
position: absolute;
left: 50%;
transform: translate(-50%, 0) rotate(var(--tooltip-rotation));
content: attr(tooltip);
background-color: var(--gray);
color: var(--light);
font-size: 16px;
padding: 4px 16px;
border-radius: 4px;
font-weight: 400;
max-width: var(--tooltip-width);
box-sizing: border-box;
line-height: normal;
white-space: normal;
animation: fadeIn 0.3s;
}
.has-tooltip--bottom::after {
@include tooltipBase;
top: calc(100% + 5px);
}
.has-tooltip--bottom-rotated::after {
@include tooltipBase;
bottom: calc(100% + 5px);
}
.has-tooltip--top::after {
@include tooltipBase;
bottom: calc(100% + 5px);
}
.has-tooltip--top-rotated::after {
@include tooltipBase;
top: calc(100% + 5px);
}
.has-tooltip--left::after {
@include tooltipBase;
top: 50%;
left: auto;
right: calc(100% + 5px);
transform: translate(0, -50%) rotate(var(--tooltip-rotation));
}
.has-tooltip--left-rotated::after {
@include tooltipBase;
top: 50%;
left: calc(100% + 5px);
transform: translate(0, -50%) rotate(var(--tooltip-rotation));
}
.has-tooltip--right::after {
@include tooltipBase;
top: 50%;
left: calc(100% + 5px);
transform: translate(0, -50%) rotate(var(--tooltip-rotation));
}
.has-tooltip--right-rotated::after {
@include tooltipBase;
top: 50%;
left: auto;
right: calc(100% + 5px);
transform: translate(0, -50%) rotate(var(--tooltip-rotation));
}

View File

@@ -0,0 +1,415 @@
html,
body {
font-size: 14px;
font-family: 'DM Sans', 'Roboto', sans-serif !important;
font-weight: 400;
-ms-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.btn {
&--cta {
text-transform: uppercase;
font-weight: 500;
padding-left: 2.3rem;
padding-right: 2.3rem;
}
}
.primary-header {
color: var(--primaryText);
&--grey {
color: var(--ligth-text);
}
font-weight: 700;
margin-bottom: 1rem;
text-align: center;
}
.secondary-header {
margin-bottom: 1rem;
text-align: center;
}
h1,
.h1 {
font-size: 24px;
line-height: 32px;
font-weight: 700;
}
h2,
.h2 {
font-size: 20px;
line-height: 32px;
font-weight: 700;
}
h3,
.h3 {
font-size: 16px;
line-height: 24px;
font-weight: 700;
}
h4,
.h4,
.h-button,
.btn,
button {
font-size: 14px;
line-height: 24px;
font-weight: 700;
}
body,
li,
p {
font-size: 14px;
line-height: 24px;
font-weight: 400;
margin: 0;
}
.helper-text {
font-size: 12px;
line-height: 20px;
font-weight: 400;
}
p {
color: var(--secondaryText);
}
b,
strong {
font-weight: bold;
}
a {
color: var(--primary);
&:hover {
color: var(--primary);
}
}
.link-disabled {
color: var(--ligth-text);
}
.text-warning-dark,
a.text-warning-dark {
color: var(--warning-dark) !important;
&:hover {
color: var(--warning-dark) !important;
}
}
.text-warning-light,
a.text-warning-light {
color: var(--warning-light) !important;
&:hover {
color: var(--warning-light) !important;
}
}
.text-success-light,
a.text-success-light {
color: var(--success-light) !important;
&:hover {
color: var(--success-light) !important;
}
}
.text-success-dark,
a.text-success-dark {
color: var(--success-dark) !important;
&:hover {
color: var(--success-dark) !important;
}
}
.text-danger-light,
a.text-danger-light {
color: var(--danger-light) !important;
&:hover {
color: var(--danger-light) !important;
}
}
.text-header-bg,
a.text-header-bg {
color: var(--header-bg) !important;
&:hover {
color: var(--header-bg) !important;
}
}
.text-gray-light,
a.text-gray-light {
color: var(--gray-light) !important;
&:hover {
color: var(--gray-dark) !important;
}
}
.text-gray {
color: var(--gray) !important;
}
a.text-gray {
color: var(--gray) !important;
&:hover {
color: var(--gray-dark) !important;
}
}
.text-black,
a.text-black {
color: var(--dark) !important;
&:hover {
color: var(--dark) !important;
}
}
.text-dark,
a.text-dark {
color: var(--dark) !important;
&:hover {
color: var(--dark) !important;
}
}
.text-light,
a.text-light {
color: var(--light) !important;
&:hover {
color: var(--light) !important;
}
}
.text-danger,
a.text-danger {
color: var(--danger) !important;
&:hover {
color: var(--danger) !important;
}
}
.text-warning-regular,
a.text-warning-regular {
color: var(--warning-regular) !important;
&:hover {
color: var(--warning-regular) !important;
}
}
.text-warning,
a.text-warning {
color: var(--warning) !important;
&:hover {
color: var(--warning) !important;
}
}
.text-info,
a.text-info {
color: var(--primary-hostinger) !important;
&:hover {
color: var(--primary-hostinger) !important;
}
}
.text-success,
a.text-success {
color: var(--success) !important;
&:hover {
color: var(--success) !important;
}
}
.text-header-text,
a.text-header-text {
color: var(--header-text) !important;
&:hover {
color: var(--header-text) !important;
}
}
.text-secondary,
a.text-secondary {
color: var(--secondary) !important;
&:hover {
color: var(--secondary) !important;
}
}
.text-primary,
a.text-primary {
color: var(--primary) !important;
&:hover {
color: var(--primary) !important;
}
}
.text-primary-light,
a.text-primary-light {
color: var(--primary-light) !important;
&:hover {
color: var(--primary-light) !important;
}
}
.text-meteorite-dark,
a.text-meteorite-dark {
color: var(--meteorite-dark) !important;
&:hover {
color: var(--meteorite-dark) !important;
}
}
.text-title-1 {
font-size: 40px;
font-weight: 700;
line-height: 48px;
}
.text-title-2 {
font-size: 36px;
font-weight: 700;
line-height: 40px;
}
.text-title-3 {
font-size: 32px;
font-style: normal;
font-weight: 700;
}
.text-heading-1 {
font-size: 24px;
font-weight: 700;
line-height: 32px;
}
.text-heading-2 {
font-size: 20px;
font-weight: 700;
line-height: 32px;
}
.text-heading-3 {
font-size: 16px;
font-weight: 700;
line-height: 24px;
}
.text-bold-1 {
font-size: 16px;
font-weight: 700;
line-height: 24px;
}
.text-bold-2 {
font-size: 14px;
font-weight: 700;
line-height: 24px;
}
.text-bold-3 {
font-size: 12px;
font-weight: 700;
line-height: 20px;
}
.text-body-1 {
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: var(--text_gray);
}
.text-body-2 {
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: var(--text_gray);
}
.text-body-3 {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: var(--text_gray);
}
.text-button-1 {
font-size: 16px;
font-weight: 700;
line-height: 24px;
}
.text-button-2 {
font-size: 14px;
font-weight: 700;
line-height: 24px;
}
.text-button-3 {
font-size: 12px;
font-weight: 700;
line-height: 20px;
}
.text-crossed-1 {
font-size: 14px;
font-weight: 400;
line-height: 24px;
text-decoration: line-through;
}
.text-crossed-2 {
font-size: 12px;
font-weight: 400;
line-height: 20px;
text-decoration: line-through;
}
.text-caption {
font-size: 12px;
font-weight: 400;
line-height: 12px;
}
.text-overline {
font-size: 12px;
font-weight: 700;
line-height: 16px;
text-transform: uppercase;
}
.text-link-1 {
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: var(--primary);
text-decoration: underline;
}
.text-link-2 {
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: var(--primary);
text-decoration: underline;
}
.text-link-3 {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: var(--primary);
text-decoration: underline;
}

View File

@@ -0,0 +1,85 @@
@mixin padding-margin($mode) {
@if $mode == general {
$mode: '';
} @else {
$mode: '-' + $mode;
}
@for $i from 0 through 50 {
$num: $i * 4;
.h-m-#{$num}#{$mode} {
margin: #{$num}px !important;
}
.h-mx-#{$num}#{$mode} {
margin-left: #{$num}px !important;
margin-right: #{$num}px !important;
}
.h-my-#{$num}#{$mode} {
margin-top: #{$num}px !important;
margin-bottom: #{$num}px !important;
}
.h-mt-#{$num}#{$mode} {
margin-top: #{$num}px !important;
}
.h-mb-#{$num}#{$mode} {
margin-bottom: #{$num}px !important;
}
.h-ml-#{$num}#{$mode} {
margin-left: #{$num}px !important;
}
.h-mr-#{$num}#{$mode} {
margin-right: #{$num}px !important;
}
.h-m-#{$num}#{$mode}-minus {
margin: -#{$num}px !important;
}
.h-mx-#{$num}#{$mode}-minus {
margin-left: -#{$num}px !important;
margin-right: -#{$num}px !important;
}
.h-my-#{$num}#{$mode}-minus {
margin-top: -#{$num}px !important;
margin-bottom: -#{$num}px !important;
}
.h-mt-#{$num}#{$mode}-minus {
margin-top: -#{$num}px !important;
}
.h-mb-#{$num}#{$mode}-minus {
margin-bottom: -#{$num}px !important;
}
.h-ml-#{$num}#{$mode}-minus {
margin-left: -#{$num}px !important;
}
.h-mr-#{$num}#{$mode}-minus {
margin-right: -#{$num}px !important;
}
.h-p-#{$num}#{$mode} {
padding: #{$num}px !important;
}
.h-px-#{$num}#{$mode} {
padding-left: #{$num}px !important;
padding-right: #{$num}px !important;
}
.h-py-#{$num}#{$mode} {
padding-top: #{$num}px !important;
padding-bottom: #{$num}px !important;
}
.h-pt-#{$num}#{$mode} {
padding-top: #{$num}px !important;
}
.h-pb-#{$num}#{$mode} {
padding-bottom: #{$num}px !important;
}
.h-pl-#{$num}#{$mode} {
padding-left: #{$num}px !important;
}
.h-pr-#{$num}#{$mode} {
padding-right: #{$num}px !important;
}
}
}
// no media query
@include padding-margin(general);

View File

@@ -0,0 +1,26 @@
:root {
/* component (not used) */
--z-index-1: 100;
/* Hlist, mobile dialog (upgrade comparison on mobile), banner */
--z-index-2: 200;
/* HPopup, HpSidemenu (must stay above banner for mobile) */
--z-index-3: 300;
--z-index-4: 400;
/* HpAction */
--z-index-hp-action: 400;
/* header fixed */
--z-index-5: 500;
--z-index-6: 600;
/* modals */
--z-index-modal: 600;
--z-index-hp-action-modal: 700;
/*overlay*/
--z-index-10: 1000;
/* not used, value set by intercom */
--z-index-intercom-1: 1100;
--z-index-intercom-2: 1200;
--z-index-intercom-3: 1300;
--z-index-max: 2147483647;
--z-index-child-1: 10;
--z-index-child-2: 20;
}

View File

@@ -0,0 +1,38 @@
@import '@/scss/base/mixins.scss';
.banner {
@include base-card;
padding: 16px 24px;
margin-bottom: 24px;
&__heading {
display: flex;
align-items: center;
margin-bottom: 8px;
}
&__heading-icon {
margin-right: 8px;
color: var(--gray);
}
&__heading-title {
margin-bottom: 0px;
font-weight: 700;
}
&__body {
display: flex;
align-items: center;
justify-content: space-between;
@media (max-width: theme('screens.md')) {
flex-wrap: wrap;
}
}
&__contents {
display: flex;
flex-direction: column;
text-align: left;
}
}

View File

@@ -0,0 +1,24 @@
#hostinger-tools-vue-app {
@import './base/z-index.scss';
@import './base/animations.scss';
@import './base/tooltip.scss';
@import './base/utilities.scss';
@import './base/typography.scss';
@import 'bootstrap-scss/bootstrap-utilities';
@import './base/colors.scss';
* {
box-sizing: border-box;
}
body {
font-size: 14px;
font-family: 'DM Sans', 'Roboto', sans-serif !important;
font-weight: 400;
-ms-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--main-bg-color);
color: var(--dark);
}
}

View File

@@ -0,0 +1,22 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { HostingerToolsData, STORE_PERSISTENT_KEYS } from "@/types";
import { snakeToCamelObj } from "@/utils/services";
export const useGeneralStoreData = defineStore(
"generalStoreData",
() => {
const toolsData = ref<HostingerToolsData>(
// @ts-expect-error - hostinger_tools_data is a global variable
snakeToCamelObj(hostinger_tools_data)
);
return {
...toolsData.value
};
},
{
persist: { key: STORE_PERSISTENT_KEYS.TOOLS_GENERAL_DATA_STORE }
}
);

View File

@@ -0,0 +1,3 @@
export * from './generalStoreData';
export * from './modalStore';
export * from './settingsStore';

View File

@@ -0,0 +1,27 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { ModalContent, ModalSettings } from "@/types";
import { ModalName } from "@/types/enums";
export const useModalStore = defineStore("modalStore", () => {
interface Modal {
name: ModalName;
data?: ModalContent;
settings?: ModalSettings;
}
const activeModal = ref<Modal | null>(null);
const openModal = (
name: ModalName,
data?: ModalContent,
settings?: ModalSettings
) => {
activeModal.value = { name, data, settings };
};
const closeModal = () => (activeModal.value = null);
return { activeModal, openModal, closeModal };
});

View File

@@ -0,0 +1,81 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { toast } from "vue3-toastify";
import { generalDataRepo } from "@/repositories";
import { STORE_PERSISTENT_KEYS } from "@/types/enums";
import { SettingsData } from "@/types/models";
import { translate } from "@/utils/helpers";
export const useSettingsStore = defineStore(
"settingsStore",
() => {
const settingsData = ref<SettingsData | null>(null);
const fetchSettingsData = async () => {
const [{ data }, err] = await generalDataRepo.getSettings();
if (err) return;
settingsData.value = data;
};
const regenerateByPassCode = async () => {
const [{ data }, err] =
await generalDataRepo.getRegenerateByPassCode();
if (err) return;
toast.success(translate("bypass_link_reset_success"));
const tempSettingsData = settingsData.value;
settingsData.value = {
...data,
currentWpVersion: tempSettingsData?.currentWpVersion || "",
phpVersion: tempSettingsData?.phpVersion || "",
newestWpVersion: tempSettingsData?.newestWpVersion || "",
isEligibleWwwRedirect:
tempSettingsData?.isEligibleWwwRedirect || false
};
};
const updateSettingsData = async (
settings: SettingsData
): Promise<boolean> => {
const [{ data }, err] =
await generalDataRepo.postSettings(settings);
if (err) {
toast.error(translate("hostinger_tools_settings_error"));
return false;
}
const tempSettingsData = settingsData.value;
settingsData.value = {
...data,
currentWpVersion: tempSettingsData?.currentWpVersion || "",
phpVersion: tempSettingsData?.phpVersion || "",
newestWpVersion: tempSettingsData?.newestWpVersion || "",
isEligibleWwwRedirect:
tempSettingsData?.isEligibleWwwRedirect || false
};
toast.success(translate("hostinger_tools_settings_updated"));
return true;
};
return {
fetchSettingsData,
updateSettingsData,
regenerateByPassCode,
settingsData
};
},
{
persist: { key: STORE_PERSISTENT_KEYS.SETTINGS_STORE }
}
);

View File

@@ -0,0 +1,6 @@
export const Header = {
WP_NONCE: "X-WP-Nonce",
HPANEL_ORDER_TOKEN: "X-Hpanel-Order-Token"
} as const;
export type HeaderType = (typeof Header)[keyof typeof Header];

View File

@@ -0,0 +1,112 @@
export const Icons = [
"icon-account-circle",
"icon-account-failure",
"icon-add-shopping-cart",
"icon-add",
"icon-ai-filled",
"icon-ai",
"icon-arrow-back",
"icon-arrow-forward",
"icon-assignment-1",
"icon-assignment",
"icon-block",
"icon-bookmark",
"icon-builder",
"icon-calendar-empty",
"icon-calendar-filled",
"icon-cancel",
"icon-cards",
"icon-chat",
"icon-check-circle",
"icon-check",
"icon-chevron-down",
"icon-chevron-up",
"icon-close",
"icon-contact",
"icon-content-copy",
"icon-cpanel",
"icon-dashboard",
"icon-data-usage",
"icon-delete",
"icon-do-not-disturb-on",
"icon-domain",
"icon-eco-energy",
"icon-edit",
"icon-email",
"icon-error",
"icon-feedback",
"icon-few-accounts",
"icon-file-download",
"icon-file",
"icon-filter",
"icon-folder",
"icon-generate-password",
"icon-google",
"icon-graph",
"icon-help",
"icon-history",
"icon-info",
"icon-key",
"icon-keyboard-arrow-down",
"icon-keyboard-arrow-left-light",
"icon-keyboard-arrow-right",
"icon-keyboard-arrow-up",
"icon-laptop-1",
"icon-laptop",
"icon-launch-light",
"icon-launch",
"icon-lightbulb",
"icon-list-1",
"icon-list",
"icon-lock-locked",
"icon-mail",
"icon-malware-scanner",
"icon-menu",
"icon-migrate",
"icon-monetization-on",
"icon-more-horiz-light",
"icon-more-vert",
"icon-open-in-new",
"icon-os-panel",
"icon-pause-circle-filled",
"icon-payments",
"icon-person",
"icon-play-circle",
"icon-receipt",
"icon-recovery",
"icon-refresh",
"icon-restart",
"icon-search",
"icon-security",
"icon-send",
"icon-server",
"icon-settings",
"icon-share",
"icon-show_chart",
"icon-smartphone",
"icon-speed",
"icon-star",
"icon-storage",
"icon-store",
"icon-support-1",
"icon-support",
"icon-sync",
"icon-tablet-android",
"icon-terminal",
"icon-timelapse",
"icon-tips",
"icon-titan",
"icon-trending-up",
"icon-trupet",
"icon-update",
"icon-upgrade",
"icon-visibility-off",
"icon-visibility",
"icon-vps",
"icon-website",
"icon-widget",
"icon-wordpress",
"icon-zip-folder"
] as const;
export type IconUnion = (typeof Icons)[number];

View File

@@ -0,0 +1,6 @@
export * from '@/types/enums/headerEnum';
export * from '@/types/enums/iconModels';
export * from '@/types/enums/modalsEnum';
export * from '@/types/enums/mouseEventEnum';
export * from '@/types/enums/routeEnum';
export * from '@/types/enums/storePersistKeyEnum';

View File

@@ -0,0 +1,5 @@
export enum ModalName {
XmlSecurityModal = "XmlSecurityModal",
ByPassLinkResetModal = "ByPassLinkResetModal",
EnableLlmsTxtModal = "EnableLlmsTxtModal",
}

View File

@@ -0,0 +1,8 @@
export enum MouseEvent {
Click = "click",
DoubleClick = "dblclick",
MouseUp = "mouseup",
MouseDown = "mousedown",
MouseOver = "mouseover",
MouseLeave = "mouseleave",
}

View File

@@ -0,0 +1,3 @@
export enum RouteBase {
HOSTINGER_TOOLS = "hostinger-tools",
}

View File

@@ -0,0 +1,4 @@
export const STORE_PERSISTENT_KEYS = {
TOOLS_GENERAL_DATA_STORE: "tools-general-data-store",
SETTINGS_STORE: "settings-store"
} as const;

View File

@@ -0,0 +1,22 @@
declare global {
const hostinger_tools_data: {
rest_base_url: string;
nonce: string;
translations: { [key: string]: string };
hplatform: string;
home_url: string;
site_url: string;
plugin_url: string;
asset_url: string;
edit_site_url: string;
llmstxt_file_url: string;
llmstxt_file_user_generated: boolean;
mcp_choice: boolean;
ai_plugin_compatibility: boolean;
wp_version: string;
php_version: string;
recommended_php_version: string;
};
}
export {};

View File

@@ -0,0 +1,2 @@
export * from './enums';
export * from './models';

View File

@@ -0,0 +1,57 @@
import type { RouteLocationRaw } from "vue-router";
import { IconUnion } from "@/types/enums";
export type ButtonSize = "small" | "medium" | "large";
export type ButtonVariant = "contain" | "outline" | "text";
export type ButtonColor = "primary" | "danger" | "warning" | "dark";
export interface IButtonV2Styling {
backgroundColor: "primary" | "danger" | "warning" | "transparent" | "dark";
hoverBackgroundColor:
| "primary-dark"
| "danger-dark"
| "danger-light"
| "warning-dark"
| "warning-light"
| "primary-light"
| "meteorite-gray";
border: "none" | "1px solid var(--gray-border)" | "1px solid var(--light)";
color: "primary" | "danger" | "warning" | "dark" | "white";
disabled: {
color: "white" | "gray";
backgroundColor: "gray" | "transparent";
};
}
export type IButtonVariantConfiguration = Record<
ButtonVariant,
Record<ButtonColor, IButtonV2Styling>
>;
export interface IButtonProps {
size?: ButtonSize;
variant?: ButtonVariant;
color?: ButtonColor;
isDisabled?: boolean | null;
isHovered?: boolean;
isLoading?: boolean;
iconPrepend?: IconUnion;
iconAppend?: IconUnion;
to?: RouteLocationRaw;
target?: "_blank" | "_self" | "_parent" | "_top";
}
export interface IButtonPropsMandatory extends IButtonProps {
size: ButtonSize;
variant: ButtonVariant;
color: ButtonColor;
}
export interface SectionHeaderButton {
id: string;
text: string;
to?: string;
variant?: ButtonVariant;
onClick?: () => void;
}

View File

@@ -0,0 +1,41 @@
import { IconUnion } from "@/types/enums/iconModels";
export type SectionItem = {
id: string;
title: string;
description: string;
isVisible?: boolean;
toggleValue?: boolean;
sideButton?: {
text: string;
onClick: () => void;
};
sideButtons?: Array<{
id: string;
text: string;
to?: string;
isDisabled?: boolean;
variant?: "text" | "outline" | "contain";
icon?: IconUnion;
onClick?: () => void;
}>;
copyLink?: string;
learnMoreLink?: string;
};
export type SectionHeaderToggle = {
value: boolean;
onToggle: (value: boolean) => void;
};
export const SECTION_ID = {
MAINTENANCE_MODE: "maintenance-mode",
BYPASS_LINK: "bypass-link",
DISABLE_XML_RPC: "disable-xml-rpc",
DISABLE_AUTHENTICATION_PASSWORD: "disable-authentication-password",
FORCE_HTTPS: "force-https",
FORCE_WWW: "force-www",
ENABLE_LLMS_TXT: "enable-llms-txt",
OPTIN_MCP: "optin-mcp",
SWITCH_MCP_CHOICE: "switch-mcp-choice"
} as const;

View File

@@ -0,0 +1,37 @@
export type ToggleableSettingsData = {
disableXmlRpc: boolean;
forceHttps: boolean;
maintenanceMode: boolean;
forceWww: boolean;
isEligibleWwwRedirect: boolean;
disableAuthenticationPassword: boolean;
enableLlmsTxt: boolean;
optinMcp: boolean;
};
export type NonToggleableSettingsData = {
bypassCode: string;
currentWpVersion: string;
newestWpVersion: string;
phpVersion: string;
newestPhpVersion: string;
};
export type HostingerToolsData = {
homeUrl: string;
siteUrl: string;
editSiteUrl: string;
pluginUrl: string;
assetUrl: string;
translations: { [key: string]: string };
restBaseUrl: string;
nonce: string;
wpVersion: string;
phpVersion: string;
llmstxtFileUrl: string;
llmstxtFileUserGenerated: boolean;
mcpChoice: boolean;
aiPluginCompatibility: boolean;
};
export type SettingsData = NonToggleableSettingsData & ToggleableSettingsData;

View File

@@ -0,0 +1,51 @@
// Header is imported but not used in this file
export const Colors = [
"primary",
"primary-light",
"primary-dark",
"white",
"gray",
"gray-light",
"gray-dark",
"gray-border",
"success",
"success-light",
"success-dark",
"info-light",
"danger",
"danger-light",
"danger-dark",
"warning",
"warning-light",
"warning-dark",
"warning-dark-2",
"meteorite",
"meteorite-light",
"meteorite-dark",
"light",
"dark",
"white-blue",
"primary-timer",
"black-timer",
"transparent"
] as const;
export type Color = (typeof Colors)[number];
export type HeaderButton = {
text: string;
href: string;
onClick?: () => void;
};
export type PreviewSiteButton = {
text: string;
href: string;
onClick?: () => void;
};
export type EditSiteButton = {
text: string;
href: string;
onClick?: () => void;
};

View File

@@ -0,0 +1,7 @@
export interface ResponseError {
error: string;
errorDescription: string;
code: number;
}
export type BaseResponse<T> = Promise<[T, ResponseError | null]>;

View File

@@ -0,0 +1,7 @@
export * from './components/buttonModels';
export * from './components/sectionCardModels';
export * from './generalDataModels';
export * from './globalModels';
export * from './httpServiceModels';
export * from './labelModels';
export * from './modalModels';

View File

@@ -0,0 +1,75 @@
export const H_LABEL_THEME = {
WARNING_DARK: "warningDark",
WARNING_LIGHT: "warningLight",
SUCCESS_LIGHT: "successLight",
DANGER: "danger",
DANGER_LIGHT: "dangerLight",
PRIMARY: "primary",
GRAY: "gray",
grayLight: "grayLight",
WHITE: "white"
} as const;
export const H_LABEL_THEME_CONFIGURATION: IHLabelV2Configuration = {
warningDark: {
backgroundColor: "warning-light",
color: "warning-dark"
},
warningLight: {
backgroundColor: "warning-light",
color: "warning"
},
successLight: {
backgroundColor: "success-light",
color: "success"
},
danger: {
backgroundColor: "danger",
color: "white"
},
dangerLight: {
backgroundColor: "danger-light",
color: "danger"
},
primary: {
backgroundColor: "primary-light",
color: "primary"
},
gray: {
backgroundColor: "gray",
color: "white"
},
grayLight: {
backgroundColor: "gray-light",
color: "gray"
},
white: {
backgroundColor: "white",
color: "dark"
}
} as const;
export interface IHLabelV2Styling {
backgroundColor:
| "warning-light"
| "success-light"
| "danger"
| "danger-light"
| "primary-light"
| "gray"
| "gray-light"
| "white";
color:
| "warning-dark"
| "warning"
| "success"
| "danger"
| "primary"
| "gray"
| "white"
| "dark";
}
export type HLabelTheme = (typeof H_LABEL_THEME)[keyof typeof H_LABEL_THEME];
export type IHLabelV2Configuration = Record<HLabelTheme, IHLabelV2Styling>;

View File

@@ -0,0 +1,17 @@
export interface ModalContent {
title?: string;
subtitle?: string;
data?: Record<string, unknown>;
onSuccess?: () => void;
onClose?: () => void;
[key: string]: unknown;
}
export interface ModalSettings {
isXL?: boolean;
isXXL?: boolean;
isLG?: boolean;
hasCloseButton?: boolean;
noContentPadding?: boolean;
noBorder?: boolean;
}

View File

@@ -0,0 +1,7 @@
import { App } from "vue";
import { vTooltip } from "@/utils/directives/tooltipDirective";
export const setDirectives = (app: App) => {
app.directive("tooltip", vTooltip);
};

View File

@@ -0,0 +1,120 @@
import { DirectiveBinding } from "vue";
import { MouseEvent } from "@/types/enums";
const TOOLTIP_ATTR = "tooltip";
interface ComplexTooltip {
content: string;
autoWidth?: boolean;
}
type TooltipBinding =
| DirectiveBinding<string>
| DirectiveBinding<ComplexTooltip>;
const isString = (value: unknown) => typeof value === "string";
const getCurrentRotation = (el: HTMLElement) => {
const computedStyles = window.getComputedStyle(el, null);
const transformProp =
computedStyles.getPropertyValue("-webkit-transform") ||
computedStyles.getPropertyValue("-moz-transform") ||
computedStyles.getPropertyValue("-ms-transform") ||
computedStyles.getPropertyValue("-o-transform") ||
computedStyles.getPropertyValue("transform") ||
"none";
if (transformProp != "none") {
const values = transformProp.split("(")[1].split(")")[0].split(",");
const angle = Math.round(
Math.atan2(Number(values[1]), Number(values[0])) * (180 / Math.PI)
);
return angle < 0 ? angle + 360 : angle;
}
return 0;
};
const getTooltipClass = (
{ modifiers }: DirectiveBinding,
isRotated: boolean
) => {
const position = Object.keys(modifiers)[0];
return `has-tooltip--${position || "bottom"}${isRotated ? "-rotated" : ""}`;
};
const getTooltipContent = (binding: TooltipBinding) => {
if (!binding.value) return null;
return isString(binding.value)
? binding.value
: (binding.value as ComplexTooltip).content;
};
const addTooltip = (el: HTMLElement, binding: TooltipBinding) => {
const content = getTooltipContent(binding);
if (!content) return removeTooltip(el, binding);
const rotation = getCurrentRotation(el);
el.setAttribute(TOOLTIP_ATTR, content as string);
document.documentElement.style.setProperty(
"--tooltip-rotation",
`-${rotation}deg`
);
if (
!isString(binding.value) &&
(binding.value as ComplexTooltip).autoWidth
) {
document.documentElement.style.setProperty("--tooltip-width", "auto");
}
el.style.transition = "0s";
const zIndex = getComputedStyle(document.documentElement).getPropertyValue(
"--z-index-child-2"
);
el.style.zIndex = zIndex;
el.style.position = "relative";
el.classList.add(getTooltipClass(binding, rotation === 180));
};
const removeTooltip = (el: HTMLElement, binding: TooltipBinding) => {
el.removeAttribute(TOOLTIP_ATTR);
el.style.zIndex = "";
el.style.position = "";
el.classList.remove(getTooltipClass(binding, true));
el.classList.remove(getTooltipClass(binding, false));
};
const unbind = (el: HTMLElement, binding: TooltipBinding) => {
el.removeEventListener(MouseEvent.MouseOver, () => addTooltip(el, binding));
el.removeEventListener(MouseEvent.MouseLeave, () =>
removeTooltip(el, binding)
);
el.removeEventListener(MouseEvent.Click, () => removeTooltip(el, binding));
};
const bind = (el: HTMLElement, binding: TooltipBinding) => {
el.addEventListener(MouseEvent.MouseOver, () => addTooltip(el, binding));
el.addEventListener(MouseEvent.MouseLeave, () =>
removeTooltip(el, binding)
);
el.addEventListener(MouseEvent.Click, () => removeTooltip(el, binding));
};
export const vTooltip = {
mounted: (el: HTMLElement, binding: TooltipBinding) => bind(el, binding),
updated: (el: HTMLElement, binding: TooltipBinding) => bind(el, binding),
beforeUnmount: (el: HTMLElement, binding: TooltipBinding) =>
unbind(el, binding)
};

View File

@@ -0,0 +1,173 @@
import { toUnicode as punyUnicode } from "punycode";
import { AxiosResponse } from "axios";
import { toast } from "vue3-toastify";
import { useGeneralStoreData } from "@/stores";
import { ResponseError } from "@/types";
/**
* Converts a string to Unicode using Punycode encoding.
* If the input string is falsy, it returns the input string itself.
*
* @param str - The string to convert to Unicode.
* @returns The Unicode representation of the input string, or the input string itself if it is falsy.
*/
export const toUnicode = (str: string) => (str ? punyUnicode(str) : str);
/**
* Converts the first character of a word to uppercase and returns the modified word.
* If the word is an empty string, an empty string is returned.
*
* @param word - The word to convert to title case.
* @returns The word with the first character in uppercase.
*/
export const toTitleCase = (word: string = "") =>
word.charAt(0).toUpperCase() + word.slice(1);
/**
* Delays the execution for the specified number of milliseconds.
* @param ms - The number of milliseconds to delay the execution.
* @returns A Promise that resolves after the specified delay.
*/
export const timeout = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
/**
* Copies the given string to the clipboard.
*
* @param copiedString - The string to be copied.
* @param message - The success message to be displayed.
* @param toastrParams - Additional parameters for the toast notification.
*/
export const copyString = (
copiedString: string,
message = translate("hostinger_tools_copied_successfully"),
toastrParams = {}
) => {
const el = document.createElement("textarea");
el.value = copiedString;
el.setAttribute("readonly", "");
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
const copyString = translate("hostinger_tools_text_copied_successfully");
if (!message) return;
toast.success(copyString, toastrParams);
};
/**
* Wraps the given value in a CSS variable.
* @param value - The value to be wrapped.
* @returns The wrapped value as a CSS variable.
*/
export const wrapInCssVar = (value: string | number) => `var(--${value})`;
/**
* Capitalizes the first letter of a string.
* @param str - The input string.
* @returns The input string with the first letter capitalized.
*/
export const capitalize = (str: string) =>
str.charAt(0).toUpperCase() + str.substring(1);
/**
* Calls an asynchronous function and handles the response.
* @param promise - The promise to be resolved.
* @returns A tuple containing the response data and any error that occurred.
*/
export const asyncCall = async <T>(
promise: Promise<AxiosResponse<T>>
): Promise<[T, ResponseError | null]> => {
try {
const response = await promise;
if (
response.data &&
typeof response.data === "object" &&
"error" in response.data
) {
const apiResponse = response.data as { error?: unknown; data?: T };
if (
!apiResponse.error ||
(Array.isArray(apiResponse.error) && !apiResponse.error.length)
) {
return [apiResponse.data as T, null];
}
}
return [response.data as T, null];
} catch (er) {
return [{} as T, er as ResponseError];
}
};
/**
* Retrieves the URL of an asset based on the provided path.
* @param path - The path of the asset.
* @returns The URL of the asset.
*/
export const getAssetSource = (path: string) => {
const { assetUrl } = useGeneralStoreData();
return `${assetUrl}vue-frontend/src/assets/${path}`;
};
/**
* Converts a kebab-case string to camelCase.
* @param string - The kebab-case string to convert.
* @returns The camelCase version of the input string.
*/
export const kebabToCamel = (string: string) =>
string.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
/**
* Compares two version numbers and returns a comparison result.
* @param newVersion - The old version number.
* @param currentVersion - The new version number.
* @returns -1 if currentVersion is greater than newVersion, 1 if currentVersion is less than newVersion, 0 if they are equal.
*/
export const isNewerVerison = ({
newVersion,
currentVersion
}: {
newVersion: string;
currentVersion: string;
}) => {
if (!newVersion || !currentVersion) return false;
const newVersionParts = newVersion.split(".");
const currentVersionParts = currentVersion.split(".");
for (
let i = 0;
i < Math.max(currentVersionParts.length, newVersionParts.length);
i++
) {
const newPart = parseInt(currentVersionParts[i]) || 0;
const oldPart = parseInt(newVersionParts[i]) || 0;
if (newPart > oldPart) return false;
if (newPart < oldPart) return true;
}
return false;
};
/**
* Returns the base URL of a given URL.
* @param url - The input URL.
* @returns The base URL of the input URL.
*/
export const getBaseUrl = (url: string) => {
const parsedUrl = new URL(url);
return `${parsedUrl.protocol}//${parsedUrl.host}${parsedUrl.pathname.split("/").slice(0, -1).join("/")}/`;
};
export const translate = (key: string) =>
hostinger_tools_data.translations[key] || key;

View File

@@ -0,0 +1 @@
export * from './helpers';

View File

@@ -0,0 +1,70 @@
import axios, {
AxiosRequestConfig,
AxiosResponse,
InternalAxiosRequestConfig
} from "axios";
import { asyncCall } from "@/utils/helpers";
import { camelToSnakeObj, snakeToCamelObj } from "@/utils/services";
const TIMEOUT_TIME = 120_000;
export const axiosInstance = axios.create({
timeout: TIMEOUT_TIME,
withCredentials: false,
headers: {
Accept: "application/json;charset=UTF-8",
"Content-Type": "application/json;charset=UTF-8"
}
});
// REQUEST INTERCEPTOR - camel to snake
axiosInstance.interceptors.request.use((req: InternalAxiosRequestConfig) => {
if ((req as InternalAxiosRequestConfig & { plain?: boolean }).plain)
return req;
if (req.data) {
req.data = camelToSnakeObj(req.data);
}
if (req.params) {
req.params = camelToSnakeObj(req.params);
}
return req;
});
axiosInstance.interceptors.response.use(
(res: AxiosResponse) => {
const transformedResponse = snakeToCamelObj({
...res,
data: res.data
});
return transformedResponse as AxiosResponse;
},
(error: Error) => Promise.reject(error)
);
const httpService = {
get<T>(url: string, config?: AxiosRequestConfig) {
return asyncCall<T>(axiosInstance.get(url, config));
},
post<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
return asyncCall<T>(axiosInstance.post(url, data, config));
},
put<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
return asyncCall<T>(axiosInstance.put(url, data, config));
},
patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig) {
return asyncCall<T>(axiosInstance.patch(url, data, config));
},
delete<T>(url: string, config?: AxiosRequestConfig) {
return asyncCall<T>(axiosInstance.delete(url, config));
},
request<T>(config: AxiosRequestConfig) {
return asyncCall<T>(axiosInstance(config));
}
};
export default httpService;

View File

@@ -0,0 +1,2 @@
export * from './httpService';
export * from './snakeCamelService';

View File

@@ -0,0 +1,125 @@
import { isArray, isObject } from "lodash";
export const snakeToCamel = (string: string, pascal?: boolean) => {
const converter = (matches: string) => matches[1]?.toUpperCase();
let result = string?.toString().replace(/(_\w)/g, converter);
if (pascal) {
result = result?.charAt(0)?.toUpperCase() + result?.slice(1);
}
return result;
};
export const kebabToCamel = (string: string, pascal?: boolean) => {
const converter = (matches: string) => matches[1]?.toUpperCase();
let result = string?.toString().replace(/(-\w)/g, converter);
if (pascal) {
result = result?.charAt(0)?.toUpperCase() + result?.slice(1);
}
return result;
};
export const snakeToPascal = (string: string) =>
string
.split("/")
.map((snake: string) =>
snake
.split("_")
.map(
(substr: string) =>
substr.charAt(0).toUpperCase() + substr.slice(1)
)
.join("")
)
.join("/");
export const stringToPascal = (string: string) =>
string
.replace(
/(\w)(\w*)/g,
(w: string) => w[0].toUpperCase() + w.slice(1).toLowerCase()
)
.split(" ")
.join("")
.replace(/(\w)(\w*)/g, (w: string) => w[0].toLowerCase() + w.slice(1));
export const camelToSnake = (str: string) =>
str
.replace(/(^[A-Z])/, ([first]) => first.toLowerCase())
.replace(/([A-Z])/g, ([letter]) => `_${letter.toLowerCase()}`);
export const camelToReadable = (str: string) =>
camelToSnake(str).replace("_", " ");
export const snakeToCamelObj = (obj: unknown) => {
if (isObject(obj) && !isArray(obj)) {
const n: Record<string, unknown> = {};
Object.keys(obj as Record<string, unknown>).forEach((k) => {
n[snakeToCamel(k)] = snakeToCamelObj(
(obj as Record<string, unknown>)[k]
);
});
return n;
}
if (isArray(obj)) {
const n: unknown[] = [];
(obj as unknown[]).forEach((k: unknown) => n.push(snakeToCamelObj(k)));
return n;
}
return obj;
};
export const camelToSnakeObj = (obj: unknown) => {
if (isObject(obj) && !isArray(obj)) {
const n: Record<string, unknown> = {};
Object.keys(obj as Record<string, unknown>).forEach(
(k) =>
(n[camelToSnake(k)] = camelToSnakeObj(
(obj as Record<string, unknown>)[k]
))
);
return n;
}
if (isArray(obj)) {
const n: unknown[] = [];
(obj as unknown[]).forEach((k: unknown) => n.push(camelToSnakeObj(k)));
return n;
}
return obj;
};
export const kebabToCamelObj = (obj: unknown) => {
if (isObject(obj) && !isArray(obj)) {
const n: Record<string, unknown> = {};
Object.keys(obj as Record<string, unknown>).forEach((k) => {
n[kebabToCamel(k)] = kebabToCamelObj(
(obj as Record<string, unknown>)[k]
);
});
return n;
}
if (isArray(obj)) {
const n: unknown[] = [];
(obj as unknown[]).forEach((k: unknown) => n.push(kebabToCamelObj(k)));
return n;
}
return obj;
};
export const snakeToReadable = (str: string) => str?.replace(/_/g, " ");

View File

@@ -0,0 +1,537 @@
<script lang="ts" setup>
import { storeToRefs } from "pinia";
import { computed, ref } from "vue";
import Button from "@/components/Button/Button.vue";
import SectionCard from "@/components/HostingerTools/SectionCard.vue";
import ToolVersionCard from "@/components/HostingerTools/ToolVersionCard.vue";
import Icon from "@/components/Icon/Icon.vue";
import { useModal } from "@/composables";
import { useGeneralStoreData, useSettingsStore } from "@/stores";
import {
Header,
ModalName,
SectionItem,
SettingsData,
ToggleableSettingsData
} from "@/types";
import { SECTION_ID } from "@/types/models/components/sectionCardModels";
import {
getAssetSource,
getBaseUrl,
isNewerVerison,
kebabToCamel,
translate
} from "@/utils/helpers";
import http from "@/utils/services/httpService";
import {toast} from "vue3-toastify";
const { fetchSettingsData, updateSettingsData, regenerateByPassCode } =
useSettingsStore();
const { settingsData } = storeToRefs(useSettingsStore());
const {
siteUrl,
llmstxtFileUrl,
llmstxtFileUserGenerated,
mcpChoice,
aiPluginCompatibility,
nonce,
restBaseUrl
} = useGeneralStoreData();
const WORDPRESS_UPDATE_LINK = getBaseUrl(location.href) + "update-core.php";
const isPageLoading = ref(false);
const HOSTINGER_FREE_DOMAINS = /hostingersite\.com|hostinger\.dev/;
const initialMcpChoice = ref(false);
const llmMasterToggle = ref(false);
const isLLMSSectionDisabled = computed(
() => isFreeDomain.value || !isHostingerPlatform.value
);
const maintenanceSection = computed(() => [
{
id: SECTION_ID.MAINTENANCE_MODE,
title: translate("hostinger_tools_maintenance_mode"),
description: translate("hostinger_tools_disable_public_access"),
isVisible: true,
toggleValue: settingsData.value?.maintenanceMode
},
{
id: SECTION_ID.BYPASS_LINK,
title: translate("hostinger_tools_bypass_link"),
description: translate("hostinger_tools_skip_link_maintenance_mode"),
isVisible: true,
sideButton: {
text: translate("hostinger_tools_reset_link"),
onClick: () => {
openModal(
ModalName.ByPassLinkResetModal,
{
data: {
onConfirm: () => regenerateByPassCode()
}
},
{ isLG: true }
);
}
},
copyLink: settingsData.value?.bypassCode
? `${siteUrl}/?bypass_code=${settingsData.value.bypassCode}`
: undefined
}
]);
const securitySection = computed(() =>
[
{
id: SECTION_ID.DISABLE_XML_RPC,
title: translate("hostinger_tools_disable_xml_rpc"),
description: translate("hostinger_tools_xml_rpc_description"),
isVisible: true,
toggleValue: settingsData.value?.disableXmlRpc
},
{
id: SECTION_ID.DISABLE_AUTHENTICATION_PASSWORD,
title: translate("hostinger_tools_disable_authentication_password"),
description: translate(
"hostinger_tools_authentication_password_description"
),
isVisible: true,
toggleValue: settingsData.value?.disableAuthenticationPassword
}
].filter((item) => item.isVisible)
);
const redirectsSection = computed(() => {
const allItems = [
{
id: SECTION_ID.FORCE_HTTPS,
title: translate("hostinger_tools_force_https"),
description: translate("hostinger_tools_force_https_description"),
isVisible: true,
toggleValue: settingsData.value?.forceHttps
},
{
id: SECTION_ID.FORCE_WWW,
title: translate("hostinger_tools_force_www"),
description: !settingsData.value?.isEligibleWwwRedirect
? translate(
"hostinger_tools_force_www_description_not_available"
)
: translate("hostinger_tools_force_www_description"),
isVisible: !!settingsData.value?.isEligibleWwwRedirect,
toggleValue: settingsData.value?.forceWww
}
];
return allItems.filter((item) => item.isVisible);
});
const llmsSection = computed(() => {
const allItems = [
{
id: SECTION_ID.ENABLE_LLMS_TXT,
title: translate("hostinger_tools_enable_llms_txt"),
description: translate("hostinger_tools_llms_txt_description"),
isVisible: true,
toggleValue: settingsData.value?.enableLlmsTxt,
learnMoreLink: "https://www.hostinger.com/support/how-to-enable-llms-txt-on-your-website/",
sideButtons: [
{
id: "hostinger_tools_llms_txt_llmstxt",
text: translate("hostinger_tools_llms_txt_llmstxt"),
isDisabled: !settingsData.value?.enableLlmsTxt,
to:
settingsData.value?.enableLlmsTxt &&
llmMasterToggle.value
? llmstxtFileUrl
: undefined,
variant: "outline" as const
},
{
id: "hostinger_tools_llms_txt_check_validity",
text: translate("hostinger_tools_llms_txt_check_validity"),
isDisabled: !settingsData.value?.enableLlmsTxt,
to:
settingsData.value?.enableLlmsTxt &&
llmMasterToggle.value
? `https://llmstxtvalidator.org/?url=${llmstxtFileUrl}`
: undefined,
variant: "outline" as const
}
]
},
{
id: SECTION_ID.OPTIN_MCP,
title: translate("hostinger_tools_optin_mcp"),
description: translate("hostinger_tools_optin_mcp_description"),
isVisible: true,
toggleValue: settingsData.value?.optinMcp,
learnMoreLink:
"https://support.hostinger.com/en/articles/11729400-ai-agent-access-smart-ai-discovery"
}
];
return allItems.filter((item) => item.isVisible);
});
const aiSection = computed(() => [
{
id: SECTION_ID.SWITCH_MCP_CHOICE,
title: translate("hostinger_tools_mcp_choice"),
description: translate("hostinger_tools_mcp_description"),
isVisible: true,
toggleValue: initialMcpChoice.value
}
]);
const llmsSectionHeaderToggle = computed(() => {
const visibleItems = llmsSection.value.filter((item) => item.isVisible);
if (visibleItems.length <= 1) return undefined;
return {
value: llmMasterToggle.value,
onToggle: async (value: boolean) => {
llmMasterToggle.value = value;
if (!settingsData.value) return;
const updatedSettings = {
...settingsData.value,
enableLlmsTxt: value,
optinMcp: value
};
const success = await updateSettingsData(updatedSettings);
if (success && settingsData.value) {
settingsData.value.enableLlmsTxt = value;
settingsData.value.optinMcp = value;
}
}
};
});
const { openModal } = useModal();
const isWordPressUpdateDisplayed = computed(() => {
if (!settingsData.value) {
return false;
}
return isNewerVerison({
currentVersion: settingsData.value.currentWpVersion,
newVersion: settingsData.value.newestWpVersion
});
});
const isPhpUpdateDisplayed = computed(() => {
if (!settingsData.value) {
return false;
}
return isNewerVerison({
currentVersion: settingsData.value.phpVersion,
newVersion: "8.2" // Hardcoded for now
});
});
const isHostingerPlatform = computed(
() => parseInt(hostinger_tools_data.hplatform) > 0
);
const isFreeDomain = computed(() =>
HOSTINGER_FREE_DOMAINS.test(String(siteUrl))
);
const createUpdateButton = (onClick: () => void) => ({
text: translate("hostinger_tools_update"),
onClick
});
const resellerLocale = computed(() => {
const { pluginUrl } = useGeneralStoreData();
return pluginUrl.match(/^[^/]+/)?.[0] || "hostinger.com";
});
const connectDomainUrl = computed(() => {
if (!isHostingerPlatform.value) return undefined;
const domain = location.host;
return `https://auth.${resellerLocale.value}/login?section=website-dashboard&domain=${domain}`;
});
const phpVersionCard = computed(() => ({
title: translate("hostinger_tools_php_version"),
toolImageSrc: getAssetSource("images/icons/icon-php.svg"),
version: settingsData.value?.phpVersion,
actionButton:
isHostingerPlatform.value && isPhpUpdateDisplayed.value
? createUpdateButton(() => {
window.open(
`https://auth.${resellerLocale.value}/login?r=/section/php-configuration/domain/${location.host}`,
"_blank"
);
})
: undefined
}));
const wordPressVersionCard = computed(() => ({
title: translate("hostinger_tools_wordpress_version"),
toolImageSrc: getAssetSource("images/icons/icon-wordpress-light.svg"),
version: settingsData.value?.currentWpVersion,
actionButton: isWordPressUpdateDisplayed.value
? createUpdateButton(() => {
window.location.href = WORDPRESS_UPDATE_LINK;
})
: undefined
}));
const onSaveSection = (value: boolean, item: SectionItem) => {
const isTurnedOn = value === false;
if (item.id === SECTION_ID.DISABLE_XML_RPC && isTurnedOn) {
openModal(
ModalName.XmlSecurityModal,
{
data: {
onConfirm: () => {
onUpdateSettings(value, item);
}
}
},
{ isLG: true }
);
return;
}
onUpdateSettings(value, item);
};
const onSaveLLmsSection = async (isEnabled: boolean, item: SectionItem) => {
const updateSetting = async () => {
await onUpdateSettings(isEnabled, item);
// Update the master toggle state based on current settings
llmMasterToggle.value = !!(
settingsData.value?.enableLlmsTxt || settingsData.value?.optinMcp
);
};
if (
llmstxtFileUserGenerated &&
isEnabled &&
item.id === SECTION_ID.ENABLE_LLMS_TXT
) {
openModal(
ModalName.EnableLlmsTxtModal,
{
data: {
onConfirm: () => {
updateSetting();
}
}
},
{ isLG: true }
);
return;
}
await updateSetting();
};
const onSaveAiSection = async (isEnabled: boolean) => {
try {
const response = await http.post<SettingsData>(
`${restBaseUrl}hostinger-ai-assistant/v1/toggle-mcp-plugin`,
{ action: isEnabled ? "setup" : "deny" },
{
headers: { [Header.WP_NONCE]: nonce }
}
);
const error = response[1];
if (error) {
toast.error(translate("hostinger_tools_settings_error"));
return false;
}
initialMcpChoice.value = isEnabled;
window.dispatchEvent(
new CustomEvent("mcp-choice-changed", {
detail: {
choice: initialMcpChoice.value
}
})
);
toast.success(translate("hostinger_tools_settings_updated"));
} catch (error) {
console.error("Failed to save MCP choice: ", error);
toast.error(translate("hostinger_tools_settings_error"));
}
};
const onUpdateSettings = async (value: boolean, item: SectionItem) => {
if (!settingsData.value) return;
const id = kebabToCamel(item.id) as keyof ToggleableSettingsData;
const updatedSettings = {
...settingsData.value,
[id]: value
};
const success = await updateSettingsData(updatedSettings);
if (success && settingsData.value) {
settingsData.value[id] = value;
}
};
(async () => {
isPageLoading.value = true;
await fetchSettingsData();
isPageLoading.value = false;
if (parseInt(String(mcpChoice)) === 1) {
initialMcpChoice.value = true;
}
llmMasterToggle.value = !!(
settingsData.value?.enableLlmsTxt || settingsData.value?.optinMcp
);
})();
</script>
<template>
<div v-if="settingsData">
<div class="hostinger-tools__tool-version-cards">
<ToolVersionCard
:is-loading="isPageLoading"
v-bind="wordPressVersionCard"
class="h-mr-16"
/>
<ToolVersionCard
:is-loading="isPageLoading"
v-bind="phpVersionCard"
/>
</div>
<div>
<SectionCard
:is-loading="isPageLoading"
:title="translate('hostinger_tools_llms')"
:section-items="llmsSection"
:is-disabled="isLLMSSectionDisabled"
:header-toggle="llmsSectionHeaderToggle"
:warning="
llmstxtFileUserGenerated
? translate(
'hostinger_tools_llms_txt_external_file_found',
)
: ''
"
@save-section="onSaveLLmsSection"
>
<template
v-if="isLLMSSectionDisabled"
#snackbar
>
<div
class="hostinger-notice d-flex align-items-center w-100 h-mb-16"
>
<Icon
name="icon-info"
color="gray-dark"
/>
<p class="text-body-3">
{{
translate(
"hostinger_tools_free_domain_llm_unavailable",
)
}}
</p>
<Button
v-if="connectDomainUrl"
size="small"
variant="text"
color="primary"
class="h-ml-8"
:to="connectDomainUrl"
target="_blank"
>
{{
translate("hostinger_tools_connect_domain_cta")
}}
</Button>
</div>
</template>
</SectionCard>
<SectionCard
v-if="aiPluginCompatibility"
:is-loading="isPageLoading"
:title="translate('hostinger_tools_ai')"
:section-items="aiSection"
@save-section="onSaveAiSection"
/>
<SectionCard
:is-loading="isPageLoading"
:title="translate('hostinger_tools_maintenance')"
:section-items="maintenanceSection"
@save-section="onSaveSection"
/>
<SectionCard
:is-loading="isPageLoading"
:title="translate('hostinger_tools_security')"
:section-items="securitySection"
@save-section="onSaveSection"
/>
<SectionCard
:is-loading="isPageLoading"
:title="translate('hostinger_tools_redirects')"
:section-items="redirectsSection"
@save-section="onSaveSection"
/>
</div>
</div>
</template>
<style lang="scss">
.hostinger-tools {
&__tool-version-cards {
display: flex;
width: 100%;
@media (max-width: 590px) {
flex-direction: column;
}
}
}
.hostinger-notice {
background: var(--gray-light);
color: var(--gray-dark);
border: 1px solid var(--gray-border);
border-radius: 12px;
padding: 12px 16px;
font-size: var(--font-size-sm);
gap: 1em;
}
</style>

View File

@@ -0,0 +1,9 @@
declare module "*.vue" {
import { DefineComponent } from "vue";
const component: DefineComponent<
Record<string, unknown>,
Record<string, unknown>,
unknown
>;
export default component;
}