2 files deleted
100 files modified
6 files added
| | |
| | | use JVBase\rest\routes\QueueRoutes; |
| | | use JVBase\rest\routes\ErrorRoutes; |
| | | use JVBase\rest\routes\FormRoutes; |
| | | use JVBase\rest\routes\LoginRoutes; |
| | | use JVBase\rest\routes\NewsRoutes; |
| | | use JVBase\rest\routes\ReferralRoutes; |
| | | use JVBase\rest\routes\MagicLinkRoutes; |
| | |
| | | 'gmb' => 'JVBase\integrations\GoogleMyBusiness', |
| | | 'cloudflare' => 'JVBase\integrations\Cloudflare', |
| | | 'umami' => 'JVBase\integrations\Umami', |
| | | 'postmark' => 'JVBase\integrations\PostMark', |
| | | ]; |
| | | |
| | | public static function getInstance():JVB |
| | |
| | | } |
| | | |
| | | $this->routes = [ |
| | | 'login' => new LoginRoutes(), |
| | | 'integrations' => new IntegrationsRoutes(), |
| | | ]; |
| | | if (jvbSiteUsesFeedBlock()) { |
| | |
| | | $this->routes['term'] = new TermRoutes(); |
| | | } |
| | | |
| | | $this->routes['queue'] = new QueueRoutes(); |
| | | $this->routes['settings']= new SettingsRoutes(); |
| | | $this->routes['upload'] = new UploadRoutes(); |
| | | if (jvbSiteHasDashboard()) { |
| | | $this->routes['upload'] = new UploadRoutes(); |
| | | $this->routes['settings']= new SettingsRoutes(); |
| | | $this->routes['queue'] = new QueueRoutes(); |
| | | $this->routes['error'] = new ErrorRoutes(); |
| | | $this->routes['admin'] = new AdminRoutes(); |
| | | $this->routes['content']= new ContentRoutes(); |
| | |
| | | :target{outline:0!important;padding:0!important}.dashboard h1:first-of-type{margin-top:4rem!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img{max-height:4rem}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:fit-content;padding:.5rem!important;min-width:0;min-height:0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}.dashboard.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--outerRadius);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545} |
| | | :target{outline:0!important;padding:0!important}.dashboard>header{justify-content:flex-end}.dashboard>header img{width:var(--height)}.dashboard h1:first-of-type{margin-top:4rem!important}main>footer{max-width:100%!important;position:fixed;z-index:var(--z-top);bottom:0;left:0;right:0;width:100%;margin:4rem 0 0 0!important;height:var(--height);padding:0!important;background-color:var(--base);box-shadow:var(--shadow)}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace{margin-bottom:var(--offHeight)!important}.item-grid{margin-bottom:4rem}.item-grid:has(.select-item:checked) .item{padding:.75rem;opacity:.8;filter:var(--filter)}.item-grid .item:has(.select-item:checked){padding:.5rem;filter:none;opacity:1;background-color:var(--action-0)}.grid-view .item>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.grid-view .item>input[type=checkbox]+label::before{transform:unset;top:.5rem;left:.5rem}.grid-view .item>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.grid-view .item .item-actions{position:absolute;bottom:0;right:0}.list-view h3,.list-view p{margin:0!important}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}.grid-view .item .item-actions{bottom:unset;top:0}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--small);text-transform:none;font-style:italic;display:flex;gap:.25rem;margin-left:2rem}.selected-count::before{content:'{'}.selected-count::after{content:'}'}.bulk-edit-form .selected{display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:4px}.selected label{padding:.5rem;opacity:.6;filter:var(--filter);border:2px solid transparent;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--transition-base),opacity var(--transition-base),border var(--transition-base),padding var(--transition-base)}form.table img{max-height:4rem}.all-filters{margin:2rem 0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200)}details.uploader+.items-list .all-filters{border-top:none}.all-filters .filters{width:100%}.controls .radio-options,.filters.row.start{--align:center;--justify:flex-start;--gap:.5rem}.all-filters span.label{text-transform:uppercase;font-size:var(--small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:fit-content;padding:.5rem!important;min-width:0;min-height:0}.all-filters .btn+label:focus,.all-filters .btn+label:hover,.all-filters button:focus,.all-filters button:hover{background-color:transparent;color:var(--action-0);border-color:var(--action-0)}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--transition-base),width var(--transition-base),padding var(--transition-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}form.table textarea{width:250px;padding:.5rem}.multi-select summary{--gap:2rem;padding-right:2.5rem}dialog.bulk-edit[open],dialog.create[open],dialog.edit[open]{height:85vh;top:5vh}.tab-content h2{display:none}.group-fields.hours .group-fields,.group-fields.hours .group-fields .field{display:flex;justify-content:space-between;align-items:center}.group-fields.hours .group-fields{padding:1rem .5rem;gap:1rem}.group-fields.hours .group-fields:nth-of-type(2n+1){background-color:var(--base)}.group-fields.hours .group-fields .field{margin:0}.group-fields.hours .true-false{flex:1}.group-fields.hours .time{position:relative}.group-fields.hours .time label{margin:0;font-size:var(--small);position:absolute;top:-1rem;left:0;color:var(--contrast-200)}.today_hours{width:min(500px,90vw)}.today_hours .group-fields{width:100%;padding:0;display:flex;justify-content:center;gap:.5rem}@media (min-width:768px){.today_hours .group-fields{padding:2rem}}.today_hours .field{margin:0}.dash .true-false{margin:0}.dash [type=submit]{width:90%}.dashboard.dash h2{text-transform:none;font-size:var(--large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}.dashboard.settings nav.tabs{--height:3.5rem;--x:var(--offHeight);position:fixed;bottom:var(--height);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}nav.integrations,nav.integrations a,nav.integrations li,nav.integrations ul{height:auto}.replace{overflow:hidden}body.dash form#options{display:flex;flex-flow:column nowrap;justify-content:center;align-items:center}.item-grid.integrations{grid-template-columns:repeat(2,1fr);gap:2rem}.integration{background:var(--base);border:2px solid var(--base-200);border-radius:var(--outerRadius);padding:1rem;position:relative;transition:all var(--transition-base);box-shadow:var(--shadow)}.integration.connected{border-color:var(--success)}.integration.disconnected,.integration.error{border-color:var(--error)}.integration.hasChanges{border-color:var(--warning)}.integration .header{margin-bottom:.75rem;padding-bottom:.75rem;border-bottom:2px solid var(--base-200)}.integration h3{letter-spacing:1px;font-size:var(--medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--small)}.integration .setup{font-size:var(--small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--medium)}.integration .connected .indicator,.integration .setup .connected{color:var(--success)}.integration .disconnected .indicator,.integration .setup .disconnected{color:var(--error)}.integration.hasChanges .disconnected{color:var(--warning)}.connection-status.connected{background-color:var(--successBack);color:var(--successText)}.connection-status.disconnected{background-color:var(--errorBack);color:var(--errorText)}.integration code{display:inline-block;width:90%;margin:0 .5rem;user-select:all;padding:.75rem;border:2px solid var(--base);background-color:var(--base-200);word-break:break-all}.integration details+details{margin-top:1rem}.integration .actions{margin-top:1rem}.hint{line-height:1.2;font-style:italic;font-size:var(--small)}.hasChanges button[data-action=save_credentials]{border-color:var(--warning);animation:pulse-color 1s infinite;animation-delay:1s}.flash{animation:flash .5s}.flash.connected{--b:var(--success)}.flash.disconnected{--b:var(--error)}.flash.syncing{--b:var(--success)}.flash.error,.flash.hasChanges{--b:var(--warning)}@keyframes flash{0%,100%{border-color:inherit}50%{border-color:var(--b)}}.location.field{width:80vw}.location.field>p{text-align:center}.location.field>p+p{margin:0 .5rem 0 0}.location.field .location-map{height:20vh}.location.field .location-links{padding:.5rem 0;display:flex;justify-content:space-evenly}.field.upload [data-upload-id],.item-grid .item{touch-action:none}.empty-state{grid-column:1/-1;padding:1rem 10vw;margin:0 10vw;border-radius:var(--outerRadius);background-color:var(--base-100)}.jvb-oauth-connect{position:relative;transition:opacity .2s}.jvb-oauth-connect.loading{opacity:.6;pointer-events:none}.jvb-oauth-connect.loading::after{content:'';position:absolute;right:-30px;top:50%;transform:translateY(-50%);width:16px;height:16px;border:2px solid #ccc;border-top-color:#0073aa;border-radius:50%;animation:oauth-spin .8s linear infinite}@keyframes oauth-spin{to{transform:translateY(-50%) rotate(360deg)}}.integration-status-message{padding:12px 16px;margin:16px 0;border-radius:4px;display:none;font-size:14px;line-height:1.5}.integration-status-message.success{display:block;background:#d4edda;color:#155724;border-left:4px solid #28a745}.integration-status-message.error{display:block;background:#f8d7da;color:#721c24;border-left:4px solid #dc3545}.integration-status-message.info{display:block;background:#d1ecf1;color:#0c5460;border-left:4px solid #17a2b8}.connection-status{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border-radius:4px;font-size:13px;font-weight:500}.connection-status.connected{background:#d4edda;color:#155724}.connection-status.disconnected{background:#f8d7da;color:#721c24}.status-indicator{font-size:10px;line-height:1}.connection-status.connected .status-indicator{color:#28a745}.connection-status.disconnected .status-indicator{color:#dc3545} |
| | |
| | | details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .star+.star,[type=radio].featured:checked+label .star{display:none}[type=radio].featured+label .star,[type=radio].featured:checked+label .star+.star{display:inline-block}.restore.item,.upload.item{border-radius:var(--innerRadius);overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--innerRadius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--small)}form nav.tabs button h2{font-size:var(--small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{--height:fit-content;padding:.5rem} |
| | | details.uploader .file-upload-container{margin:1rem 0;max-width:100%}@media (min-width:768px){details.uploader .file-upload-container{margin:1rem var(--mr) 1rem var(--ml);max-width:var(--maxWidth)}}.file-upload-wrapper{border:2px dashed var(--action-0);border-radius:4px;padding:2rem;text-align:center;transition:all .3s ease;background:rgba(var(--action-rgb),var(--rgb-subtle));position:relative;cursor:pointer}.file-upload-wrapper h2{margin:0!important;font-size:var(--large)}.dragover,.file-upload-wrapper:hover{background:rgba(var(--action-rgb),var(--rgb-subtle-hover));border-color:var(--action-0)!important}.file-upload-wrapper input[type=file]{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer}.file-upload-text{color:var(--contrast);margin:0;font-family:var(--body)}.file-upload-text strong{color:var(--action-0);text-decoration:underline}.field.upload:has(.upload-item) .file-upload-container{display:none}.field.upload{position:relative}.field.upload:not(.uploading) .progress{display:none}.field.upload .actions{position:absolute;top:0;right:0}.item-grid.group,.item-grid.preview,.item-grid.restore{grid-template-columns:repeat(3,1fr)}.item-grid.group .item,.item-grid.preview .item,.item-grid.restore .item{display:block}.item-grid.group button,.item-grid.preview button,.item-grid.restore button{padding:.25rem .5rem}.item-grid.group button .icon,.item-grid.preview button .icon,.item-grid.restore button .icon{--w:1.1em}.item-grid.group .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.preview .item .preview>input[type=checkbox]:not(.label-button)+label,.item-grid.restore .item .preview>input[type=checkbox]:not(.label-button)+label{padding-left:0;margin:0}.item-grid.group .item .preview>input[type=checkbox]+label:before,.item-grid.preview .item .preview>input[type=checkbox]+label:before,.item-grid.restore .item .preview>input[type=checkbox]+label:before{transform:unset;top:.5rem;left:.5rem}.item-grid.group .item .preview>input[type=checkbox]+label::after,.item-grid.preview .item .preview>input[type=checkbox]+label::after,.item-grid.restore .item .preview>input[type=checkbox]+label::after{top:.5rem;left:.75rem;transform:translateY(20%) rotate(45deg)}.item-grid.group .item .item-actions,.item-grid.preview .item .item-actions,.item-grid.restore .item .item-actions{position:absolute;top:0;right:0}.item-grid.group summary,.item-grid.preview summary,.item-grid.restore summary{padding:.5rem}.item-grid.group:has([type=checkbox]:checked),.item-grid.preview:has([type=checkbox]:checked),.item-grid.restore:has([type=checkbox]:checked){padding:1rem;background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}.item-grid.group:has([type=checkbox]:checked) .item,.item-grid.preview:has([type=checkbox]:checked) .item,.item-grid.restore:has([type=checkbox]:checked) .item{padding:.75rem;opacity:.8}.item-grid.group:has([type=checkbox]:checked) .item img,.item-grid.preview:has([type=checkbox]:checked) .item img,.item-grid.restore:has([type=checkbox]:checked) .item img{filter:var(--filter)}.item-grid.group:has([type=checkbox]:checked) details,.item-grid.preview:has([type=checkbox]:checked) details,.item-grid.restore:has([type=checkbox]:checked) details{display:none}.item-grid.group .item:has([type=checkbox]:checked),.item-grid.preview .item:has([type=checkbox]:checked),.item-grid.restore .item:has([type=checkbox]:checked){padding:.5rem;background-color:rgba(var(--action-rgb),var(--rgb-medium));opacity:1}.item-grid.group .item:has([type=checkbox]:checked) img,.item-grid.preview .item:has([type=checkbox]:checked) img,.item-grid.restore .item:has([type=checkbox]:checked) img{filter:none}[type=radio].featured+label .star+.star,[type=radio].featured:checked+label .star{display:none}[type=radio].featured+label .star,[type=radio].featured:checked+label .star+.star{display:inline-block}.restore.restore.item,.upload.upload.item{border-radius:var(--innerRadius);aspect-ratio:unset;overflow:hidden;background:var(--base);border:1px solid var(--base-200)}.restore-item [for=select-item],.upload.item [for=select-item]{aspect-ratio:1}.upload.item:has(details[open]){grid-column:1/-1}.restore.item img,.upload.item img{transition:transform var(--transition-base)}.restore.item:hover img,.upload.item:hover img{transform:scale(1.02);transition:transform var(--transition-base)}.upload-group{background-image:var(--dashed-action);padding:5px;border-radius:var(--innerRadius);background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.submit-uploads{position:fixed;bottom:var(--offHeight);right:var(--offHeight);z-index:var(--z-6);height:var(--height);box-shadow:var(--shadow);border-radius:var(--innerRadius);animation:pulse-color 5s infinite;animation-delay:1s;background-color:var(--action-0);color:var(--action-contrast)}.submit-uploads:hover{background-color:var(--base-200);color:var(--contrast-200)}.empty-group{grid-column:1/-1;padding:20px;background-image:var(--dashed-action);border-radius:var(--innerRadius);margin:10px 0;cursor:pointer;transition:all var(--transition-base);text-align:center;background-color:rgba(var(--action-rgb),var(--rgb-subtle))}.group-display:not([hidden])~.file-upload-container{display:none}.dragging,.upload.item.dragging{opacity:.7;transform:scale(.95) rotate(3deg);z-index:var(--z-top);box-shadow:0 8px 25px rgba(0,0,0,.3)}.dragover{background:rgba(var(--action-rgb),var(--rgb-light))!important;border-color:var(--action-0)!important;transform:scale(1.05);animation:drop-pulse .8s infinite ease-in-out}.drag-preview{position:fixed;z-index:var(--zz-top);width:fit-content;overflow:visible;pointer-events:none;opacity:.9;transform:scale(1.05);transition:transform .2s ease}.drag-preview .drag-items{width:max-content;height:max-content;position:relative}.drag-preview .drag-items .drag-item{width:120px;height:120px;position:absolute;top:0;left:0;background:var(--base);border-radius:var(--outerRadius);box-shadow:var(--shadow)}.drag-preview .drag-items .drag-item:nth-child(1){transform:rotate(-3deg);z-index:3}.drag-preview .drag-items .drag-item:nth-child(2){left:8px;top:-4px;transform:rotate(4deg);z-index:2;transition-delay:30ms}.drag-preview .drag-items .drag-item:nth-child(3){left:-6px;top:-8px;transform:rotate(-5deg);z-index:1;transition-delay:60ms}.drag-preview .drag-items .drag-item:nth-child(4){left:12px;top:-12px;transform:rotate(3deg);z-index:0;transition-delay:90ms}.drag-preview .drag-items .drag-item:nth-child(n+5){left:-10px;top:-16px;transform:rotate(-4deg);z-index:0;opacity:.8}.drag-preview .drag-items img,.drag-preview .drag-items video{width:100%;height:100%;object-fit:cover;display:block}.drag-preview .drag-count{position:absolute;top:-8px;right:-8px;background:var(--base-200);color:var(--contrast);border-radius:50%;width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;box-shadow:var(--shadow);z-index:var(--z-3)}.item.dragging{opacity:.5;transform:scale(.95);filter:grayscale(50%);transition:opacity .2s ease,transform .2s ease,filter .2s ease}@keyframes drop-pulse{0%,100%{background-color:rgba(var(--action-rgb),var(--rgb-light));transform:scale(1.02)}50%{background-color:var(rgba(var(--action-rgb),var(--rgb-medium)));transform:scale(1.04)}}.group-actions{display:flex;gap:.25rem}@media (max-width:767px){body:not(.uploading):has(.group-display:not([hidden])){overflow:hidden}body:not(.uploading):has(.group-display:not([hidden])) .qtoggle{z-index:var(--z-1)}.group-display.group-display{position:fixed;top:var(--height);bottom:var(--height);left:0;right:0;max-height:var(--maxHeight);overflow:hidden;z-index:var(--z-6);width:calc(100% - 1rem);height:calc(100% - 1rem);padding:0 0 3rem;--justify:flex-start;--align:flex-start;--gap:0}.group-display::before{content:'';display:block;z-index:-1;top:-.5rem;bottom:-.5rem;left:-.5rem;right:-.5rem;position:absolute;background-color:rgba(var(--base-rgb),var(--rgb-heavy));filter:blur(5px)}.group-display .preview-wrap,.group-display .sidebar{height:50%;overflow:hidden auto;position:relative;padding:.5rem}.group-display .preview-wrap{top:0}.group-display .preview-wrap .selected{display:flex;justify-content:space-between;align-items:center}.group-display .sidebar{bottom:0;flex-wrap:nowrap;overflow:hidden auto;background-color:var(--contrast-200);color:var(--base)}.group-display .sidebar>.hint{color:var(--contrast)}.group-display .sidebar .header{display:none}.group-display .preview-actions{top:0;flex-shrink:0}.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{bottom:0;margin:0;text-align:center}.group-display .preview-actions,.group-display .preview-wrap>.hint,.group-display .sidebar>.hint{position:absolute;left:0;right:0;background-color:rgba(var(--base-rgb),var(--rgb-heavy));z-index:var(--z-3);box-shadow:var(--shadow)}.group-display .item-grid{height:100%;overflow:hidden auto;grid-template-columns:repeat(3,1fr);padding:2rem 0}.group-display .sidebar>.item-grid{grid-template-columns:repeat(1,1fr);gap:1rem;padding:0}.group-display .sidebar .empty-group{order:0;position:sticky;height:fit-content;top:0;z-index:var(--z-3);background-color:rgba(var(--action-rgb),var(--rgb-heavy))}.group-display .sidebar .upload-group{order:1}.group-display .sidebar .empty-group p{margin:0}.group-display .field,.group-display .field label{margin:0;padding:0}.group-display .sidebar h4{margin:.25rem}.group-display .item{width:100%;height:max-content}.submit-uploads{bottom:var(--height);left:0;right:0;width:100%;height:3rem}body.uploading .group-display.group-display{position:relative;top:unset;bottom:unset;right:unset;left:unset}}@media (min-width:768px){.group-display.group-display{--wrap:nowrap;--dir:row;--gap:1rem;--align:flex-start}.group-display .preview-wrap,.group-display .sidebar{--justify:flex-start;max-height:calc(100vh - var(--doubleHeight));overflow:hidden auto}.group-display .preview-wrap,.group-display .sidebar{width:50%}.preview-actions,.preview-wrap .hint{position:sticky;z-index:var(--z-3);box-shadow:var(--shadow);background-color:var(--base);width:100%}.preview-actions{top:0;left:0;right:0}.preview-actions .field{margin:0}.preview-wrap .hint,.sidebar>.hint{bottom:-1rem;padding-bottom:1rem;margin:0;left:0;right:0;text-align:center}}.restore-uploads{position:fixed;top:var(--offHeight);bottom:var(--offHeight);left:1rem;right:1rem;border-radius:var(--outerRadius);padding:1rem;z-index:var(--z-top);box-shadow:var(--shadow);background-color:var(--base-200);overflow:hidden auto}dialog nav.tabs{position:sticky;top:0;background-color:var(--base-50);z-index:var(--z-6);box-shadow:var(--shadow-down);margin-bottom:2rem}.editor-container .ql-toolbar{display:flex;background-color:var(--base-50);justify-content:flex-start;flex-wrap:wrap;padding:.25rem;gap:.5rem 1rem;border-top-left-radius:var(--innerRadius);border-top-right-radius:var(--innerRadius);border-bottom:4px solid var(--base-50)}.ql-toolbar .ql-formats{display:flex;gap:.25rem}.editor-container .ql-container{--padding:1rem;background-color:var(--base);border-bottom-left-radius:var(--innerRadius);border-bottom-right-radius:var(--innerRadius);height:fit-content;padding:2px;border:1px solid var(--base-200)}.editor-container .ql-container .ql-editor{padding:var(--padding);width:100%;height:100%}.ql-editor img{max-width:50%;height:auto}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-hidden{display:none}.ql-tooltip{position:absolute;transform:translateY(10px);background-color:var(--base-100);border:1px solid var(--base);box-shadow:0 0 5px var(--overlay-heavy);color:var(--contrast);padding:5px 12px;white-space:nowrap}[data-type=single] .item-grid{display:flex}.repeater-row details summary::after{margin-left:0}.repeater-row details summary button{margin-left:auto}/*!* Group actions buttons - more visible *!*//*!* Group item grid - distinct from preview grid *!*//*!* Group count hint *!*//*!* ============================================================================*//*!* Base drag preview *!*//*!* Single item drag preview *!*//*!* Multi-item drag preview container *!*//*!* Items being dragged - reduce opacity on originals *!*//*!* Count badge on multi-item preview *!*//*!* ============================================================================*//*!* Ensure progress bar is visible when needed *!*//*!* Progress bar track *!*//*!* Progress bar fill *!*//*!* Progress details - styled for row layout with text and count *!*//*!* Individual item progress - overlay style *!*//*!* Item progress icon and status text *!*//*!* ============================================================================*//*!* Hide uploader when we have uploads *!*//*!* Show group display when we have uploads *!*//*!* ============================================================================*//*!* Selected items - more obvious *!*//*!* Selection checkbox - always visible on hover or when checked *!*//*!* Selection controls - more prominent *!*//*!* ============================================================================*//*!* Smooth dragover animation *!*//*!* ============================================================================*//*!* ============================================================================*//*!* Notification container - fixed overlay *!*//*!* Content card *!*//*!* Message section *!*//*!* Scrollable field list *!*//*!* Item grid for restore preview *!*//*!* Restore item *!*//*!* Checked state *!*//*!* Preview section *!*//*!* Item info *!*//*!* Checkbox controls *!*//*!* Actions section *!*//*!* Selection controls *!*//*!* Action buttons *!*//*!* Restore button - primary action *!*//*!* Scrap cache button - destructive action *!*//*!* Dismiss button - secondary action *!*//*!* Mobile responsive *!*//*!* Animation *!*//*!* Scrollbar styling for restore field list *!*/form{--step-size:2.5rem}.form-progress{padding:0 1rem}.form-progress .progress{background:var(--base-100);border-radius:var(--innerRadius);padding:1rem}.form-progress .bar{height:6px;background:var(--base-200);border-radius:3px;overflow:hidden;margin-bottom:.5rem}.form-progress .fill{height:100%;background:linear-gradient(90deg,var(--action-0),var(--action-200));width:0%;transition:width .4s ease;border-radius:3px}.form-progress .step-text{font-size:var(--small);font-weight:600;color:var(--contrast-200)}form nav.tabs{position:relative;top:0;left:0;right:0;padding:1rem 0;gap:0;z-index:0}form nav.tabs button{position:relative;background:0 0;border:none;padding:.5rem 1rem .5rem 3rem;z-index:1}form nav.tabs .step-number{width:2.5rem;height:100%;border-radius:50% 0 0 50%;position:absolute;left:0;top:0;background:var(--base-200);color:var(--contrast-50);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--small);border:3px solid var(--base)}form nav.tabs button.pending .step-number{background:var(--base-100);color:var(--contrast-200)}form nav.tabs button.active .step-number,form nav.tabs button.current .step-number{background:var(--action-0);color:var(--action-contrast);border-color:var(--action-200)}form nav.tabs button.completed .step-number{background:var(--successBack);color:var(--successBack);border-color:var(--successText)}form nav.tabs button.completed .step-number::before{content:'✓';font-size:1.2rem;color:var(--successText);position:absolute}form nav.tabs button.completed h2{color:var(--contrast-200)}.step-navigation{margin-top:2rem;padding-top:2rem;border-top:1px solid var(--base-200);gap:1rem}.step-navigation .prev-step{background:var(--base-100)}.step-navigation .next-step,.step-navigation button[type=submit]{margin-left:auto}.field input.error,.field select.error,.field textarea.error{border-color:var(--errorBack)}.error-message{color:var(--errorText);font-size:var(--small);margin-top:.25rem;display:block}@media (max-width:768px){form nav.tabs button{min-width:80px;font-size:var(--small)}form nav.tabs button h2{font-size:var(--small)}form{--step-size:2rem}}.field-input-wrapper{position:relative;display:flex;align-items:center;gap:.5rem}.field-input-wrapper input,.field-input-wrapper select,.field-input-wrapper textarea{flex:1}.validation-icon{display:flex;align-items:center;justify-content:center;font-size:1.25rem;animation:scaleIn .3s ease;--w:1.25rem}.validation-icon.error{color:var(--error)}.validation-icon.success{color:var(--success)}@keyframes scaleIn{from{transform:scale(0);opacity:0}to{transform:scale(1);opacity:1}}.validation-message{color:var(--error-0);font-size:var(--small);margin-top:.25rem;display:block;animation:slideDown .2s ease}@keyframes slideDown{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.field.has-error input,.field.has-error select,.field.has-error textarea{border-color:var(--error);background-color:var(--errorBack)}.field.has-error input:focus,.field.has-error select:focus,.field.has-error textarea:focus{outline-color:var(--error);box-shadow:0 0 0 3px rgba(var(--error-rgb),.2)}.field.has-success input,.field.has-success select,.field.has-success textarea{border-color:var(--success)}.field label .required{color:var(--error);margin-left:.25rem}.form-summary{padding:2rem;border-radius:8px;margin-top:2rem;border:2px dashed var(--contrast-200)}.form-summary .message{margin-bottom:2rem}.form-summary .result+.result{position:relative;margin-top:1.5rem;padding-top:1.5rem}.form-summary .result+.result::before{position:absolute;top:0;left:16.5%;content:'';width:67%;height:1px;border-bottom:1px solid var(--base-200)}.form-summary h2{margin:1rem 0}.form-summary h4{background-color:var(--base-100);padding:.5rem 2rem;position:relative;left:-2rem;color:var(--contrast-200);font-size:.875rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:.75rem}.form-summary p{color:var(--text);margin:0}.group-summary,.repeater-summary{background:var(--base-100);padding:1rem;border-radius:4px;margin-top:.5rem}.repeater-row{margin-bottom:1rem}.repeater-row:last-child{margin-bottom:0}.ql-toolbar button{--height:fit-content;padding:.5rem}.success-message{color:var(--success,#16a34a);background-color:var(--success-bg,#f0fdf4);border:1px solid var(--success,#16a34a);padding:.75rem 1rem;border-radius:var(--radius);margin-bottom:1rem;display:flex;align-items:center;gap:.5rem}.success-message .success-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.success-box{background-color:var(--success-bg,#f0fdf4);border:2px solid var(--success,#16a34a);padding:1.5rem;border-radius:var(--outerRadius);margin-bottom:1rem;text-align:center}.success-box h3{color:var(--success,#16a34a);margin-bottom:.5rem}.success-box p{margin:.5rem 0}.form-success{opacity:.9}.form-success .field:not(.form-success-message):not(.success-box){display:none}.form-success button[type=submit]{opacity:.6;pointer-events:none}.field-error input,.field-error select,.field-error textarea{border-color:var(--error,#dc2626)}.error-message{color:var(--error,#dc2626);font-size:var(--small);margin-top:.25rem;display:block}.form-error{background-color:var(--error-bg,#fee);border:1px solid var(--error,#dc2626);padding:.75rem;border-radius:var(--radius);margin-bottom:1rem}.has-success input,.has-success select,.has-success textarea{border-color:var(--success,#16a34a)}.form-error{display:flex;align-items:center;gap:.5rem}.form-error .error-icon{width:1.25rem;height:1.25rem;flex-shrink:0}.autocomplete-dropdown{width:100%;background-color:var(--base-100);padding:.5rem;box-shadow:var(--shadow)} |
| New file |
| | |
| | | .icon-house{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSwxMTAuMWwtODAtODBhMTQsMTQsMCwwLDAtMTkuOCwwbC04MCw4MEExMy45MiwxMy45MiwwLDAsMCwzNCwxMjB2OTZhNiw2LDAsMCwwLDYsNmg2NGE2LDYsMCwwLDAsNi02VjE1OGgzNnY1OGE2LDYsMCwwLDAsNiw2aDY0YTYsNiwwLDAsMCw2LTZWMTIwQTEzLjkyLDEzLjkyLDAsMCwwLDIxNy45LDExMC4xWk0yMTAsMjEwSDE1OFYxNTJhNiw2LDAsMCwwLTYtNkgxMDRhNiw2LDAsMCwwLTYsNnY1OEg0NlYxMjBhMiwyLDAsMCwxLC41OC0xLjQybDgwLTgwYTIsMiwwLDAsMSwyLjg0LDBsODAsODBBMiwyLDAsMCwxLDIxMCwxMjBaIi8+PC9zdmc+');}.icon-sun-dim{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMzJhNiw2LDAsMCwxLDEyLDB2OGE2LDYsMCwwLDEtMTIsMFptNjgsODhhNjIsNjIsMCwxLDEtNjItNjJBNjIuMDcsNjIuMDcsMCwwLDEsMTkwLDEyOFptLTEyLDBhNTAsNTAsMCwxLDAtNTAsNTBBNTAuMDYsNTAuMDYsMCwwLDAsMTc4LDEyOFpNNTkuNzYsNjguMjRhNiw2LDAsMSwwLDguNDgtOC40OGwtOC04YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwxLDAtOC40OC04LjQ4Wm0xMzYtMTM2LTgsOGE2LDYsMCwxLDAsOC40OCw4LjQ4bDgtOGE2LDYsMCwwLDAtOC40OC04LjQ4Wm0uNDgsMTM2YTYsNiwwLDAsMC04LjQ4LDguNDhsOCw4YTYsNiwwLDAsMCw4LjQ4LTguNDhaTTQwLDEyMkgzMmE2LDYsMCwwLDAsMCwxMmg4YTYsNiwwLDAsMCwwLTEyWm04OCw4OGE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LThBNiw2LDAsMCwwLDEyOCwyMTBabTk2LTg4aC04YTYsNiwwLDAsMCwwLDEyaDhhNiw2LDAsMCwwLDAtMTJaIi8+PC9zdmc+');}.icon-moon{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==');}.icon-sign-out{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMTgsMjE2YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLTYtNlY0MGE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDU0VjIxMGg1OEE2LDYsMCwwLDEsMTE4LDIxNlptMTEwLjI0LTkyLjI0LTQwLTQwYTYsNiwwLDAsMC04LjQ4LDguNDhMMjA5LjUxLDEyMkgxMTJhNiw2LDAsMCwwLDAsMTJoOTcuNTFsLTI5Ljc1LDI5Ljc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsNDAtNDBBNiw2LDAsMCwwLDIyOC4yNCwxMjMuNzZaIi8+PC9zdmc+');}.icon-star{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=');}.icon-plus-square{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY0OEExNCwxNCwwLDAsMCwyMDgsMzRabTIsMTc0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWm0tMzYtODBhNiw2LDAsMCwxLTYsNkgxMzR2MzRhNiw2LDAsMCwxLTEyLDBWMTM0SDg4YTYsNiwwLDAsMSwwLTEyaDM0Vjg4YTYsNiwwLDAsMSwxMiwwdjM0aDM0QTYsNiwwLDAsMSwxNzQsMTI4WiIvPjwvc3ZnPg==');}.icon-trash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBIMTc0VjQwYTIyLDIyLDAsMCwwLTIyLTIySDEwNEEyMiwyMiwwLDAsMCw4Miw0MFY1MEg0MGE2LDYsMCwwLDAsMCwxMkg1MFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRIMTkyYTE0LDE0LDAsMCwwLDE0LTE0VjYyaDEwYTYsNiwwLDAsMCwwLTEyWk05NCw0MGExMCwxMCwwLDAsMSwxMC0xMGg0OGExMCwxMCwwLDAsMSwxMCwxMFY1MEg5NFpNMTk0LDIwOGEyLDIsMCwwLDEtMiwySDY0YTIsMiwwLDAsMS0yLTJWNjJIMTk0Wk0xMTAsMTA0djY0YTYsNiwwLDAsMS0xMiwwVjEwNGE2LDYsMCwwLDEsMTIsMFptNDgsMHY2NGE2LDYsMCwwLDEtMTIsMFYxMDRhNiw2LDAsMCwxLDEyLDBaIi8+PC9zdmc+');}.icon-cloud-arrow-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xODguMjQsMTY0LjI0YTYsNiwwLDAsMS04LjQ4LDBMMTU4LDE0Mi40OVYyMDhhNiw2LDAsMCwxLTEyLDBWMTQyLjQ5bC0yMS43NiwyMS43NWE2LDYsMCwwLDEtOC40OC04LjQ4bDMyLTMyYTYsNiwwLDAsMSw4LjQ4LDBsMzIsMzJBNiw2LDAsMCwxLDE4OC4yNCwxNjQuMjRaTTE2MCw0MkE4Ni4xLDg2LjEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDQwYTYsNiwwLDAsMCwwLTEySDcyYTUwLDUwLDAsMCwxLDAtMTAwLDUwLjY4LDUwLjY4LDAsMCwxLDUuOTEuMzZBODUuNTQsODUuNTQsMCwwLDAsNzQsMTI4YTYsNiwwLDAsMCwxMiwwLDc0LDc0LDAsMSwxLDEwMy42LDY3Ljg1LDYsNiwwLDAsMCw0LjgsMTFBODYsODYsMCwwLDAsMTYwLDQyWiIvPjwvc3ZnPg==');}.icon-arrow-elbow-left-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTkyYTYsNiwwLDAsMS02LDZIODhhNiw2LDAsMCwxLTYtNlY2Mi40OUw0NC4yNCwxMDAuMjRhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4YTYsNiwwLDEsMS04LjQ4LDguNDhMOTQsNjIuNDlWMTg2SDIzMkE2LDYsMCwwLDEsMjM4LDE5MloiLz48L3N2Zz4=');}.icon-arrow-elbow-right-up{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMjQsMTAwLjI0YTYsNiwwLDAsMS04LjQ4LDBMMTc0LDYyLjQ5VjE5MmE2LDYsMCwwLDEtNiw2SDI0YTYsNiwwLDAsMSwwLTEySDE2MlY2Mi40OWwtMzcuNzYsMzcuNzVhNiw2LDAsMCwxLTguNDgtOC40OGw0OC00OGE2LDYsMCwwLDEsOC40OCwwbDQ4LDQ4QTYsNiwwLDAsMSwyMjAuMjQsMTAwLjI0WiIvPjwvc3ZnPg==');}.icon-x{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+');}.icon-magnifying-glass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMjE5Ljc2bC01MS4zOC01MS4zOGE4Ni4xNSw4Ni4xNSwwLDEsMC04LjQ4LDguNDhsNTEuMzgsNTEuMzhhNiw2LDAsMCwwLDguNDgtOC40OFpNMzgsMTEyYTc0LDc0LDAsMSwxLDc0LDc0QTc0LjA5LDc0LjA5LDAsMCwxLDM4LDExMloiLz48L3N2Zz4=');}.icon-squares-four{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDQsNDJINTZBMTQsMTQsMCwwLDAsNDIsNTZ2NDhhMTQsMTQsMCwwLDAsMTQsMTRoNDhhMTQsMTQsMCwwLDAsMTQtMTRWNTZBMTQsMTQsMCwwLDAsMTA0LDQyWm0yLDYyYTIsMiwwLDAsMS0yLDJINTZhMiwyLDAsMCwxLTItMlY1NmEyLDIsMCwwLDEsMi0yaDQ4YTIsMiwwLDAsMSwyLDJabTk0LTYySDE1MmExNCwxNCwwLDAsMC0xNCwxNHY0OGExNCwxNCwwLDAsMCwxNCwxNGg0OGExNCwxNCwwLDAsMCwxNC0xNFY1NkExNCwxNCwwLDAsMCwyMDAsNDJabTIsNjJhMiwyLDAsMCwxLTIsMkgxNTJhMiwyLDAsMCwxLTItMlY1NmEyLDIsMCwwLDEsMi0yaDQ4YTIsMiwwLDAsMSwyLDJabS05OCwzNEg1NmExNCwxNCwwLDAsMC0xNCwxNHY0OGExNCwxNCwwLDAsMCwxNCwxNGg0OGExNCwxNCwwLDAsMCwxNC0xNFYxNTJBMTQsMTQsMCwwLDAsMTA0LDEzOFptMiw2MmEyLDIsMCwwLDEtMiwySDU2YTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJoNDhhMiwyLDAsMCwxLDIsMlptOTQtNjJIMTUyYTE0LDE0LDAsMCwwLTE0LDE0djQ4YTE0LDE0LDAsMCwwLDE0LDE0aDQ4YTE0LDE0LDAsMCwwLDE0LTE0VjE1MkExNCwxNCwwLDAsMCwyMDAsMTM4Wm0yLDYyYTIsMiwwLDAsMS0yLDJIMTUyYTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJoNDhhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-rows{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMTM4SDQ4YTE0LDE0LDAsMCwwLTE0LDE0djQwYTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFYxNTJBMTQsMTQsMCwwLDAsMjA4LDEzOFptMiw1NGEyLDIsMCwwLDEtMiwySDQ4YTIsMiwwLDAsMS0yLTJWMTUyYTIsMiwwLDAsMSwyLTJIMjA4YTIsMiwwLDAsMSwyLDJaTTIwOCw1MEg0OEExNCwxNCwwLDAsMCwzNCw2NHY0MGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjA4LDUwWm0yLDU0YTIsMiwwLDAsMS0yLDJINDhhMiwyLDAsMCwxLTItMlY2NGEyLDIsMCwwLDEsMi0ySDIwOGEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-table{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBaTTM4LDExMEg4MnYzNkgzOFptNTYsMEgyMTh2MzZIOTRaTTIxOCw2MlY5OEgzOFY2MlpNMzgsMTkyVjE1OEg4MnYzNkg0MEEyLDIsMCwwLDEsMzgsMTkyWm0xNzgsMkg5NFYxNThIMjE4djM0QTIsMiwwLDAsMSwyMTYsMTk0WiIvPjwvc3ZnPg==');}.icon-infinity{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDYsMTI4YTU0LDU0LDAsMCwxLTkyLjE4LDM4LjE4LDMuMDcsMy4wNywwLDAsMS0uMjUtLjI2bC02MC02Ny43NGE0Miw0MiwwLDEsMCwwLDU5LjY0bDguNTctOS42N2E2LDYsMCwxLDEsOSw4bC04LjY5LDkuODFhMy4wNywzLjA3LDAsMCwxLS4yNS4yNiw1NCw1NCwwLDEsMSwwLTc2LjM2LDMuMDcsMy4wNywwLDAsMSwuMjUuMjZsNjAsNjcuNzRhNDIsNDIsMCwxLDAsMC01OS42NGwtOC41Nyw5LjY3YTYsNiwwLDEsMS05LThsOC42OS05LjgxYTMuMDcsMy4wNywwLDAsMSwuMjUtLjI2QTU0LDU0LDAsMCwxLDI0NiwxMjhaIi8+PC9zdmc+');}.icon-eye{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDUuNDgsMTI1LjU3Yy0uMzQtLjc4LTguNjYtMTkuMjMtMjcuMjQtMzcuODFDMjAxLDcwLjU0LDE3MS4zOCw1MCwxMjgsNTBTNTUsNzAuNTQsMzcuNzYsODcuNzZjLTE4LjU4LDE4LjU4LTI2LjksMzctMjcuMjQsMzcuODFhNiw2LDAsMCwwLDAsNC44OGMuMzQuNzcsOC42NiwxOS4yMiwyNy4yNCwzNy44QzU1LDE4NS40Nyw4NC42MiwyMDYsMTI4LDIwNnM3My0yMC41Myw5MC4yNC0zNy43NWMxOC41OC0xOC41OCwyNi45LTM3LDI3LjI0LTM3LjhBNiw2LDAsMCwwLDI0NS40OCwxMjUuNTdaTTEyOCwxOTRjLTMxLjM4LDAtNTguNzgtMTEuNDItODEuNDUtMzMuOTNBMTM0Ljc3LDEzNC43NywwLDAsMSwyMi42OSwxMjgsMTM0LjU2LDEzNC41NiwwLDAsMSw0Ni41NSw5NS45NEM2OS4yMiw3My40Miw5Ni42Miw2MiwxMjgsNjJzNTguNzgsMTEuNDIsODEuNDUsMzMuOTRBMTM0LjU2LDEzNC41NiwwLDAsMSwyMzMuMzEsMTI4QzIyNi45NCwxNDAuMjEsMTk1LDE5NCwxMjgsMTk0Wm0wLTExMmE0Niw0NiwwLDEsMCw0Niw0NkE0Ni4wNiw0Ni4wNiwwLDAsMCwxMjgsODJabTAsODBhMzQsMzQsMCwxLDEsMzQtMzRBMzQsMzQsMCwwLDEsMTI4LDE2MloiLz48L3N2Zz4=');}.icon-eye-closed{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMjEsMTY1YTYsNiwwLDAsMS0xMC40Miw2bC0yMC0zNS4wOGExMjIsMTIyLDAsMCwxLTM5LDE4LjA5bDYuMTcsMzdhNiw2LDAsMCwxLTQuOTMsNi45MSw2Ljg1LDYuODUsMCwwLDEtMSwuMDgsNiw2LDAsMCwxLTUuOTEtNUwxNDgsMTU2LjQ0YTEyOC44NiwxMjguODYsMCwwLDEtNDAsMEwxMDEuOTIsMTkzQTYsNiwwLDAsMSw5NiwxOThhNi44NSw2Ljg1LDAsMCwxLTEtLjA4QTYsNiwwLDAsMSw5MC4wOCwxOTFsNi4xNy0zN2ExMjIsMTIyLDAsMCwxLTM5LTE4LjA5TDM3LjIxLDE3MWE2LDYsMCwxLDEtMTAuNDItNmwyMC44NS0zNi40OGExNTIsMTUyLDAsMCwxLTIwLjMxLTIwLjc3LDYsNiwwLDAsMSw5LjM0LTcuNTRDNTMuNTQsMTIxLjExLDgzLjA3LDE0NiwxMjgsMTQ2czc0LjQ2LTI0Ljg5LDkxLjMzLTQ1Ljc3YTYsNiwwLDAsMSw5LjM0LDcuNTQsMTUyLDE1MiwwLDAsMS0yMC4zMSwyMC43N1oiLz48L3N2Zz4=');}.icon-calendar{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==');}.icon-alphabetical{--icon:url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9ImN1cnJlbnRDb2xvciIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMTgzLjc4IDE4NC4wNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtNTkuNTg2IDY5Ljc0MmMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAyMjktMC41Mjk1OCAxLjk0OTNzMC4xNzkwMyAxLjU5MzcgMC41Mjk1OCAxLjk5NDRjMC4zNzU1OCAwLjM3NTU4IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGg3LjAxOTdsLTEyLjQyOCAzNC4zNjZoLTIuMTA3Yy0wLjg1MTMgMC0xLjQ2MSAwLjE5NjU2LTEuODM2NiAwLjU5NzE4LTAuMzUwNTQgMC4zNzU1OC0wLjUyOTU3IDEuMDM0MS0wLjUyOTU3IDEuOTYwNiAwIDAuOTI2NDQgMC4xNzkwMyAxLjU4MjUgMC41Mjk1NyAxLjk4MyAwLjM3NTU4IDAuMzc1NTkgMC45ODUyOSAwLjU2MzM4IDEuODM2NiAwLjU2MzM4aDEyLjU1MmMwLjg1MTMgMCAxLjQ1MjItMC4xODc3OSAxLjgwMjgtMC41NjMzOCAwLjM3NTU4LTAuNDAwNjIgMC41NjMzNy0xLjA1NjYgMC41NjMzNy0xLjk4MyAwLTAuOTI2NDUtMC4xODc3OS0xLjU4NS0wLjU2MzM3LTEuOTYwNi0wLjM1MDU0LTAuNDAwNjItMC45NTE0Ny0wLjU5NzE4LTEuODAyOC0wLjU5NzE4aC00LjU1MjFsMy4xMjExLTguOTM0OWgxOC4yMmwzLjA3NiA4LjkzNDloLTUuMDcwNGMtMC44NTEzIDAtMS40NjEgMC4xOTY1Ni0xLjgzNjYgMC41OTcxOC0wLjM1MDU0IDAuMzc1NTgtMC41Mjk1OCAxLjAzNDEtMC41Mjk1OCAxLjk2MDYgMCAwLjkyNjQ0IDAuMTc5MDMgMS41ODI1IDAuNTI5NTggMS45ODMgMC4zNzU1OCAwLjM3NTU5IDAuOTg1MjkgMC41NjMzOCAxLjgzNjYgMC41NjMzOGgxMy4yOTZjMC44NTEzIDAgMS40NTIyLTAuMTg3NzkgMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzctMS4wNTY2IDAuNTYzMzctMS45ODMgMC0wLjkyNjQ1LTAuMTg3NzktMS41ODUtMC41NjMzNy0xLjk2MDYtMC4zNTA1NC0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOGgtMi4yODczbC0xMy4yNjItMzcuMDM2Yy0wLjMwMDQ3LTAuODUxMy0wLjc1OTk0LTEuNDYxLTEuMzg1OS0xLjgzNjYtMC42MDA5My0wLjQwMDYyLTEuNDA5Ny0wLjU5NzE4LTIuNDExMy0wLjU5NzE4em00NC4xNDYgMGMtMC44NTEzIDAtMS40NzIzIDAuMTk2NTYtMS44NDc4IDAuNTk3MTgtMC4zNTA1NSAwLjM3NTU4LTAuNTE4MyAxLjAyMjktMC41MTgzIDEuOTQ5M3YxMS45MWMwIDAuODc2MzMgMC4yMDUzMiAxLjUwNjEgMC42MzA5OCAxLjg4MTcgMC40MjU2NiAwLjM3NTU4IDEuMTU5MyAwLjU2MzM3IDIuMTg1OSAwLjU2MzM3czEuNzQ5LTAuMTg3NzkgMi4xNzQ3LTAuNTYzMzdjMC40MjU2OS0wLjM3NTU4IDAuNjQyMjYtMS4wMDUzIDAuNjQyMjYtMS44ODE3di05LjM1MTdoMTguODUxbC0yNC43NTQgMzUuMzAxYy0wLjM1MDU0IDAuNTI1ODItMC41MTgzMSAxLjA3MTctMC41MTgzMSAxLjYyMjYgMCAwLjkyNjQ1IDAuMTY3NzcgMS41ODI1IDAuNTE4MzEgMS45ODMxIDAuMzc1NTggMC4zNzU1OCAwLjk5NjU0IDAuNTYzMzggMS44NDc4IDAuNTYzMzhoMjguNzY2YzAuODUxMyAwIDEuNDUyMi0wLjE4NzggMS44MDI4LTAuNTYzMzggMC4zNzU1OC0wLjQwMDYyIDAuNTYzMzgtMS4wNTY2IDAuNTYzMzgtMS45ODMxdi0xMi42NjVjMC0wLjg3NjMzLTAuMjE2NTgtMS40OTQ4LTAuNjQyMjUtMS44NzA0LTAuNDI1NjYtMC4zNzU1OC0xLjE0OC0wLjU2MzM4LTIuMTc0Ny0wLjU2MzM4LTEuMDI2NiAwLTEuNzQ5IDAuMTg3NzktMi4xNzQ3IDAuNTYzMzgtMC40MjU2NiAwLjM3NTU4LTAuNjQyMjQgMC45OTQwMi0wLjY0MjI0IDEuODcwNHYxMC4xMDdoLTE5Ljk3OGwyNC45MDEtMzUuNDU5YzAuMjUwMzktMC4zNTA1NCAwLjM3MTgzLTAuODM4ODMgMC4zNzE4My0xLjQ2NDggMC0wLjkyNjQ1LTAuMTg3OC0xLjU3MzctMC41NjMzOC0xLjk0OTMtMC4zNTA1NS0wLjQwMDYyLTAuOTUxNDctMC41OTcxOC0xLjgwMjgtMC41OTcxOHptLTMxLjc1MiA1LjEwNDJoMC43MDk4NWw2Ljk4NTkgMjAuMzE1aC0xNC43MTZ6bS0zNy43MjMtNDkuMTgzYy00LjczNDIgMC04LjYzMTMgMy44OTctOC42MzEzIDguNjMxM3YxMTUuNDdjMCA0LjczNDIgMy44OTcgOC42MzEzIDguNjMxMyA4LjYzMTNoMTE1LjI2YzQuNzM0MiAwIDguNjQyMS0zLjg5NyA4LjY0MjEtOC42MzEzdi0xMTUuNDdjMC00LjczNDItMy45MDgyLTguNjMxMy04LjY0MjEtOC42MzEzem0wIDUuNzI0aDExNS4yNmMxLjY1OCAwIDIuOTA3IDEuMjQ5MSAyLjkwNyAyLjkwNzF2MTE1LjQ3YzAgMS42NTgtMS4yNDkxIDIuOTA3LTIuOTA3IDIuOTA3aC0xMTUuMjZjLTEuNjU4IDAtMi44OTU4LTEuMjQ5MS0yLjg5NTgtMi45MDd2LTExNS40N2MwLTEuNjU4IDEuMjM3OC0yLjkwNzEgMi44OTU4LTIuOTA3MXoiIGZpbGw9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIuNzIxMTQiLz48L3N2Zz4=');}.icon-sort-ascending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjYsMTI4YTYsNiwwLDAsMS02LDZINDhhNiw2LDAsMCwxLDAtMTJoNzJBNiw2LDAsMCwxLDEyNiwxMjhaTTQ4LDcwSDE4NGE2LDYsMCwwLDAsMC0xMkg0OGE2LDYsMCwwLDAsMCwxMlptNTYsMTE2SDQ4YTYsNiwwLDAsMCwwLDEyaDU2YTYsNiwwLDAsMCwwLTEyWm0xMjQuMjQtMjIuMjRhNiw2LDAsMCwwLTguNDgsMEwxOTAsMTkzLjUxVjExMmE2LDYsMCwwLDAtMTIsMHY4MS41MWwtMjkuNzYtMjkuNzVhNiw2LDAsMCwwLTguNDgsOC40OGw0MCw0MGE2LDYsMCwwLDAsOC40OCwwbDQwLTQwQTYsNiwwLDAsMCwyMjguMjQsMTYzLjc2WiIvPjwvc3ZnPg==');}.icon-sort-descending{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00MiwxMjhhNiw2LDAsMCwxLDYtNmg3MmE2LDYsMCwwLDEsMCwxMkg0OEE2LDYsMCwwLDEsNDIsMTI4Wm02LTU4aDU2YTYsNiwwLDAsMCwwLTEySDQ4YTYsNiwwLDAsMCwwLDEyWk0xODQsMTg2SDQ4YTYsNiwwLDAsMCwwLDEySDE4NGE2LDYsMCwwLDAsMC0xMlpNMjI4LjI0LDgzLjc2bC00MC00MGE2LDYsMCwwLDAtOC40OCwwbC00MCw0MGE2LDYsMCwwLDAsOC40OCw4LjQ4TDE3OCw2Mi40OVYxNDRhNiw2LDAsMCwwLDEyLDBWNjIuNDlsMjkuNzYsMjkuNzVhNiw2LDAsMCwwLDguNDgtOC40OFoiLz48L3N2Zz4=');}.icon-floppy-disk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTcuOSw3My40MiwxODIuNTgsMzguMWExMy45LDEzLjksMCwwLDAtOS44OS00LjFINDhBMTQsMTQsMCwwLDAsMzQsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDIwOGExNCwxNCwwLDAsMCwxNC0xNFY4My4zMUExMy45LDEzLjksMCwwLDAsMjE3LjksNzMuNDJaTTE3MCwyMTBIODZWMTUyYTIsMiwwLDAsMSwyLTJoODBhMiwyLDAsMCwxLDIsMlptNDAtMmEyLDIsMCwwLDEtMiwySDE4MlYxNTJhMTQsMTQsMCwwLDAtMTQtMTRIODhhMTQsMTQsMCwwLDAtMTQsMTR2NThINDhhMiwyLDAsMCwxLTItMlY0OGEyLDIsMCwwLDEsMi0ySDE3Mi42OWEyLDIsMCwwLDEsMS40MS41OEwyMDkuNDIsODEuOWEyLDIsMCwwLDEsLjU4LDEuNDFaTTE1OCw3MmE2LDYsMCwwLDEtNiw2SDk2YTYsNiwwLDAsMSwwLTEyaDU2QTYsNiwwLDAsMSwxNTgsNzJaIi8+PC9zdmc+');}.icon-columns{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDQsMzRINjRBMTQsMTQsMCwwLDAsNTAsNDhWMjA4YTE0LDE0LDAsMCwwLDE0LDE0aDQwYTE0LDE0LDAsMCwwLDE0LTE0VjQ4QTE0LDE0LDAsMCwwLDEwNCwzNFptMiwxNzRhMiwyLDAsMCwxLTIsMkg2NGEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMlpNMTkyLDM0SDE1MmExNCwxNCwwLDAsMC0xNCwxNFYyMDhhMTQsMTQsMCwwLDAsMTQsMTRoNDBhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMTkyLDM0Wm0yLDE3NGEyLDIsMCwwLDEtMiwySDE1MmEyLDIsMCwwLDEtMi0yVjQ4YTIsMiwwLDAsMSwyLTJoNDBhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-arrows-left-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTcxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTMyLDMyYTYsNiwwLDAsMS04LjQ4LTguNDhMMTkzLjUxLDE4Mkg0OGE2LDYsMCwwLDEsMC0xMkgxOTMuNTFsLTIxLjc1LTIxLjc2YTYsNiwwLDAsMSw4LjQ4LTguNDhaTTc1Ljc2LDExNi4yNGE2LDYsMCwwLDAsOC40OC04LjQ4TDYyLjQ5LDg2SDIwOGE2LDYsMCwwLDAsMC0xMkg2Mi40OUw4NC4yNCw1Mi4yNGE2LDYsMCwwLDAtOC40OC04LjQ4bC0zMiwzMmE2LDYsMCwwLDAsMCw4LjQ4WiIvPjwvc3ZnPg==');}.icon-arrows-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=');}.icon-check-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=');}.icon-x-circle{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-user{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTksMjEzYy0xNS44MS0yNy4zMi00MC42My00Ni40OS02OS40Ny01NC42MmE3MCw3MCwwLDEsMC02My40NCwwQzY3LjQ0LDE2Ni41LDQyLjYyLDE4NS42NywyNi44MSwyMTNhNiw2LDAsMSwwLDEwLjM4LDZDNTYuNCwxODUuODEsOTAuMzQsMTY2LDEyOCwxNjZzNzEuNiwxOS44MSw5MC44MSw1M2E2LDYsMCwxLDAsMTAuMzgtNlpNNzAsOTZhNTgsNTgsMCwxLDEsNTgsNThBNTguMDcsNTguMDcsMCwwLDEsNzAsOTZaIi8+PC9zdmc+');}.icon-crosshair{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIySDIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDEzNCwzNC4yVjI0YTYsNiwwLDAsMC0xMiwwVjM0LjJBOTQuMTMsOTQuMTMsMCwwLDAsMzQuMiwxMjJIMjRhNiw2LDAsMCwwLDAsMTJIMzQuMkE5NC4xMyw5NC4xMywwLDAsMCwxMjIsMjIxLjhWMjMyYTYsNiwwLDAsMCwxMiwwVjIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDIyMS44LDEzNEgyMzJhNiw2LDAsMCwwLDAtMTJabS05OCw4Ny43NlYyMDBhNiw2LDAsMCwwLTEyLDB2OS43NkE4Mi4wOSw4Mi4wOSwwLDAsMSw0Ni4yNCwxMzRINTZhNiw2LDAsMCwwLDAtMTJINDYuMjRBODIuMDksODIuMDksMCwwLDEsMTIyLDQ2LjI0VjU2YTYsNiwwLDAsMCwxMiwwVjQ2LjI0QTgyLjA5LDgyLjA5LDAsMCwxLDIwOS43NiwxMjJIMjAwYTYsNiwwLDAsMCwwLDEyaDkuNzZBODIuMDksODIuMDksMCwwLDEsMTM0LDIwOS43NlpNMTI4LDkwYTM4LDM4LDAsMSwwLDM4LDM4QTM4LDM4LDAsMCwwLDEyOCw5MFptMCw2NGEyNiwyNiwwLDEsMSwyNi0yNkEyNiwyNiwwLDAsMSwxMjgsMTU0WiIvPjwvc3ZnPg==');}.icon-person{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsNzBBMzAsMzAsMCwxLDAsOTgsNDAsMzAsMzAsMCwwLDAsMTI4LDcwWm0wLTQ4YTE4LDE4LDAsMSwxLTE4LDE4QTE4LDE4LDAsMCwxLDEyOCwyMlptODguODgsMTEzLjQyTDE3MS42Nyw4NC4xNkEzMCwzMCwwLDAsMCwxNDkuMTcsNzRIMTA2LjgzYTMwLDMwLDAsMCwwLTIyLjUsMTAuMTVMMzkuMTIsMTM1LjQyQTE4LDE4LDAsMCwwLDY0LjQ2LDE2MWwyMS4xMS0xNi45M0w2Ny40NCwyMTIuOTJhMTgsMTgsMCwwLDAsMzIuNzUsMTQuOTRMMTI4LDE4MGwyNy44MSw0Ny45MWExOCwxOCwwLDAsMCwzMi43NS0xNC45NGwtMTguMTMtNjguODdMMTkxLjU0LDE2MWExOCwxOCwwLDAsMCwyNS4zNC0yNS41NlptLTguNjMsMTYuODJhNiw2LDAsMCwxLTguNDksMCw0LjE1LDQuMTUsMCwwLDAtLjQ5LS40NGwtMzUuNTEtMjguNDhhNiw2LDAsMCwwLTkuNTYsNi4ybDIyLjg3LDg2LjkzYTcuNjYsNy42NiwwLDAsMCwuMzcsMSw2LDYsMCwwLDEtMTAuODgsNS4wNyw0LjM3LDQuMzcsMCwwLDAtLjI1LS40OEwxMzMuMTksMTY1YTYsNiwwLDAsMC0xMC4zOCwwTDg5LjY5LDIyMi4wNWE0LjM3LDQuMzcsMCwwLDAtLjI1LjQ4LDYsNiwwLDAsMS0xMC44OC01LjA3LDcuNjYsNy42NiwwLDAsMCwuMzctMWwyMi44Ny04Ni45M0E2LDYsMCwwLDAsOTkuMjcsMTIzLDYuMDcsNi4wNywwLDAsMCw5NiwxMjJhNiw2LDAsMCwwLTMuNzYsMS4zMkw1Ni43MywxNTEuOGE0LjE1LDQuMTUsMCwwLDAtLjQ5LjQ0LDYsNiwwLDAsMS04LjQ5LTguNDlsLjI2LS4yN0w5My4zMyw5Mi4wOUExOCwxOCwwLDAsMSwxMDYuODMsODZoNDIuMzRhMTgsMTgsMCwwLDEsMTMuNSw2LjA5TDIwOCwxNDMuNDhsLjI2LjI3QTYsNiwwLDAsMSwyMDguMjUsMTUyLjI0WiIvPjwvc3ZnPg==');}.icon-folder{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==');}.icon-dots-six{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03MCw5MkExMCwxMCwwLDEsMSw2MCw4MiwxMCwxMCwwLDAsMSw3MCw5MlptNTgtMTBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTI4LDgyWm02OCwyMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxOTYsMTAyWk02MCwxNTRhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNjAsMTU0Wm02OCwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDEyOCwxNTRabTY4LDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTk2LDE1NFoiLz48L3N2Zz4=');}.icon-cake{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEyYTIyLDIyLDAsMCwwLTIyLTIySDEzNFY3Ny40QTMwLjA1LDMwLjA1LDAsMCwwLDE1OCw0OGMwLTI2LjktMjUuNTgtNDQuMjctMjYuNjctNDVhNiw2LDAsMCwwLTYuNjYsMEMxMjMuNTgsMy43Myw5OCwyMS4xLDk4LDQ4YTMwLjA1LDMwLjA1LDAsMCwwLDI0LDI5LjRWOTBINDhhMjIsMjIsMCwwLDAtMjIsMjJ2MjMuMzNhMzguODEsMzguODEsMCwwLDAsOCwyMy41N1YyMDBhMjIsMjIsMCwwLDAsMjIsMjJIMjAwYTIyLDIyLDAsMCwwLDIyLTIyVjE1OC45YTM4LjgxLDM4LjgxLDAsMCwwLDgtMjMuNTdaTTExMCw0OGMwLTE1LjQsMTItMjcuNDEsMTgtMzIuNDQsNiw1LDE4LDE3LDE4LDMyLjQ0YTE4LDE4LDAsMCwxLTM2LDBaTTM4LDExMmExMCwxMCwwLDAsMSwxMC0xMEgyMDhhMTAsMTAsMCwwLDEsMTAsMTB2MjMuMzNjMCwxNC4zMi0xMS4zMywyNi4yOC0yNS4yNiwyNi42NkEyNiwyNiwwLDAsMSwxNjYsMTM2YTYsNiwwLDAsMC0xMiwwLDI2LDI2LDAsMCwxLTUyLDAsNiw2LDAsMCwwLTEyLDAsMjYsMjYsMCwwLDEtMjYuNzQsMjZDNDkuMzMsMTYxLjYxLDM4LDE0OS42NSwzOCwxMzUuMzNabTE2Miw5OEg1NmExMCwxMCwwLDAsMS0xMC0xMFYxNjkuMzFBMzYuOCwzNi44LDAsMCwwLDYyLjk0LDE3NCwzNy44MiwzNy44MiwwLDAsMCw5MC41LDE2My4yNCwzOC44MywzOC44MywwLDAsMCw5NiwxNTYuNWEzOCwzOCwwLDAsMCw2NCwwLDM4LjgzLDM4LjgzLDAsMCwwLDUuNTIsNi43NEEzNy43MywzNy43MywwLDAsMCwxOTIsMTc0aDEuMDdBMzYuOCwzNi44LDAsMCwwLDIxMCwxNjkuMzFWMjAwQTEwLDEwLDAsMCwxLDIwMCwyMTBaIi8+PC9zdmc+');}.icon-hourglass{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNzUuNjRWNDBhMTQsMTQsMCwwLDAtMTQtMTRINzJBMTQsMTQsMCwwLDAsNTgsNDBWNzZhMTQuMDYsMTQuMDYsMCwwLDAsNS42LDExLjJMMTE4LDEyOCw2My42LDE2OC44QTE0LjA2LDE0LjA2LDAsMCwwLDU4LDE4MHYzNmExNCwxNCwwLDAsMCwxNCwxNEgxODRhMTQsMTQsMCwwLDAsMTQtMTRWMTgwLjM2YTE0LjA4LDE0LjA4LDAsMCwwLTUuNTYtMTEuMTdMMTM4LDEyOGw1NC40OS00MS4xOUExNC4wOCwxNC4wOCwwLDAsMCwxOTgsNzUuNjRaTTE4NiwxODAuMzZWMjE2YTIsMiwwLDAsMS0yLDJINzJhMiwyLDAsMCwxLTItMlYxODBhMiwyLDAsMCwxLC44LTEuNkwxMjgsMTM1LjUxbDU3LjIyLDQzLjI1QTIsMiwwLDAsMSwxODYsMTgwLjM2Wm0wLTEwNC43MmEyLDIsMCwwLDEtLjc5LDEuNkwxMjgsMTIwLjQ5LDcwLjgsNzcuNkEyLDIsMCwwLDEsNzAsNzZWNDBhMiwyLDAsMCwxLDItMkgxODRhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-hash{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsOTBIMTczbDguODktNDguOTNhNiw2LDAsMSwwLTExLjgtMi4xNEwxNjAuODEsOTBIMTA5bDguODktNDguOTNhNiw2LDAsMCwwLTExLjgtMi4xNEw5Ni44MSw5MEg0OGE2LDYsMCwwLDAsMCwxMkg5NC42M2wtOS40Niw1MkgzMmE2LDYsMCwwLDAsMCwxMkg4M0w3NC4xLDIxNC45M2E2LDYsMCwwLDAsNC44Myw3QTUuNjQsNS42NCwwLDAsMCw4MCwyMjJhNiw2LDAsMCwwLDUuODktNC45M0w5NS4xOSwxNjZIMTQ3bC04Ljg5LDQ4LjkzYTYsNiwwLDAsMCw0LjgzLDcsNS42NCw1LjY0LDAsMCwwLDEuMDguMSw2LDYsMCwwLDAsNS44OS00LjkzTDE1OS4xOSwxNjZIMjA4YTYsNiwwLDAsMCwwLTEySDE2MS4zN2w5LjQ2LTUySDIyNGE2LDYsMCwwLDAsMC0xMlptLTc0LjgzLDY0SDk3LjM3bDkuNDYtNTJoNTEuOFoiLz48L3N2Zz4=');}.icon-pencil-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjUuOSw3NC43OCwxODEuMjEsMzAuMDlhMTQsMTQsMCwwLDAtMTkuOCwwTDM4LjEsMTUzLjQxYTEzLjk0LDEzLjk0LDAsMCwwLTQuMSw5LjlWMjA4YTE0LDE0LDAsMCwwLDE0LDE0SDkyLjY5YTEzLjk0LDEzLjk0LDAsMCwwLDkuOS00LjFMMjI1LjksOTQuNThhMTQsMTQsMCwwLDAsMC0xOS44Wk05NC4xLDIwOS40MWEyLDIsMCwwLDEtMS40MS41OUg0OGEyLDIsMCwwLDEtMi0yVjE2My4zMWEyLDIsMCwwLDEsLjU5LTEuNDFMMTM2LDcyLjQ4LDE4My41MSwxMjBaTTIxNy40MSw4Ni4xLDE5MiwxMTEuNTEsMTQ0LjQ5LDY0LDE2OS45LDM4LjU4YTIsMiwwLDAsMSwyLjgzLDBsNDQuNjgsNDQuNjlhMiwyLDAsMCwxLDAsMi44M1oiLz48L3N2Zz4=');}.icon-caret-double-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTMxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTgwLDgwYTYsNiwwLDAsMS04LjQ4LDBsLTgwLTgwYTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDIwNy41MWw3NS43Ni03NS43NUE2LDYsMCwwLDEsMjEyLjI0LDEzMS43NlptLTg4LjQ4LDguNDhhNiw2LDAsMCwwLDguNDgsMGw4MC04MGE2LDYsMCwwLDAtOC40OC04LjQ4TDEyOCwxMjcuNTEsNTIuMjQsNTEuNzZhNiw2LDAsMCwwLTguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-caret-double-right{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDAuMjQsMTMyLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OC04LjQ4TDEyNy41MSwxMjgsNTEuNzYsNTIuMjRhNiw2LDAsMCwxLDguNDgtOC40OGw4MCw4MEE2LDYsMCwwLDEsMTQwLjI0LDEzMi4yNFptODAtOC40OC04MC04MGE2LDYsMCwwLDAtOC40OCw4LjQ4TDIwNy41MSwxMjhsLTc1Ljc1LDc1Ljc2YTYsNiwwLDEsMCw4LjQ4LDguNDhsODAtODBBNiw2LDAsMCwwLDIyMC4yNCwxMjMuNzZaIi8+PC9zdmc+');}.icon-dots-six-vertical{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMDIsNjBBMTAsMTAsMCwxLDEsOTIsNTAsMTAsMTAsMCwwLDEsMTAyLDYwWm02MiwxMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxNjQsNzBaTTkyLDExOGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCw5MiwxMThabTcyLDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTY0LDExOFpNOTIsMTg2YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDkyLDE4NlptNzIsMGExMCwxMCwwLDEsMCwxMCwxMEExMCwxMCwwLDAsMCwxNjQsMTg2WiIvPjwvc3ZnPg==');}.icon-drop-simple{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuNTMsNDkuMDZhMjUxLjQyLDI1MS40MiwwLDAsMC00MS4wOS0zOCw2LDYsMCwwLDAtNi44OCwwLDI1MS40MiwyNTEuNDIsMCwwLDAtNDEuMDksMzhDNTYuMzQsODAuMjYsNDIsMTEzLjA5LDQyLDE0NGE4Niw4NiwwLDAsMCwxNzIsMEMyMTQsMTEzLjA5LDE5OS42Niw4MC4yNiwxNzIuNTMsNDkuMDZaTTEyOCwyMThhNzQuMDksNzQuMDksMCwwLDEtNzQtNzRjMC01OS42Miw1OS0xMDguOTMsNzQtMTIwLjUxQzE0MywzNS4wNywyMDIsODQuMzgsMjAyLDE0NEE3NC4wOSw3NC4wOSwwLDAsMSwxMjgsMjE4WiIvPjwvc3ZnPg==');}.icon-door{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMjE4SDIwNlY0MGExNCwxNCwwLDAsMC0xNC0xNEg2NEExNCwxNCwwLDAsMCw1MCw0MFYyMThIMjRhNiw2LDAsMCwwLDAsMTJIMjMyYTYsNiwwLDAsMCwwLTEyWk02Miw0MGEyLDIsMCwwLDEsMi0ySDE5MmEyLDIsMCwwLDEsMiwyVjIxOEg2MlptMTA0LDkyYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE2NiwxMzJaIi8+PC9zdmc+');}.icon-hand-heart{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTIsMTQyLjY1YTIyLjQzLDIyLjQzLDAsMCwwLTE5LjU1LTMuODhsLTQuMzIsMUMyMjcsMTE5LjU1LDIzOCw5OS41MSwyMzgsODBjMC0yNS4zNi0yMC4zOS00Ni00NS40Ni00NkE0NS41MSw0NS41MSwwLDAsMCwxNTYsNTJhNDUuNTEsNDUuNTEsMCwwLDAtMzYuNTQtMThDOTQuMzksMzQsNzQsNTQuNjQsNzQsODBjMCwxMS4zOCwzLjYzLDIyLjQ5LDExLjI5LDM0LjM2YTI5LjczLDI5LjczLDAsMCwwLTE2LjU2LDguNDNMNDUuNTIsMTQ2SDE2QTE0LDE0LDAsMCwwLDIsMTYwdjQwYTE0LDE0LDAsMCwwLDE0LDE0SDEyMGE2LDYsMCwwLDAsMS40Ni0uMThsNjQtMTZhNy4xNiw3LjE2LDAsMCwwLC44OS0uM0wyMjUuMTcsMTgxbC4zMy0uMTVhMjIuNiwyMi42LDAsMCwwLDMuNjItMzguMThaTTExOS40Niw0NmEzMy4xNiwzMy4xNiwwLDAsMSwzMSwyMC4yOCw2LDYsMCwwLDAsMTEuMSwwLDMzLjE2LDMzLjE2LDAsMCwxLDMxLTIwLjI4QzIxMC42OCw0NiwyMjYsNjEuNTcsMjI2LDgwYzAsMjAuMjQtMTYuMTgsNDMtNDYuOCw2NS43NWwtMTQuODcsMy40MkEyNiwyNiwwLDAsMCwxNDAsMTE0SDk5LjY3QzkwLjM2LDEwMS42Nyw4Niw5MC44MSw4Niw4MCw4Niw2MS41NywxMDEuMzIsNDYsMTE5LjQ2LDQ2Wk0xNCwyMDBWMTYwYTIsMiwwLDAsMSwyLTJINDJ2NDRIMTZBMiwyLDAsMCwxLDE0LDIwMFptMjA2LjI4LTMwLTM4LjIsMTYuMjdMMTE5LjI2LDIwMkg1NFYxNTQuNDlsMjMuMjEtMjMuMjJBMTcuODgsMTcuODgsMCwwLDEsODkuOTQsMTI2SDE0MGExNCwxNCwwLDAsMSwwLDI4SDExMmE2LDYsMCwwLDAsMCwxMmgzMmE2LDYsMCwwLDAsMS4zNC0uMTVsNjctMTUuNDEuMjQtLjA2QTEwLjYsMTAuNiwwLDAsMSwyMjAuMjgsMTcwWiIvPjwvc3ZnPg==');}.icon-plugs-connected{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzYuMjQsMTkuNzZhNiw2LDAsMCwwLTguNDgsMEwxNzMuOTQsNzMuNTdsLTYuNzktNi43OGEzMCwzMCwwLDAsMC00Mi40MiwwTDEwMCw5MS41MWwtNy43Ni03Ljc1YTYsNiwwLDAsMC04LjQ4LDguNDhMOTEuNTEsMTAwLDY2Ljc5LDEyNC43M2EzMCwzMCwwLDAsMCwwLDQyLjQybDYuNzgsNi43OUwxOS43NiwyMjcuNzZhNiw2LDAsMSwwLDguNDgsOC40OGw1My44Mi01My44MSw2Ljc5LDYuNzhhMzAsMzAsMCwwLDAsNDIuNDIsMEwxNTYsMTY0LjQ5bDcuNzYsNy43NWE2LDYsMCwwLDAsOC40OC04LjQ4TDE2NC40OSwxNTZsMjQuNzItMjQuNzNhMzAsMzAsMCwwLDAsMC00Mi40MmwtNi43OC02Ljc5LDUzLjgxLTUzLjgyQTYsNiwwLDAsMCwyMzYuMjQsMTkuNzZabS0xMTMuNDUsMTYxYTE4LDE4LDAsMCwxLTI1LjQ2LDBMNzUuMjcsMTU4LjY3YTE4LDE4LDAsMCwxLDAtMjUuNDZMMTAwLDEwOC40OSwxNDcuNTEsMTU2Wm01Ny45NC01Ny45NEwxNTYsMTQ3LjUxLDEwOC40OSwxMDBsMjQuNzItMjQuNzNhMTgsMTgsMCwwLDEsMjUuNDYsMGwyMi4wNiwyMi4wNmExOCwxOCwwLDAsMSwwLDI1LjQ2Wk05MC40MywzNC4yM2E2LDYsMCwwLDEsMTEuMTQtNC40Nmw4LDIwYTYsNiwwLDEsMS0xMS4xNCw0LjQ2Wm0tNjQsNTkuNTRhNiw2LDAsMCwxLDcuOC0zLjM0bDIwLDhhNiw2LDAsMSwxLTQuNDYsMTEuMTRsLTIwLThBNiw2LDAsMCwxLDI2LjQzLDkzLjc3Wm0yMDMuMTQsNjguNDZhNiw2LDAsMCwxLTcuOCwzLjM0bC0yMC04YTYsNiwwLDAsMSw0LjQ2LTExLjE0bDIwLDhBNiw2LDAsMCwxLDIyOS41NywxNjIuMjNabS02NCw1OS41NGE2LDYsMCwxLDEtMTEuMTQsNC40NmwtOC0yMGE2LDYsMCwwLDEsMTEuMTQtNC40NloiLz48L3N2Zz4=');}.icon-question{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+');}.icon-asterisk{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuMTQsMTc5LjA5YTYsNiwwLDAsMS04LjIzLDIuMDZMMTM0LDEzOC42VjIxNmE2LDYsMCwwLDEtMTIsMFYxMzguNkw1MS4wOSwxODEuMTVBNi4wNyw2LjA3LDAsMCwxLDQ4LDE4MmE2LDYsMCwwLDEtMy4xLTExLjE1TDExNi4zNCwxMjgsNDQuOTEsODUuMTVhNiw2LDAsMCwxLDYuMTgtMTAuM0wxMjIsMTE3LjRWNDBhNiw2LDAsMCwxLDEyLDB2NzcuNGw3MC45MS00Mi41NWE2LDYsMCwwLDEsNi4xOCwxMC4zTDEzOS42NiwxMjhsNzEuNDMsNDIuODVBNiw2LDAsMCwxLDIxMy4xNCwxNzkuMDlaIi8+PC9zdmc+');}.icon-arrow-counter-clockwise{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMCwxLTkyLjc0LDk0SDEyOGE5My40Myw5My40MywwLDAsMS02NC41LTI1LjY1LDYsNiwwLDEsMSw4LjI0LTguNzJBODIsODIsMCwxLDAsNzAsNzBsLS4xOS4xOUwzOS40NCw5OEg3MmE2LDYsMCwwLDEsMCwxMkgyNGE2LDYsMCwwLDEtNi02VjU2YTYsNiwwLDAsMSwxMiwwVjkwLjM0TDYxLjYzLDYxLjRBOTQsOTQsMCwwLDEsMjIyLDEyOFoiLz48L3N2Zz4=');}.icon-clock{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=');}.icon-envelope{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+');}.icon-facebook-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm02LDE5MS44VjE1MGgyNmE2LDYsMCwwLDAsMC0xMkgxMzRWMTEyYTE4LDE4LDAsMCwxLDE4LTE4aDE2YTYsNiwwLDAsMCwwLTEySDE1MmEzMCwzMCwwLDAsMC0zMCwzMHYyNkg5NmE2LDYsMCwwLDAsMCwxMmgyNnY2Ny44YTkwLDkwLDAsMSwxLDEyLDBaIi8+PC9zdmc+');}.icon-twitter-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDUuNTQsNjkuNzFBNiw2LDAsMCwwLDI0MCw2NkgyMDguNGE0Ni42LDQ2LjYsMCwwLDAtNDAuMzMtMjQsNDQuOTMsNDQuOTMsMCwwLDAtMzIuMzEsMTMuMTJBNDUuOTIsNDUuOTIsMCwwLDAsMTIyLDg4djguNjZjLTQyLTEwLTc2LjYtNDQuNTItNzctNDQuODhBNiw2LDAsMCwwLDM0LjgsNTUuNDZjLTQuMjUsNDcuMjIsOS40Miw3OC43NSwyMS42NCw5Ni44OWExMDcuNzEsMTA3LjcxLDAsMCwwLDIzLjA3LDI1Yy0xNS40OSwxOS00MS4zNCwyOC44OS00MS42MiwyOWE2LDYsMCwwLDAtMi44OCw5QzM2LDIxNi44Myw0NS43OCwyMzAsODAsMjMwYzY5Ljg4LDAsMTI4LjIzLTU0LDEzMy44Mi0xMjMuMzRsMzAuNDItMzAuNDFBNiw2LDAsMCwwLDI0NS41NCw2OS43MVpNMjAzLjc2LDk5Ljc2YTYsNiwwLDAsMC0xLjc1LDMuODZDMTk3LjkzLDE2Ny43NiwxNDQuMzMsMjE4LDgwLDIxOGMtMTQsMC0yMi43Ni0yLjQxLTI4LjA2LTQuOCwxMS4zLTUuNjgsMjkuNzItMTYuODgsNDEuMDUtMzMuODdhNiw2LDAsMCwwLC44NS00LjY3QTYsNiwwLDAsMCw5MSwxNzAuODJjLS4xMy0uMDgtMTMuMTMtNy44Ni0yNS0yNS43MkM1MiwxMjQsNDUuMzEsOTguNDMsNDYsNjguODhjMTQuODgsMTIuNiw0NS41NywzNS4wOSw4MSw0MWE2LDYsMCwwLDAsNy01LjkyVjg4YTM0LDM0LDAsMCwxLDEwLjE5LTI0LjM0QTMzLjA1LDMzLjA1LDAsMCwxLDE2Ny45Myw1NGMxMy40My4xNywyNiw4LjM3LDMxLjI0LDIwLjRhNiw2LDAsMCwwLDUuNSwzLjZoMjAuODRaIi8+PC9zdmc+');}.icon-caret-down{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTAwLjI0bC04MCw4MGE2LDYsMCwwLDEtOC40OCwwbC04MC04MGE2LDYsMCwwLDEsOC40OC04LjQ4TDEyOCwxNjcuNTFsNzUuNzYtNzUuNzVhNiw2LDAsMCwxLDguNDgsOC40OFoiLz48L3N2Zz4=');}.icon-list{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==');}.icon-instagram-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsODJhNDYsNDYsMCwxLDAsNDYsNDZBNDYuMDYsNDYuMDYsMCwwLDAsMTI4LDgyWm0wLDgwYTM0LDM0LDAsMSwxLDM0LTM0QTM0LDM0LDAsMCwxLDEyOCwxNjJaTTE3NiwyNkg4MEE1NC4wNiw1NC4wNiwwLDAsMCwyNiw4MHY5NmE1NC4wNiw1NC4wNiwwLDAsMCw1NCw1NGg5NmE1NC4wNiw1NC4wNiwwLDAsMCw1NC01NFY4MEE1NC4wNiw1NC4wNiwwLDAsMCwxNzYsMjZabTQyLDE1MGE0Miw0MiwwLDAsMS00Miw0Mkg4MGE0Miw0MiwwLDAsMS00Mi00MlY4MEE0Miw0MiwwLDAsMSw4MCwzOGg5NmE0Miw0MiwwLDAsMSw0Miw0MlpNMTkwLDc2YTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDE5MCw3NloiLz48L3N2Zz4=');}.icon-butterfly{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMSw1MS43MUMyMjYuMDksNDUuMjcsMjE4LjY0LDQyLDIwOSw0MmMtMTYuMzMsMC0zNy40MSwxMS4wNi01Ni40LDI5LjU5QTEzMiwxMzIsMCwwLDAsMTM0LDkzLjk0VjU2YTYsNiwwLDAsMC0xMiwwVjkzLjk0YTEzMiwxMzIsMCwwLDAtMTguNTQtMjIuMzVDODQuNDksNTMuMDYsNjMuNCw0Miw0Ny4wNyw0MmMtOS42OSwwLTE3LjE0LDMuMjctMjIuMTUsOS43MS01LjUzLDcuMTEtNy43MSwxNy42OS02LjY2LDMyLjM0LjkxLDEyLjczLDQuMTIsMjYuNTMsNi44MSwzNy4xMyw2LjI4LDI0Ljc0LDIwLjc3LDMzLDMxLjc4LDM1LjY4QTQyLDQyLDAsMSwwLDEyOCwyMDEuNjJhNDIsNDIsMCwxLDAsNzEuMTYtNDQuNzZjMTEtMi42MywyNS41LTEwLjk0LDMxLjc4LTM1LjY4QzIzNy41NSw5NS4yMSwyNDIuNzIsNjYuNjUsMjMxLjEsNTEuNzFaTTkyLDIxMGEzMC4xMiwzMC4xMiwwLDAsMS0zLjM0LTYwQTYsNiwwLDAsMCw4Ny4zNSwxMzhhNDEuNzEsNDEuNzEsMCwwLDAtMjAuMjgsOGMtOSwuMzEtMjQuMTItMy4xNi0zMC4zNy0yNy43Ni0zLjI1LTEyLjgxLTExLjg5LTQ2LjgzLTIuMzEtNTkuMTVDMzcuMDUsNTUuNjYsNDEuMiw1NCw0Ny4wNyw1NGMxMi44OCwwLDMxLjcyLDEwLjI4LDQ4LDI2LjE4QzExMS42OSw5Ni4zOSwxMjIsMTE0LjU5LDEyMiwxMjcuNjdWMTgwQTMwLDMwLDAsMCwxLDkyLDIxMFptMTI3LjMxLTkxLjc3QzIxMy4wNywxNDIuODMsMTk4LDE0Ni4yOSwxODksMTQ2YTQxLjYyLDQxLjYyLDAsMCwwLTIwLjI4LThBNiw2LDAsMSwwLDE2Ny4zNiwxNTAsMzAuMTEsMzAuMTEsMCwxLDEsMTM0LDE4MFYxMjcuNjdjMC0xMy4wOCwxMC4zMi0zMS4yOCwyNi45My00Ny40OUMxNzcuMjMsNjQuMjgsMTk2LjA3LDU0LDIwOSw1NGM1Ljg3LDAsMTAsMS42NiwxMi42OCw1LjA4QzIzMS4yMSw3MS40LDIyMi41NywxMDUuNDIsMjE5LjMyLDExOC4yM1oiLz48L3N2Zz4=');}.icon-phone{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjEuNTksMTYwLjNsLTQ3LjI0LTIxLjE3YTE0LDE0LDAsMCwwLTEzLjI4LDEuMjIsNC44MSw0LjgxLDAsMCwwLS41Ni40MmwtMjQuNjksMjFhMS44OCwxLjg4LDAsMCwxLTEuNjguMDZjLTE1Ljg3LTcuNjYtMzIuMzEtMjQtNDAtMzkuNjVhMS45MSwxLjkxLDAsMCwxLDAtMS42OGwyMS4wNy0yNWE2LjEzLDYuMTMsMCwwLDAsLjQyLS41OCwxNCwxNCwwLDAsMCwxLjEyLTEzLjI3TDk1LjczLDM0LjQ5YTE0LDE0LDAsMCwwLTE0LjU2LTguMzhBNTQuMjQsNTQuMjQsMCwwLDAsMzQsODBjMCw3OC4zLDYzLjcsMTQyLDE0MiwxNDJhNTQuMjUsNTQuMjUsMCwwLDAsNTMuODktNDcuMTdBMTQsMTQsMCwwLDAsMjIxLjU5LDE2MC4zWk0xNzYsMjEwQzEwNC4zMiwyMTAsNDYsMTUxLjY4LDQ2LDgwQTQyLjIzLDQyLjIzLDAsMCwxLDgyLjY3LDM4aC4yM2EyLDIsMCwwLDEsMS44NCwxLjMxbDIxLjEsNDcuMTFhMiwyLDAsMCwxLDAsMS42N0w4NC43MywxMTMuMTVhNC43Myw0LjczLDAsMCwwLS40My41NywxNCwxNCwwLDAsMC0uOTEsMTMuNzNjOC44NywxOC4xNiwyNy4xNywzNi4zMiw0NS41Myw0NS4xOWExNCwxNCwwLDAsMCwxMy43Ny0xYy4xOS0uMTMuMzgtLjI3LjU2LS40MmwyNC42OC0yMWExLjkyLDEuOTIsMCwwLDEsMS42LS4xbDQ3LjI1LDIxLjE3YTIsMiwwLDAsMSwxLjIxLDJBNDIuMjQsNDIuMjQsMCwwLDEsMTc2LDIxMFoiLz48L3N2Zz4=');}.icon-chat{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=');}.icon-google-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTk0LDk0LDAsMSwxLTIxLjQ5LTU5LjgyLDYsNiwwLDEsMS05LjI1LDcuNjRBODIsODIsMCwxLDAsMjA5Ljc4LDEzNEgxMjhhNiw2LDAsMCwxLDAtMTJoODhBNiw2LDAsMCwxLDIyMiwxMjhaIi8+PC9zdmc+');}.icon-apple-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTkuNCwxNjcuODRDMjAxLjcxLDE1NS42OSwxOTgsMTM1LjEyLDE5OCwxMjBjMC0xOC40MiwxMy44Ni0zNC4yOSwyMi4xMi00Mi4xMmE2LDYsMCwwLDAsMC04LjcxQzIwOCw1Ny43LDE4Ny4wNyw1MCwxNjgsNTBhNzAuMjMsNzAuMjMsMCwwLDAtNDAsMTIuNTUsNjkuNiw2OS42LDAsMCwwLTg5LjMxLDguMDhBNzIuNjMsNzIuNjMsMCwwLDAsMTgsMTIzLjM1YTEyNS4xMSwxMjUuMTEsMCwwLDAsMzkuNTMsODguMzNBMzcuODUsMzcuODUsMCwwLDAsODMuNiwyMjJoODcuN0EzNy44MywzNy44MywwLDAsMCwxOTksMjEwLjA3YTEyMi42LDEyMi42LDAsMCwwLDE3LjU0LTI0LjJjNi41NS0xMiw1Ljc3LTEzLjc1LDUtMTUuNDhBNi4wNyw2LjA3LDAsMCwwLDIxOS40LDE2Ny44NFptLTI5LjIzLDM0QTI1LjgyLDI1LjgyLDAsMCwxLDE3MS4zLDIxMEg4My42QTI1Ljg1LDI1Ljg1LDAsMCwxLDY1Ljc4LDIwMywxMTMuMjEsMTEzLjIxLDAsMCwxLDMwLDEyM2E2MC41NSw2MC41NSwwLDAsMSwxNy4yMS00NEE1Ni44Miw1Ni44MiwwLDAsMSw4OCw2MmguODFhNTcuMzUsNTcuMzUsMCwwLDEsMzUuNDQsMTIuNzEsNiw2LDAsMCwwLDcuNSwwQTU3LjM5LDU3LjM5LDAsMCwxLDE2OCw2MmMxMy44OSwwLDI4LjgxLDQuNjgsMzkuMTEsMTItOS40NCwxMC4xNC0yMS4xLDI2LjU5LTIxLjEsNDYsMCwyMy43OCw3LjgxLDQyLjYsMjIuNjYsNTQuNzdBMTA3LjMzLDEwNy4zMywwLDAsMSwxOTAuMTcsMjAxLjg5Wm0tNjAtMTcxLjM5QTM4LDM4LDAsMCwxLDE2NywyaDFhNiw2LDAsMCwxLDAsMTJoLTFhMjYsMjYsMCwwLDAtMjUuMTgsMTkuNSw2LDYsMCwxLDEtMTEuNjItM1oiLz48L3N2Zz4=');}.icon-tiktok-logo{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNzRhNTAuMDYsNTAuMDYsMCwwLDEtNTAtNTAsNiw2LDAsMCwwLTYtNkgxMjhhNiw2LDAsMCwwLTYsNlYxNTZhMjIsMjIsMCwxLDEtMzEuNDMtMTkuODlBNiw2LDAsMCwwLDk0LDEzMC42OVY4OGE2LDYsMCwwLDAtNy01LjkxQzUyLjIsODguMjgsMjYsMTIwLjA1LDI2LDE1NmE3NCw3NCwwLDAsMCwxNDgsMFYxMTIuOTNBMTAxLjI4LDEwMS4yOCwwLDAsMCwyMjQsMTI2YTYsNiwwLDAsMCw2LTZWODBBNiw2LDAsMCwwLDIyNCw3NFptLTYsMzkuOGE4OS4xMyw4OS4xMywwLDAsMS00Ni41LTE2LjY5QTYsNiwwLDAsMCwxNjIsMTAydjU0YTYyLDYyLDAsMCwxLTEyNCwwYzAtMjcuNzIsMTguNDctNTIuNDgsNDQtNjAuMzh2MzEuNTNBMzQsMzQsMCwxLDAsMTM0LDE1NlYzMGgyOC4yOUE2Mi4wOSw2Mi4wOSwwLDAsMCwyMTgsODUuNzFaIi8+PC9zdmc+');}.icon-link{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=');}.icon-star-fi{--icon:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=');} |
| New file |
| | |
| | | .icon-align-left{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw0MFYyMTZhNiw2LDAsMCwxLTEyLDBWNDBhNiw2LDAsMCwxLDEyLDBabTIwLDY0VjY0QTE0LDE0LDAsMCwxLDgwLDUwaDk2YTE0LDE0LDAsMCwxLDE0LDE0djQwYTE0LDE0LDAsMCwxLTE0LDE0SDgwQTE0LDE0LDAsMCwxLDY2LDEwNFptMTIsMGEyLDIsMCwwLDAsMiwyaDk2YTIsMiwwLDAsMCwyLTJWNjRhMiwyLDAsMCwwLTItMkg4MGEyLDIsMCwwLDAtMiwyWm0xNTIsNDh2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWMTUyYTE0LDE0LDAsMCwxLDE0LTE0SDIxNkExNCwxNCwwLDAsMSwyMzAsMTUyWm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMkgyMTZhMiwyLDAsMCwwLDItMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw0MFYyMTZhNiw2LDAsMCwxLTEyLDBWNDBhNiw2LDAsMCwxLDEyLDBabTIwLDY0VjY0QTE0LDE0LDAsMCwxLDgwLDUwaDk2YTE0LDE0LDAsMCwxLDE0LDE0djQwYTE0LDE0LDAsMCwxLTE0LDE0SDgwQTE0LDE0LDAsMCwxLDY2LDEwNFptMTIsMGEyLDIsMCwwLDAsMiwyaDk2YTIsMiwwLDAsMCwyLTJWNjRhMiwyLDAsMCwwLTItMkg4MGEyLDIsMCwwLDAtMiwyWm0xNTIsNDh2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWMTUyYTE0LDE0LDAsMCwxLDE0LTE0SDIxNkExNCwxNCwwLDAsMSwyMzAsMTUyWm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMkgyMTZhMiwyLDAsMCwwLDItMloiLz48L3N2Zz4=)}.icon-align-right{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDBWMjE2YTYsNiwwLDAsMS0xMiwwVjQwYTYsNiwwLDAsMSwxMiwwWk0xOTAsNjR2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWNjRBMTQsMTQsMCwwLDEsODAsNTBoOTZBMTQsMTQsMCwwLDEsMTkwLDY0Wm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMmg5NmEyLDIsMCwwLDAsMi0yWm0xMiw4OHY0MGExNCwxNCwwLDAsMS0xNCwxNEg0MGExNCwxNCwwLDAsMS0xNC0xNFYxNTJhMTQsMTQsMCwwLDEsMTQtMTRIMTc2QTE0LDE0LDAsMCwxLDE5MCwxNTJabS0xMiwwYTIsMiwwLDAsMC0yLTJINDBhMiwyLDAsMCwwLTIsMnY0MGEyLDIsMCwwLDAsMiwySDE3NmEyLDIsMCwwLDAsMi0yWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDBWMjE2YTYsNiwwLDAsMS0xMiwwVjQwYTYsNiwwLDAsMSwxMiwwWk0xOTAsNjR2NDBhMTQsMTQsMCwwLDEtMTQsMTRIODBhMTQsMTQsMCwwLDEtMTQtMTRWNjRBMTQsMTQsMCwwLDEsODAsNTBoOTZBMTQsMTQsMCwwLDEsMTkwLDY0Wm0tMTIsMGEyLDIsMCwwLDAtMi0ySDgwYTIsMiwwLDAsMC0yLDJ2NDBhMiwyLDAsMCwwLDIsMmg5NmEyLDIsMCwwLDAsMi0yWm0xMiw4OHY0MGExNCwxNCwwLDAsMS0xNCwxNEg0MGExNCwxNCwwLDAsMS0xNC0xNFYxNTJhMTQsMTQsMCwwLDEsMTQtMTRIMTc2QTE0LDE0LDAsMCwxLDE5MCwxNTJabS0xMiwwYTIsMiwwLDAsMC0yLTJINDBhMiwyLDAsMCwwLTIsMnY0MGEyLDIsMCwwLDAsMiwySDE3NmEyLDIsMCwwLDAsMi0yWiIvPjwvc3ZnPg==)}.icon-bell{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMDcsMTc2Ljk0QzIxNC40MSwxNjcuMiwyMDYsMTM5LjczLDIwNiwxMDRhNzgsNzgsMCwxLDAtMTU2LDBjMCwzNS43NC04LjQyLDYzLjItMTQuMDgsNzIuOTRBMTQsMTQsMCwwLDAsNDgsMTk4SDkwLjQ4YTM4LDM4LDAsMCwwLDc1LDBIMjA4YTE0LDE0LDAsMCwwLDEyLjA2LTIxLjA2Wk0xMjgsMjE4YTI2LDI2LDAsMCwxLTI1LjI5LTIwaDUwLjU4QTI2LDI2LDAsMCwxLDEyOCwyMThabTgxLjcxLTMzYTEuOSwxLjksMCwwLDEtMS43LDFINDhhMS45LDEuOSwwLDAsMS0xLjctMSwyLDIsMCwwLDEsMC0yQzUzLjg3LDE3MCw2MiwxMzkuNjksNjIsMTA0YTY2LDY2LDAsMSwxLDEzMiwwYzAsMzUuNjgsOC4xNCw2NS45NSwxNS43MSw3OUEyLDIsMCwwLDEsMjA5LjcxLDE4NVoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjAuMDcsMTc2Ljk0QzIxNC40MSwxNjcuMiwyMDYsMTM5LjczLDIwNiwxMDRhNzgsNzgsMCwxLDAtMTU2LDBjMCwzNS43NC04LjQyLDYzLjItMTQuMDgsNzIuOTRBMTQsMTQsMCwwLDAsNDgsMTk4SDkwLjQ4YTM4LDM4LDAsMCwwLDc1LDBIMjA4YTE0LDE0LDAsMCwwLDEyLjA2LTIxLjA2Wk0xMjgsMjE4YTI2LDI2LDAsMCwxLTI1LjI5LTIwaDUwLjU4QTI2LDI2LDAsMCwxLDEyOCwyMThabTgxLjcxLTMzYTEuOSwxLjksMCwwLDEtMS43LDFINDhhMS45LDEuOSwwLDAsMS0xLjctMSwyLDIsMCwwLDEsMC0yQzUzLjg3LDE3MCw2MiwxMzkuNjksNjIsMTA0YTY2LDY2LDAsMSwxLDEzMiwwYzAsMzUuNjgsOC4xNCw2NS45NSwxNS43MSw3OUEyLDIsMCwwLDEsMjA5LjcxLDE4NVoiLz48L3N2Zz4=)}.icon-bell-ringing{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMDUsNjkuMzNBNiw2LDAsMCwxLDIxNSw2Ni43Niw5Niw5NiwwLDAsMCwxODAuOCwyOS4wOGE2LDYsMCwwLDEsNi40LTEwLjE1LDEwOS4yNiwxMDkuMjYsMCwwLDEsMzguNDEsNDIuMzFBNiw2LDAsMCwxLDIyMy4wNSw2OS4zM1pNNDEsNjYuNzZBOTYsOTYsMCwwLDEsNzUuMiwyOS4wOGE2LDYsMCwwLDAtNi40LTEwLjE1QTEwOS4yNiwxMDkuMjYsMCwwLDAsMzAuMzksNjEuMjQsNiw2LDAsMSwwLDQxLDY2Ljc2Wm0xNzksMTEwLjE4QTE0LDE0LDAsMCwxLDIwOCwxOThoLTQyLjVhMzgsMzgsMCwwLDEtNzUsMEg0OGExNCwxNCwwLDAsMS0xMi4wNi0yMS4wNkM0NS4xMywxNjEuMDksNTAsMTM4LjYzLDUwLDExMmE3OCw3OCwwLDAsMSwxNTYsMEMyMDYsMTM5LDIxMC43NCwxNjAuODYsMjIwLjA4LDE3Ni45NFpNMTUzLjI5LDE5OEgxMDIuNzFhMjYsMjYsMCwwLDAsNTAuNTgsMFptNTYuNDItMTVDMTk5LjI5LDE2NSwxOTQsMTQxLjE1LDE5NCwxMTJhNjYsNjYsMCwwLDAtMTMyLDBjMCwyOS4xNi01LjI5LDUzLTE1LjcxLDcxYTIsMiwwLDAsMCwwLDIsMS45LDEuOSwwLDAsMCwxLjcsMUgyMDhhMS45LDEuOSwwLDAsMCwxLjctMUEyLDIsMCwwLDAsMjA5LjcxLDE4M1oiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMDUsNjkuMzNBNiw2LDAsMCwxLDIxNSw2Ni43Niw5Niw5NiwwLDAsMCwxODAuOCwyOS4wOGE2LDYsMCwwLDEsNi40LTEwLjE1LDEwOS4yNiwxMDkuMjYsMCwwLDEsMzguNDEsNDIuMzFBNiw2LDAsMCwxLDIyMy4wNSw2OS4zM1pNNDEsNjYuNzZBOTYsOTYsMCwwLDEsNzUuMiwyOS4wOGE2LDYsMCwwLDAtNi40LTEwLjE1QTEwOS4yNiwxMDkuMjYsMCwwLDAsMzAuMzksNjEuMjQsNiw2LDAsMSwwLDQxLDY2Ljc2Wm0xNzksMTEwLjE4QTE0LDE0LDAsMCwxLDIwOCwxOThoLTQyLjVhMzgsMzgsMCwwLDEtNzUsMEg0OGExNCwxNCwwLDAsMS0xMi4wNi0yMS4wNkM0NS4xMywxNjEuMDksNTAsMTM4LjYzLDUwLDExMmE3OCw3OCwwLDAsMSwxNTYsMEMyMDYsMTM5LDIxMC43NCwxNjAuODYsMjIwLjA4LDE3Ni45NFpNMTUzLjI5LDE5OEgxMDIuNzFhMjYsMjYsMCwwLDAsNTAuNTgsMFptNTYuNDItMTVDMTk5LjI5LDE2NSwxOTQsMTQxLjE1LDE5NCwxMTJhNjYsNjYsMCwwLDAtMTMyLDBjMCwyOS4xNi01LjI5LDUzLTE1LjcxLDcxYTIsMiwwLDAsMCwwLDIsMS45LDEuOSwwLDAsMCwxLjcsMUgyMDhhMS45LDEuOSwwLDAsMCwxLjctMUEyLDIsMCwwLDAsMjA5LjcxLDE4M1oiLz48L3N2Zz4=)}.icon-calendar{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsMzRIMTgyVjI0YTYsNiwwLDAsMC0xMiwwVjM0SDg2VjI0YTYsNiwwLDAsMC0xMiwwVjM0SDQ4QTE0LDE0LDAsMCwwLDM0LDQ4VjIwOGExNCwxNCwwLDAsMCwxNCwxNEgyMDhhMTQsMTQsMCwwLDAsMTQtMTRWNDhBMTQsMTQsMCwwLDAsMjA4LDM0Wk00OCw0Nkg3NFY1NmE2LDYsMCwwLDAsMTIsMFY0Nmg4NFY1NmE2LDYsMCwwLDAsMTIsMFY0NmgyNmEyLDIsMCwwLDEsMiwyVjgySDQ2VjQ4QTIsMiwwLDAsMSw0OCw0NlpNMjA4LDIxMEg0OGEyLDIsMCwwLDEtMi0yVjk0SDIxMFYyMDhBMiwyLDAsMCwxLDIwOCwyMTBabS05OC05MHY2NGE2LDYsMCwwLDEtMTIsMFYxMjkuNzFsLTcuMzIsMy42NmE2LDYsMCwxLDEtNS4zNi0xMC43NGwxNi04QTYsNiwwLDAsMSwxMTAsMTIwWm01OS41NywyOS4yNUwxNDgsMTc4aDIwYTYsNiwwLDAsMSwwLDEySDEzNmE2LDYsMCwwLDEtNC44LTkuNkwxNjAsMTQyYTEwLDEwLDAsMSwwLTE2LjY1LTExQTYsNiwwLDEsMSwxMzMsMTI1YTIyLDIyLDAsMSwxLDM2LjYyLDI0LjI2WiIvPjwvc3ZnPg==)}.icon-check{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsNzYuMjRsLTEyOCwxMjhhNiw2LDAsMCwxLTguNDgsMGwtNTYtNTZhNiw2LDAsMCwxLDguNDgtOC40OEw5NiwxOTEuNTEsMjE5Ljc2LDY3Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsNzYuMjRsLTEyOCwxMjhhNiw2LDAsMCwxLTguNDgsMGwtNTYtNTZhNiw2LDAsMCwxLDguNDgtOC40OEw5NiwxOTEuNTEsMjE5Ljc2LDY3Ljc2YTYsNiwwLDAsMSw4LjQ4LDguNDhaIi8+PC9zdmc+)}.icon-clock{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjZBMTAyLDEwMiwwLDEsMCwyMzAsMTI4LDEwMi4xMiwxMDIuMTIsMCwwLDAsMTI4LDI2Wm0wLDE5MmE5MCw5MCwwLDEsMSw5MC05MEE5MC4xLDkwLjEsMCwwLDEsMTI4LDIxOFptNjItOTBhNiw2LDAsMCwxLTYsNkgxMjhhNiw2LDAsMCwxLTYtNlY3MmE2LDYsMCwwLDEsMTIsMHY1MGg1MEE2LDYsMCwwLDEsMTkwLDEyOFoiLz48L3N2Zz4=)}.icon-cloud{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDJBODYuMTEsODYuMTEsMCwwLDAsODIuNDMsOTAuODgsNjIsNjIsMCwxLDAsNzIsMjE0aDg4YTg2LDg2LDAsMCwwLDAtMTcyWm0wLDE2MEg3MmE1MCw1MCwwLDAsMSwwLTEwMCw1MC42Nyw1MC42NywwLDAsMSw1LjkxLjM1QTg1LjYxLDg1LjYxLDAsMCwwLDc0LDEyOGE2LDYsMCwwLDAsMTIsMCw3NCw3NCwwLDEsMSw3NCw3NFoiLz48L3N2Zz4=)}.icon-copy{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsMzRIODhhNiw2LDAsMCwwLTYsNlY4Mkg0MGE2LDYsMCwwLDAtNiw2VjIxNmE2LDYsMCwwLDAsNiw2SDE2OGE2LDYsMCwwLDAsNi02VjE3NGg0MmE2LDYsMCwwLDAsNi02VjQwQTYsNiwwLDAsMCwyMTYsMzRaTTE2MiwyMTBINDZWOTRIMTYyWm00OC00OEgxNzRWODhhNiw2LDAsMCwwLTYtNkg5NFY0NkgyMTBaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsMzRIODhhNiw2LDAsMCwwLTYsNlY4Mkg0MGE2LDYsMCwwLDAtNiw2VjIxNmE2LDYsMCwwLDAsNiw2SDE2OGE2LDYsMCwwLDAsNi02VjE3NGg0MmE2LDYsMCwwLDAsNi02VjQwQTYsNiwwLDAsMCwyMTYsMzRaTTE2MiwyMTBINDZWOTRIMTYyWm00OC00OEgxNzRWODhhNiw2LDAsMCwwLTYtNkg5NFY0NkgyMTBaIi8+PC9zdmc+)}.icon-download{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg3MmE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTg0YTYsNiwwLDAsMSwwLTEyaDQwQTE0LDE0LDAsMCwxLDIzOCwxMzZabS0xMTQuMjQtMy43NmE2LDYsMCwwLDAsOC40OCwwbDQ4LTQ4YTYsNiwwLDAsMC04LjQ4LTguNDhMMTM0LDExMy41MVYyNGE2LDYsMCwwLDAtMTIsMHY4OS41MUw4NC4yNCw3NS43NmE2LDYsMCwwLDAtOC40OCw4LjQ4Wk0xOTgsMTY4YTEwLDEwLDAsMSwwLTEwLDEwQTEwLDEwLDAsMCwwLDE5OCwxNjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg3MmE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTg0YTYsNiwwLDAsMSwwLTEyaDQwQTE0LDE0LDAsMCwxLDIzOCwxMzZabS0xMTQuMjQtMy43NmE2LDYsMCwwLDAsOC40OCwwbDQ4LTQ4YTYsNiwwLDAsMC04LjQ4LTguNDhMMTM0LDExMy41MVYyNGE2LDYsMCwwLDAtMTIsMHY4OS41MUw4NC4yNCw3NS43NmE2LDYsMCwwLDAtOC40OCw4LjQ4Wk0xOTgsMTY4YTEwLDEwLDAsMSwwLTEwLDEwQTEwLDEwLDAsMCwwLDE5OCwxNjhaIi8+PC9zdmc+)}.icon-exclamation-mark{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDIsMjAwYTE0LDE0LDAsMSwxLTE0LTE0QTE0LDE0LDAsMCwxLDE0MiwyMDBabS0xNC00MmE2LDYsMCwwLDAsNi02VjQ4YTYsNiwwLDAsMC0xMiwwVjE1MkE2LDYsMCwwLDAsMTI4LDE1OFoiLz48L3N2Zz4=)}.icon-hamburger{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OC4wNywxMDJIMjA3LjkzYTE0LDE0LDAsMCwwLDEzLjc2LTE3QzIxNC40NSw1MC44MywxNzUuMDUsMjYsMTI4LDI2UzQxLjU1LDUwLjgzLDM0LjMxLDg1YTE0LDE0LDAsMCwwLDEzLjc2LDE3Wm0tMi0xNC40OEM1Miw1OS4yOSw4Ny4yNSwzOCwxMjgsMzhTMjA0LDU5LjI5LDIxMCw4Ny41MmEyLDIsMCwwLDEtMiwyLjQ4SDQ4LjA3YTIsMiwwLDAsMS0yLTIuNDhaTTIzMCwxNTQuMzYsMTg4LjEsMTY5LjU4bC0zNy44Ny0xNS4xNWE2LDYsMCwwLDAtNC40NiwwTDEwOCwxNjkuNTQsNzAuMjMsMTU0LjQzYTYsNiwwLDAsMC00LjI4LS4wN2wtNDQsMTZBNiw2LDAsMCwwLDI0LDE4MmE2LjExLDYuMTEsMCwwLDAsMi4wNS0uMzZsMTYtNS44VjE4NGEzOCwzOCwwLDAsMCwzOCwzOGg5NmEzOCwzOCwwLDAsMCwzOC0zOFYxNzIuOTNsMjAuMDUtNy4yOWE2LDYsMCwwLDAtNC4xLTExLjI4Wk0yMDIsMTg0YTI2LDI2LDAsMCwxLTI2LDI2SDgwYTI2LDI2LDAsMCwxLTI2LTI2VjE3MS40OGwxMy45LTUuMDYsMzcuODcsMTUuMTVhNiw2LDAsMCwwLDQuNDYsMEwxNDgsMTY2LjQ2bDM3Ljc3LDE1LjExYTYsNiwwLDAsMCw0LjI4LjA3TDIwMiwxNzcuMjlaTTE4LDEyOGE2LDYsMCwwLDEsNi02SDIzMmE2LDYsMCwwLDEsMCwxMkgyNEE2LDYsMCwwLDEsMTgsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OC4wNywxMDJIMjA3LjkzYTE0LDE0LDAsMCwwLDEzLjc2LTE3QzIxNC40NSw1MC44MywxNzUuMDUsMjYsMTI4LDI2UzQxLjU1LDUwLjgzLDM0LjMxLDg1YTE0LDE0LDAsMCwwLDEzLjc2LDE3Wm0tMi0xNC40OEM1Miw1OS4yOSw4Ny4yNSwzOCwxMjgsMzhTMjA0LDU5LjI5LDIxMCw4Ny41MmEyLDIsMCwwLDEtMiwyLjQ4SDQ4LjA3YTIsMiwwLDAsMS0yLTIuNDhaTTIzMCwxNTQuMzYsMTg4LjEsMTY5LjU4bC0zNy44Ny0xNS4xNWE2LDYsMCwwLDAtNC40NiwwTDEwOCwxNjkuNTQsNzAuMjMsMTU0LjQzYTYsNiwwLDAsMC00LjI4LS4wN2wtNDQsMTZBNiw2LDAsMCwwLDI0LDE4MmE2LjExLDYuMTEsMCwwLDAsMi4wNS0uMzZsMTYtNS44VjE4NGEzOCwzOCwwLDAsMCwzOCwzOGg5NmEzOCwzOCwwLDAsMCwzOC0zOFYxNzIuOTNsMjAuMDUtNy4yOWE2LDYsMCwwLDAtNC4xLTExLjI4Wk0yMDIsMTg0YTI2LDI2LDAsMCwxLTI2LDI2SDgwYTI2LDI2LDAsMCwxLTI2LTI2VjE3MS40OGwxMy45LTUuMDYsMzcuODcsMTUuMTVhNiw2LDAsMCwwLDQuNDYsMEwxNDgsMTY2LjQ2bDM3Ljc3LDE1LjExYTYsNiwwLDAsMCw0LjI4LjA3TDIwMiwxNzcuMjlaTTE4LDEyOGE2LDYsMCwwLDEsNi02SDIzMmE2LDYsMCwwLDEsMCwxMkgyNEE2LDYsMCwwLDEsMTgsMTI4WiIvPjwvc3ZnPg==)}.icon-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzgsNDJjLTIxLDAtMzkuMjYsOS40Ny01MCwyNS4zNEMxMTcuMjYsNTEuNDcsOTksNDIsNzgsNDJhNjAuMDcsNjAuMDcsMCwwLDAtNjAsNjBjMCwyOS4yLDE4LjIsNTkuNTksNTQuMSw5MC4zMWEzMzQuNjgsMzM0LjY4LDAsMCwwLDUzLjA2LDM3LDYsNiwwLDAsMCw1LjY4LDAsMzM0LjY4LDMzNC42OCwwLDAsMCw1My4wNi0zN0MyMTkuOCwxNjEuNTksMjM4LDEzMS4yLDIzOCwxMDJBNjAuMDcsNjAuMDcsMCwwLDAsMTc4LDQyWk0xMjgsMjE3LjExQzExMS41OSwyMDcuNjQsMzAsMTU3LjcyLDMwLDEwMkE0OC4wNSw0OC4wNSwwLDAsMSw3OCw1NGMyMC4yOCwwLDM3LjMxLDEwLjgzLDQ0LjQ1LDI4LjI3YTYsNiwwLDAsMCwxMS4xLDBDMTQwLjY5LDY0LjgzLDE1Ny43Miw1NCwxNzgsNTRhNDguMDUsNDguMDUsMCwwLDEsNDgsNDhDMjI2LDE1Ny43MiwxNDQuNDEsMjA3LjY0LDEyOCwyMTcuMTFaIi8+PC9zdmc+)}.icon-image{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDJINDBBMTQsMTQsMCwwLDAsMjYsNTZWMjAwYTE0LDE0LDAsMCwwLDE0LDE0SDIxNmExNCwxNCwwLDAsMCwxNC0xNFY1NkExNCwxNCwwLDAsMCwyMTYsNDJaTTQwLDU0SDIxNmEyLDIsMCwwLDEsMiwyVjE2My41N0wxODguNTMsMTM0LjFhMTQsMTQsMCwwLDAtMTkuOCwwbC0yMS40MiwyMS40MkwxMDEuOSwxMTAuMWExNCwxNCwwLDAsMC0xOS44LDBMMzgsMTU0LjJWNTZBMiwyLDAsMCwxLDQwLDU0Wk0zOCwyMDBWMTcxLjE3bDUyLjU4LTUyLjU4YTIsMiwwLDAsMSwyLjg0LDBMMTc2LjgzLDIwMkg0MEEyLDIsMCwwLDEsMzgsMjAwWm0xNzgsMkgxOTMuOGwtMzgtMzgsMjEuNDEtMjEuNDJhMiwyLDAsMCwxLDIuODMsMGwzOCwzOFYyMDBBMiwyLDAsMCwxLDIxNiwyMDJaTTE0NiwxMDBhMTAsMTAsMCwxLDEsMTAsMTBBMTAsMTAsMCwwLDEsMTQ2LDEwMFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDJINDBBMTQsMTQsMCwwLDAsMjYsNTZWMjAwYTE0LDE0LDAsMCwwLDE0LDE0SDIxNmExNCwxNCwwLDAsMCwxNC0xNFY1NkExNCwxNCwwLDAsMCwyMTYsNDJaTTQwLDU0SDIxNmEyLDIsMCwwLDEsMiwyVjE2My41N0wxODguNTMsMTM0LjFhMTQsMTQsMCwwLDAtMTkuOCwwbC0yMS40MiwyMS40MkwxMDEuOSwxMTAuMWExNCwxNCwwLDAsMC0xOS44LDBMMzgsMTU0LjJWNTZBMiwyLDAsMCwxLDQwLDU0Wk0zOCwyMDBWMTcxLjE3bDUyLjU4LTUyLjU4YTIsMiwwLDAsMSwyLjg0LDBMMTc2LjgzLDIwMkg0MEEyLDIsMCwwLDEsMzgsMjAwWm0xNzgsMkgxOTMuOGwtMzgtMzgsMjEuNDEtMjEuNDJhMiwyLDAsMCwxLDIuODMsMGwzOCwzOFYyMDBBMiwyLDAsMCwxLDIxNiwyMDJaTTE0NiwxMDBhMTAsMTAsMCwxLDEsMTAsMTBBMTAsMTAsMCwwLDEsMTQ2LDEwMFoiLz48L3N2Zz4=)}.icon-link{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsODguMThhNTIuNDIsNTIuNDIsMCwwLDEtMTUuNCwzNS42NmwtMzQuNzUsMzQuNzVBNTIuMjgsNTIuMjgsMCwwLDEsMTUwLjYyLDE3NGgtLjA1QTUyLjYzLDUyLjYzLDAsMCwxLDk4LDExOS45YTYsNiwwLDAsMSw2LTUuODRoLjE3YTYsNiwwLDAsMSw1LjgzLDYuMTZBNDAuNjIsNDAuNjIsMCwwLDAsMTUwLjU4LDE2MmgwYTQwLjQsNDAuNCwwLDAsMCwyOC43My0xMS45bDM0Ljc1LTM0Ljc0QTQwLjYzLDQwLjYzLDAsMCwwLDE1Ni42Myw1Ny45bC0xMSwxMWE2LDYsMCwwLDEtOC40OS04LjQ5bDExLTExYTUyLjYyLDUyLjYyLDAsMCwxLDc0LjQzLDBBNTIuODMsNTIuODMsMCwwLDEsMjM4LDg4LjE4Wm0tMTI3LjYyLDk4LjktMTEsMTFBNDAuMzYsNDAuMzYsMCwwLDEsNzAuNiwyMTBoMGE0MC42Myw0MC42MywwLDAsMS0yOC43LTY5LjM2TDc2LjYyLDEwNS45QTQwLjYzLDQwLjYzLDAsMCwxLDE0NiwxMzUuNzdhNiw2LDAsMCwwLDUuODMsNi4xNkgxNTJhNiw2LDAsMCwwLDYtNS44NEE1Mi42Myw1Mi42MywwLDAsMCw2OC4xNCw5Ny40MkwzMy4zOCwxMzIuMTZBNTIuNjMsNTIuNjMsMCwwLDAsNzAuNTYsMjIyaDBhNTIuMjYsNTIuMjYsMCwwLDAsMzcuMjItMTUuNDJsMTEtMTFhNiw2LDAsMSwwLTguNDktOC40OFoiLz48L3N2Zz4=)}.icon-list{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4Wk00MCw3MEgyMTZhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZINDBhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==)}.icon-list-bullets{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04Miw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg4OEE2LDYsMCwwLDEsODIsNjRabTEzNCw1OEg4OGE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNjRIODhhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWk00NCw1NEExMCwxMCwwLDEsMCw1NCw2NCwxMCwxMCwwLDAsMCw0NCw1NFptMCwxMjhhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNDQsMTgyWm0wLTY0YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDQ0LDExOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik04Miw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg4OEE2LDYsMCwwLDEsODIsNjRabTEzNCw1OEg4OGE2LDYsMCwwLDAsMCwxMkgyMTZhNiw2LDAsMCwwLDAtMTJabTAsNjRIODhhNiw2LDAsMCwwLDAsMTJIMjE2YTYsNiwwLDAsMCwwLTEyWk00NCw1NEExMCwxMCwwLDEsMCw1NCw2NCwxMCwxMCwwLDAsMCw0NCw1NFptMCwxMjhhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNDQsMTgyWm0wLTY0YTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDQ0LDExOFoiLz48L3N2Zz4=)}.icon-list-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRabTYsNzBoNjRhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJabTgwLDUySDQwYTYsNiwwLDAsMCwwLDEyaDgwYTYsNiwwLDAsMCwwLTEyWm0xMjYtNDJjMCwzNS4zLTQ5LjIyLDYwLjMyLTUxLjMyLDYxLjM3YTYsNiwwLDAsMS01LjM2LDBDMTg3LjIyLDIwNC4zMiwxMzgsMTc5LjMsMTM4LDE0NGEzMCwzMCwwLDAsMSw1NC0xOCwzMCwzMCwwLDAsMSw1NCwxOFptLTEyLDBhMTgsMTgsMCwwLDAtMzYsMCw2LDYsMCwwLDEtMTIsMCwxOCwxOCwwLDAsMC0zNiwwYzAsMTQuMTUsMTEuNzgsMjcsMjEuNjcsMzUuMjVBMTM4LjgyLDEzOC44MiwwLDAsMCwxOTIsMTkzLjE5YTEzOCwxMzgsMCwwLDAsMjAuMzMtMTMuOTRDMjIyLjIyLDE3MSwyMzQsMTU4LjE1LDIzNCwxNDRaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0zNCw2NGE2LDYsMCwwLDEsNi02SDIxNmE2LDYsMCwwLDEsMCwxMkg0MEE2LDYsMCwwLDEsMzQsNjRabTYsNzBoNjRhNiw2LDAsMCwwLDAtMTJINDBhNiw2LDAsMCwwLDAsMTJabTgwLDUySDQwYTYsNiwwLDAsMCwwLDEyaDgwYTYsNiwwLDAsMCwwLTEyWm0xMjYtNDJjMCwzNS4zLTQ5LjIyLDYwLjMyLTUxLjMyLDYxLjM3YTYsNiwwLDAsMS01LjM2LDBDMTg3LjIyLDIwNC4zMiwxMzgsMTc5LjMsMTM4LDE0NGEzMCwzMCwwLDAsMSw1NC0xOCwzMCwzMCwwLDAsMSw1NCwxOFptLTEyLDBhMTgsMTgsMCwwLDAtMzYsMCw2LDYsMCwwLDEtMTIsMCwxOCwxOCwwLDAsMC0zNiwwYzAsMTQuMTUsMTEuNzgsMjcsMjEuNjcsMzUuMjVBMTM4LjgyLDEzOC44MiwwLDAsMCwxOTIsMTkzLjE5YTEzOCwxMzgsMCwwLDAsMjAuMzMtMTMuOTRDMjIyLjIyLDE3MSwyMzQsMTU4LjE1LDIzNCwxNDRaIi8+PC9zdmc+)}.icon-list-numbers{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTA0YTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNMTA0LDcwSDIxNmE2LDYsMCwwLDAsMC0xMkgxMDRhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZIMTA0YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlpNNDIuNjgsNTMuMzcsNTAsNDkuNzFWMTA0YTYsNiwwLDAsMCwxMiwwVjQwYTYsNiwwLDAsMC04LjY4LTUuMzdsLTE2LDhhNiw2LDAsMCwwLDUuMzYsMTAuNzRaTTcyLDIwMkg1MmwyMS40OC0yOC43NEEyMS41LDIxLjUsMCwwLDAsNzcuNzksMTU3LDIxLjc1LDIxLjc1LDAsMCwwLDY5LDE0Mi4zOGEyMi44NiwyMi44NiwwLDAsMC0zMS4zNSw0LjMxLDIyLjE4LDIyLjE4LDAsMCwwLTMuMjgsNS45Miw2LDYsMCwwLDAsMTEuMjgsNC4xMSw5Ljg3LDkuODcsMCwwLDEsMS40OC0yLjY3LDEwLjc4LDEwLjc4LDAsMCwxLDE0Ljc4LTIsOS44OSw5Ljg5LDAsMCwxLDQsNi42MSw5LjY0LDkuNjQsMCwwLDEtMiw3LjI4bC0uMDYuMDlMMzUuMiwyMDQuNDFBNiw2LDAsMCwwLDQwLDIxNEg3MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZIMTA0YTYsNiwwLDAsMSwwLTEySDIxNkE2LDYsMCwwLDEsMjIyLDEyOFpNMTA0LDcwSDIxNmE2LDYsMCwwLDAsMC0xMkgxMDRhNiw2LDAsMCwwLDAsMTJaTTIxNiwxODZIMTA0YTYsNiwwLDAsMCwwLDEySDIxNmE2LDYsMCwwLDAsMC0xMlpNNDIuNjgsNTMuMzcsNTAsNDkuNzFWMTA0YTYsNiwwLDAsMCwxMiwwVjQwYTYsNiwwLDAsMC04LjY4LTUuMzdsLTE2LDhhNiw2LDAsMCwwLDUuMzYsMTAuNzRaTTcyLDIwMkg1MmwyMS40OC0yOC43NEEyMS41LDIxLjUsMCwwLDAsNzcuNzksMTU3LDIxLjc1LDIxLjc1LDAsMCwwLDY5LDE0Mi4zOGEyMi44NiwyMi44NiwwLDAsMC0zMS4zNSw0LjMxLDIyLjE4LDIyLjE4LDAsMCwwLTMuMjgsNS45Miw2LDYsMCwwLDAsMTEuMjgsNC4xMSw5Ljg3LDkuODcsMCwwLDEsMS40OC0yLjY3LDEwLjc4LDEwLjc4LDAsMCwxLDE0Ljc4LTIsOS44OSw5Ljg5LDAsMCwxLDQsNi42MSw5LjY0LDkuNjQsMCwwLDEtMiw3LjI4bC0uMDYuMDlMMzUuMiwyMDQuNDFBNiw2LDAsMCwwLDQwLDIxNEg3MmE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=)}.icon-minus{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsMTI4YTYsNiwwLDAsMS02LDZINDBhNiw2LDAsMCwxLDAtMTJIMjE2QTYsNiwwLDAsMSwyMjIsMTI4WiIvPjwvc3ZnPg==)}.icon-note{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik05MCw5NmE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDk2QTYsNiwwLDAsMSw5MCw5NlptNiwzOGg2NGE2LDYsMCwwLDAsMC0xMkg5NmE2LDYsMCwwLDAsMCwxMlptMzIsMjBIOTZhNiw2LDAsMCwwLDAsMTJoMzJhNiw2LDAsMCwwLDAtMTJaTTIyMiw0OFYxNTYuNjlhMTMuOTQsMTMuOTQsMCwwLDEtNC4xLDkuOUwxNjYuNTksMjE3LjlhMTMuOTQsMTMuOTQsMCwwLDEtOS45LDQuMUg0OGExNCwxNCwwLDAsMS0xNC0xNFY0OEExNCwxNCwwLDAsMSw0OCwzNEgyMDhBMTQsMTQsMCwwLDEsMjIyLDQ4Wk00OCwyMTBIMTU0VjE2MGE2LDYsMCwwLDEsNi02aDUwVjQ4YTIsMiwwLDAsMC0yLTJINDhhMiwyLDAsMCwwLTIsMlYyMDhBMiwyLDAsMCwwLDQ4LDIxMFptMTUzLjUyLTQ0SDE2NnYzNS41MloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik05MCw5NmE2LDYsMCwwLDEsNi02aDY0YTYsNiwwLDAsMSwwLDEySDk2QTYsNiwwLDAsMSw5MCw5NlptNiwzOGg2NGE2LDYsMCwwLDAsMC0xMkg5NmE2LDYsMCwwLDAsMCwxMlptMzIsMjBIOTZhNiw2LDAsMCwwLDAsMTJoMzJhNiw2LDAsMCwwLDAtMTJaTTIyMiw0OFYxNTYuNjlhMTMuOTQsMTMuOTQsMCwwLDEtNC4xLDkuOUwxNjYuNTksMjE3LjlhMTMuOTQsMTMuOTQsMCwwLDEtOS45LDQuMUg0OGExNCwxNCwwLDAsMS0xNC0xNFY0OEExNCwxNCwwLDAsMSw0OCwzNEgyMDhBMTQsMTQsMCwwLDEsMjIyLDQ4Wk00OCwyMTBIMTU0VjE2MGE2LDYsMCwwLDEsNi02aDUwVjQ4YTIsMiwwLDAsMC0yLTJINDhhMiwyLDAsMCwwLTIsMlYyMDhBMiwyLDAsMCwwLDQ4LDIxMFptMTUzLjUyLTQ0SDE2NnYzNS41MloiLz48L3N2Zz4=)}.icon-paragraph{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsNDJIOTZhNjIsNjIsMCwwLDAsMCwxMjRoNDJ2NDJhNiw2LDAsMCwwLDEyLDBWNTRoMjhWMjA4YTYsNiwwLDAsMCwxMiwwVjU0aDE4YTYsNiwwLDAsMCwwLTEyWk0xMzgsMTU0SDk2QTUwLDUwLDAsMCwxLDk2LDU0aDQyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDgsNDJIOTZhNjIsNjIsMCwwLDAsMCwxMjRoNDJ2NDJhNiw2LDAsMCwwLDEyLDBWNTRoMjhWMjA4YTYsNiwwLDAsMCwxMiwwVjU0aDE4YTYsNiwwLDAsMCwwLTEyWk0xMzgsMTU0SDk2QTUwLDUwLDAsMCwxLDk2LDU0aDQyWiIvPjwvc3ZnPg==)}.icon-password{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw1NlYyMDBhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDBabTk0LjU4LDU2LjQxTDExOCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xNCwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xNC0xOS4yMSwyMi41OC03LjMzYTYsNiwwLDEsMC0zLjcxLTExLjQxWm0xMDMuNTYsMy44NWE2LDYsMCwwLDAtNy41Ni0zLjg1TDIxNCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xMy45NSwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xMy45NS0xOS4yMSwyMi41OC03LjMzQTYsNiwwLDAsMCwyNDQuMTQsMTE2LjI2WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00Niw1NlYyMDBhNiw2LDAsMCwxLTEyLDBWNTZhNiw2LDAsMCwxLDEyLDBabTk0LjU4LDU2LjQxTDExOCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xNCwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xNC0xOS4yMSwyMi41OC03LjMzYTYsNiwwLDEsMC0zLjcxLTExLjQxWm0xMDMuNTYsMy44NWE2LDYsMCwwLDAtNy41Ni0zLjg1TDIxNCwxMTkuNzRWOTZhNiw2LDAsMCwwLTEyLDB2MjMuNzRsLTIyLjU4LTcuMzNhNiw2LDAsMSwwLTMuNzEsMTEuNDFsMjIuNTgsNy4zMy0xMy45NSwxOS4yMWE2LDYsMCwxLDAsOS43LDcuMDZsMTQtMTkuMjEsMTQsMTkuMjFhNiw2LDAsMCwwLDkuNy03LjA2bC0xMy45NS0xOS4yMSwyMi41OC03LjMzQTYsNiwwLDAsMCwyNDQuMTQsMTE2LjI2WiIvPjwvc3ZnPg==)}.icon-share{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTA4LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OC04LjQ4TDIwOS41MSwxMTBIMTY1YTg5Ljk0LDg5Ljk0LDAsMCwwLTg3LjE3LDY3LjUsNiw2LDAsMCwxLTExLjYyLTNBMTAxLjk0LDEwMS45NCwwLDAsMSwxNjUsOThoNDQuNTNMMTcxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhsNDgsNDhBNiw2LDAsMCwxLDIyOC4yNCwxMDguMjRaTTE5MiwyMTBIMzhWODhhNiw2LDAsMCwwLTEyLDBWMjE2YTYsNiwwLDAsMCw2LDZIMTkyYTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjguMjQsMTA4LjI0bC00OCw0OGE2LDYsMCwwLDEtOC40OC04LjQ4TDIwOS41MSwxMTBIMTY1YTg5Ljk0LDg5Ljk0LDAsMCwwLTg3LjE3LDY3LjUsNiw2LDAsMCwxLTExLjYyLTNBMTAxLjk0LDEwMS45NCwwLDAsMSwxNjUsOThoNDQuNTNMMTcxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhsNDgsNDhBNiw2LDAsMCwxLDIyOC4yNCwxMDguMjRaTTE5MiwyMTBIMzhWODhhNiw2LDAsMCwwLTEyLDBWMjE2YTYsNiwwLDAsMCw2LDZIMTkyYTYsNiwwLDAsMCwwLTEyWiIvPjwvc3ZnPg==)}.icon-star{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzcuMjgsOTcuODdBMTQuMTgsMTQuMTgsMCwwLDAsMjI0Ljc2LDg4bC02MC4yNS00Ljg3LTIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDAtMjYuNTgsMEw5MS40OSw4My4xMSwzMS4yNCw4OGExNC4xOCwxNC4xOCwwLDAsMC0xMi41Miw5Ljg5QTE0LjQzLDE0LjQzLDAsMCwwLDIzLDExMy4zMkw2OSwxNTIuOTNsLTE0LDU5LjI1YTE0LjQsMTQuNCwwLDAsMCw1LjU5LDE1LDE0LjEsMTQuMSwwLDAsMCwxNS45MS42TDEyOCwxOTYuMTJsNTEuNTgsMzEuNzFhMTQuMSwxNC4xLDAsMCwwLDE1LjkxLS42LDE0LjQsMTQuNCwwLDAsMCw1LjU5LTE1bC0xNC01OS4yNUwyMzMsMTEzLjMyQTE0LjQzLDE0LjQzLDAsMCwwLDIzNy4yOCw5Ny44N1ptLTEyLjE0LDYuMzctNDguNjksNDJhNiw2LDAsMCwwLTEuOTIsNS45MmwxNC44OCw2Mi43OWEyLjM1LDIuMzUsMCwwLDEtLjk1LDIuNTcsMi4yNCwyLjI0LDAsMCwxLTIuNi4xTDEzMS4xNCwxODRhNiw2LDAsMCwwLTYuMjgsMEw3MC4xNCwyMTcuNjFhMi4yNCwyLjI0LDAsMCwxLTIuNi0uMSwyLjM1LDIuMzUsMCwwLDEtMS0yLjU3bDE0Ljg4LTYyLjc5YTYsNiwwLDAsMC0xLjkyLTUuOTJsLTQ4LjY5LTQyYTIuMzcsMi4zNywwLDAsMS0uNzMtMi42NSwyLjI4LDIuMjgsMCwwLDEsMi4wNy0xLjY1bDYzLjkyLTUuMTZhNiw2LDAsMCwwLDUuMDYtMy42OWwyNC42My01OS42YTIuMzUsMi4zNSwwLDAsMSw0LjM4LDBsMjQuNjMsNTkuNmE2LDYsMCwwLDAsNS4wNiwzLjY5bDYzLjkyLDUuMTZhMi4yOCwyLjI4LDAsMCwxLDIuMDcsMS42NUEyLjM3LDIuMzcsMCwwLDEsMjI1LjE0LDEwNC4yNFoiLz48L3N2Zz4=)}.icon-star-half{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzMsMTEzLjMybC01LjA2LDQuMzZhNiw2LDAsMSwxLTcuODMtOS4wOGw1LjA2LTQuMzZhMi4zOCwyLjM4LDAsMCwwLC43Mi0yLjY1LDIuMjgsMi4yOCwwLDAsMC0yLjA3LTEuNjVsLTguMjgtLjY3YTYsNiwwLDEsMSwxLTEybDguMjcuNjdhMTQuMTcsMTQuMTcsMCwwLDEsMTIuNTIsOS44OUExNC40MywxNC40MywwLDAsMSwyMzMsMTEzLjMyWm0tNTguNDUsMzguODMsNC4wOCwxNy4yMmE2LDYsMCwwLDAsNS44Myw0LjYyLDUuNTEsNS41MSwwLDAsMCwxLjM5LS4xNyw2LDYsMCwwLDAsNC40Ni03LjIybC0zLjI0LTEzLjY3LDguODctNy42NWE2LDYsMCwxLDAtNy44NC05LjA5bC0xMS42MywxMEE2LDYsMCwwLDAsMTc0LjUzLDE1Mi4xNVptMjUuMjMsNTQuNDVhNiw2LDAsMSwwLTExLjY3LDIuNzdsMS4zMiw1LjU3YTIuMzUsMi4zNSwwLDAsMS0uOTUsMi41NywyLjI0LDIuMjQsMCwwLDEtMi42LjFsLTYuNzEtNC4xM2E2LDYsMCwxLDAtNi4yOSwxMC4yMmw2LjcyLDQuMTNhMTQuMzksMTQuMzksMCwwLDAsMjEuNS0xNS42NVpNMTc1LjUyLDk2bC0xNS42NC0xLjI2YTYsNiwwLDAsMS01LjA2LTMuNjlMMTM0LDQwLjd2MTQ1bDguMzksNS4xNWE2LDYsMCwwLDEtNi4yOSwxMC4yM2wtOC4xLTVMNzYuNDIsMjI3LjgzYTE0LjM5LDE0LjM5LDAsMCwxLTIxLjUtMTUuNjVsMTQtNTkuMjVMMjMsMTEzLjMyYTE0LjQzLDE0LjQzLDAsMCwxLTQuMy0xNS40NUExNC4xNywxNC4xNywwLDAsMSwzMS4yNCw4OGw2MC4yNS00Ljg3LDIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDEsMjYuNTgsMGwyMy4yMiw1Ni4yLDEyLDFhNiw2LDAsMCwxLTEsMTJaTTEyMiwxODUuNzJWNDAuN0wxMDEuMTgsOTEuMDlhNiw2LDAsMCwxLTUuMDYsMy42OUwzMi4yLDk5Ljk0YTIuMjgsMi4yOCwwLDAsMC0yLjA3LDEuNjUsMi4zOCwyLjM4LDAsMCwwLC43MiwyLjY1bDQ4LjcsNDJhNiw2LDAsMCwxLDEuOTIsNS45Mkw2Ni41OSwyMTQuOTRhMi4zNSwyLjM1LDAsMCwwLDEsMi41NywyLjI0LDIuMjQsMCwwLDAsMi42LjFaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzMsMTEzLjMybC01LjA2LDQuMzZhNiw2LDAsMSwxLTcuODMtOS4wOGw1LjA2LTQuMzZhMi4zOCwyLjM4LDAsMCwwLC43Mi0yLjY1LDIuMjgsMi4yOCwwLDAsMC0yLjA3LTEuNjVsLTguMjgtLjY3YTYsNiwwLDEsMSwxLTEybDguMjcuNjdhMTQuMTcsMTQuMTcsMCwwLDEsMTIuNTIsOS44OUExNC40MywxNC40MywwLDAsMSwyMzMsMTEzLjMyWm0tNTguNDUsMzguODMsNC4wOCwxNy4yMmE2LDYsMCwwLDAsNS44Myw0LjYyLDUuNTEsNS41MSwwLDAsMCwxLjM5LS4xNyw2LDYsMCwwLDAsNC40Ni03LjIybC0zLjI0LTEzLjY3LDguODctNy42NWE2LDYsMCwxLDAtNy44NC05LjA5bC0xMS42MywxMEE2LDYsMCwwLDAsMTc0LjUzLDE1Mi4xNVptMjUuMjMsNTQuNDVhNiw2LDAsMSwwLTExLjY3LDIuNzdsMS4zMiw1LjU3YTIuMzUsMi4zNSwwLDAsMS0uOTUsMi41NywyLjI0LDIuMjQsMCwwLDEtMi42LjFsLTYuNzEtNC4xM2E2LDYsMCwxLDAtNi4yOSwxMC4yMmw2LjcyLDQuMTNhMTQuMzksMTQuMzksMCwwLDAsMjEuNS0xNS42NVpNMTc1LjUyLDk2bC0xNS42NC0xLjI2YTYsNiwwLDAsMS01LjA2LTMuNjlMMTM0LDQwLjd2MTQ1bDguMzksNS4xNWE2LDYsMCwwLDEtNi4yOSwxMC4yM2wtOC4xLTVMNzYuNDIsMjI3LjgzYTE0LjM5LDE0LjM5LDAsMCwxLTIxLjUtMTUuNjVsMTQtNTkuMjVMMjMsMTEzLjMyYTE0LjQzLDE0LjQzLDAsMCwxLTQuMy0xNS40NUExNC4xNywxNC4xNywwLDAsMSwzMS4yNCw4OGw2MC4yNS00Ljg3LDIzLjIyLTU2LjJhMTQuMzcsMTQuMzcsMCwwLDEsMjYuNTgsMGwyMy4yMiw1Ni4yLDEyLDFhNiw2LDAsMCwxLTEsMTJaTTEyMiwxODUuNzJWNDAuN0wxMDEuMTgsOTEuMDlhNiw2LDAsMCwxLTUuMDYsMy42OUwzMi4yLDk5Ljk0YTIuMjgsMi4yOCwwLDAsMC0yLjA3LDEuNjUsMi4zOCwyLjM4LDAsMCwwLC43MiwyLjY1bDQ4LjcsNDJhNiw2LDAsMCwxLDEuOTIsNS45Mkw2Ni41OSwyMTQuOTRhMi4zNSwyLjM1LDAsMCwwLDEsMi41NywyLjI0LDIuMjQsMCwwLDAsMi42LjFaIi8+PC9zdmc+)}.icon-upload{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg4MGE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTc2YTYsNiwwLDAsMSwwLTEyaDQ4QTE0LDE0LDAsMCwxLDIzOCwxMzZaTTg0LjI0LDc2LjI0LDEyMiwzOC40OVYxMjhhNiw2LDAsMCwwLDEyLDBWMzguNDlsMzcuNzYsMzcuNzVhNiw2LDAsMCwwLDguNDgtOC40OGwtNDgtNDhhNiw2LDAsMCwwLTguNDgsMGwtNDgsNDhhNiw2LDAsMCwwLDguNDgsOC40OFpNMTk4LDE2OGExMCwxMCwwLDEsMC0xMCwxMEExMCwxMCwwLDAsMCwxOTgsMTY4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzgsMTM2djY0YTE0LDE0LDAsMCwxLTE0LDE0SDMyYTE0LDE0LDAsMCwxLTE0LTE0VjEzNmExNCwxNCwwLDAsMSwxNC0xNEg4MGE2LDYsMCwwLDEsMCwxMkgzMmEyLDIsMCwwLDAtMiwydjY0YTIsMiwwLDAsMCwyLDJIMjI0YTIsMiwwLDAsMCwyLTJWMTM2YTIsMiwwLDAsMC0yLTJIMTc2YTYsNiwwLDAsMSwwLTEyaDQ4QTE0LDE0LDAsMCwxLDIzOCwxMzZaTTg0LjI0LDc2LjI0LDEyMiwzOC40OVYxMjhhNiw2LDAsMCwwLDEyLDBWMzguNDlsMzcuNzYsMzcuNzVhNiw2LDAsMCwwLDguNDgtOC40OGwtNDgtNDhhNiw2LDAsMCwwLTguNDgsMGwtNDgsNDhhNiw2LDAsMCwwLDguNDgsOC40OFpNMTk4LDE2OGExMCwxMCwwLDEsMC0xMCwxMEExMCwxMCwwLDAsMCwxOTgsMTY4WiIvPjwvc3ZnPg==)}.icon-sun{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMTZhNiw2LDAsMCwxLDEyLDBWNDBhNiw2LDAsMCwxLTEyLDBabTY4LDg4YTYyLDYyLDAsMSwxLTYyLTYyQTYyLjA3LDYyLjA3LDAsMCwxLDE5MCwxMjhabS0xMiwwYTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDE3OCwxMjhaTTU5Ljc2LDY4LjI0YTYsNiwwLDEsMCw4LjQ4LTguNDhsLTE2LTE2YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTE2LDE2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMTYtMTZhNiw2LDAsMSwwLTguNDgtOC40OFpNMTkyLDcwYTYsNiwwLDAsMCw0LjI0LTEuNzZsMTYtMTZhNiw2LDAsMCwwLTguNDgtOC40OGwtMTYsMTZBNiw2LDAsMCwwLDE5Miw3MFptNC4yNCwxMTcuNzZhNiw2LDAsMCwwLTguNDgsOC40OGwxNiwxNmE2LDYsMCwwLDAsOC40OC04LjQ4Wk00NiwxMjhhNiw2LDAsMCwwLTYtNkgxNmE2LDYsMCwwLDAsMCwxMkg0MEE2LDYsMCwwLDAsNDYsMTI4Wm04Miw4MmE2LDYsMCwwLDAtNiw2djI0YTYsNiwwLDAsMCwxMiwwVjIxNkE2LDYsMCwwLDAsMTI4LDIxMFptMTEyLTg4SDIxNmE2LDYsMCwwLDAsMCwxMmgyNGE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjIsNDBWMTZhNiw2LDAsMCwxLDEyLDBWNDBhNiw2LDAsMCwxLTEyLDBabTY4LDg4YTYyLDYyLDAsMSwxLTYyLTYyQTYyLjA3LDYyLjA3LDAsMCwxLDE5MCwxMjhabS0xMiwwYTUwLDUwLDAsMSwwLTUwLDUwQTUwLjA2LDUwLjA2LDAsMCwwLDE3OCwxMjhaTTU5Ljc2LDY4LjI0YTYsNiwwLDEsMCw4LjQ4LTguNDhsLTE2LTE2YTYsNiwwLDAsMC04LjQ4LDguNDhabTAsMTE5LjUyLTE2LDE2YTYsNiwwLDEsMCw4LjQ4LDguNDhsMTYtMTZhNiw2LDAsMSwwLTguNDgtOC40OFpNMTkyLDcwYTYsNiwwLDAsMCw0LjI0LTEuNzZsMTYtMTZhNiw2LDAsMCwwLTguNDgtOC40OGwtMTYsMTZBNiw2LDAsMCwwLDE5Miw3MFptNC4yNCwxMTcuNzZhNiw2LDAsMCwwLTguNDgsOC40OGwxNiwxNmE2LDYsMCwwLDAsOC40OC04LjQ4Wk00NiwxMjhhNiw2LDAsMCwwLTYtNkgxNmE2LDYsMCwwLDAsMCwxMkg0MEE2LDYsMCwwLDAsNDYsMTI4Wm04Miw4MmE2LDYsMCwwLDAtNiw2djI0YTYsNiwwLDAsMCwxMiwwVjIxNkE2LDYsMCwwLDAsMTI4LDIxMFptMTEyLTg4SDIxNmE2LDYsMCwwLDAsMCwxMmgyNGE2LDYsMCwwLDAsMC0xMloiLz48L3N2Zz4=)}.icon-moon{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuMTMsMTQzLjY0YTYsNiwwLDAsMC02LTEuNDlBOTAuMDcsOTAuMDcsMCwwLDEsMTEzLjg2LDI5Ljg1YTYsNiwwLDAsMC03LjQ5LTcuNDhBMTAyLjg4LDEwMi44OCwwLDAsMCw1NC40OCw1OC42OCwxMDIsMTAyLDAsMCwwLDE5Ny4zMiwyMDEuNTJhMTAyLjg4LDEwMi44OCwwLDAsMCwzNi4zMS01MS44OUE2LDYsMCwwLDAsMjMyLjEzLDE0My42NFptLTQyLDQ4LjI5YTkwLDkwLDAsMCwxLTEyNi0xMjZBOTAuOSw5MC45LDAsMCwxLDk5LjY1LDM3LjY2LDEwMi4wNiwxMDIuMDYsMCwwLDAsMjE4LjM0LDE1Ni4zNSw5MC45LDkwLjksMCwwLDEsMTkwLjEsMTkxLjkzWiIvPjwvc3ZnPg==)}.icon-x{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDQuMjQsMTk1Ljc2YTYsNiwwLDEsMS04LjQ4LDguNDhMMTI4LDEzNi40OSw2MC4yNCwyMDQuMjRhNiw2LDAsMCwxLTguNDgtOC40OEwxMTkuNTEsMTI4LDUxLjc2LDYwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MWw2Ny43Ni02Ny43NWE2LDYsMCwwLDEsOC40OCw4LjQ4TDEzNi40OSwxMjhaIi8+PC9zdmc+)}.icon-question{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMzgsMTgwYTEwLDEwLDAsMSwxLTEwLTEwQTEwLDEwLDAsMCwxLDEzOCwxODBaTTEyOCw3NGMtMjEsMC0zOCwxNS4yNS0zOCwzNHY0YTYsNiwwLDAsMCwxMiwwdi00YzAtMTIuMTMsMTEuNjYtMjIsMjYtMjJzMjYsOS44NywyNiwyMi0xMS42NiwyMi0yNiwyMmE2LDYsMCwwLDAtNiw2djhhNiw2LDAsMCwwLDEyLDB2LTIuNDJjMTguMTEtMi41OCwzMi0xNi42NiwzMi0zMy41OEMxNjYsODkuMjUsMTQ5LDc0LDEyOCw3NFptMTAyLDU0QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+)}.icon-arrows-left-right{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTcxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTMyLDMyYTYsNiwwLDAsMS04LjQ4LTguNDhMMTkzLjUxLDE4Mkg0OGE2LDYsMCwwLDEsMC0xMkgxOTMuNTFsLTIxLjc1LTIxLjc2YTYsNiwwLDAsMSw4LjQ4LTguNDhaTTc1Ljc2LDExNi4yNGE2LDYsMCwwLDAsOC40OC04LjQ4TDYyLjQ5LDg2SDIwOGE2LDYsMCwwLDAsMC0xMkg2Mi40OUw4NC4yNCw1Mi4yNGE2LDYsMCwwLDAtOC40OC04LjQ4bC0zMiwzMmE2LDYsMCwwLDAsMCw4LjQ4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTIuMjQsMTcxLjc2YTYsNiwwLDAsMSwwLDguNDhsLTMyLDMyYTYsNiwwLDAsMS04LjQ4LTguNDhMMTkzLjUxLDE4Mkg0OGE2LDYsMCwwLDEsMC0xMkgxOTMuNTFsLTIxLjc1LTIxLjc2YTYsNiwwLDAsMSw4LjQ4LTguNDhaTTc1Ljc2LDExNi4yNGE2LDYsMCwwLDAsOC40OC04LjQ4TDYyLjQ5LDg2SDIwOGE2LDYsMCwwLDAsMC0xMkg2Mi40OUw4NC4yNCw1Mi4yNGE2LDYsMCwwLDAtOC40OC04LjQ4bC0zMiwzMmE2LDYsMCwwLDAsMCw4LjQ4WiIvPjwvc3ZnPg==)}.icon-asterisk{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuMTQsMTc5LjA5YTYsNiwwLDAsMS04LjIzLDIuMDZMMTM0LDEzOC42VjIxNmE2LDYsMCwwLDEtMTIsMFYxMzguNkw1MS4wOSwxODEuMTVBNi4wNyw2LjA3LDAsMCwxLDQ4LDE4MmE2LDYsMCwwLDEtMy4xLTExLjE1TDExNi4zNCwxMjgsNDQuOTEsODUuMTVhNiw2LDAsMCwxLDYuMTgtMTAuM0wxMjIsMTE3LjRWNDBhNiw2LDAsMCwxLDEyLDB2NzcuNGw3MC45MS00Mi41NWE2LDYsMCwwLDEsNi4xOCwxMC4zTDEzOS42NiwxMjhsNzEuNDMsNDIuODVBNiw2LDAsMCwxLDIxMy4xNCwxNzkuMDlaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuMTQsMTc5LjA5YTYsNiwwLDAsMS04LjIzLDIuMDZMMTM0LDEzOC42VjIxNmE2LDYsMCwwLDEtMTIsMFYxMzguNkw1MS4wOSwxODEuMTVBNi4wNyw2LjA3LDAsMCwxLDQ4LDE4MmE2LDYsMCwwLDEtMy4xLTExLjE1TDExNi4zNCwxMjgsNDQuOTEsODUuMTVhNiw2LDAsMCwxLDYuMTgtMTAuM0wxMjIsMTE3LjRWNDBhNiw2LDAsMCwxLDEyLDB2NzcuNGw3MC45MS00Mi41NWE2LDYsMCwwLDEsNi4xOCwxMC4zTDEzOS42NiwxMjhsNzEuNDMsNDIuODVBNiw2LDAsMCwxLDIxMy4xNCwxNzkuMDlaIi8+PC9zdmc+)}.icon-cake{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEyYTIyLDIyLDAsMCwwLTIyLTIySDEzNFY3Ny40QTMwLjA1LDMwLjA1LDAsMCwwLDE1OCw0OGMwLTI2LjktMjUuNTgtNDQuMjctMjYuNjctNDVhNiw2LDAsMCwwLTYuNjYsMEMxMjMuNTgsMy43Myw5OCwyMS4xLDk4LDQ4YTMwLjA1LDMwLjA1LDAsMCwwLDI0LDI5LjRWOTBINDhhMjIsMjIsMCwwLDAtMjIsMjJ2MjMuMzNhMzguODEsMzguODEsMCwwLDAsOCwyMy41N1YyMDBhMjIsMjIsMCwwLDAsMjIsMjJIMjAwYTIyLDIyLDAsMCwwLDIyLTIyVjE1OC45YTM4LjgxLDM4LjgxLDAsMCwwLDgtMjMuNTdaTTExMCw0OGMwLTE1LjQsMTItMjcuNDEsMTgtMzIuNDQsNiw1LDE4LDE3LDE4LDMyLjQ0YTE4LDE4LDAsMCwxLTM2LDBaTTM4LDExMmExMCwxMCwwLDAsMSwxMC0xMEgyMDhhMTAsMTAsMCwwLDEsMTAsMTB2MjMuMzNjMCwxNC4zMi0xMS4zMywyNi4yOC0yNS4yNiwyNi42NkEyNiwyNiwwLDAsMSwxNjYsMTM2YTYsNiwwLDAsMC0xMiwwLDI2LDI2LDAsMCwxLTUyLDAsNiw2LDAsMCwwLTEyLDAsMjYsMjYsMCwwLDEtMjYuNzQsMjZDNDkuMzMsMTYxLjYxLDM4LDE0OS42NSwzOCwxMzUuMzNabTE2Miw5OEg1NmExMCwxMCwwLDAsMS0xMC0xMFYxNjkuMzFBMzYuOCwzNi44LDAsMCwwLDYyLjk0LDE3NCwzNy44MiwzNy44MiwwLDAsMCw5MC41LDE2My4yNCwzOC44MywzOC44MywwLDAsMCw5NiwxNTYuNWEzOCwzOCwwLDAsMCw2NCwwLDM4LjgzLDM4LjgzLDAsMCwwLDUuNTIsNi43NEEzNy43MywzNy43MywwLDAsMCwxOTIsMTc0aDEuMDdBMzYuOCwzNi44LDAsMCwwLDIxMCwxNjkuMzFWMjAwQTEwLDEwLDAsMCwxLDIwMCwyMTBaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAsMTEyYTIyLDIyLDAsMCwwLTIyLTIySDEzNFY3Ny40QTMwLjA1LDMwLjA1LDAsMCwwLDE1OCw0OGMwLTI2LjktMjUuNTgtNDQuMjctMjYuNjctNDVhNiw2LDAsMCwwLTYuNjYsMEMxMjMuNTgsMy43Myw5OCwyMS4xLDk4LDQ4YTMwLjA1LDMwLjA1LDAsMCwwLDI0LDI5LjRWOTBINDhhMjIsMjIsMCwwLDAtMjIsMjJ2MjMuMzNhMzguODEsMzguODEsMCwwLDAsOCwyMy41N1YyMDBhMjIsMjIsMCwwLDAsMjIsMjJIMjAwYTIyLDIyLDAsMCwwLDIyLTIyVjE1OC45YTM4LjgxLDM4LjgxLDAsMCwwLDgtMjMuNTdaTTExMCw0OGMwLTE1LjQsMTItMjcuNDEsMTgtMzIuNDQsNiw1LDE4LDE3LDE4LDMyLjQ0YTE4LDE4LDAsMCwxLTM2LDBaTTM4LDExMmExMCwxMCwwLDAsMSwxMC0xMEgyMDhhMTAsMTAsMCwwLDEsMTAsMTB2MjMuMzNjMCwxNC4zMi0xMS4zMywyNi4yOC0yNS4yNiwyNi42NkEyNiwyNiwwLDAsMSwxNjYsMTM2YTYsNiwwLDAsMC0xMiwwLDI2LDI2LDAsMCwxLTUyLDAsNiw2LDAsMCwwLTEyLDAsMjYsMjYsMCwwLDEtMjYuNzQsMjZDNDkuMzMsMTYxLjYxLDM4LDE0OS42NSwzOCwxMzUuMzNabTE2Miw5OEg1NmExMCwxMCwwLDAsMS0xMC0xMFYxNjkuMzFBMzYuOCwzNi44LDAsMCwwLDYyLjk0LDE3NCwzNy44MiwzNy44MiwwLDAsMCw5MC41LDE2My4yNCwzOC44MywzOC44MywwLDAsMCw5NiwxNTYuNWEzOCwzOCwwLDAsMCw2NCwwLDM4LjgzLDM4LjgzLDAsMCwwLDUuNTIsNi43NEEzNy43MywzNy43MywwLDAsMCwxOTIsMTc0aDEuMDdBMzYuOCwzNi44LDAsMCwwLDIxMCwxNjkuMzFWMjAwQTEwLDEwLDAsMCwxLDIwMCwyMTBaIi8+PC9zdmc+)}.icon-person{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsNzBBMzAsMzAsMCwxLDAsOTgsNDAsMzAsMzAsMCwwLDAsMTI4LDcwWm0wLTQ4YTE4LDE4LDAsMSwxLTE4LDE4QTE4LDE4LDAsMCwxLDEyOCwyMlptODguODgsMTEzLjQyTDE3MS42Nyw4NC4xNkEzMCwzMCwwLDAsMCwxNDkuMTcsNzRIMTA2LjgzYTMwLDMwLDAsMCwwLTIyLjUsMTAuMTVMMzkuMTIsMTM1LjQyQTE4LDE4LDAsMCwwLDY0LjQ2LDE2MWwyMS4xMS0xNi45M0w2Ny40NCwyMTIuOTJhMTgsMTgsMCwwLDAsMzIuNzUsMTQuOTRMMTI4LDE4MGwyNy44MSw0Ny45MWExOCwxOCwwLDAsMCwzMi43NS0xNC45NGwtMTguMTMtNjguODdMMTkxLjU0LDE2MWExOCwxOCwwLDAsMCwyNS4zNC0yNS41NlptLTguNjMsMTYuODJhNiw2LDAsMCwxLTguNDksMCw0LjE1LDQuMTUsMCwwLDAtLjQ5LS40NGwtMzUuNTEtMjguNDhhNiw2LDAsMCwwLTkuNTYsNi4ybDIyLjg3LDg2LjkzYTcuNjYsNy42NiwwLDAsMCwuMzcsMSw2LDYsMCwwLDEtMTAuODgsNS4wNyw0LjM3LDQuMzcsMCwwLDAtLjI1LS40OEwxMzMuMTksMTY1YTYsNiwwLDAsMC0xMC4zOCwwTDg5LjY5LDIyMi4wNWE0LjM3LDQuMzcsMCwwLDAtLjI1LjQ4LDYsNiwwLDAsMS0xMC44OC01LjA3LDcuNjYsNy42NiwwLDAsMCwuMzctMWwyMi44Ny04Ni45M0E2LDYsMCwwLDAsOTkuMjcsMTIzLDYuMDcsNi4wNywwLDAsMCw5NiwxMjJhNiw2LDAsMCwwLTMuNzYsMS4zMkw1Ni43MywxNTEuOGE0LjE1LDQuMTUsMCwwLDAtLjQ5LjQ0LDYsNiwwLDAsMS04LjQ5LTguNDlsLjI2LS4yN0w5My4zMyw5Mi4wOUExOCwxOCwwLDAsMSwxMDYuODMsODZoNDIuMzRhMTgsMTgsMCwwLDEsMTMuNSw2LjA5TDIwOCwxNDMuNDhsLjI2LjI3QTYsNiwwLDAsMSwyMDguMjUsMTUyLjI0WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsNzBBMzAsMzAsMCwxLDAsOTgsNDAsMzAsMzAsMCwwLDAsMTI4LDcwWm0wLTQ4YTE4LDE4LDAsMSwxLTE4LDE4QTE4LDE4LDAsMCwxLDEyOCwyMlptODguODgsMTEzLjQyTDE3MS42Nyw4NC4xNkEzMCwzMCwwLDAsMCwxNDkuMTcsNzRIMTA2LjgzYTMwLDMwLDAsMCwwLTIyLjUsMTAuMTVMMzkuMTIsMTM1LjQyQTE4LDE4LDAsMCwwLDY0LjQ2LDE2MWwyMS4xMS0xNi45M0w2Ny40NCwyMTIuOTJhMTgsMTgsMCwwLDAsMzIuNzUsMTQuOTRMMTI4LDE4MGwyNy44MSw0Ny45MWExOCwxOCwwLDAsMCwzMi43NS0xNC45NGwtMTguMTMtNjguODdMMTkxLjU0LDE2MWExOCwxOCwwLDAsMCwyNS4zNC0yNS41NlptLTguNjMsMTYuODJhNiw2LDAsMCwxLTguNDksMCw0LjE1LDQuMTUsMCwwLDAtLjQ5LS40NGwtMzUuNTEtMjguNDhhNiw2LDAsMCwwLTkuNTYsNi4ybDIyLjg3LDg2LjkzYTcuNjYsNy42NiwwLDAsMCwuMzcsMSw2LDYsMCwwLDEtMTAuODgsNS4wNyw0LjM3LDQuMzcsMCwwLDAtLjI1LS40OEwxMzMuMTksMTY1YTYsNiwwLDAsMC0xMC4zOCwwTDg5LjY5LDIyMi4wNWE0LjM3LDQuMzcsMCwwLDAtLjI1LjQ4LDYsNiwwLDAsMS0xMC44OC01LjA3LDcuNjYsNy42NiwwLDAsMCwuMzctMWwyMi44Ny04Ni45M0E2LDYsMCwwLDAsOTkuMjcsMTIzLDYuMDcsNi4wNywwLDAsMCw5NiwxMjJhNiw2LDAsMCwwLTMuNzYsMS4zMkw1Ni43MywxNTEuOGE0LjE1LDQuMTUsMCwwLDAtLjQ5LjQ0LDYsNiwwLDAsMS04LjQ5LTguNDlsLjI2LS4yN0w5My4zMyw5Mi4wOUExOCwxOCwwLDAsMSwxMDYuODMsODZoNDIuMzRhMTgsMTgsMCwwLDEsMTMuNSw2LjA5TDIwOCwxNDMuNDhsLjI2LjI3QTYsNiwwLDAsMSwyMDguMjUsMTUyLjI0WiIvPjwvc3ZnPg==)}.icon-crosshair{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIySDIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDEzNCwzNC4yVjI0YTYsNiwwLDAsMC0xMiwwVjM0LjJBOTQuMTMsOTQuMTMsMCwwLDAsMzQuMiwxMjJIMjRhNiw2LDAsMCwwLDAsMTJIMzQuMkE5NC4xMyw5NC4xMywwLDAsMCwxMjIsMjIxLjhWMjMyYTYsNiwwLDAsMCwxMiwwVjIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDIyMS44LDEzNEgyMzJhNiw2LDAsMCwwLDAtMTJabS05OCw4Ny43NlYyMDBhNiw2LDAsMCwwLTEyLDB2OS43NkE4Mi4wOSw4Mi4wOSwwLDAsMSw0Ni4yNCwxMzRINTZhNiw2LDAsMCwwLDAtMTJINDYuMjRBODIuMDksODIuMDksMCwwLDEsMTIyLDQ2LjI0VjU2YTYsNiwwLDAsMCwxMiwwVjQ2LjI0QTgyLjA5LDgyLjA5LDAsMCwxLDIwOS43NiwxMjJIMjAwYTYsNiwwLDAsMCwwLDEyaDkuNzZBODIuMDksODIuMDksMCwwLDEsMTM0LDIwOS43NlpNMTI4LDkwYTM4LDM4LDAsMSwwLDM4LDM4QTM4LDM4LDAsMCwwLDEyOCw5MFptMCw2NGEyNiwyNiwwLDEsMSwyNi0yNkEyNiwyNiwwLDAsMSwxMjgsMTU0WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIySDIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDEzNCwzNC4yVjI0YTYsNiwwLDAsMC0xMiwwVjM0LjJBOTQuMTMsOTQuMTMsMCwwLDAsMzQuMiwxMjJIMjRhNiw2LDAsMCwwLDAsMTJIMzQuMkE5NC4xMyw5NC4xMywwLDAsMCwxMjIsMjIxLjhWMjMyYTYsNiwwLDAsMCwxMiwwVjIyMS44QTk0LjEzLDk0LjEzLDAsMCwwLDIyMS44LDEzNEgyMzJhNiw2LDAsMCwwLDAtMTJabS05OCw4Ny43NlYyMDBhNiw2LDAsMCwwLTEyLDB2OS43NkE4Mi4wOSw4Mi4wOSwwLDAsMSw0Ni4yNCwxMzRINTZhNiw2LDAsMCwwLDAtMTJINDYuMjRBODIuMDksODIuMDksMCwwLDEsMTIyLDQ2LjI0VjU2YTYsNiwwLDAsMCwxMiwwVjQ2LjI0QTgyLjA5LDgyLjA5LDAsMCwxLDIwOS43NiwxMjJIMjAwYTYsNiwwLDAsMCwwLDEyaDkuNzZBODIuMDksODIuMDksMCwwLDEsMTM0LDIwOS43NlpNMTI4LDkwYTM4LDM4LDAsMSwwLDM4LDM4QTM4LDM4LDAsMCwwLDEyOCw5MFptMCw2NGEyNiwyNiwwLDEsMSwyNi0yNkEyNiwyNiwwLDAsMSwxMjgsMTU0WiIvPjwvc3ZnPg==)}.icon-dots-six{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03MCw5MkExMCwxMCwwLDEsMSw2MCw4MiwxMCwxMCwwLDAsMSw3MCw5MlptNTgtMTBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTI4LDgyWm02OCwyMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxOTYsMTAyWk02MCwxNTRhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNjAsMTU0Wm02OCwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDEyOCwxNTRabTY4LDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTk2LDE1NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03MCw5MkExMCwxMCwwLDEsMSw2MCw4MiwxMCwxMCwwLDAsMSw3MCw5MlptNTgtMTBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTI4LDgyWm02OCwyMGExMCwxMCwwLDEsMC0xMC0xMEExMCwxMCwwLDAsMCwxOTYsMTAyWk02MCwxNTRhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsNjAsMTU0Wm02OCwwYTEwLDEwLDAsMSwwLDEwLDEwQTEwLDEwLDAsMCwwLDEyOCwxNTRabTY4LDBhMTAsMTAsMCwxLDAsMTAsMTBBMTAsMTAsMCwwLDAsMTk2LDE1NFoiLz48L3N2Zz4=)}.icon-hourglass{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNzUuNjRWNDBhMTQsMTQsMCwwLDAtMTQtMTRINzJBMTQsMTQsMCwwLDAsNTgsNDBWNzZhMTQuMDYsMTQuMDYsMCwwLDAsNS42LDExLjJMMTE4LDEyOCw2My42LDE2OC44QTE0LjA2LDE0LjA2LDAsMCwwLDU4LDE4MHYzNmExNCwxNCwwLDAsMCwxNCwxNEgxODRhMTQsMTQsMCwwLDAsMTQtMTRWMTgwLjM2YTE0LjA4LDE0LjA4LDAsMCwwLTUuNTYtMTEuMTdMMTM4LDEyOGw1NC40OS00MS4xOUExNC4wOCwxNC4wOCwwLDAsMCwxOTgsNzUuNjRaTTE4NiwxODAuMzZWMjE2YTIsMiwwLDAsMS0yLDJINzJhMiwyLDAsMCwxLTItMlYxODBhMiwyLDAsMCwxLC44LTEuNkwxMjgsMTM1LjUxbDU3LjIyLDQzLjI1QTIsMiwwLDAsMSwxODYsMTgwLjM2Wm0wLTEwNC43MmEyLDIsMCwwLDEtLjc5LDEuNkwxMjgsMTIwLjQ5LDcwLjgsNzcuNkEyLDIsMCwwLDEsNzAsNzZWNDBhMiwyLDAsMCwxLDItMkgxODRhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xOTgsNzUuNjRWNDBhMTQsMTQsMCwwLDAtMTQtMTRINzJBMTQsMTQsMCwwLDAsNTgsNDBWNzZhMTQuMDYsMTQuMDYsMCwwLDAsNS42LDExLjJMMTE4LDEyOCw2My42LDE2OC44QTE0LjA2LDE0LjA2LDAsMCwwLDU4LDE4MHYzNmExNCwxNCwwLDAsMCwxNCwxNEgxODRhMTQsMTQsMCwwLDAsMTQtMTRWMTgwLjM2YTE0LjA4LDE0LjA4LDAsMCwwLTUuNTYtMTEuMTdMMTM4LDEyOGw1NC40OS00MS4xOUExNC4wOCwxNC4wOCwwLDAsMCwxOTgsNzUuNjRaTTE4NiwxODAuMzZWMjE2YTIsMiwwLDAsMS0yLDJINzJhMiwyLDAsMCwxLTItMlYxODBhMiwyLDAsMCwxLC44LTEuNkwxMjgsMTM1LjUxbDU3LjIyLDQzLjI1QTIsMiwwLDAsMSwxODYsMTgwLjM2Wm0wLTEwNC43MmEyLDIsMCwwLDEtLjc5LDEuNkwxMjgsMTIwLjQ5LDcwLjgsNzcuNkEyLDIsMCwwLDEsNzAsNzZWNDBhMiwyLDAsMCwxLDItMkgxODRhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=)}.icon-folder{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzRIMTMwLjQ5bC0yNy45LTI3LjlhMTMuOTQsMTMuOTQsMCwwLDAtOS45LTQuMUg0MEExNCwxNCwwLDAsMCwyNiw1NlYyMDAuNjJBMTMuMzksMTMuMzksMCwwLDAsMzkuMzgsMjE0SDIxNi44OUExMy4xMiwxMy4xMiwwLDAsMCwyMzAsMjAwLjg5Vjg4QTE0LDE0LDAsMCwwLDIxNiw3NFpNNDAsNTRIOTIuNjlhMiwyLDAsMCwxLDEuNDEuNTlMMTEzLjUxLDc0SDM4VjU2QTIsMiwwLDAsMSw0MCw1NFpNMjE4LDIwMC44OWExLjExLDEuMTEsMCwwLDEtMS4xMSwxLjExSDM5LjM4QTEuNCwxLjQsMCwwLDEsMzgsMjAwLjYyVjg2SDIxNmEyLDIsMCwwLDEsMiwyWiIvPjwvc3ZnPg==)}.icon-envelope{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNTBIMzJhNiw2LDAsMCwwLTYsNlYxOTJhMTQsMTQsMCwwLDAsMTQsMTRIMjE2YTE0LDE0LDAsMCwwLDE0LTE0VjU2QTYsNiwwLDAsMCwyMjQsNTBabS05Niw4NS44Nkw0Ny40Miw2MkgyMDguNThaTTEwMS42NywxMjgsMzgsMTg2LjM2VjY5LjY0Wm04Ljg4LDguMTRMMTI0LDE0OC40MmE2LDYsMCwwLDAsOC4xLDBsMTMuNC0xMi4yOEwyMDguNTgsMTk0SDQ3LjQzWk0xNTQuMzMsMTI4LDIxOCw2OS42NFYxODYuMzZaIi8+PC9zdmc+)}.icon-hand-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTIsMTQyLjY1YTIyLjQzLDIyLjQzLDAsMCwwLTE5LjU1LTMuODhsLTQuMzIsMUMyMjcsMTE5LjU1LDIzOCw5OS41MSwyMzgsODBjMC0yNS4zNi0yMC4zOS00Ni00NS40Ni00NkE0NS41MSw0NS41MSwwLDAsMCwxNTYsNTJhNDUuNTEsNDUuNTEsMCwwLDAtMzYuNTQtMThDOTQuMzksMzQsNzQsNTQuNjQsNzQsODBjMCwxMS4zOCwzLjYzLDIyLjQ5LDExLjI5LDM0LjM2YTI5LjczLDI5LjczLDAsMCwwLTE2LjU2LDguNDNMNDUuNTIsMTQ2SDE2QTE0LDE0LDAsMCwwLDIsMTYwdjQwYTE0LDE0LDAsMCwwLDE0LDE0SDEyMGE2LDYsMCwwLDAsMS40Ni0uMThsNjQtMTZhNy4xNiw3LjE2LDAsMCwwLC44OS0uM0wyMjUuMTcsMTgxbC4zMy0uMTVhMjIuNiwyMi42LDAsMCwwLDMuNjItMzguMThaTTExOS40Niw0NmEzMy4xNiwzMy4xNiwwLDAsMSwzMSwyMC4yOCw2LDYsMCwwLDAsMTEuMSwwLDMzLjE2LDMzLjE2LDAsMCwxLDMxLTIwLjI4QzIxMC42OCw0NiwyMjYsNjEuNTcsMjI2LDgwYzAsMjAuMjQtMTYuMTgsNDMtNDYuOCw2NS43NWwtMTQuODcsMy40MkEyNiwyNiwwLDAsMCwxNDAsMTE0SDk5LjY3QzkwLjM2LDEwMS42Nyw4Niw5MC44MSw4Niw4MCw4Niw2MS41NywxMDEuMzIsNDYsMTE5LjQ2LDQ2Wk0xNCwyMDBWMTYwYTIsMiwwLDAsMSwyLTJINDJ2NDRIMTZBMiwyLDAsMCwxLDE0LDIwMFptMjA2LjI4LTMwLTM4LjIsMTYuMjdMMTE5LjI2LDIwMkg1NFYxNTQuNDlsMjMuMjEtMjMuMjJBMTcuODgsMTcuODgsMCwwLDEsODkuOTQsMTI2SDE0MGExNCwxNCwwLDAsMSwwLDI4SDExMmE2LDYsMCwwLDAsMCwxMmgzMmE2LDYsMCwwLDAsMS4zNC0uMTVsNjctMTUuNDEuMjQtLjA2QTEwLjYsMTAuNiwwLDAsMSwyMjAuMjgsMTcwWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuMTIsMTQyLjY1YTIyLjQzLDIyLjQzLDAsMCwwLTE5LjU1LTMuODhsLTQuMzIsMUMyMjcsMTE5LjU1LDIzOCw5OS41MSwyMzgsODBjMC0yNS4zNi0yMC4zOS00Ni00NS40Ni00NkE0NS41MSw0NS41MSwwLDAsMCwxNTYsNTJhNDUuNTEsNDUuNTEsMCwwLDAtMzYuNTQtMThDOTQuMzksMzQsNzQsNTQuNjQsNzQsODBjMCwxMS4zOCwzLjYzLDIyLjQ5LDExLjI5LDM0LjM2YTI5LjczLDI5LjczLDAsMCwwLTE2LjU2LDguNDNMNDUuNTIsMTQ2SDE2QTE0LDE0LDAsMCwwLDIsMTYwdjQwYTE0LDE0LDAsMCwwLDE0LDE0SDEyMGE2LDYsMCwwLDAsMS40Ni0uMThsNjQtMTZhNy4xNiw3LjE2LDAsMCwwLC44OS0uM0wyMjUuMTcsMTgxbC4zMy0uMTVhMjIuNiwyMi42LDAsMCwwLDMuNjItMzguMThaTTExOS40Niw0NmEzMy4xNiwzMy4xNiwwLDAsMSwzMSwyMC4yOCw2LDYsMCwwLDAsMTEuMSwwLDMzLjE2LDMzLjE2LDAsMCwxLDMxLTIwLjI4QzIxMC42OCw0NiwyMjYsNjEuNTcsMjI2LDgwYzAsMjAuMjQtMTYuMTgsNDMtNDYuOCw2NS43NWwtMTQuODcsMy40MkEyNiwyNiwwLDAsMCwxNDAsMTE0SDk5LjY3QzkwLjM2LDEwMS42Nyw4Niw5MC44MSw4Niw4MCw4Niw2MS41NywxMDEuMzIsNDYsMTE5LjQ2LDQ2Wk0xNCwyMDBWMTYwYTIsMiwwLDAsMSwyLTJINDJ2NDRIMTZBMiwyLDAsMCwxLDE0LDIwMFptMjA2LjI4LTMwLTM4LjIsMTYuMjdMMTE5LjI2LDIwMkg1NFYxNTQuNDlsMjMuMjEtMjMuMjJBMTcuODgsMTcuODgsMCwwLDEsODkuOTQsMTI2SDE0MGExNCwxNCwwLDAsMSwwLDI4SDExMmE2LDYsMCwwLDAsMCwxMmgzMmE2LDYsMCwwLDAsMS4zNC0uMTVsNjctMTUuNDEuMjQtLjA2QTEwLjYsMTAuNiwwLDAsMSwyMjAuMjgsMTcwWiIvPjwvc3ZnPg==)}.icon-butterfly{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMSw1MS43MUMyMjYuMDksNDUuMjcsMjE4LjY0LDQyLDIwOSw0MmMtMTYuMzMsMC0zNy40MSwxMS4wNi01Ni40LDI5LjU5QTEzMiwxMzIsMCwwLDAsMTM0LDkzLjk0VjU2YTYsNiwwLDAsMC0xMiwwVjkzLjk0YTEzMiwxMzIsMCwwLDAtMTguNTQtMjIuMzVDODQuNDksNTMuMDYsNjMuNCw0Miw0Ny4wNyw0MmMtOS42OSwwLTE3LjE0LDMuMjctMjIuMTUsOS43MS01LjUzLDcuMTEtNy43MSwxNy42OS02LjY2LDMyLjM0LjkxLDEyLjczLDQuMTIsMjYuNTMsNi44MSwzNy4xMyw2LjI4LDI0Ljc0LDIwLjc3LDMzLDMxLjc4LDM1LjY4QTQyLDQyLDAsMSwwLDEyOCwyMDEuNjJhNDIsNDIsMCwxLDAsNzEuMTYtNDQuNzZjMTEtMi42MywyNS41LTEwLjk0LDMxLjc4LTM1LjY4QzIzNy41NSw5NS4yMSwyNDIuNzIsNjYuNjUsMjMxLjEsNTEuNzFaTTkyLDIxMGEzMC4xMiwzMC4xMiwwLDAsMS0zLjM0LTYwQTYsNiwwLDAsMCw4Ny4zNSwxMzhhNDEuNzEsNDEuNzEsMCwwLDAtMjAuMjgsOGMtOSwuMzEtMjQuMTItMy4xNi0zMC4zNy0yNy43Ni0zLjI1LTEyLjgxLTExLjg5LTQ2LjgzLTIuMzEtNTkuMTVDMzcuMDUsNTUuNjYsNDEuMiw1NCw0Ny4wNyw1NGMxMi44OCwwLDMxLjcyLDEwLjI4LDQ4LDI2LjE4QzExMS42OSw5Ni4zOSwxMjIsMTE0LjU5LDEyMiwxMjcuNjdWMTgwQTMwLDMwLDAsMCwxLDkyLDIxMFptMTI3LjMxLTkxLjc3QzIxMy4wNywxNDIuODMsMTk4LDE0Ni4yOSwxODksMTQ2YTQxLjYyLDQxLjYyLDAsMCwwLTIwLjI4LThBNiw2LDAsMSwwLDE2Ny4zNiwxNTAsMzAuMTEsMzAuMTEsMCwxLDEsMTM0LDE4MFYxMjcuNjdjMC0xMy4wOCwxMC4zMi0zMS4yOCwyNi45My00Ny40OUMxNzcuMjMsNjQuMjgsMTk2LjA3LDU0LDIwOSw1NGM1Ljg3LDAsMTAsMS42NiwxMi42OCw1LjA4QzIzMS4yMSw3MS40LDIyMi41NywxMDUuNDIsMjE5LjMyLDExOC4yM1oiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMSw1MS43MUMyMjYuMDksNDUuMjcsMjE4LjY0LDQyLDIwOSw0MmMtMTYuMzMsMC0zNy40MSwxMS4wNi01Ni40LDI5LjU5QTEzMiwxMzIsMCwwLDAsMTM0LDkzLjk0VjU2YTYsNiwwLDAsMC0xMiwwVjkzLjk0YTEzMiwxMzIsMCwwLDAtMTguNTQtMjIuMzVDODQuNDksNTMuMDYsNjMuNCw0Miw0Ny4wNyw0MmMtOS42OSwwLTE3LjE0LDMuMjctMjIuMTUsOS43MS01LjUzLDcuMTEtNy43MSwxNy42OS02LjY2LDMyLjM0LjkxLDEyLjczLDQuMTIsMjYuNTMsNi44MSwzNy4xMyw2LjI4LDI0Ljc0LDIwLjc3LDMzLDMxLjc4LDM1LjY4QTQyLDQyLDAsMSwwLDEyOCwyMDEuNjJhNDIsNDIsMCwxLDAsNzEuMTYtNDQuNzZjMTEtMi42MywyNS41LTEwLjk0LDMxLjc4LTM1LjY4QzIzNy41NSw5NS4yMSwyNDIuNzIsNjYuNjUsMjMxLjEsNTEuNzFaTTkyLDIxMGEzMC4xMiwzMC4xMiwwLDAsMS0zLjM0LTYwQTYsNiwwLDAsMCw4Ny4zNSwxMzhhNDEuNzEsNDEuNzEsMCwwLDAtMjAuMjgsOGMtOSwuMzEtMjQuMTItMy4xNi0zMC4zNy0yNy43Ni0zLjI1LTEyLjgxLTExLjg5LTQ2LjgzLTIuMzEtNTkuMTVDMzcuMDUsNTUuNjYsNDEuMiw1NCw0Ny4wNyw1NGMxMi44OCwwLDMxLjcyLDEwLjI4LDQ4LDI2LjE4QzExMS42OSw5Ni4zOSwxMjIsMTE0LjU5LDEyMiwxMjcuNjdWMTgwQTMwLDMwLDAsMCwxLDkyLDIxMFptMTI3LjMxLTkxLjc3QzIxMy4wNywxNDIuODMsMTk4LDE0Ni4yOSwxODksMTQ2YTQxLjYyLDQxLjYyLDAsMCwwLTIwLjI4LThBNiw2LDAsMSwwLDE2Ny4zNiwxNTAsMzAuMTEsMzAuMTEsMCwxLDEsMTM0LDE4MFYxMjcuNjdjMC0xMy4wOCwxMC4zMi0zMS4yOCwyNi45My00Ny40OUMxNzcuMjMsNjQuMjgsMTk2LjA3LDU0LDIwOSw1NGM1Ljg3LDAsMTAsMS42NiwxMi42OCw1LjA4QzIzMS4yMSw3MS40LDIyMi41NywxMDUuNDIsMjE5LjMyLDExOC4yM1oiLz48L3N2Zz4=)}.icon-phone{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjEuNTksMTYwLjNsLTQ3LjI0LTIxLjE3YTE0LDE0LDAsMCwwLTEzLjI4LDEuMjIsNC44MSw0LjgxLDAsMCwwLS41Ni40MmwtMjQuNjksMjFhMS44OCwxLjg4LDAsMCwxLTEuNjguMDZjLTE1Ljg3LTcuNjYtMzIuMzEtMjQtNDAtMzkuNjVhMS45MSwxLjkxLDAsMCwxLDAtMS42OGwyMS4wNy0yNWE2LjEzLDYuMTMsMCwwLDAsLjQyLS41OCwxNCwxNCwwLDAsMCwxLjEyLTEzLjI3TDk1LjczLDM0LjQ5YTE0LDE0LDAsMCwwLTE0LjU2LTguMzhBNTQuMjQsNTQuMjQsMCwwLDAsMzQsODBjMCw3OC4zLDYzLjcsMTQyLDE0MiwxNDJhNTQuMjUsNTQuMjUsMCwwLDAsNTMuODktNDcuMTdBMTQsMTQsMCwwLDAsMjIxLjU5LDE2MC4zWk0xNzYsMjEwQzEwNC4zMiwyMTAsNDYsMTUxLjY4LDQ2LDgwQTQyLjIzLDQyLjIzLDAsMCwxLDgyLjY3LDM4aC4yM2EyLDIsMCwwLDEsMS44NCwxLjMxbDIxLjEsNDcuMTFhMiwyLDAsMCwxLDAsMS42N0w4NC43MywxMTMuMTVhNC43Myw0LjczLDAsMCwwLS40My41NywxNCwxNCwwLDAsMC0uOTEsMTMuNzNjOC44NywxOC4xNiwyNy4xNywzNi4zMiw0NS41Myw0NS4xOWExNCwxNCwwLDAsMCwxMy43Ny0xYy4xOS0uMTMuMzgtLjI3LjU2LS40MmwyNC42OC0yMWExLjkyLDEuOTIsMCwwLDEsMS42LS4xbDQ3LjI1LDIxLjE3YTIsMiwwLDAsMSwxLjIxLDJBNDIuMjQsNDIuMjQsMCwwLDEsMTc2LDIxMFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjEuNTksMTYwLjNsLTQ3LjI0LTIxLjE3YTE0LDE0LDAsMCwwLTEzLjI4LDEuMjIsNC44MSw0LjgxLDAsMCwwLS41Ni40MmwtMjQuNjksMjFhMS44OCwxLjg4LDAsMCwxLTEuNjguMDZjLTE1Ljg3LTcuNjYtMzIuMzEtMjQtNDAtMzkuNjVhMS45MSwxLjkxLDAsMCwxLDAtMS42OGwyMS4wNy0yNWE2LjEzLDYuMTMsMCwwLDAsLjQyLS41OCwxNCwxNCwwLDAsMCwxLjEyLTEzLjI3TDk1LjczLDM0LjQ5YTE0LDE0LDAsMCwwLTE0LjU2LTguMzhBNTQuMjQsNTQuMjQsMCwwLDAsMzQsODBjMCw3OC4zLDYzLjcsMTQyLDE0MiwxNDJhNTQuMjUsNTQuMjUsMCwwLDAsNTMuODktNDcuMTdBMTQsMTQsMCwwLDAsMjIxLjU5LDE2MC4zWk0xNzYsMjEwQzEwNC4zMiwyMTAsNDYsMTUxLjY4LDQ2LDgwQTQyLjIzLDQyLjIzLDAsMCwxLDgyLjY3LDM4aC4yM2EyLDIsMCwwLDEsMS44NCwxLjMxbDIxLjEsNDcuMTFhMiwyLDAsMCwxLDAsMS42N0w4NC43MywxMTMuMTVhNC43Myw0LjczLDAsMCwwLS40My41NywxNCwxNCwwLDAsMC0uOTEsMTMuNzNjOC44NywxOC4xNiwyNy4xNywzNi4zMiw0NS41Myw0NS4xOWExNCwxNCwwLDAsMCwxMy43Ny0xYy4xOS0uMTMuMzgtLjI3LjU2LS40MmwyNC42OC0yMWExLjkyLDEuOTIsMCwwLDEsMS42LS4xbDQ3LjI1LDIxLjE3YTIsMiwwLDAsMSwxLjIxLDJBNDIuMjQsNDIuMjQsMCwwLDEsMTc2LDIxMFoiLz48L3N2Zz4=)}.icon-chat{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNTBINDBBMTQsMTQsMCwwLDAsMjYsNjRWMjI0YTEzLjg4LDEzLjg4LDAsMCwwLDguMDksMTIuNjlBMTQuMTEsMTQuMTEsMCwwLDAsNDAsMjM4YTEzLjg3LDEzLjg3LDAsMCwwLDktMy4zMWwuMDYtLjA1TDgyLjIzLDIwNkgyMTZhMTQsMTQsMCwwLDAsMTQtMTRWNjRBMTQsMTQsMCwwLDAsMjE2LDUwWm0yLDE0MmEyLDIsMCwwLDEtMiwySDgwYTYsNiwwLDAsMC0zLjkyLDEuNDZMNDEuMjYsMjI1LjUzQTIsMiwwLDAsMSwzOCwyMjRWNjRhMiwyLDAsMCwxLDItMkgyMTZhMiwyLDAsMCwxLDIsMloiLz48L3N2Zz4=)}.icon-arrows-clockwise{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIsNDhWOTZhNiw2LDAsMCwxLTYsNkgxNjhhNiw2LDAsMCwxLDAtMTJoMzMuNTJMMTgzLjQ3LDcyYTgxLjUxLDgxLjUxLDAsMCwwLTU3LjUzLTI0aC0uNDZBODEuNSw4MS41LDAsMCwwLDY4LjE5LDcxLjI4YTYsNiwwLDEsMS04LjM4LTguNTgsOTMuMzgsOTMuMzgsMCwwLDEsNjUuNjctMjYuNzZIMTI2YTkzLjQ1LDkzLjQ1LDAsMCwxLDY2LDI3LjUzbDE4LDE4VjQ4YTYsNiwwLDAsMSwxMiwwWk0xODcuODEsMTg0LjcyYTgxLjUsODEuNSwwLDAsMS01Ny4yOSwyMy4zNGgtLjQ2YTgxLjUxLDgxLjUxLDAsMCwxLTU3LjUzLTI0TDU0LjQ4LDE2Nkg4OGE2LDYsMCwwLDAsMC0xMkg0MGE2LDYsMCwwLDAtNiw2djQ4YTYsNiwwLDAsMCwxMiwwVjE3NC40OGwxOCwxOC4wNWE5My40NSw5My40NSwwLDAsMCw2NiwyNy41M2guNTJhOTMuMzgsOTMuMzgsMCwwLDAsNjUuNjctMjYuNzYsNiw2LDAsMSwwLTguMzgtOC41OFoiLz48L3N2Zz4=)}.icon-check-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzIuMjQsOTkuNzZhNiw2LDAsMCwxLDAsOC40OGwtNTYsNTZhNiw2LDAsMCwxLTguNDgsMGwtMjQtMjRhNiw2LDAsMCwxLDguNDgtOC40OEwxMTIsMTUxLjUxbDUxLjc2LTUxLjc1QTYsNiwwLDAsMSwxNzIuMjQsOTkuNzZaTTIzMCwxMjhBMTAyLDEwMiwwLDEsMSwxMjgsMjYsMTAyLjEyLDEwMi4xMiwwLDAsMSwyMzAsMTI4Wm0tMTIsMGE5MCw5MCwwLDEsMC05MCw5MEE5MC4xLDkwLjEsMCwwLDAsMjE4LDEyOFoiLz48L3N2Zz4=)}.icon-x-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjQuMjQsMTAwLjI0LDEzNi40OCwxMjhsMjcuNzYsMjcuNzZhNiw2LDAsMSwxLTguNDgsOC40OEwxMjgsMTM2LjQ4bC0yNy43NiwyNy43NmE2LDYsMCwwLDEtOC40OC04LjQ4TDExOS41MiwxMjgsOTEuNzYsMTAwLjI0YTYsNiwwLDAsMSw4LjQ4LTguNDhMMTI4LDExOS41MmwyNy43Ni0yNy43NmE2LDYsMCwwLDEsOC40OCw4LjQ4Wk0yMzAsMTI4QTEwMiwxMDIsMCwxLDEsMTI4LDI2LDEwMi4xMiwxMDIuMTIsMCwwLDEsMjMwLDEyOFptLTEyLDBhOTAsOTAsMCwxLDAtOTAsOTBBOTAuMSw5MC4xLDAsMCwwLDIxOCwxMjhaIi8+PC9zdmc+)}.icon-arrow-fat-down{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuNjYsMTQxLjY2bC05Niw5NmE4LDgsMCwwLDEtMTEuMzIsMGwtOTYtOTZBOCw4LDAsMCwxLDMyLDEyOEg3MlY0OEExNiwxNiwwLDAsMSw4OCwzMmg4MGExNiwxNiwwLDAsMSwxNiwxNnY4MGg0MGE4LDgsMCwwLDEsNS42NiwxMy42NloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjkuNjYsMTQxLjY2bC05Niw5NmE4LDgsMCwwLDEtMTEuMzIsMGwtOTYtOTZBOCw4LDAsMCwxLDMyLDEyOEg3MlY0OEExNiwxNiwwLDAsMSw4OCwzMmg4MGExNiwxNiwwLDAsMSwxNiwxNnY4MGg0MGE4LDgsMCwwLDEsNS42NiwxMy42NloiLz48L3N2Zz4=)}.icon-arrow-fat-up{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMzksMTIzLjA2QTgsOCwwLDAsMSwyMjQsMTI4SDE4NHY4MGExNiwxNiwwLDAsMS0xNiwxNkg4OGExNiwxNiwwLDAsMS0xNi0xNlYxMjhIMzJhOCw4LDAsMCwxLTUuNjYtMTMuNjZsOTYtOTZhOCw4LDAsMCwxLDExLjMyLDBsOTYsOTZBOCw4LDAsMCwxLDIzMS4zOSwxMjMuMDZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzEuMzksMTIzLjA2QTgsOCwwLDAsMSwyMjQsMTI4SDE4NHY4MGExNiwxNiwwLDAsMS0xNiwxNkg4OGExNiwxNiwwLDAsMS0xNi0xNlYxMjhIMzJhOCw4LDAsMCwxLTUuNjYtMTMuNjZsOTYtOTZhOCw4LDAsMCwxLDExLjMyLDBsOTYsOTZBOCw4LDAsMCwxLDIzMS4zOSwxMjMuMDZaIi8+PC9zdmc+)}.icon-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsMTAyYzAsNzAtMTAzLjc5LDEyNi42Ni0xMDguMjEsMTI5YTgsOCwwLDAsMS03LjU4LDBDMTE5Ljc5LDIyOC42NiwxNiwxNzIsMTYsMTAyQTYyLjA3LDYyLjA3LDAsMCwxLDc4LDQwYzIwLjY1LDAsMzguNzMsOC44OCw1MCwyMy44OUMxMzkuMjcsNDguODgsMTU3LjM1LDQwLDE3OCw0MEE2Mi4wNyw2Mi4wNywwLDAsMSwyNDAsMTAyWiIvPjwvc3ZnPg==)}.icon-star{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzQuMjksMTE0Ljg1bC00NSwzOC44M0wyMDMsMjExLjc1YTE2LjQsMTYuNCwwLDAsMS0yNC41LDE3LjgyTDEyOCwxOTguNDksNzcuNDcsMjI5LjU3QTE2LjQsMTYuNCwwLDAsMSw1MywyMTEuNzVsMTMuNzYtNTguMDctNDUtMzguODNBMTYuNDYsMTYuNDYsMCwwLDEsMzEuMDgsODZsNTktNC43NiwyMi43Ni01NS4wOGExNi4zNiwxNi4zNiwwLDAsMSwzMC4yNywwbDIyLjc1LDU1LjA4LDU5LDQuNzZhMTYuNDYsMTYuNDYsMCwwLDEsOS4zNywyOC44NloiLz48L3N2Zz4=)}.icon-apple-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMywxNjkuNTlhOC4wNyw4LjA3LDAsMCwwLTIuOC0zLjRDMjAzLjUzLDE1NC41MywyMDAsMTM0LjY0LDIwMCwxMjBjMC0xNy42NywxMy40Ny0zMy4wNiwyMS41LTQwLjY3YTgsOCwwLDAsMCwwLTExLjYyQzIwOC44Miw1NS43NCwxODcuODIsNDgsMTY4LDQ4YTcyLjIsNzIuMiwwLDAsMC00MCwxMi4xMyw3MS41Niw3MS41NiwwLDAsMC05MC43MSw5LjA5QTc0LjYzLDc0LjYzLDAsMCwwLDE2LDEyMy40YTEyNy4wNiwxMjcuMDYsMCwwLDAsNDAuMTQsODkuNzNBMzkuOCwzOS44LDAsMCwwLDgzLjU5LDIyNGg4Ny42OGEzOS44NCwzOS44NCwwLDAsMCwyOS4xMi0xMi41NywxMjUsMTI1LDAsMCwwLDE3LjgyLTI0LjZDMjI1LjIzLDE3NCwyMjQuMzMsMTcyLDIyMy4zLDE2OS41OVptLTM0LjYzLDMwLjk0YTIzLjc2LDIzLjc2LDAsMCwxLTE3LjQsNy40N0g4My41OWEyMy44MiwyMy44MiwwLDAsMS0xNi40NC02LjUxQTExMS4xNCwxMTEuMTQsMCwwLDEsMzIsMTIzLDU4LjUsNTguNSwwLDAsMSw0OC42NSw4MC40Nyw1NC44MSw1NC44MSwwLDAsMSw4OCw2NGguNzhBNTUuNDUsNTUuNDUsMCwwLDEsMTIzLDc2LjI4YTgsOCwwLDAsMCwxMCwwQTU1LjQ0LDU1LjQ0LDAsMCwxLDE2OCw2NGE3MC42NCw3MC42NCwwLDAsMSwzNiwxMC4zNWMtMTMsMTQuNTItMjAsMzAuNDctMjAsNDUuNjUsMCwyMy43Nyw3LjY0LDQyLjczLDIyLjE4LDU1LjNBMTA1LjgyLDEwNS44MiwwLDAsMSwxODguNjcsMjAwLjUzWk0xMjguMjMsMzBBNDAsNDAsMCwwLDEsMTY3LDBoMWE4LDgsMCwwLDEsMCwxNmgtMWEyNCwyNCwwLDAsMC0yMy4yNCwxOCw4LDgsMCwxLDEtMTUuNS00WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjMuMywxNjkuNTlhOC4wNyw4LjA3LDAsMCwwLTIuOC0zLjRDMjAzLjUzLDE1NC41MywyMDAsMTM0LjY0LDIwMCwxMjBjMC0xNy42NywxMy40Ny0zMy4wNiwyMS41LTQwLjY3YTgsOCwwLDAsMCwwLTExLjYyQzIwOC44Miw1NS43NCwxODcuODIsNDgsMTY4LDQ4YTcyLjIsNzIuMiwwLDAsMC00MCwxMi4xMyw3MS41Niw3MS41NiwwLDAsMC05MC43MSw5LjA5QTc0LjYzLDc0LjYzLDAsMCwwLDE2LDEyMy40YTEyNy4wNiwxMjcuMDYsMCwwLDAsNDAuMTQsODkuNzNBMzkuOCwzOS44LDAsMCwwLDgzLjU5LDIyNGg4Ny42OGEzOS44NCwzOS44NCwwLDAsMCwyOS4xMi0xMi41NywxMjUsMTI1LDAsMCwwLDE3LjgyLTI0LjZDMjI1LjIzLDE3NCwyMjQuMzMsMTcyLDIyMy4zLDE2OS41OVptLTM0LjYzLDMwLjk0YTIzLjc2LDIzLjc2LDAsMCwxLTE3LjQsNy40N0g4My41OWEyMy44MiwyMy44MiwwLDAsMS0xNi40NC02LjUxQTExMS4xNCwxMTEuMTQsMCwwLDEsMzIsMTIzLDU4LjUsNTguNSwwLDAsMSw0OC42NSw4MC40Nyw1NC44MSw1NC44MSwwLDAsMSw4OCw2NGguNzhBNTUuNDUsNTUuNDUsMCwwLDEsMTIzLDc2LjI4YTgsOCwwLDAsMCwxMCwwQTU1LjQ0LDU1LjQ0LDAsMCwxLDE2OCw2NGE3MC42NCw3MC42NCwwLDAsMSwzNiwxMC4zNWMtMTMsMTQuNTItMjAsMzAuNDctMjAsNDUuNjUsMCwyMy43Nyw3LjY0LDQyLjczLDIyLjE4LDU1LjNBMTA1LjgyLDEwNS44MiwwLDAsMSwxODguNjcsMjAwLjUzWk0xMjguMjMsMzBBNDAsNDAsMCwwLDEsMTY3LDBoMWE4LDgsMCwwLDEsMCwxNmgtMWEyNCwyNCwwLDAsMC0yMy4yNCwxOCw4LDgsMCwxLDEtMTUuNS00WiIvPjwvc3ZnPg==)}.icon-arrows-clockwise{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhWOTZhOCw4LDAsMCwxLTgsOEgxNjhhOCw4LDAsMCwxLDAtMTZoMjguNjlMMTgyLjA2LDczLjM3YTc5LjU2LDc5LjU2LDAsMCwwLTU2LjEzLTIzLjQzaC0uNDVBNzkuNTIsNzkuNTIsMCwwLDAsNjkuNTksNzIuNzEsOCw4LDAsMCwxLDU4LjQxLDYxLjI3YTk2LDk2LDAsMCwxLDEzNSwuNzlMMjA4LDc2LjY5VjQ4YTgsOCwwLDAsMSwxNiwwWk0xODYuNDEsMTgzLjI5YTgwLDgwLDAsMCwxLTExMi40Ny0uNjZMNTkuMzEsMTY4SDg4YTgsOCwwLDAsMCwwLTE2SDQwYTgsOCwwLDAsMC04LDh2NDhhOCw4LDAsMCwwLDE2LDBWMTc5LjMxbDE0LjYzLDE0LjYzQTk1LjQzLDk1LjQzLDAsMCwwLDEzMCwyMjIuMDZoLjUzYTk1LjM2LDk1LjM2LDAsMCwwLDY3LjA3LTI3LjMzLDgsOCwwLDAsMC0xMS4xOC0xMS40NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhWOTZhOCw4LDAsMCwxLTgsOEgxNjhhOCw4LDAsMCwxLDAtMTZoMjguNjlMMTgyLjA2LDczLjM3YTc5LjU2LDc5LjU2LDAsMCwwLTU2LjEzLTIzLjQzaC0uNDVBNzkuNTIsNzkuNTIsMCwwLDAsNjkuNTksNzIuNzEsOCw4LDAsMCwxLDU4LjQxLDYxLjI3YTk2LDk2LDAsMCwxLDEzNSwuNzlMMjA4LDc2LjY5VjQ4YTgsOCwwLDAsMSwxNiwwWk0xODYuNDEsMTgzLjI5YTgwLDgwLDAsMCwxLTExMi40Ny0uNjZMNTkuMzEsMTY4SDg4YTgsOCwwLDAsMCwwLTE2SDQwYTgsOCwwLDAsMC04LDh2NDhhOCw4LDAsMCwwLDE2LDBWMTc5LjMxbDE0LjYzLDE0LjYzQTk1LjQzLDk1LjQzLDAsMCwwLDEzMCwyMjIuMDZoLjUzYTk1LjM2LDk1LjM2LDAsMCwwLDY3LjA3LTI3LjMzLDgsOCwwLDAsMC0xMS4xOC0xMS40NFoiLz48L3N2Zz4=)}.icon-arrows-left-right{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuNjYsMTgxLjY2bC0zMiwzMmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTg4LjY5LDE4NEg0OGE4LDgsMCwwLDEsMC0xNkgxODguNjlsLTE4LjM1LTE4LjM0YTgsOCwwLDAsMSwxMS4zMi0xMS4zMmwzMiwzMkE4LDgsMCwwLDEsMjEzLjY2LDE4MS42NlptLTEzOS4zMi02NGE4LDgsMCwwLDAsMTEuMzItMTEuMzJMNjcuMzEsODhIMjA4YTgsOCwwLDAsMCwwLTE2SDY3LjMxTDg1LjY2LDUzLjY2QTgsOCwwLDAsMCw3NC4zNCw0Mi4zNGwtMzIsMzJhOCw4LDAsMCwwLDAsMTEuMzJaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTMuNjYsMTgxLjY2bC0zMiwzMmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTg4LjY5LDE4NEg0OGE4LDgsMCwwLDEsMC0xNkgxODguNjlsLTE4LjM1LTE4LjM0YTgsOCwwLDAsMSwxMS4zMi0xMS4zMmwzMiwzMkE4LDgsMCwwLDEsMjEzLjY2LDE4MS42NlptLTEzOS4zMi02NGE4LDgsMCwwLDAsMTEuMzItMTEuMzJMNjcuMzEsODhIMjA4YTgsOCwwLDAsMCwwLTE2SDY3LjMxTDg1LjY2LDUzLjY2QTgsOCwwLDAsMCw3NC4zNCw0Mi4zNGwtMzIsMzJhOCw4LDAsMCwwLDAsMTEuMzJaIi8+PC9zdmc+)}.icon-asterisk{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTQuODYsMTgwLjEyYTgsOCwwLDAsMS0xMSwyLjc0TDEzNiwxNDIuMTNWMjE2YTgsOCwwLDAsMS0xNiwwVjE0Mi4xM0w1Mi4xMiwxODIuODZhOCw4LDAsMSwxLTguMjMtMTMuNzJMMTEyLjQ1LDEyOCw0My44OSw4Ni44NmE4LDgsMCwxLDEsOC4yMy0xMy43MkwxMjAsMTEzLjg3VjQwYTgsOCwwLDAsMSwxNiwwdjczLjg3bDY3Ljg4LTQwLjczYTgsOCwwLDEsMSw4LjIzLDEzLjcyTDE0My41NSwxMjhsNjguNTYsNDEuMTRBOCw4LDAsMCwxLDIxNC44NiwxODAuMTJaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTQuODYsMTgwLjEyYTgsOCwwLDAsMS0xMSwyLjc0TDEzNiwxNDIuMTNWMjE2YTgsOCwwLDAsMS0xNiwwVjE0Mi4xM0w1Mi4xMiwxODIuODZhOCw4LDAsMSwxLTguMjMtMTMuNzJMMTEyLjQ1LDEyOCw0My44OSw4Ni44NmE4LDgsMCwxLDEsOC4yMy0xMy43MkwxMjAsMTEzLjg3VjQwYTgsOCwwLDAsMSwxNiwwdjczLjg3bDY3Ljg4LTQwLjczYTgsOCwwLDEsMSw4LjIzLDEzLjcyTDE0My41NSwxMjhsNjguNTYsNDEuMTRBOCw4LDAsMCwxLDIxNC44NiwxODAuMTJaIi8+PC9zdmc+)}.icon-butterfly{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuNyw1MC40OEMyMjksNDUuNywyMjEuODQsNDAsMjA5LDQwYy0xNi44NSwwLTM4LjQ2LDExLjI4LTU3LjgxLDMwLjE2QTE0MC4wNywxNDAuMDcsMCwwLDAsMTM2LDg3LjUzVjU2YTgsOCwwLDAsMC0xNiwwVjg3LjUzYTE0MC4wNywxNDAuMDcsMCwwLDAtMTUuMTUtMTcuMzdDODUuNDksNTEuMjgsNjMuODgsNDAsNDcsNDAsMzQuMTYsNDAsMjcsNDUuNywyMy4zLDUwLjQ4Yy02LjgyLDguNzctMTIuMTgsMjQuMDgtLjIxLDcxLjIsNi4wNSwyMy44MywxOS41MSwzMywzMC42MywzNi40MkE0NCw0NCwwLDAsMCwxMjgsMjA1LjI3YTQ0LDQ0LDAsMCwwLDc0LjI4LTQ3LjE3YzExLjEyLTMuNCwyNC41Ny0xMi41OSwzMC42My0zNi40MkMyMzkuNjMsOTUuMjQsMjQ0Ljg1LDY2LjEsMjMyLjcsNTAuNDhaTTkyLDIwOEEyOC4xMiwyOC4xMiwwLDAsMSw4OC44NiwxNTJhOCw4LDAsMSwwLTEuNzYtMTUuOUE0My42NCw0My42NCwwLDAsMCw2Ni4zNiwxNDRjLTguNDMuMDktMjItMy41Ny0yNy43Ni0yNi4yNkMzNS43MiwxMDYuMzksMjcsNzEuODYsMzUuOTQsNjAuMywzNy4zNyw1OC40Niw0MC4wOSw1Niw0Nyw1NmMyNy4yNywwLDczLDQ0Ljg4LDczLDcxLjY3VjE4MEEyOCwyOCwwLDAsMSw5MiwyMDhaTTIxNy40LDExNy43NGMtNS43NywyMi42OS0xOS4zMywyNi4zNC0yNy43NywyNi4yNmE0My42LDQzLjYsMCwwLDAtMjAuNzQtNy45NSw4LDgsMCwxLDAtMS43NiwxNS45QTI4LjExLDI4LjExLDAsMSwxLDEzNiwxODBWMTI3LjY3QzEzNiwxMDAuODgsMTgxLjY5LDU2LDIwOSw1NmM2Ljk1LDAsOS42NiwyLjQ2LDExLjEsNC4zQzIyOS4wNSw3MS44NiwyMjAuMjgsMTA2LjM5LDIxNy40LDExNy43NFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIuNyw1MC40OEMyMjksNDUuNywyMjEuODQsNDAsMjA5LDQwYy0xNi44NSwwLTM4LjQ2LDExLjI4LTU3LjgxLDMwLjE2QTE0MC4wNywxNDAuMDcsMCwwLDAsMTM2LDg3LjUzVjU2YTgsOCwwLDAsMC0xNiwwVjg3LjUzYTE0MC4wNywxNDAuMDcsMCwwLDAtMTUuMTUtMTcuMzdDODUuNDksNTEuMjgsNjMuODgsNDAsNDcsNDAsMzQuMTYsNDAsMjcsNDUuNywyMy4zLDUwLjQ4Yy02LjgyLDguNzctMTIuMTgsMjQuMDgtLjIxLDcxLjIsNi4wNSwyMy44MywxOS41MSwzMywzMC42MywzNi40MkE0NCw0NCwwLDAsMCwxMjgsMjA1LjI3YTQ0LDQ0LDAsMCwwLDc0LjI4LTQ3LjE3YzExLjEyLTMuNCwyNC41Ny0xMi41OSwzMC42My0zNi40MkMyMzkuNjMsOTUuMjQsMjQ0Ljg1LDY2LjEsMjMyLjcsNTAuNDhaTTkyLDIwOEEyOC4xMiwyOC4xMiwwLDAsMSw4OC44NiwxNTJhOCw4LDAsMSwwLTEuNzYtMTUuOUE0My42NCw0My42NCwwLDAsMCw2Ni4zNiwxNDRjLTguNDMuMDktMjItMy41Ny0yNy43Ni0yNi4yNkMzNS43MiwxMDYuMzksMjcsNzEuODYsMzUuOTQsNjAuMywzNy4zNyw1OC40Niw0MC4wOSw1Niw0Nyw1NmMyNy4yNywwLDczLDQ0Ljg4LDczLDcxLjY3VjE4MEEyOCwyOCwwLDAsMSw5MiwyMDhaTTIxNy40LDExNy43NGMtNS43NywyMi42OS0xOS4zMywyNi4zNC0yNy43NywyNi4yNmE0My42LDQzLjYsMCwwLDAtMjAuNzQtNy45NSw4LDgsMCwxLDAtMS43NiwxNS45QTI4LjExLDI4LjExLDAsMSwxLDEzNiwxODBWMTI3LjY3QzEzNiwxMDAuODgsMTgxLjY5LDU2LDIwOSw1NmM2Ljk1LDAsOS42NiwyLjQ2LDExLjEsNC4zQzIyOS4wNSw3MS44NiwyMjAuMjgsMTA2LjM5LDIxNy40LDExNy43NFoiLz48L3N2Zz4=)}.icon-cake{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTEyYTI0LDI0LDAsMCwwLTI0LTI0SDEzNlY3OWEzMi4wNiwzMi4wNiwwLDAsMCwyNC0zMWMwLTI4LTI2LjQ0LTQ1LjkxLTI3LjU2LTQ2LjY2YTgsOCwwLDAsMC04Ljg4LDBDMTIyLjQ0LDIuMDksOTYsMjAsOTYsNDhhMzIuMDYsMzIuMDYsMCwwLDAsMjQsMzF2OUg0OGEyNCwyNCwwLDAsMC0yNCwyNHYyMy4zM2E0MC44NCw0MC44NCwwLDAsMCw4LDI0LjI0VjIwMGEyNCwyNCwwLDAsMCwyNCwyNEgyMDBhMjQsMjQsMCwwLDAsMjQtMjRWMTU5LjU3YTQwLjg0LDQwLjg0LDAsMCwwLDgtMjQuMjRaTTExMiw0OGMwLTEzLjU3LDEwLTI0LjQ2LDE2LTI5Ljc5LDYsNS4zMywxNiwxNi4yMiwxNiwyOS43OWExNiwxNiwwLDAsMS0zMiwwWk00MCwxMTJhOCw4LDAsMCwxLDgtOEgyMDhhOCw4LDAsMCwxLDgsOHYyMy4zM2MwLDEzLjI1LTEwLjQ2LDI0LjMxLTIzLjMyLDI0LjY2QTI0LDI0LDAsMCwxLDE2OCwxMzZhOCw4LDAsMCwwLTE2LDAsMjQsMjQsMCwwLDEtNDgsMCw4LDgsMCwwLDAtMTYsMCwyNCwyNCwwLDAsMS0yNC42OCwyNEM1MC40NiwxNTkuNjQsNDAsMTQ4LjU4LDQwLDEzNS4zM1ptMTYwLDk2SDU2YTgsOCwwLDAsMS04LThWMTcyLjU2QTM4Ljc3LDM4Ljc3LDAsMCwwLDYyLjg4LDE3NmEzOS42OSwzOS42OSwwLDAsMCwyOS0xMS4zMUE0MC4zNiw0MC4zNiwwLDAsMCw5NiwxNjBhNDAsNDAsMCwwLDAsNjQsMCw0MC4zNiw0MC4zNiwwLDAsMCw0LjEzLDQuNjdBMzkuNjcsMzkuNjcsMCwwLDAsMTkyLDE3NmMuMzgsMCwuNzYsMCwxLjE0LDBBMzguNzcsMzguNzcsMCwwLDAsMjA4LDE3Mi41NlYyMDBBOCw4LDAsMCwxLDIwMCwyMDhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTEyYTI0LDI0LDAsMCwwLTI0LTI0SDEzNlY3OWEzMi4wNiwzMi4wNiwwLDAsMCwyNC0zMWMwLTI4LTI2LjQ0LTQ1LjkxLTI3LjU2LTQ2LjY2YTgsOCwwLDAsMC04Ljg4LDBDMTIyLjQ0LDIuMDksOTYsMjAsOTYsNDhhMzIuMDYsMzIuMDYsMCwwLDAsMjQsMzF2OUg0OGEyNCwyNCwwLDAsMC0yNCwyNHYyMy4zM2E0MC44NCw0MC44NCwwLDAsMCw4LDI0LjI0VjIwMGEyNCwyNCwwLDAsMCwyNCwyNEgyMDBhMjQsMjQsMCwwLDAsMjQtMjRWMTU5LjU3YTQwLjg0LDQwLjg0LDAsMCwwLDgtMjQuMjRaTTExMiw0OGMwLTEzLjU3LDEwLTI0LjQ2LDE2LTI5Ljc5LDYsNS4zMywxNiwxNi4yMiwxNiwyOS43OWExNiwxNiwwLDAsMS0zMiwwWk00MCwxMTJhOCw4LDAsMCwxLDgtOEgyMDhhOCw4LDAsMCwxLDgsOHYyMy4zM2MwLDEzLjI1LTEwLjQ2LDI0LjMxLTIzLjMyLDI0LjY2QTI0LDI0LDAsMCwxLDE2OCwxMzZhOCw4LDAsMCwwLTE2LDAsMjQsMjQsMCwwLDEtNDgsMCw4LDgsMCwwLDAtMTYsMCwyNCwyNCwwLDAsMS0yNC42OCwyNEM1MC40NiwxNTkuNjQsNDAsMTQ4LjU4LDQwLDEzNS4zM1ptMTYwLDk2SDU2YTgsOCwwLDAsMS04LThWMTcyLjU2QTM4Ljc3LDM4Ljc3LDAsMCwwLDYyLjg4LDE3NmEzOS42OSwzOS42OSwwLDAsMCwyOS0xMS4zMUE0MC4zNiw0MC4zNiwwLDAsMCw5NiwxNjBhNDAsNDAsMCwwLDAsNjQsMCw0MC4zNiw0MC4zNiwwLDAsMCw0LjEzLDQuNjdBMzkuNjcsMzkuNjcsMCwwLDAsMTkyLDE3NmMuMzgsMCwuNzYsMCwxLjE0LDBBMzguNzcsMzguNzcsMCwwLDAsMjA4LDE3Mi41NlYyMDBBOCw4LDAsMCwxLDIwMCwyMDhaIi8+PC9zdmc+)}.icon-chat{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDhINDBBMTYsMTYsMCwwLDAsMjQsNjRWMjI0YTE1Ljg0LDE1Ljg0LDAsMCwwLDkuMjUsMTQuNUExNi4wNSwxNi4wNSwwLDAsMCw0MCwyNDBhMTUuODksMTUuODksMCwwLDAsMTAuMjUtMy43OGwuMDktLjA3TDgzLDIwOEgyMTZhMTYsMTYsMCwwLDAsMTYtMTZWNjRBMTYsMTYsMCwwLDAsMjE2LDQ4Wk00MCwyMjRoMFpNMjE2LDE5Mkg4MGE4LDgsMCwwLDAtNS4yMywxLjk1TDQwLDIyNFY2NEgyMTZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNDhINDBBMTYsMTYsMCwwLDAsMjQsNjRWMjI0YTE1Ljg0LDE1Ljg0LDAsMCwwLDkuMjUsMTQuNUExNi4wNSwxNi4wNSwwLDAsMCw0MCwyNDBhMTUuODksMTUuODksMCwwLDAsMTAuMjUtMy43OGwuMDktLjA3TDgzLDIwOEgyMTZhMTYsMTYsMCwwLDAsMTYtMTZWNjRBMTYsMTYsMCwwLDAsMjE2LDQ4Wk00MCwyMjRoMFpNMjE2LDE5Mkg4MGE4LDgsMCwwLDAtNS4yMywxLjk1TDQwLDIyNFY2NEgyMTZaIi8+PC9zdmc+)}.icon-check-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzMuNjYsOTguMzRhOCw4LDAsMCwxLDAsMTEuMzJsLTU2LDU2YTgsOCwwLDAsMS0xMS4zMiwwbC0yNC0yNGE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTEyLDE0OC42OWw1MC4zNC01MC4zNUE4LDgsMCwwLDEsMTczLjY2LDk4LjM0Wk0yMzIsMTI4QTEwNCwxMDQsMCwxLDEsMTI4LDI0LDEwNC4xMSwxMDQuMTEsMCwwLDEsMjMyLDEyOFptLTE2LDBhODgsODgsMCwxLDAtODgsODhBODguMSw4OC4xLDAsMCwwLDIxNiwxMjhaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNzMuNjYsOTguMzRhOCw4LDAsMCwxLDAsMTEuMzJsLTU2LDU2YTgsOCwwLDAsMS0xMS4zMiwwbC0yNC0yNGE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTEyLDE0OC42OWw1MC4zNC01MC4zNUE4LDgsMCwwLDEsMTczLjY2LDk4LjM0Wk0yMzIsMTI4QTEwNCwxMDQsMCwxLDEsMTI4LDI0LDEwNC4xMSwxMDQuMTEsMCwwLDEsMjMyLDEyOFptLTE2LDBhODgsODgsMCwxLDAtODgsODhBODguMSw4OC4xLDAsMCwwLDIxNiwxMjhaIi8+PC9zdmc+)}.icon-clock{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm0wLDE5MmE4OCw4OCwwLDEsMSw4OC04OEE4OC4xLDg4LjEsMCwwLDEsMTI4LDIxNlptNjQtODhhOCw4LDAsMCwxLTgsOEgxMjhhOCw4LDAsMCwxLTgtOFY3MmE4LDgsMCwwLDEsMTYsMHY0OGg0OEE4LDgsMCwwLDEsMTkyLDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm0wLDE5MmE4OCw4OCwwLDEsMSw4OC04OEE4OC4xLDg4LjEsMCwwLDEsMTI4LDIxNlptNjQtODhhOCw4LDAsMCwxLTgsOEgxMjhhOCw4LDAsMCwxLTgtOFY3MmE4LDgsMCwwLDEsMTYsMHY0OGg0OEE4LDgsMCwwLDEsMTkyLDEyOFoiLz48L3N2Zz4=)}.icon-crosshair{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIwaC04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEzNiwzMi4zNFYyNGE4LDgsMCwwLDAtMTYsMHY4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDMyLjM0LDEyMEgyNGE4LDgsMCwwLDAsMCwxNmg4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEyMCwyMjMuNjZWMjMyYTgsOCwwLDAsMCwxNiwwdi04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDIyMy42NiwxMzZIMjMyYTgsOCwwLDAsMCwwLTE2Wm0tOTYsODcuNlYyMDBhOCw4LDAsMCwwLTE2LDB2Ny42QTgwLjE1LDgwLjE1LDAsMCwxLDQ4LjQsMTM2SDU2YTgsOCwwLDAsMCwwLTE2SDQ4LjRBODAuMTUsODAuMTUsMCwwLDEsMTIwLDQ4LjRWNTZhOCw4LDAsMCwwLDE2LDBWNDguNEE4MC4xNSw4MC4xNSwwLDAsMSwyMDcuNiwxMjBIMjAwYTgsOCwwLDAsMCwwLDE2aDcuNkE4MC4xNSw4MC4xNSwwLDAsMSwxMzYsMjA3LjZaTTEyOCw4OGE0MCw0MCwwLDEsMCw0MCw0MEE0MCw0MCwwLDAsMCwxMjgsODhabTAsNjRhMjQsMjQsMCwxLDEsMjQtMjRBMjQsMjQsMCwwLDEsMTI4LDE1MloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzIsMTIwaC04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEzNiwzMi4zNFYyNGE4LDgsMCwwLDAtMTYsMHY4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDMyLjM0LDEyMEgyNGE4LDgsMCwwLDAsMCwxNmg4LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDEyMCwyMjMuNjZWMjMyYTgsOCwwLDAsMCwxNiwwdi04LjM0QTk2LjE0LDk2LjE0LDAsMCwwLDIyMy42NiwxMzZIMjMyYTgsOCwwLDAsMCwwLTE2Wm0tOTYsODcuNlYyMDBhOCw4LDAsMCwwLTE2LDB2Ny42QTgwLjE1LDgwLjE1LDAsMCwxLDQ4LjQsMTM2SDU2YTgsOCwwLDAsMCwwLTE2SDQ4LjRBODAuMTUsODAuMTUsMCwwLDEsMTIwLDQ4LjRWNTZhOCw4LDAsMCwwLDE2LDBWNDguNEE4MC4xNSw4MC4xNSwwLDAsMSwyMDcuNiwxMjBIMjAwYTgsOCwwLDAsMCwwLDE2aDcuNkE4MC4xNSw4MC4xNSwwLDAsMSwxMzYsMjA3LjZaTTEyOCw4OGE0MCw0MCwwLDEsMCw0MCw0MEE0MCw0MCwwLDAsMCwxMjgsODhabTAsNjRhMjQsMjQsMCwxLDEsMjQtMjRBMjQsMjQsMCwwLDEsMTI4LDE1MloiLz48L3N2Zz4=)}.icon-dots-six{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03Miw5MkExMiwxMiwwLDEsMSw2MCw4MCwxMiwxMiwwLDAsMSw3Miw5MlptNTYtMTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTI4LDgwWm02OCwyNGExMiwxMiwwLDEsMC0xMi0xMkExMiwxMiwwLDAsMCwxOTYsMTA0Wk02MCwxNTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsNjAsMTUyWm02OCwwYTEyLDEyLDAsMSwwLDEyLDEyQTEyLDEyLDAsMCwwLDEyOCwxNTJabTY4LDBhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTk2LDE1MloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik03Miw5MkExMiwxMiwwLDEsMSw2MCw4MCwxMiwxMiwwLDAsMSw3Miw5MlptNTYtMTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTI4LDgwWm02OCwyNGExMiwxMiwwLDEsMC0xMi0xMkExMiwxMiwwLDAsMCwxOTYsMTA0Wk02MCwxNTJhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsNjAsMTUyWm02OCwwYTEyLDEyLDAsMSwwLDEyLDEyQTEyLDEyLDAsMCwwLDEyOCwxNTJabTY4LDBhMTIsMTIsMCwxLDAsMTIsMTJBMTIsMTIsMCwwLDAsMTk2LDE1MloiLz48L3N2Zz4=)}.icon-envelope{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhIMzJhOCw4LDAsMCwwLTgsOFYxOTJhMTYsMTYsMCwwLDAsMTYsMTZIMjE2YTE2LDE2LDAsMCwwLDE2LTE2VjU2QTgsOCwwLDAsMCwyMjQsNDhabS05Niw4NS4xNUw1Mi41Nyw2NEgyMDMuNDNaTTk4LjcxLDEyOCw0MCwxODEuODFWNzQuMTlabTExLjg0LDEwLjg1LDEyLDExLjA1YTgsOCwwLDAsMCwxMC44MiwwbDEyLTExLjA1LDU4LDUzLjE1SDUyLjU3Wk0xNTcuMjksMTI4LDIxNiw3NC4xOFYxODEuODJaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNDhIMzJhOCw4LDAsMCwwLTgsOFYxOTJhMTYsMTYsMCwwLDAsMTYsMTZIMjE2YTE2LDE2LDAsMCwwLDE2LTE2VjU2QTgsOCwwLDAsMCwyMjQsNDhabS05Niw4NS4xNUw1Mi41Nyw2NEgyMDMuNDNaTTk4LjcxLDEyOCw0MCwxODEuODFWNzQuMTlabTExLjg0LDEwLjg1LDEyLDExLjA1YTgsOCwwLDAsMCwxMC44MiwwbDEyLTExLjA1LDU4LDUzLjE1SDUyLjU3Wk0xNTcuMjksMTI4LDIxNiw3NC4xOFYxODEuODJaIi8+PC9zdmc+)}.icon-facebook-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm04LDE5MS42M1YxNTJoMjRhOCw4LDAsMCwwLDAtMTZIMTM2VjExMmExNiwxNiwwLDAsMSwxNi0xNmgxNmE4LDgsMCwwLDAsMC0xNkgxNTJhMzIsMzIsMCwwLDAtMzIsMzJ2MjRIOTZhOCw4LDAsMCwwLDAsMTZoMjR2NjMuNjNhODgsODgsMCwxLDEsMTYsMFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsMjRBMTA0LDEwNCwwLDEsMCwyMzIsMTI4LDEwNC4xMSwxMDQuMTEsMCwwLDAsMTI4LDI0Wm04LDE5MS42M1YxNTJoMjRhOCw4LDAsMCwwLDAtMTZIMTM2VjExMmExNiwxNiwwLDAsMSwxNi0xNmgxNmE4LDgsMCwwLDAsMC0xNkgxNTJhMzIsMzIsMCwwLDAtMzIsMzJ2MjRIOTZhOCw4LDAsMCwwLDAsMTZoMjR2NjMuNjNhODgsODgsMCwxLDEsMTYsMFoiLz48L3N2Zz4=)}.icon-folder{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzJIMTMxLjMxTDEwNCw0NC42OUExNS44NiwxNS44NiwwLDAsMCw5Mi42OSw0MEg0MEExNiwxNiwwLDAsMCwyNCw1NlYyMDAuNjJBMTUuNCwxNS40LDAsMCwwLDM5LjM4LDIxNkgyMTYuODlBMTUuMTMsMTUuMTMsMCwwLDAsMjMyLDIwMC44OVY4OEExNiwxNiwwLDAsMCwyMTYsNzJaTTQwLDU2SDkyLjY5bDE2LDE2SDQwWk0yMTYsMjAwSDQwVjg4SDIxNloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMTYsNzJIMTMxLjMxTDEwNCw0NC42OUExNS44NiwxNS44NiwwLDAsMCw5Mi42OSw0MEg0MEExNiwxNiwwLDAsMCwyNCw1NlYyMDAuNjJBMTUuNCwxNS40LDAsMCwwLDM5LjM4LDIxNkgyMTYuODlBMTUuMTMsMTUuMTMsMCwwLDAsMjMyLDIwMC44OVY4OEExNiwxNiwwLDAsMCwyMTYsNzJaTTQwLDU2SDkyLjY5bDE2LDE2SDQwWk0yMTYsMjAwSDQwVjg4SDIxNloiLz48L3N2Zz4=)}.icon-google-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsMTI4YTk2LDk2LDAsMSwxLTIxLjk1LTYxLjA5LDgsOCwwLDEsMS0xMi4zMywxMC4xOEE4MCw4MCwwLDEsMCwyMDcuNiwxMzZIMTI4YTgsOCwwLDAsMSwwLTE2aDg4QTgsOCwwLDAsMSwyMjQsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsMTI4YTk2LDk2LDAsMSwxLTIxLjk1LTYxLjA5LDgsOCwwLDEsMS0xMi4zMywxMC4xOEE4MCw4MCwwLDEsMCwyMDcuNiwxMzZIMTI4YTgsOCwwLDAsMSwwLTE2aDg4QTgsOCwwLDAsMSwyMjQsMTI4WiIvPjwvc3ZnPg==)}.icon-hand-heart{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuMzMsMTQxLjA2YTI0LjM0LDI0LjM0LDAsMCwwLTE4LjYxLTQuNzdDMjMwLjUsMTE3LjMzLDI0MCw5OC40OCwyNDAsODBjMC0yNi40Ny0yMS4yOS00OC00Ny40Ni00OEE0Ny41OCw0Ny41OCwwLDAsMCwxNTYsNDguNzUsNDcuNTgsNDcuNTgsMCwwLDAsMTE5LjQ2LDMyQzkzLjI5LDMyLDcyLDUzLjUzLDcyLDgwYzAsMTEsMy4yNCwyMS42OSwxMC4wNiwzM2EzMS44NywzMS44NywwLDAsMC0xNC43NSw4LjRMNDQuNjksMTQ0SDE2QTE2LDE2LDAsMCwwLDAsMTYwdjQwYTE2LDE2LDAsMCwwLDE2LDE2SDEyMGE3LjkzLDcuOTMsMCwwLDAsMS45NC0uMjRsNjQtMTZhNi45NCw2Ljk0LDAsMCwwLDEuMTktLjRMMjI2LDE4Mi44MmwuNDQtLjJhMjQuNiwyNC42LDAsMCwwLDMuOTMtNDEuNTZaTTExOS40Niw0OEEzMS4xNSwzMS4xNSwwLDAsMSwxNDguNiw2N2E4LDgsMCwwLDAsMTQuOCwwLDMxLjE1LDMxLjE1LDAsMCwxLDI5LjE0LTE5QzIwOS41OSw0OCwyMjQsNjIuNjUsMjI0LDgwYzAsMTkuNTEtMTUuNzksNDEuNTgtNDUuNjYsNjMuOWwtMTEuMDksMi41NUEyOCwyOCwwLDAsMCwxNDAsMTEySDEwMC42OEM5Mi4wNSwxMDAuMzYsODgsOTAuMTIsODgsODAsODgsNjIuNjUsMTAyLjQxLDQ4LDExOS40Niw0OFpNMTYsMTYwSDQwdjQwSDE2Wm0yMDMuNDMsOC4yMS0zOCwxNi4xOEwxMTksMjAwSDU2VjE1NS4zMWwyMi42My0yMi42MkExNS44NiwxNS44NiwwLDAsMSw4OS45NCwxMjhIMTQwYTEyLDEyLDAsMCwxLDAsMjRIMTEyYTgsOCwwLDAsMCwwLDE2aDMyYTguMzIsOC4zMiwwLDAsMCwxLjc5LS4ybDY3LTE1LjQxLjMxLS4wOGE4LjYsOC42LDAsMCwxLDYuMywxNS45WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuMzMsMTQxLjA2YTI0LjM0LDI0LjM0LDAsMCwwLTE4LjYxLTQuNzdDMjMwLjUsMTE3LjMzLDI0MCw5OC40OCwyNDAsODBjMC0yNi40Ny0yMS4yOS00OC00Ny40Ni00OEE0Ny41OCw0Ny41OCwwLDAsMCwxNTYsNDguNzUsNDcuNTgsNDcuNTgsMCwwLDAsMTE5LjQ2LDMyQzkzLjI5LDMyLDcyLDUzLjUzLDcyLDgwYzAsMTEsMy4yNCwyMS42OSwxMC4wNiwzM2EzMS44NywzMS44NywwLDAsMC0xNC43NSw4LjRMNDQuNjksMTQ0SDE2QTE2LDE2LDAsMCwwLDAsMTYwdjQwYTE2LDE2LDAsMCwwLDE2LDE2SDEyMGE3LjkzLDcuOTMsMCwwLDAsMS45NC0uMjRsNjQtMTZhNi45NCw2Ljk0LDAsMCwwLDEuMTktLjRMMjI2LDE4Mi44MmwuNDQtLjJhMjQuNiwyNC42LDAsMCwwLDMuOTMtNDEuNTZaTTExOS40Niw0OEEzMS4xNSwzMS4xNSwwLDAsMSwxNDguNiw2N2E4LDgsMCwwLDAsMTQuOCwwLDMxLjE1LDMxLjE1LDAsMCwxLDI5LjE0LTE5QzIwOS41OSw0OCwyMjQsNjIuNjUsMjI0LDgwYzAsMTkuNTEtMTUuNzksNDEuNTgtNDUuNjYsNjMuOWwtMTEuMDksMi41NUEyOCwyOCwwLDAsMCwxNDAsMTEySDEwMC42OEM5Mi4wNSwxMDAuMzYsODgsOTAuMTIsODgsODAsODgsNjIuNjUsMTAyLjQxLDQ4LDExOS40Niw0OFpNMTYsMTYwSDQwdjQwSDE2Wm0yMDMuNDMsOC4yMS0zOCwxNi4xOEwxMTksMjAwSDU2VjE1NS4zMWwyMi42My0yMi42MkExNS44NiwxNS44NiwwLDAsMSw4OS45NCwxMjhIMTQwYTEyLDEyLDAsMCwxLDAsMjRIMTEyYTgsOCwwLDAsMCwwLDE2aDMyYTguMzIsOC4zMiwwLDAsMCwxLjc5LS4ybDY3LTE1LjQxLjMxLS4wOGE4LjYsOC42LDAsMCwxLDYuMywxNS45WiIvPjwvc3ZnPg==)}.icon-hourglass{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDAsNzUuNjRWNDBhMTYsMTYsMCwwLDAtMTYtMTZINzJBMTYsMTYsMCwwLDAsNTYsNDBWNzZhMTYuMDcsMTYuMDcsMCwwLDAsNi40LDEyLjhMMTE0LjY3LDEyOCw2Mi40LDE2Ny4yQTE2LjA3LDE2LjA3LDAsMCwwLDU2LDE4MHYzNmExNiwxNiwwLDAsMCwxNiwxNkgxODRhMTYsMTYsMCwwLDAsMTYtMTZWMTgwLjM2YTE2LjA5LDE2LjA5LDAsMCwwLTYuMzUtMTIuNzdMMTQxLjI3LDEyOGw1Mi4zOC0zOS42QTE2LjA1LDE2LjA1LDAsMCwwLDIwMCw3NS42NFpNMTg0LDIxNkg3MlYxODBsNTYtNDIsNTYsNDIuMzVabTAtMTQwLjM2TDEyOCwxMTgsNzIsNzZWNDBIMTg0WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDAsNzUuNjRWNDBhMTYsMTYsMCwwLDAtMTYtMTZINzJBMTYsMTYsMCwwLDAsNTYsNDBWNzZhMTYuMDcsMTYuMDcsMCwwLDAsNi40LDEyLjhMMTE0LjY3LDEyOCw2Mi40LDE2Ny4yQTE2LjA3LDE2LjA3LDAsMCwwLDU2LDE4MHYzNmExNiwxNiwwLDAsMCwxNiwxNkgxODRhMTYsMTYsMCwwLDAsMTYtMTZWMTgwLjM2YTE2LjA5LDE2LjA5LDAsMCwwLTYuMzUtMTIuNzdMMTQxLjI3LDEyOGw1Mi4zOC0zOS42QTE2LjA1LDE2LjA1LDAsMCwwLDIwMCw3NS42NFpNMTg0LDIxNkg3MlYxODBsNTYtNDIsNTYsNDIuMzVabTAtMTQwLjM2TDEyOCwxMTgsNzIsNzZWNDBIMTg0WiIvPjwvc3ZnPg==)}.icon-instagram-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsODBhNDgsNDgsMCwxLDAsNDgsNDhBNDguMDUsNDguMDUsMCwwLDAsMTI4LDgwWm0wLDgwYTMyLDMyLDAsMSwxLDMyLTMyQTMyLDMyLDAsMCwxLDEyOCwxNjBaTTE3NiwyNEg4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwyNCw4MHY5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Niw1Nmg5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Ni01NlY4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwxNzYsMjRabTQwLDE1MmE0MCw0MCwwLDAsMS00MCw0MEg4MGE0MCw0MCwwLDAsMS00MC00MFY4MEE0MCw0MCwwLDAsMSw4MCw0MGg5NmE0MCw0MCwwLDAsMSw0MCw0MFpNMTkyLDc2YTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE5Miw3NloiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xMjgsODBhNDgsNDgsMCwxLDAsNDgsNDhBNDguMDUsNDguMDUsMCwwLDAsMTI4LDgwWm0wLDgwYTMyLDMyLDAsMSwxLDMyLTMyQTMyLDMyLDAsMCwxLDEyOCwxNjBaTTE3NiwyNEg4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwyNCw4MHY5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Niw1Nmg5NmE1Ni4wNiw1Ni4wNiwwLDAsMCw1Ni01NlY4MEE1Ni4wNiw1Ni4wNiwwLDAsMCwxNzYsMjRabTQwLDE1MmE0MCw0MCwwLDAsMS00MCw0MEg4MGE0MCw0MCwwLDAsMS00MC00MFY4MEE0MCw0MCwwLDAsMSw4MCw0MGg5NmE0MCw0MCwwLDAsMSw0MCw0MFpNMTkyLDc2YTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE5Miw3NloiLz48L3N2Zz4=)}.icon-link{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsODguMjNhNTQuNDMsNTQuNDMsMCwwLDEtMTYsMzdMMTg5LjI1LDE2MGE1NC4yNyw1NC4yNywwLDAsMS0zOC42MywxNmgtLjA1QTU0LjYzLDU0LjYzLDAsMCwxLDk2LDExOS44NGE4LDgsMCwwLDEsMTYsLjQ1QTM4LjYyLDM4LjYyLDAsMCwwLDE1MC41OCwxNjBoMGEzOC4zOSwzOC4zOSwwLDAsMCwyNy4zMS0xMS4zMWwzNC43NS0zNC43NWEzOC42MywzOC42MywwLDAsMC01NC42My01NC42M2wtMTEsMTFBOCw4LDAsMCwxLDEzNS43LDU5bDExLTExQTU0LjY1LDU0LjY1LDAsMCwxLDIyNCw0OCw1NC44Niw1NC44NiwwLDAsMSwyNDAsODguMjNaTTEwOSwxODUuNjZsLTExLDExQTM4LjQxLDM4LjQxLDAsMCwxLDcwLjYsMjA4aDBhMzguNjMsMzguNjMsMCwwLDEtMjcuMjktNjUuOTRMNzgsMTA3LjMxQTM4LjYzLDM4LjYzLDAsMCwxLDE0NCwxMzUuNzFhOCw4LDAsMCwwLDE2LC40NUE1NC44Niw1NC44NiwwLDAsMCwxNDQsOTZhNTQuNjUsNTQuNjUsMCwwLDAtNzcuMjcsMEwzMiwxMzAuNzVBNTQuNjIsNTQuNjIsMCwwLDAsNzAuNTYsMjI0aDBhNTQuMjgsNTQuMjgsMCwwLDAsMzguNjQtMTZsMTEtMTFBOCw4LDAsMCwwLDEwOSwxODUuNjZaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yNDAsODguMjNhNTQuNDMsNTQuNDMsMCwwLDEtMTYsMzdMMTg5LjI1LDE2MGE1NC4yNyw1NC4yNywwLDAsMS0zOC42MywxNmgtLjA1QTU0LjYzLDU0LjYzLDAsMCwxLDk2LDExOS44NGE4LDgsMCwwLDEsMTYsLjQ1QTM4LjYyLDM4LjYyLDAsMCwwLDE1MC41OCwxNjBoMGEzOC4zOSwzOC4zOSwwLDAsMCwyNy4zMS0xMS4zMWwzNC43NS0zNC43NWEzOC42MywzOC42MywwLDAsMC01NC42My01NC42M2wtMTEsMTFBOCw4LDAsMCwxLDEzNS43LDU5bDExLTExQTU0LjY1LDU0LjY1LDAsMCwxLDIyNCw0OCw1NC44Niw1NC44NiwwLDAsMSwyNDAsODguMjNaTTEwOSwxODUuNjZsLTExLDExQTM4LjQxLDM4LjQxLDAsMCwxLDcwLjYsMjA4aDBhMzguNjMsMzguNjMsMCwwLDEtMjcuMjktNjUuOTRMNzgsMTA3LjMxQTM4LjYzLDM4LjYzLDAsMCwxLDE0NCwxMzUuNzFhOCw4LDAsMCwwLDE2LC40NUE1NC44Niw1NC44NiwwLDAsMCwxNDQsOTZhNTQuNjUsNTQuNjUsMCwwLDAtNzcuMjcsMEwzMiwxMzAuNzVBNTQuNjIsNTQuNjIsMCwwLDAsNzAuNTYsMjI0aDBhNTQuMjgsNTQuMjgsMCwwLDAsMzguNjQtMTZsMTEtMTFBOCw4LDAsMCwwLDEwOSwxODUuNjZaIi8+PC9zdmc+)}.icon-magic-wand{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OCw2NGE4LDgsMCwwLDEsOC04SDcyVjQwYTgsOCwwLDAsMSwxNiwwVjU2aDE2YTgsOCwwLDAsMSwwLDE2SDg4Vjg4YTgsOCwwLDAsMS0xNiwwVjcySDU2QTgsOCwwLDAsMSw0OCw2NFpNMTg0LDE5MmgtOHYtOGE4LDgsMCwwLDAtMTYsMHY4aC04YTgsOCwwLDAsMCwwLDE2aDh2OGE4LDgsMCwwLDAsMTYsMHYtOGg4YTgsOCwwLDAsMCwwLTE2Wm01Ni00OEgyMjRWMTI4YTgsOCwwLDAsMC0xNiwwdjE2SDE5MmE4LDgsMCwwLDAsMCwxNmgxNnYxNmE4LDgsMCwwLDAsMTYsMFYxNjBoMTZhOCw4LDAsMCwwLDAtMTZaTTIxOS4zMSw4MCw4MCwyMTkuMzFhMTYsMTYsMCwwLDEtMjIuNjIsMEwzNi42OCwxOTguNjNhMTYsMTYsMCwwLDEsMC0yMi42M0wxNzYsMzYuNjlhMTYsMTYsMCwwLDEsMjIuNjMsMGwyMC42OCwyMC42OEExNiwxNiwwLDAsMSwyMTkuMzEsODBabS01NC42MywzMkwxNDQsOTEuMzFsLTk2LDk2TDY4LjY4LDIwOFpNMjA4LDY4LjY5LDE4Ny4zMSw0OGwtMzIsMzJMMTc2LDEwMC42OVoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik00OCw2NGE4LDgsMCwwLDEsOC04SDcyVjQwYTgsOCwwLDAsMSwxNiwwVjU2aDE2YTgsOCwwLDAsMSwwLDE2SDg4Vjg4YTgsOCwwLDAsMS0xNiwwVjcySDU2QTgsOCwwLDAsMSw0OCw2NFpNMTg0LDE5MmgtOHYtOGE4LDgsMCwwLDAtMTYsMHY4aC04YTgsOCwwLDAsMCwwLDE2aDh2OGE4LDgsMCwwLDAsMTYsMHYtOGg4YTgsOCwwLDAsMCwwLTE2Wm01Ni00OEgyMjRWMTI4YTgsOCwwLDAsMC0xNiwwdjE2SDE5MmE4LDgsMCwwLDAsMCwxNmgxNnYxNmE4LDgsMCwwLDAsMTYsMFYxNjBoMTZhOCw4LDAsMCwwLDAtMTZaTTIxOS4zMSw4MCw4MCwyMTkuMzFhMTYsMTYsMCwwLDEtMjIuNjIsMEwzNi42OCwxOTguNjNhMTYsMTYsMCwwLDEsMC0yMi42M0wxNzYsMzYuNjlhMTYsMTYsMCwwLDEsMjIuNjMsMGwyMC42OCwyMC42OEExNiwxNiwwLDAsMSwyMTkuMzEsODBabS01NC42MywzMkwxNDQsOTEuMzFsLTk2LDk2TDY4LjY4LDIwOFpNMjA4LDY4LjY5LDE4Ny4zMSw0OGwtMzIsMzJMMTc2LDEwMC42OVoiLz48L3N2Zz4=)}.icon-person{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDBhMzIsMzIsMCwxLDAtMzIsMzJBMzIsMzIsMCwwLDAsMTYwLDQwWk0xMjgsNTZhMTYsMTYsMCwxLDEsMTYtMTZBMTYsMTYsMCwwLDEsMTI4LDU2Wm05MC4zNCw3OC4wNUwxNzMuMTcsODIuODNhMzIsMzIsMCwwLDAtMjQtMTAuODNIMTA2LjgzYTMyLDMyLDAsMCwwLTI0LDEwLjgzTDM3LjY2LDEzNC4wNWEyMCwyMCwwLDAsMCwyOC4xMywyOC40M2wxNi4zLTEzLjA4TDY1LjU1LDIxMi4yOEEyMCwyMCwwLDAsMCwxMDIsMjI4LjhsMjYtNDQuODcsMjYsNDQuODdhMjAsMjAsMCwwLDAsMzYuNDEtMTYuNTJMMTczLjkxLDE0OS40bDE2LjMsMTMuMDhhMjAsMjAsMCwwLDAsMjguMTMtMjguNDNabS0xMS41MSwxNi43N2E0LDQsMCwwLDEtNS42NiwwYy0uMjEtLjItLjQyLS40LS42NS0uNThMMTY1LDEyMS43NkE4LDgsMCwwLDAsMTUyLjI2LDEzMEwxNzUuMTQsMjE3YTcuNzIsNy43MiwwLDAsMCwuNDgsMS4zNSw0LDQsMCwxLDEtNy4yNSwzLjM4LDYuMjUsNi4yNSwwLDAsMC0uMzMtLjYzTDEzNC45MiwxNjRhOCw4LDAsMCwwLTEzLjg0LDBMODgsMjIxLjA1YTYuMjUsNi4yNSwwLDAsMC0uMzMuNjMsNCw0LDAsMCwxLTIuMjYsMi4wNyw0LDQsMCwwLDEtNS01LjQ1LDcuNzIsNy43MiwwLDAsMCwuNDgtMS4zNUwxMDMuNzQsMTMwQTgsOCwwLDAsMCw5MSwxMjEuNzZMNTUuNDgsMTUwLjI0Yy0uMjMuMTgtLjQ0LjM4LS42NS41OGE0LDQsMCwxLDEtNS42Ni01LjY1Yy4xMi0uMTIuMjMtLjI0LjM0LS4zN0w5NC44Myw5My40MWExNiwxNiwwLDAsMSwxMi01LjQxaDQyLjM0YTE2LDE2LDAsMCwxLDEyLDUuNDFsNDUuMzIsNTEuMzljLjExLjEzLjIyLjI1LjM0LjM3QTQsNCwwLDAsMSwyMDYuODMsMTUwLjgyWiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjAsNDBhMzIsMzIsMCwxLDAtMzIsMzJBMzIsMzIsMCwwLDAsMTYwLDQwWk0xMjgsNTZhMTYsMTYsMCwxLDEsMTYtMTZBMTYsMTYsMCwwLDEsMTI4LDU2Wm05MC4zNCw3OC4wNUwxNzMuMTcsODIuODNhMzIsMzIsMCwwLDAtMjQtMTAuODNIMTA2LjgzYTMyLDMyLDAsMCwwLTI0LDEwLjgzTDM3LjY2LDEzNC4wNWEyMCwyMCwwLDAsMCwyOC4xMywyOC40M2wxNi4zLTEzLjA4TDY1LjU1LDIxMi4yOEEyMCwyMCwwLDAsMCwxMDIsMjI4LjhsMjYtNDQuODcsMjYsNDQuODdhMjAsMjAsMCwwLDAsMzYuNDEtMTYuNTJMMTczLjkxLDE0OS40bDE2LjMsMTMuMDhhMjAsMjAsMCwwLDAsMjguMTMtMjguNDNabS0xMS41MSwxNi43N2E0LDQsMCwwLDEtNS42NiwwYy0uMjEtLjItLjQyLS40LS42NS0uNThMMTY1LDEyMS43NkE4LDgsMCwwLDAsMTUyLjI2LDEzMEwxNzUuMTQsMjE3YTcuNzIsNy43MiwwLDAsMCwuNDgsMS4zNSw0LDQsMCwxLDEtNy4yNSwzLjM4LDYuMjUsNi4yNSwwLDAsMC0uMzMtLjYzTDEzNC45MiwxNjRhOCw4LDAsMCwwLTEzLjg0LDBMODgsMjIxLjA1YTYuMjUsNi4yNSwwLDAsMC0uMzMuNjMsNCw0LDAsMCwxLTIuMjYsMi4wNyw0LDQsMCwwLDEtNS01LjQ1LDcuNzIsNy43MiwwLDAsMCwuNDgtMS4zNUwxMDMuNzQsMTMwQTgsOCwwLDAsMCw5MSwxMjEuNzZMNTUuNDgsMTUwLjI0Yy0uMjMuMTgtLjQ0LjM4LS42NS41OGE0LDQsMCwxLDEtNS42Ni01LjY1Yy4xMi0uMTIuMjMtLjI0LjM0LS4zN0w5NC44Myw5My40MWExNiwxNiwwLDAsMSwxMi01LjQxaDQyLjM0YTE2LDE2LDAsMCwxLDEyLDUuNDFsNDUuMzIsNTEuMzljLjExLjEzLjIyLjI1LjM0LjM3QTQsNCwwLDAsMSwyMDYuODMsMTUwLjgyWiIvPjwvc3ZnPg==)}.icon-phone{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIuMzcsMTU4LjQ2bC00Ny4xMS0yMS4xMS0uMTMtLjA2YTE2LDE2LDAsMCwwLTE1LjE3LDEuNCw4LjEyLDguMTIsMCwwLDAtLjc1LjU2TDEzNC44NywxNjBjLTE1LjQyLTcuNDktMzEuMzQtMjMuMjktMzguODMtMzguNTFsMjAuNzgtMjQuNzFjLjItLjI1LjM5LS41LjU3LS43N2ExNiwxNiwwLDAsMCwxLjMyLTE1LjA2bDAtLjEyTDk3LjU0LDMzLjY0YTE2LDE2LDAsMCwwLTE2LjYyLTkuNTJBNTYuMjYsNTYuMjYsMCwwLDAsMzIsODBjMCw3OS40LDY0LjYsMTQ0LDE0NCwxNDRhNTYuMjYsNTYuMjYsMCwwLDAsNTUuODgtNDguOTJBMTYsMTYsMCwwLDAsMjIyLjM3LDE1OC40NlpNMTc2LDIwOEExMjguMTQsMTI4LjE0LDAsMCwxLDQ4LDgwLDQwLjIsNDAuMiwwLDAsMSw4Mi44Nyw0MGEuNjEuNjEsMCwwLDAsMCwuMTJsMjEsNDdMODMuMiwxMTEuODZhNi4xMyw2LjEzLDAsMCwwLS41Ny43NywxNiwxNiwwLDAsMC0xLDE1LjdjOS4wNiwxOC41MywyNy43MywzNy4wNiw0Ni40Niw0Ni4xMWExNiwxNiwwLDAsMCwxNS43NS0xLjE0LDguNDQsOC40NCwwLDAsMCwuNzQtLjU2TDE2OC44OSwxNTJsNDcsMjEuMDVoMHMuMDgsMCwuMTEsMEE0MC4yMSw0MC4yMSwwLDAsMSwxNzYsMjA4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjIuMzcsMTU4LjQ2bC00Ny4xMS0yMS4xMS0uMTMtLjA2YTE2LDE2LDAsMCwwLTE1LjE3LDEuNCw4LjEyLDguMTIsMCwwLDAtLjc1LjU2TDEzNC44NywxNjBjLTE1LjQyLTcuNDktMzEuMzQtMjMuMjktMzguODMtMzguNTFsMjAuNzgtMjQuNzFjLjItLjI1LjM5LS41LjU3LS43N2ExNiwxNiwwLDAsMCwxLjMyLTE1LjA2bDAtLjEyTDk3LjU0LDMzLjY0YTE2LDE2LDAsMCwwLTE2LjYyLTkuNTJBNTYuMjYsNTYuMjYsMCwwLDAsMzIsODBjMCw3OS40LDY0LjYsMTQ0LDE0NCwxNDRhNTYuMjYsNTYuMjYsMCwwLDAsNTUuODgtNDguOTJBMTYsMTYsMCwwLDAsMjIyLjM3LDE1OC40NlpNMTc2LDIwOEExMjguMTQsMTI4LjE0LDAsMCwxLDQ4LDgwLDQwLjIsNDAuMiwwLDAsMSw4Mi44Nyw0MGEuNjEuNjEsMCwwLDAsMCwuMTJsMjEsNDdMODMuMiwxMTEuODZhNi4xMyw2LjEzLDAsMCwwLS41Ny43NywxNiwxNiwwLDAsMC0xLDE1LjdjOS4wNiwxOC41MywyNy43MywzNy4wNiw0Ni40Niw0Ni4xMWExNiwxNiwwLDAsMCwxNS43NS0xLjE0LDguNDQsOC40NCwwLDAsMCwuNzQtLjU2TDE2OC44OSwxNTJsNDcsMjEuMDVoMHMuMDgsMCwuMTEsMEE0MC4yMSw0MC4yMSwwLDAsMSwxNzYsMjA4WiIvPjwvc3ZnPg==)}.icon-question{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDAsMTgwYTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE0MCwxODBaTTEyOCw3MmMtMjIuMDYsMC00MCwxNi4xNS00MCwzNnY0YTgsOCwwLDAsMCwxNiwwdi00YzAtMTEsMTAuNzctMjAsMjQtMjBzMjQsOSwyNCwyMC0xMC43NywyMC0yNCwyMGE4LDgsMCwwLDAtOCw4djhhOCw4LDAsMCwwLDE2LDB2LS43MmMxOC4yNC0zLjM1LDMyLTE3LjksMzItMzUuMjhDMTY4LDg4LjE1LDE1MC4wNiw3MiwxMjgsNzJabTEwNCw1NkExMDQsMTA0LDAsMSwxLDEyOCwyNCwxMDQuMTEsMTA0LjExLDAsMCwxLDIzMiwxMjhabS0xNiwwYTg4LDg4LDAsMSwwLTg4LDg4QTg4LjEsODguMSwwLDAsMCwyMTYsMTI4WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNDAsMTgwYTEyLDEyLDAsMSwxLTEyLTEyQTEyLDEyLDAsMCwxLDE0MCwxODBaTTEyOCw3MmMtMjIuMDYsMC00MCwxNi4xNS00MCwzNnY0YTgsOCwwLDAsMCwxNiwwdi00YzAtMTEsMTAuNzctMjAsMjQtMjBzMjQsOSwyNCwyMC0xMC43NywyMC0yNCwyMGE4LDgsMCwwLDAtOCw4djhhOCw4LDAsMCwwLDE2LDB2LS43MmMxOC4yNC0zLjM1LDMyLTE3LjksMzItMzUuMjhDMTY4LDg4LjE1LDE1MC4wNiw3MiwxMjgsNzJabTEwNCw1NkExMDQsMTA0LDAsMSwxLDEyOCwyNCwxMDQuMTEsMTA0LjExLDAsMCwxLDIzMiwxMjhabS0xNiwwYTg4LDg4LDAsMSwwLTg4LDg4QTg4LjEsODguMSwwLDAsMCwyMTYsMTI4WiIvPjwvc3ZnPg==)}.icon-star{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjM4LDE2LjM4LDAsMCwwLDI0LjUsMTcuODJMMTI4LDE5OC40OWw1MC41MywzMS4wOEExNi40LDE2LjQsMCwwLDAsMjAzLDIxMS43NWwtMTMuNzYtNTguMDcsNDUtMzguODNBMTYuNDMsMTYuNDMsMCwwLDAsMjM5LjE4LDk3LjI2Wm0tMTUuMzQsNS40Ny00OC43LDQyYTgsOCwwLDAsMC0yLjU2LDcuOTFsMTQuODgsNjIuOGEuMzcuMzcsMCwwLDEtLjE3LjQ4Yy0uMTguMTQtLjIzLjExLS4zOCwwbC01NC43Mi0zMy42NWE4LDgsMCwwLDAtOC4zOCwwTDY5LjA5LDIxNS45NGMtLjE1LjA5LS4xOS4xMi0uMzgsMGEuMzcuMzcsMCwwLDEtLjE3LS40OGwxNC44OC02Mi44YTgsOCwwLDAsMC0yLjU2LTcuOTFsLTQ4LjctNDJjLS4xMi0uMS0uMjMtLjE5LS4xMy0uNXMuMTgtLjI3LjMzLS4yOWw2My45Mi01LjE2QTgsOCwwLDAsMCwxMDMsOTEuODZsMjQuNjItNTkuNjFjLjA4LS4xNy4xMS0uMjUuMzUtLjI1cy4yNy4wOC4zNS4yNUwxNTMsOTEuODZhOCw4LDAsMCwwLDYuNzUsNC45Mmw2My45Miw1LjE2Yy4xNSwwLC4yNCwwLC4zMy4yOVMyMjQsMTAyLjYzLDIyMy44NCwxMDIuNzNaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzkuMTgsOTcuMjZBMTYuMzgsMTYuMzgsMCwwLDAsMjI0LjkyLDg2bC01OS00Ljc2TDE0My4xNCwyNi4xNWExNi4zNiwxNi4zNiwwLDAsMC0zMC4yNywwTDkwLjExLDgxLjIzLDMxLjA4LDg2YTE2LjQ2LDE2LjQ2LDAsMCwwLTkuMzcsMjguODZsNDUsMzguODNMNTMsMjExLjc1YTE2LjM4LDE2LjM4LDAsMCwwLDI0LjUsMTcuODJMMTI4LDE5OC40OWw1MC41MywzMS4wOEExNi40LDE2LjQsMCwwLDAsMjAzLDIxMS43NWwtMTMuNzYtNTguMDcsNDUtMzguODNBMTYuNDMsMTYuNDMsMCwwLDAsMjM5LjE4LDk3LjI2Wm0tMTUuMzQsNS40Ny00OC43LDQyYTgsOCwwLDAsMC0yLjU2LDcuOTFsMTQuODgsNjIuOGEuMzcuMzcsMCwwLDEtLjE3LjQ4Yy0uMTguMTQtLjIzLjExLS4zOCwwbC01NC43Mi0zMy42NWE4LDgsMCwwLDAtOC4zOCwwTDY5LjA5LDIxNS45NGMtLjE1LjA5LS4xOS4xMi0uMzgsMGEuMzcuMzcsMCwwLDEtLjE3LS40OGwxNC44OC02Mi44YTgsOCwwLDAsMC0yLjU2LTcuOTFsLTQ4LjctNDJjLS4xMi0uMS0uMjMtLjE5LS4xMy0uNXMuMTgtLjI3LjMzLS4yOWw2My45Mi01LjE2QTgsOCwwLDAsMCwxMDMsOTEuODZsMjQuNjItNTkuNjFjLjA4LS4xNy4xMS0uMjUuMzUtLjI1cy4yNy4wOC4zNS4yNUwxNTMsOTEuODZhOCw4LDAsMCwwLDYuNzUsNC45Mmw2My45Miw1LjE2Yy4xNSwwLC4yNCwwLC4zMy4yOVMyMjQsMTAyLjYzLDIyMy44NCwxMDIuNzNaIi8+PC9zdmc+)}.icon-tiktok-logo{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNzJhNDguMDUsNDguMDUsMCwwLDEtNDgtNDgsOCw4LDAsMCwwLTgtOEgxMjhhOCw4LDAsMCwwLTgsOFYxNTZhMjAsMjAsMCwxLDEtMjguNTctMTguMDhBOCw4LDAsMCwwLDk2LDEzMC42OVY4OGE4LDgsMCwwLDAtOS40LTcuODhDNTAuOTEsODYuNDgsMjQsMTE5LjEsMjQsMTU2YTc2LDc2LDAsMCwwLDE1MiwwVjExNi4yOUExMDMuMjUsMTAzLjI1LDAsMCwwLDIyNCwxMjhhOCw4LDAsMCwwLDgtOFY4MEE4LDgsMCwwLDAsMjI0LDcyWm0tOCwzOS42NGE4Ny4xOSw4Ny4xOSwwLDAsMS00My4zMy0xNi4xNUE4LDgsMCwwLDAsMTYwLDEwMnY1NGE2MCw2MCwwLDAsMS0xMjAsMGMwLTI1LjksMTYuNjQtNDkuMTMsNDAtNTcuNnYyNy42N0EzNiwzNiwwLDEsMCwxMzYsMTU2VjMyaDI0LjVBNjQuMTQsNjQuMTQsMCwwLDAsMjE2LDg3LjVaIi8+PC9zdmc+);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMjQsNzJhNDguMDUsNDguMDUsMCwwLDEtNDgtNDgsOCw4LDAsMCwwLTgtOEgxMjhhOCw4LDAsMCwwLTgsOFYxNTZhMjAsMjAsMCwxLDEtMjguNTctMTguMDhBOCw4LDAsMCwwLDk2LDEzMC42OVY4OGE4LDgsMCwwLDAtOS40LTcuODhDNTAuOTEsODYuNDgsMjQsMTE5LjEsMjQsMTU2YTc2LDc2LDAsMCwwLDE1MiwwVjExNi4yOUExMDMuMjUsMTAzLjI1LDAsMCwwLDIyNCwxMjhhOCw4LDAsMCwwLDgtOFY4MEE4LDgsMCwwLDAsMjI0LDcyWm0tOCwzOS42NGE4Ny4xOSw4Ny4xOSwwLDAsMS00My4zMy0xNi4xNUE4LDgsMCwwLDAsMTYwLDEwMnY1NGE2MCw2MCwwLDAsMS0xMjAsMGMwLTI1LjksMTYuNjQtNDkuMTMsNDAtNTcuNnYyNy42N0EzNiwzNiwwLDEsMCwxMzYsMTU2VjMyaDI0LjVBNjQuMTQsNjQuMTQsMCwwLDAsMjE2LDg3LjVaIi8+PC9zdmc+)}.icon-user{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuOTIsMjEyYy0xNS4yMy0yNi4zMy0zOC43LTQ1LjIxLTY2LjA5LTU0LjE2YTcyLDcyLDAsMSwwLTczLjY2LDBDNjMuNzgsMTY2Ljc4LDQwLjMxLDE4NS42NiwyNS4wOCwyMTJhOCw4LDAsMSwwLDEzLjg1LDhjMTguODQtMzIuNTYsNTIuMTQtNTIsODkuMDctNTJzNzAuMjMsMTkuNDQsODkuMDcsNTJhOCw4LDAsMSwwLDEzLjg1LThaTTcyLDk2YTU2LDU2LDAsMSwxLDU2LDU2QTU2LjA2LDU2LjA2LDAsMCwxLDcyLDk2WiIvPjwvc3ZnPg==);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMzAuOTIsMjEyYy0xNS4yMy0yNi4zMy0zOC43LTQ1LjIxLTY2LjA5LTU0LjE2YTcyLDcyLDAsMSwwLTczLjY2LDBDNjMuNzgsMTY2Ljc4LDQwLjMxLDE4NS42NiwyNS4wOCwyMTJhOCw4LDAsMSwwLDEzLjg1LDhjMTguODQtMzIuNTYsNTIuMTQtNTIsODkuMDctNTJzNzAuMjMsMTkuNDQsODkuMDcsNTJhOCw4LDAsMSwwLDEzLjg1LThaTTcyLDk2YTU2LDU2LDAsMSwxLDU2LDU2QTU2LjA2LDU2LjA2LDAsMCwxLDcyLDk2WiIvPjwvc3ZnPg==)}.icon-x{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDUuNjYsMTk0LjM0YTgsOCwwLDAsMS0xMS4zMiwxMS4zMkwxMjgsMTM5LjMxLDYxLjY2LDIwNS42NmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTE2LjY5LDEyOCw1MC4zNCw2MS42NkE4LDgsMCwwLDEsNjEuNjYsNTAuMzRMMTI4LDExNi42OWw2Ni4zNC02Ni4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJMMTM5LjMxLDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0yMDUuNjYsMTk0LjM0YTgsOCwwLDAsMS0xMS4zMiwxMS4zMkwxMjgsMTM5LjMxLDYxLjY2LDIwNS42NmE4LDgsMCwwLDEtMTEuMzItMTEuMzJMMTE2LjY5LDEyOCw1MC4zNCw2MS42NkE4LDgsMCwwLDEsNjEuNjYsNTAuMzRMMTI4LDExNi42OWw2Ni4zNC02Ni4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJMMTM5LjMxLDEyOFoiLz48L3N2Zz4=)}.icon-x-circle{mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjUuNjYsMTAxLjY2LDEzOS4zMSwxMjhsMjYuMzUsMjYuMzRhOCw4LDAsMCwxLTExLjMyLDExLjMyTDEyOCwxMzkuMzFsLTI2LjM0LDI2LjM1YTgsOCwwLDAsMS0xMS4zMi0xMS4zMkwxMTYuNjksMTI4LDkwLjM0LDEwMS42NmE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTI4LDExNi42OWwyNi4zNC0yNi4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJaTTIzMiwxMjhBMTA0LDEwNCwwLDEsMSwxMjgsMjQsMTA0LjExLDEwNC4xMSwwLDAsMSwyMzIsMTI4Wm0tMTYsMGE4OCw4OCwwLDEsMC04OCw4OEE4OC4xLDg4LjEsMCwwLDAsMjE2LDEyOFoiLz48L3N2Zz4=);-webkit-mask-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiBmaWxsPSJjdXJyZW50Q29sb3IiPjxwYXRoIGQ9Ik0xNjUuNjYsMTAxLjY2LDEzOS4zMSwxMjhsMjYuMzUsMjYuMzRhOCw4LDAsMCwxLTExLjMyLDExLjMyTDEyOCwxMzkuMzFsLTI2LjM0LDI2LjM1YTgsOCwwLDAsMS0xMS4zMi0xMS4zMkwxMTYuNjksMTI4LDkwLjM0LDEwMS42NmE4LDgsMCwwLDEsMTEuMzItMTEuMzJMMTI4LDExNi42OWwyNi4zNC0yNi4zNWE4LDgsMCwwLDEsMTEuMzIsMTEuMzJaTTIzMiwxMjhBMTA0LDEwNCwwLDEsMSwxMjgsMjQsMTA0LjExLDEwNC4xMSwwLDAsMSwyMzIsMTI4Wm0tMTYsMGE4OCw4OCwwLDEsMC04OCw4OEE4OC4xLDg4LjEsMCwwLDAsMjE2LDEyOFoiLz48L3N2Zz4=)} |
| | |
| | | nav{--py:.25rem;--px:1rem;max-width:100%;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}ul.socials{--w:1.2em;--height:fit-content;gap:.5rem;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;flex-direction:row;overflow:auto hidden;touch-action:pan-x;list-style:none}nav:not(:has(>ul)),nav>ul{--justify:flex-start;--align:center;--wrap:nowrap;--w:1em;--dir:row;position:relative;overflow:auto hidden;touch-action:pan-x}nav a{padding:0 var(--px);white-space:nowrap;text-transform:uppercase}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav a:hover:visited{background-color:var(--action-0);color:var(--action-contrast);transition:background-color var(--transition-base),color var(--transition-base)}nav ol,nav ul{list-style:none;margin:0;padding:0}.has-submenu button:hover,nav a:hover{background-color:var(--action-0);color:var(--action-contrast)}.has-submenu button{height:var(--height);width:var(--height);padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}.toggle svg{transform:rotate(0);transition:transform var(--timing) var(--function);transition-property:transform,background-color,color}.has-submenu.open>button:not(.notifications,.quick-help) svg,.has-submenu:hover>button:not(.notifications,.quick-help) svg{transform:rotate(900deg)}ul.submenu{--dir:column;--wrap:nowrap;--gap:0;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max-content;min-width:100%;background-color:var(--overlay-light);border:2px solid var(--overlay-light);transition:all var(--timing) var(--function);box-shadow:var(--shadow-none)}.always ul.submenu{position:relative;top:0;left:0}.submenu li{background-color:var(--overlay-heavy);border:1px solid var(--base-50)}.submenu li:hover{--c:var(--action-rgb);background-color:var(--overlay-heavy)}.submenu a:hover{background-color:transparent}.wp-site-blocks>header ul.submenu{right:0;left:auto}.has-submenu.open>ul.submenu,.has-submenu:hover>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:var(--shadow)}nav.fixed.bottom,nav.on-this-page{--dir:row;--gap:0;width:calc(100% - var(--height));left:0;bottom:0;position:fixed;box-shadow:var(--shadow);z-index:var(--zz-top)}nav.fixed.bottom ul{width:100%;--justify:space-between;background-color:var(--base);padding:0 .25rem}nav.fixed a,nav.fixed li{--justify:center;width:100%}nav.fixed.bottom a,nav.fixed.bottom a:visited{color:var(--contrast);font-size:var(--small);padding:0}@media (min-width:768px){nav.fixed.bottom a,nav.fixed.bottom a:visited{font-size:var(--medium)}}nav.fixed.bottom a:focus,nav.fixed.bottom a:focus:visited,nav.fixed.bottom a:hover,nav.fixed.bottom a:hover:visited{color:var(--action-contrast)}.fixed.bottom li{flex:1}nav.always a{padding:0;--justify:center}nav.always .socials{width:100%}nav.always .socials li{width:100%}nav.always li{gap:0;--justify:flex-start}nav.always>ul>li>a{width:80%}nav.always .submenu{width:80%;min-width:80%;box-shadow:none!important;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}nav.always .submenu li{background-color:var(--overlay-light)}nav.always .has-submenu>a,nav.fixed .has-submenu>a{width:80%}.has-submenu>button{width:20%}nav#breadcrumbs{--height:1.5em;--w:20px;width:fit-content;max-width:var(--full);position:absolute;background-color:var(--overlay-medium);font-size:var(--small);padding:.125em;overflow:visible;--gap:0}nav#breadcrumbs li+li::before{content:'/';color:var(--contrast-200)}nav#breadcrumbs li:last-of-type{margin-right:.5em}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;white-space:nowrap;height:2em;color:var(--contrast);text-transform:none;width:max-content}nav#breadcrumbs span{display:flex;align-items:center;padding-left:.5em}nav#breadcrumbs a:focus,nav#breadcrumbs a:focus:visited,nav#breadcrumbs a:hover,nav#breadcrumbs a:hover:visited{background-color:transparent;color:var(--action-0)}nav#breadcrumbs a:has(.icon){width:2rem}nav.always{z-index:var(--z-above);position:fixed;width:var(--height);bottom:0;right:0}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--offHeight);background-color:var(--overlay-heavy);backdrop-filter:blur(5px);justify-content:flex-end;flex-direction:column;z-index:var(--z-above)}nav.always>ul{--dir:column;--wrap:nowrap;--justify:flex-start;--align:center;--gap:0;position:relative;right:-300vw;padding:1rem 0 0;width:100vw;height:fit-content;max-height:100%;overflow:hidden auto;transition:right var(--transition-base)}nav.always.open>ul{right:0;transition:right var(--transition-base)}nav.always li{max-inline-size:none;width:100%;height:fit-content;--dir:row;--wrap:wrap}nav.always a{--py:1rem;width:100%;min-height:var(--height)}nav.always>button{position:fixed;bottom:0;right:0;width:var(--height);height:var(--height);border-radius:0;background-color:var(--base);color:var(--contrast);transition:width var(--timing) var(--function);transition-property:width,background-color;box-shadow:var(--shadow)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{--c:var(--action-rgb);z-index:1000000;width:100%;background-color:var(--overlay-heavy);color:var(--contrast);backdrop-filter:blur(5px)}nav.always.open>button:focus,nav.always.open>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button .list,nav.always>button .x{transform:scale(0);height:0;width:0;position:absolute}nav.always.open>button .x,nav.always>button .list{transform:scale(1);height:32px;width:32px}nav.always .has-submenu.open>.submenu,nav.always .has-submenu:hover>.submenu{height:max-content}nav.always .has-submenu:hover>a,nav.always .submenu>li>a:focus,nav.always .submenu>li>a:hover{background-color:var(--action-0);color:var(--action-contrast)}@media (min-width:768px){nav.always>ul{padding:var(--height) 0 0}}nav.on-this-page{--justify:space-between;max-width:none;z-index:var(--z-6);margin:0;padding:0 .5rem;background-color:var(--overlay-medium);color:var(--base-200)}body:has(nav.fixed) nav.on-this-page{bottom:var(--offHeight)}.on-this-page ul{--justify:flex-start;gap:0;width:100%}.on-this-page li:not(.has){padding:0}nav.letters li{width:100%;max-width:calc(7.69% - 2px)}.on-this-page .active a{--c:var(--action-rgb);background-color:var(--overlay-heavy);color:var(--action-contrast)}@media (min-width:768px){nav.letters li{max-width:none;width:fit-content}nav.letters a,nav.letters li:not(.has){padding:.25rem .66rem}}nav.index{--justify:flex-start;--px:0;background-color:var(--overlay-heavy)}.index ul{--justify:flex-start;width:fit-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--timing) var(--function)}.index li.active{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (min-width:768px){.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index .active a:hover,.index a:hover{background-color:var(--action-0);color:var(--action-contrast)}.index label{display:flex;color:var(--contrast);align-items:center;margin:0}.index label button{margin-left:1em}.index.open{--dir:column-reverse;height:calc(100% - 8rem);z-index:var(--z-above);width:100%;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);align-items:flex-end}.index.open label{max-width:90%;margin-top:1rem;margin-right:2rem}.index.open .toggle svg{transform:rotate(45deg)}.index.open ul{--dir:column;--justify:flex-end;height:100%;max-width:100%;width:100%}.index.open li{background-color:transparent;max-width:100%!important;width:100%;height:var(--height);transform:scaleX(1);flex-shrink:1;overflow:visible}.index.open a{--justify:flex-end;background-color:transparent;padding:0 2rem 0 0}.condensed{--dir:row;--wrap:wrap;--height:1.2em;--py:.25rem;--px:.25rem;height:fit-content}.condensed>ul{--wrap:wrap;height:fit-content}.condensed ul{--justify:center;--gap:0}.condensed li{width:fit-content}.condensed li+li::before{content:'·';display:block;padding:0 .25em}nav.condensed a{text-transform:none;white-space:nowrap;border-bottom:2px solid transparent}.condensed a:focus,.condensed a:focus:visited,.condensed a:hover,.condensed a:hover:visited{border-color:var(--action-0)}.dashboard-nav{width:100%;--dir:row;--justify:flex-start;--wrap:nowrap}.wp-site-blocks>header,body>header{--dir:row;position:sticky;top:0;left:0;right:0;height:var(--height);width:100vw;display:flex;justify-content:space-between;align-items:center;padding:0 .5rem;background-color:var(--base);z-index:var(--zz-top);box-shadow:var(--shadow);border-bottom:1px solid var(--action-0)}body>header{justify-content:space-between}header a[rel=home],header>img{position:absolute;width:var(--offHeight);left:calc(50% - (var(--offHeight)/ 2))}header .title{--w:5em;margin:0;position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:flex-start;max-inline-size:none}.current-hours{position:sticky;top:var(--height);bottom:unset;width:unset;z-index:100;background-color:var(--action-0);color:var(--action-contrast);box-shadow:var(--shadow);padding:.25rem 1rem;display:flex;justify-content:space-between}.current-hours p{margin:0;display:flex;flex-wrap:wrap;flex:1}.current-hours p+p{justify-content:flex-end}.current-hours a{color:var(--action-contrast)}.current-hours a:hover{color:var(--action-200)}.current-hours b{margin-right:.25rem}.find-us{display:flex;align-items:center;gap:0 .5rem}.find-us a{display:flex;padding:.25rem 1rem;border:1px solid var(--action-contrast);border-radius:var(--innerRadius)}.find-us a:hover{background-color:var(--base);color:var(--contrast);border-color:var(--contrast)}nav.menu{--justify:flex-start}nav.menu a{padding:.5rem .66rem}nav.tabs{--gap:0;--wrap:nowrap;padding-bottom:2px;z-index:var(--z-6);position:fixed;bottom:var(--height);left:var(--doubleHeight);right:var(--doubleHeight)}nav.term-navigation:has([hidden]){display:none}ul.socials a{padding:.5rem}ul.socials a .icon{margin:0} |
| | | nav{--py:.25rem;--px:1rem;max-width:100%;font-family:var(--heading)}nav,nav a,nav li,nav ol,nav ul{height:var(--height);display:flex;justify-content:var(--justify);align-items:var(--align);gap:var(--gap);flex-wrap:var(--wrap);flex-direction:var(--dir)}ul.socials{--w:1.2em;--height:fit-content;gap:.5rem;display:flex;justify-content:center;align-items:center;flex-wrap:nowrap;flex-direction:row;overflow:auto hidden;touch-action:pan-x;list-style:none}nav:not(:has(>ul)),nav>ul{--justify:flex-start;--align:center;--wrap:nowrap;--w:1em;--dir:row;position:relative;overflow:auto hidden;touch-action:pan-x}nav a{padding:0 var(--px);white-space:nowrap;text-transform:uppercase}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav a:hover:visited{background-color:var(--action-0);color:var(--action-contrast);transition:background-color var(--transition-base),color var(--transition-base)}nav ol,nav ul{list-style:none;margin:0;padding:0}.has-submenu button:hover,nav a:hover{background-color:var(--action-0);color:var(--action-contrast)}.has-submenu button{height:var(--height);width:var(--height);padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}.toggle .icon{transform:rotate(0);transition:transform var(--timing) var(--function);transition-property:transform,background-color,color}.has-submenu.open>button:not(.notifications,.quick-help) .icon,.has-submenu:hover>button:not(.notifications,.quick-help) .icon{transform:rotate(900deg)}ul.submenu{--dir:column;--wrap:nowrap;--gap:0;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max-content;min-width:100%;background-color:var(--overlay-light);border:2px solid var(--overlay-light);transition:all var(--timing) var(--function);box-shadow:var(--shadow-none)}.always ul.submenu{position:relative;top:0;left:0}.submenu li{background-color:var(--overlay-heavy);border:1px solid var(--base-50)}.submenu li:hover{--c:var(--action-rgb);background-color:var(--overlay-heavy)}.submenu a:hover{background-color:transparent}.wp-site-blocks>header ul.submenu{right:0;left:auto}.has-submenu.open>ul.submenu,.has-submenu:hover>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:var(--shadow)}nav.fixed.bottom,nav.on-this-page{--dir:row;--gap:0;width:calc(100% - var(--height));left:0;bottom:0;position:fixed;box-shadow:var(--shadow);z-index:var(--zz-top)}nav.fixed.bottom ul{width:100%;--justify:space-between;background-color:var(--base);padding:0 .25rem}nav.fixed a,nav.fixed li{--justify:center;width:100%}nav.fixed.bottom a,nav.fixed.bottom a:visited{color:var(--contrast);font-size:var(--small);padding:0}@media (min-width:768px){nav.fixed.bottom a,nav.fixed.bottom a:visited{font-size:var(--medium)}}nav.fixed.bottom a:focus,nav.fixed.bottom a:focus:visited,nav.fixed.bottom a:hover,nav.fixed.bottom a:hover:visited{color:var(--action-contrast)}.fixed.bottom li{flex:1}nav.always a{padding:0;--justify:center}nav.always .socials{width:100%}nav.always .socials li{width:100%}nav.always li{gap:0;--justify:flex-start}nav.always>ul>li>a{width:80%}nav.always .submenu{width:80%;min-width:80%;box-shadow:none!important;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--rgb-subtle))}nav.always .submenu li{background-color:var(--overlay-light)}nav.always .has-submenu>a,nav.fixed .has-submenu>a{width:80%}.has-submenu>button{width:20%}nav#breadcrumbs{--height:1.5em;--w:20px;width:fit-content;max-width:var(--full);position:absolute;background-color:var(--overlay-medium);font-size:var(--small);padding:.125em;overflow:visible;--gap:0}nav#breadcrumbs li+li::before{content:'/';color:var(--contrast-200)}nav#breadcrumbs li:last-of-type{margin-right:.5em}nav#breadcrumbs a,nav#breadcrumbs span{padding:0 .125rem;white-space:nowrap;height:2em;color:var(--contrast);text-transform:none;width:max-content}nav#breadcrumbs span{display:flex;align-items:center;padding-left:.5em}nav#breadcrumbs a:focus,nav#breadcrumbs a:focus:visited,nav#breadcrumbs a:hover,nav#breadcrumbs a:hover:visited{background-color:transparent;color:var(--action-0)}nav#breadcrumbs a:has(.icon){width:2rem}nav.always{z-index:var(--z-above);position:fixed;width:var(--height);bottom:0;right:0}nav.always.open{width:100vw;height:100vh;padding-bottom:var(--offHeight);background-color:var(--overlay-heavy);backdrop-filter:blur(5px);justify-content:flex-end;flex-direction:column;z-index:var(--z-above)}nav.always>ul{--dir:column;--wrap:nowrap;--justify:flex-start;--align:center;--gap:0;position:relative;right:-300vw;padding:1rem 0 0;width:100vw;height:fit-content;max-height:100%;overflow:hidden auto;transition:right var(--transition-base)}nav.always.open>ul{right:0;transition:right var(--transition-base)}nav.always li{max-inline-size:none;width:100%;height:fit-content;--dir:row;--wrap:wrap}nav.always a{--py:1rem;width:100%;min-height:var(--height)}nav.always>button{position:fixed;bottom:0;right:0;width:var(--height);height:var(--height);border-radius:0;background-color:var(--base);color:var(--contrast);transition:width var(--timing) var(--function);transition-property:width,background-color;box-shadow:var(--shadow)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{--c:var(--action-rgb);z-index:1000000;width:100%;background-color:var(--overlay-heavy);color:var(--contrast);backdrop-filter:blur(5px)}nav.always.open>button:focus,nav.always.open>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button .icon-list,nav.always>button .icon-x{transform:scale(0);height:0;width:0;position:absolute}nav.always.open>button .icon-x,nav.always>button .icon-list{transform:scale(1);height:32px;width:32px}nav.always .has-submenu.open>.submenu,nav.always .has-submenu:hover>.submenu{height:max-content}nav.always .has-submenu:hover>a,nav.always .submenu>li>a:focus,nav.always .submenu>li>a:hover{background-color:var(--action-0);color:var(--action-contrast)}@media (min-width:768px){nav.always>ul{padding:var(--height) 0 0}}nav.on-this-page{--justify:space-between;max-width:none;z-index:var(--z-6);margin:0;padding:0 .5rem;background-color:var(--overlay-medium);color:var(--base-200)}body:has(nav.fixed) nav.on-this-page{bottom:var(--offHeight)}.on-this-page ul{--justify:flex-start;gap:0;width:100%}.on-this-page li:not(.has){padding:0}nav.letters li{width:100%;max-width:calc(7.69% - 2px)}.on-this-page .active a{--c:var(--action-rgb);background-color:var(--overlay-heavy);color:var(--action-contrast)}@media (min-width:768px){nav.letters li{max-width:none;width:fit-content}nav.letters a,nav.letters li:not(.has){padding:.25rem .66rem}}nav.index{--justify:flex-start;--px:0;background-color:var(--overlay-heavy)}.index ul{--justify:flex-start;width:fit-content}.index li{flex-shrink:0;transform:scaleX(0);transform-origin:right;max-width:0;overflow:hidden;transition:transform var(--timing) var(--function)}.index li.active{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}@media (min-width:768px){.index li.adj{transform:scaleX(1);transform-origin:left;width:100%;flex-shrink:1;max-width:fit-content}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index .active a:hover,.index a:hover{background-color:var(--action-0);color:var(--action-contrast)}.index label{display:flex;color:var(--contrast);align-items:center;margin:0}.index label button{margin-left:1em}.index.open{--dir:column-reverse;height:calc(100% - 8rem);z-index:var(--z-above);width:100%;background-color:var(--overlay-heavy);backdrop-filter:blur(5px);align-items:flex-end}.index.open label{max-width:90%;margin-top:1rem;margin-right:2rem}.index.open .toggle .icon{transform:rotate(45deg)}.index.open ul{--dir:column;--justify:flex-end;height:100%;max-width:100%;width:100%}.index.open li{background-color:transparent;max-width:100%!important;width:100%;height:var(--height);transform:scaleX(1);flex-shrink:1;overflow:visible}.index.open a{--justify:flex-end;background-color:transparent;padding:0 2rem 0 0}.condensed{--dir:row;--wrap:wrap;--height:1.2em;--py:.25rem;--px:.25rem;height:fit-content}.condensed>ul{--wrap:wrap;height:fit-content}.condensed ul{--justify:center;--gap:0}.condensed li{width:fit-content}.condensed li+li::before{content:'·';display:block;padding:0 .25em}nav.condensed a{text-transform:none;white-space:nowrap;border-bottom:2px solid transparent}.condensed a:focus,.condensed a:focus:visited,.condensed a:hover,.condensed a:hover:visited{border-color:var(--action-0)}.dashboard-nav{width:100%;--dir:row;--justify:flex-start;--wrap:nowrap}.wp-site-blocks>header,body>header{--dir:row;position:sticky;top:0;left:0;right:0;height:var(--height);width:100vw;display:flex;justify-content:space-between;align-items:center;padding:0 .5rem;background-color:var(--base);z-index:var(--zz-top);box-shadow:var(--shadow);border-bottom:1px solid var(--action-0)}.wp-site-blocks>header img{width:var(--height)}body>header{justify-content:space-between}header .title{--w:5em;margin:0;position:absolute;width:100%;height:100%;display:flex;justify-content:center;align-items:flex-start;max-inline-size:none}.current-hours{position:sticky;top:var(--height);bottom:unset;width:unset;z-index:100;background-color:var(--action-0);color:var(--action-contrast);box-shadow:var(--shadow);padding:.25rem 1rem;display:flex;justify-content:space-between}.current-hours p{margin:0;display:flex;flex-wrap:wrap;flex:1}.current-hours p+p{justify-content:flex-end}.current-hours a{color:var(--action-contrast)}.current-hours a:hover{color:var(--action-200)}.current-hours b{margin-right:.25rem}.find-us{display:flex;align-items:center;gap:0 .5rem}.find-us a{display:flex;padding:.25rem 1rem;border:1px solid var(--action-contrast);border-radius:var(--innerRadius)}.find-us a:hover{background-color:var(--base);color:var(--contrast);border-color:var(--contrast)}nav.menu{--justify:flex-start}nav.menu a{padding:.5rem .66rem}nav.tabs{--gap:0;--wrap:nowrap;padding-bottom:2px;z-index:var(--z-6);position:fixed;bottom:var(--height);left:var(--doubleHeight);right:var(--doubleHeight)}nav.term-navigation:has([hidden]){display:none}ul.socials a{padding:.5rem}ul.socials a .icon{margin:0} |
| | |
| | | class Gallery { |
| | | constructor(modal, config) { |
| | | this.imageWrapper = config.imageWrapper??null; |
| | | this.container = config.container ? document.querySelector(config.container) : document.querySelector('main'); |
| | | this.modal = new window.jvbModal( |
| | | modal, |
| | | { |
| | | onOpen: false, |
| | | onSave: false, |
| | | onClose: () => this.closeGallery(false) |
| | | } |
| | | ); |
| | | this.items = this.getGalleryItems() || []; |
| | | this.loadMore = config.loadMore??false; |
| | | constructor(modal, config = {}) { |
| | | this.container = config.container |
| | | ? document.querySelector(config.container) |
| | | : document.querySelector('main'); |
| | | |
| | | this.modalElement = (typeof modal === 'string') ? document.querySelector(modal)??false : modal; |
| | | if (!this.modal) { |
| | | return; |
| | | } |
| | | this.gallerySelector = config.gallerySelector || 'img[data-small]'; |
| | | |
| | | this.modal = new window.jvbModal(modal, { |
| | | onOpen: false, |
| | | onSave: false, |
| | | onClose: () => this.closeGallery(false) |
| | | }); |
| | | |
| | | this.modalElement = (typeof modal === 'string') |
| | | ? document.querySelector(modal) |
| | | : modal; |
| | | |
| | | if (!this.modalElement) return; |
| | | |
| | | this.a11y = window.jvbA11y; |
| | | this.openWhenReady = false; |
| | | |
| | | |
| | | this.initElements(); |
| | | this.imageWrapper = (typeof this.imageWrapper === 'string') ? this.imageWrapper : '.'+this.imageWrapper.classList.join('.'); |
| | | |
| | | this.index = 0; |
| | | this.items = []; |
| | | |
| | | this.swipe = { |
| | | touchStart: null, |
| | | touchEnd: null, |
| | | minSwipe: 50, |
| | | }; |
| | | this.isLoading = false; |
| | | |
| | | this.initElements(); |
| | | this.initListeners(); |
| | | } |
| | | |
| | | initElements() { |
| | | this.prevBtn = this.modalElement.querySelector('.prev'); |
| | | this.nextBtn = this.modalElement.querySelector('.next'); |
| | | this.favourite = this.modalElement.querySelector('.favourite'); |
| | | this.image = this.modalElement.querySelector('.image'); |
| | | this.counter = this.modalElement.querySelector('.counter'); |
| | | this.extra = this.modalElement.querySelector('.item-info'); |
| | | |
| | | //If we don't have the wrapper set up, we can remove these elements |
| | | if (!this.imageWrapper) { |
| | | this.favourite.remove(); |
| | | this.extra.remove(); |
| | | } |
| | | } |
| | | |
| | | getGalleryItems() { |
| | | let search = (this.imageWrapper) ? this.imageWrapper : 'img'; |
| | | |
| | | return Array.from(this.container.querySelectorAll(search)) |
| | | .map(item => { |
| | | const img = (this.imageWrapper) ? item.querySelector('img') : item; |
| | | if (!img) return null; |
| | | |
| | | return { |
| | | id: (this.imageWrapper) ? item.querySelector('button.favourite').dataset.id : '', |
| | | small: img.dataset.small || img.src, |
| | | large: img.dataset.large || img.src, |
| | | full: img.dataset.full || img.src, |
| | | alt: img.alt || '', |
| | | fav: (this.imageWrapper) ? item.querySelector('button.favourite')?.cloneNode(true) : '', |
| | | info: (this.imageWrapper) ? item.querySelector('.item-info')?.cloneNode(true) : '' |
| | | }; |
| | | }).filter(Boolean); |
| | | } |
| | | |
| | | initListeners() { |
| | | // Delegate click handling to container |
| | | this.container.addEventListener('click', this.handleClick.bind(this)); |
| | | document.addEventListener('keydown', this.handleKeys.bind(this)); |
| | | document.addEventListener('touchstart', this.handleTouchStart.bind(this)); |
| | | document.addEventListener('touchend', this.handleTouchEnd.bind(this)); |
| | | document.addEventListener('touchmove', this.handleTouchMove.bind(this)); |
| | | |
| | | // Modal-specific listeners added only when open |
| | | this.boundKeyHandler = this.handleKeys.bind(this); |
| | | this.boundTouchStart = this.handleTouchStart.bind(this); |
| | | this.boundTouchMove = this.handleTouchMove.bind(this); |
| | | this.boundTouchEnd = this.handleTouchEnd.bind(this); |
| | | } |
| | | destroyListeners() { |
| | | this.container.removeEventListener('click', this.handleClick.bind(this)); |
| | | document.removeEventListener('keydown', this.handleKeys.bind(this)); |
| | | document.removeEventListener('touchstart', this.handleTouchStart.bind(this)); |
| | | document.removeEventListener('touchend', this.handleTouchEnd.bind(this)); |
| | | document.removeEventListener('touchmove', this.handleTouchMove.bind(this)); |
| | | |
| | | handleClick(e) { |
| | | // Open gallery when clicking images |
| | | const img = e.target.closest(this.gallerySelector); |
| | | if (img && !this.modal.isOpen) { |
| | | e.preventDefault(); |
| | | this.openGallery(img); |
| | | return; |
| | | } |
| | | |
| | | // Navigation within gallery |
| | | if (this.modal.isOpen) { |
| | | if (e.target.closest('.next')) { |
| | | this.navigate(1); |
| | | } else if (e.target.closest('.prev')) { |
| | | this.navigate(-1); |
| | | } |
| | | } |
| | | } |
| | | |
| | | handleKeys(e) { |
| | | if (!this.modal.isOpen) return; |
| | | |
| | | switch (e.key) { |
| | | case 'ArrowLeft': |
| | | e.preventDefault(); |
| | | this.navigate(-1); |
| | | break; |
| | | case 'ArrowRight': |
| | | e.preventDefault(); |
| | | this.navigate(1); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | handleTouchStart(e) { |
| | | if (!this.modal.isOpen) return; |
| | | this.swipe.touchStart = e.touches[0].clientX; |
| | | } |
| | | |
| | | handleTouchMove(e) { |
| | | if (!this.modal.isOpen) return; |
| | | this.swipe.touchEnd = e.touches[0].clientX; |
| | | } |
| | | |
| | | handleTouchEnd(e) { |
| | | if (!this.swipe.touchStart || !this.swipe.touchEnd) return; |
| | | const distance = this.swipe.touchStart - this.swipe.touchEnd; |
| | | const isLeftSwipe = distance > this.swipe.minSwipe; |
| | | const isRightSwipe = distance < -this.swipe.minSwipe; |
| | | if (!this.modal.isOpen || !this.swipe.touchStart || !this.swipe.touchEnd) return; |
| | | |
| | | if (isLeftSwipe) { |
| | | this.navigate(1); |
| | | } else if (isRightSwipe) { |
| | | this.navigate(-1); |
| | | const distance = this.swipe.touchStart - this.swipe.touchEnd; |
| | | |
| | | if (Math.abs(distance) > this.swipe.minSwipe) { |
| | | this.navigate(distance > 0 ? 1 : -1); |
| | | } |
| | | |
| | | this.swipe.touchStart = null; |
| | | this.swipe.touchEnd = null; |
| | | } |
| | | |
| | | handleClick(e) { |
| | | //test if it's a link click, and the click has an image |
| | | if(window.targetCheck(e, '.feed-images')){ |
| | | this.handleGalleryOpen(e); |
| | | } |
| | | buildGalleryItems() { |
| | | return Array.from(this.container.querySelectorAll(this.gallerySelector)) |
| | | .map((img, index) => ({ |
| | | id: img.dataset.id || index, |
| | | small: img.dataset.small || img.src, |
| | | medium: img.dataset.medium || img.src, |
| | | full: img.dataset.full || img.src, |
| | | alt: img.alt || '', |
| | | element: img |
| | | })); |
| | | } |
| | | |
| | | if(window.targetCheck(e, '.nav')) { |
| | | if (window.targetCheck(e, '.next')) { |
| | | this.navigate(1); |
| | | } else { |
| | | this.navigate(-1); |
| | | } |
| | | } |
| | | if (window.targetCheck(e, 'button.cancel')) { |
| | | this.closeGallery(); |
| | | openGallery(clickedImg) { |
| | | // Build fresh gallery items |
| | | this.items = this.buildGalleryItems(); |
| | | |
| | | // Find clicked image index |
| | | this.index = this.items.findIndex(item => |
| | | item.element === clickedImg |
| | | ); |
| | | |
| | | if (this.index === -1) this.index = 0; |
| | | |
| | | // Attach modal-specific listeners |
| | | document.addEventListener('keydown', this.boundKeyHandler); |
| | | document.addEventListener('touchstart', this.boundTouchStart, { passive: true }); |
| | | document.addEventListener('touchmove', this.boundTouchMove, { passive: true }); |
| | | document.addEventListener('touchend', this.boundTouchEnd, { passive: true }); |
| | | |
| | | this.modal.handleOpen(); |
| | | this.updateDisplay(); |
| | | this.preloadAdjacent(); |
| | | |
| | | this.a11y.announce( |
| | | `Gallery opened. Image ${this.index + 1} of ${this.items.length}. Use arrow keys to navigate.` |
| | | ); |
| | | } |
| | | |
| | | closeGallery(useModal = true) { |
| | | // Remove modal-specific listeners |
| | | document.removeEventListener('keydown', this.boundKeyHandler); |
| | | document.removeEventListener('touchstart', this.boundTouchStart); |
| | | document.removeEventListener('touchmove', this.boundTouchMove); |
| | | document.removeEventListener('touchend', this.boundTouchEnd); |
| | | |
| | | if (useModal) { |
| | | this.modal.handleClose(); |
| | | } |
| | | } |
| | | |
| | | async navigate(direction) { |
| | | navigate(direction) { |
| | | let newIndex = this.index + direction; |
| | | |
| | | //Check if out of bounds |
| | | if (newIndex <0 || newIndex >= this.items.length) { |
| | | this.a11y.announceNavigation(newIndex, this.items.length, direction < 0, direction > 0); |
| | | if (newIndex <0) { |
| | | newIndex = this.items.length - 1; |
| | | } else { |
| | | newIndex = 0; |
| | | } |
| | | // Wrap around |
| | | if (newIndex < 0) { |
| | | newIndex = this.items.length - 1; |
| | | } else if (newIndex >= this.items.length) { |
| | | newIndex = 0; |
| | | } |
| | | |
| | | //update index |
| | | this.index = newIndex; |
| | | |
| | | this.updateDisplay(); |
| | | this.preloadImages(); |
| | | this.preloadAdjacent(); |
| | | |
| | | this.a11y.announceNavigation(this.index, this.items.length); |
| | | |
| | | if (typeof this.loadMore === 'function' && |
| | | direction > 0 && |
| | | newIndex >= (this.items.length - 3)) { |
| | | if (window.feedBlock.hasMore) { |
| | | await this.loadMore(); |
| | | this.updateGalleryItems(this.getGalleryItems()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | updateGalleryItems(newItems) { |
| | | const currentItem = this.items[this.index]; |
| | | |
| | | this.items = newItems; |
| | | if (currentItem) { |
| | | const newIndex = this.items.findIndex(item => |
| | | item.full === currentItem.full || |
| | | item.large === currentItem.large |
| | | ); |
| | | |
| | | if (newIndex !== -1) { |
| | | this.index = newIndex; |
| | | } |
| | | } |
| | | |
| | | this.updateNavigationButtons(); |
| | | if(this.items.length > 0 && this.openWhenReady) { |
| | | let index = this.findIndex('id', this.openWhenReady); |
| | | this.openGallery(index); |
| | | } |
| | | } |
| | | preloadImages() { |
| | | [-1,0,1].forEach(offset => { |
| | | const index = this.index + offset; |
| | | if (index >=0 && index < this.items.length) { |
| | | const img = new Image(); |
| | | const item = this.items[index]; |
| | | if (window.innerWidth < 1000) { |
| | | img.src = item.large || item.src; |
| | | } else { |
| | | img.src = item.full || item.src; |
| | | } |
| | | } |
| | | }); |
| | | this.a11y.announce(`Image ${this.index + 1} of ${this.items.length}`); |
| | | } |
| | | |
| | | updateDisplay() { |
| | | const item = this.items[this.index]; |
| | | if (!item) return; |
| | | |
| | | [ |
| | | this.image.src, |
| | | this.image.alt, |
| | | this.counter.textContent |
| | | ] = [ |
| | | (window.innerWidth < 1000) ? |
| | | (item.large || item.src) : |
| | | (item.full || item.src), |
| | | item.alt || '', |
| | | `${this.index + 1} / ${this.items.length}` |
| | | ]; |
| | | if (this.imageWrapper) { |
| | | if (item.fav) { |
| | | window.removeChildren(this.favourite); |
| | | this.favourite.appendChild(item.fav.cloneNode(true)); |
| | | // Use medium/full based on viewport |
| | | const src = window.innerWidth < 1000 |
| | | ? (item.medium || item.src) |
| | | : (item.full || item.src); |
| | | |
| | | this.image.src = src; |
| | | this.image.alt = item.alt; |
| | | this.counter.textContent = `${this.index + 1} / ${this.items.length}`; |
| | | |
| | | // Update button states |
| | | this.prevBtn.classList.toggle('disabled', this.items.length <= 1); |
| | | this.nextBtn.classList.toggle('disabled', this.items.length <= 1); |
| | | } |
| | | |
| | | preloadAdjacent() { |
| | | [-1, 1].forEach(offset => { |
| | | const index = this.index + offset; |
| | | if (index >= 0 && index < this.items.length) { |
| | | const item = this.items[index]; |
| | | const img = new Image(); |
| | | img.src = window.innerWidth < 1000 |
| | | ? (item.medium || item.src) |
| | | : (item.full || item.src); |
| | | } |
| | | if (item.info) { |
| | | window.removeChildren(this.extra); |
| | | this.extra.appendChild(item.info.cloneNode(true)); |
| | | } |
| | | } |
| | | |
| | | this.updateNavigationButtons(); |
| | | }); |
| | | } |
| | | |
| | | updateNavigationButtons() { |
| | | this.prevBtn.classList.toggle('end', this.index > 0 ? '' : 'none'); |
| | | this.nextBtn.classList.toggle('end', this.index < this.items.length -1 ? '' : 'none'); |
| | | } |
| | | |
| | | handleGalleryOpen(e) { |
| | | let item = (this.imageWrapper) ? e.target.closest(this.imageWrapper) : e.target.closest('img'); |
| | | let key = (this.imageWrapper) ? 'id' : 'small'; |
| | | let value = item.dataset[key]; |
| | | |
| | | let index = this.findIndex(key, value); |
| | | this.openGallery(index); |
| | | } |
| | | |
| | | findIndex(property, value) { |
| | | return this.items.findIndex(obj => obj[property] === value); |
| | | } |
| | | |
| | | openGallery(index) { |
| | | this.initListeners(); |
| | | this.index = index; |
| | | this.modal.handleOpen(); |
| | | this.updateDisplay(); |
| | | this.preloadImages(); |
| | | |
| | | this.a11y.announce(`Image ${this.index + 1} of ${this.items.length}. Use arrow keys to navigate.`); |
| | | } |
| | | |
| | | closeGallery(useModal = true) { |
| | | this.destroyListeners(); |
| | | if (useModal) { |
| | | this.modal.handleClose(); |
| | | } |
| | | cleanup() { |
| | | this.container.removeEventListener('click', this.handleClick); |
| | | this.closeGallery(false); |
| | | } |
| | | } |
| | | |
| | | window.jvbGallery = Gallery; |
| | | |
| | | document.addEventListener('DOMContentLoaded', function() { |
| | | let galleries = document.querySelectorAll('dialog.gallery'); |
| | | if (galleries.length > 0) { |
| | | window.galleries = new Map(); |
| | | |
| | | galleries.forEach(gallery => { |
| | | let id = gallery.id; |
| | | window.galleries.set(id, new Gallery(gallery)); |
| | | }); |
| | | } |
| | | }); |
| | | |
| | |
| | | * Register a standalone form (for front-end forms) |
| | | */ |
| | | registerForm(formElement, options = {}) { |
| | | if (!formElement) return; |
| | | const formId = formElement.dataset.formId || `form_${Date.now()}`; |
| | | formElement.dataset.formId = formId; |
| | | |
| | |
| | | const formConfig = { |
| | | element: formElement, |
| | | id: formId, |
| | | status: '', |
| | | options: { |
| | | autoSave: 'autosave' in formElement.dataset, |
| | | autosave: 'autosave' in formElement.dataset, |
| | | saveDelay: this.autoSaveDefaults.delay, |
| | | endpoint: formElement.dataset.save??'', |
| | | formStatus: true, |
| | | cache: true, |
| | | ...options |
| | | }, |
| | | dependencies: new Map(), |
| | | data: this.collectFormData(formElement), |
| | | isDirty: false |
| | | }; |
| | | |
| | | // Initialize special fields |
| | |
| | | |
| | | container.appendChild(row); |
| | | |
| | | // Schedule save if auto-save enabled |
| | | if (formConfig && formConfig.options.autoSave) { |
| | | if (formConfig) { |
| | | this.scheduleSave(formConfig, { |
| | | type: 'repeater', |
| | | action: 'add', |
| | |
| | | this.updateRepeaterOrder(repeater, formConfig); |
| | | |
| | | // Schedule save |
| | | if (formConfig && formConfig.options.autoSave) { |
| | | if (formConfig) { |
| | | this.scheduleSave(formConfig, { |
| | | type: 'repeater', |
| | | action: 'remove', |
| | |
| | | }); |
| | | |
| | | // Schedule save |
| | | if (formConfig && formConfig.options.autoSave) { |
| | | if (formConfig) { |
| | | this.scheduleSave(formConfig, { |
| | | type: 'repeater', |
| | | action: 'reorder', |
| | |
| | | |
| | | /* ========== Event Handlers ========== */ |
| | | |
| | | handleSubmit(event) { |
| | | //TODO: submit data, if successful, delete from store |
| | | if (this.subscribers.size > 0 ){ |
| | | const form = event.target; |
| | | if (!form.dataset.formId) return; |
| | | async handleSubmit(event) { |
| | | const form = event.target; |
| | | if (!form.dataset.formId) return; |
| | | |
| | | const formConfig = this.forms.get(form.dataset.formId); |
| | | |
| | | // Handle subscriber-based forms |
| | | if (this.subscribers.size > 0) { |
| | | event.preventDefault(); |
| | | |
| | | const formConfig = this.forms.get(form.dataset.formId); |
| | | if (!formConfig) return; |
| | | |
| | | const formData = this.collectFormData(form); |
| | | this.notify('form-submit', { |
| | | formId: formConfig.id, |
| | |
| | | } |
| | | } |
| | | |
| | | handleFormSuccess(form, data) { |
| | | // Clear previous errors |
| | | form.querySelectorAll('.error-message').forEach(el => el.remove()); |
| | | form.querySelectorAll('.field-error').forEach(el => |
| | | el.classList.remove('field-error') |
| | | ); |
| | | |
| | | // Add success class to form |
| | | form.classList.add('form-success'); |
| | | |
| | | // Show success message if provided |
| | | if (data.message) { |
| | | const success = document.createElement('div'); |
| | | success.className = 'form-success-message success-message'; |
| | | success.textContent = data.message; |
| | | form.insertBefore(success, form.firstChild); |
| | | |
| | | // Optionally add icon |
| | | const icon = window.getIcon?.('check-circle'); |
| | | if (icon) { |
| | | icon.classList.add('success-icon'); |
| | | success.prepend(icon); |
| | | } |
| | | } |
| | | |
| | | // If there's a title/description (for registration success) |
| | | if (data.title || data.description) { |
| | | const successBox = document.createElement('div'); |
| | | successBox.className = 'success-box'; |
| | | |
| | | if (data.title) { |
| | | const title = document.createElement('h3'); |
| | | title.textContent = data.title; |
| | | successBox.appendChild(title); |
| | | } |
| | | |
| | | if (data.description) { |
| | | // Handle both string and array descriptions |
| | | const descriptions = Array.isArray(data.description) |
| | | ? data.description |
| | | : [data.description]; |
| | | |
| | | descriptions.forEach(desc => { |
| | | const p = document.createElement('p'); |
| | | p.textContent = desc; |
| | | successBox.appendChild(p); |
| | | }); |
| | | } |
| | | |
| | | form.insertBefore(successBox, form.firstChild); |
| | | } |
| | | |
| | | // Announce success for accessibility |
| | | if (window.jvbA11y) { |
| | | window.jvbA11y.announce(data.message || 'Form submitted successfully'); |
| | | } |
| | | |
| | | // Trigger custom event |
| | | form.dispatchEvent(new CustomEvent('jvb-form-success', { |
| | | detail: data |
| | | })); |
| | | } |
| | | |
| | | handleFormError(form, data) { |
| | | // Clear all previous errors |
| | | form.querySelectorAll('.error-message').forEach(el => el.remove()); |
| | | form.querySelectorAll('.field-error, .has-error').forEach(el => { |
| | | el.classList.remove('field-error', 'has-error'); |
| | | }); |
| | | |
| | | // Clear validation states using existing method |
| | | form.querySelectorAll('.field').forEach(fieldWrapper => { |
| | | this.clearValidation(fieldWrapper); |
| | | }); |
| | | |
| | | // Handle field-specific errors |
| | | if (data.field) { |
| | | const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`); |
| | | if (fieldWrapper) { |
| | | // Use existing showError method for consistency |
| | | this.showError(fieldWrapper, data.message); |
| | | |
| | | // Mark as touched so validation persists |
| | | this.touchedFields.add(data.field); |
| | | |
| | | // Scroll to error |
| | | fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| | | |
| | | // Focus the input for better UX |
| | | const input = fieldWrapper.querySelector('input, textarea, select'); |
| | | if (input) { |
| | | input.focus(); |
| | | } |
| | | } |
| | | } else { |
| | | // General form error (not field-specific) |
| | | const error = document.createElement('div'); |
| | | error.className = 'form-error error-message'; |
| | | error.textContent = data.message; |
| | | |
| | | // Add icon for consistency |
| | | const icon = window.getIcon?.('close-circle'); |
| | | if (icon) { |
| | | icon.classList.add('error-icon'); |
| | | error.prepend(icon); |
| | | } |
| | | |
| | | form.insertBefore(error, form.firstChild); |
| | | |
| | | // Scroll to top to show the error |
| | | form.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| | | } |
| | | |
| | | // Announce error for accessibility |
| | | if (window.jvbA11y) { |
| | | const announcement = data.field |
| | | ? `Error in ${data.field}: ${data.message}` |
| | | : `Form error: ${data.message}`; |
| | | window.jvbA11y.announce(announcement); |
| | | } |
| | | |
| | | // Trigger custom event |
| | | form.dispatchEvent(new CustomEvent('jvb-form-error', { |
| | | detail: data |
| | | })); |
| | | } |
| | | |
| | | handleClick(e) { |
| | | if (window.targetCheck(e, 'div.quantity')) { |
| | | let container = window.targetCheck(e, 'div.quantity'); |
| | |
| | | if (event.target.closest('[data-ignore]')) { |
| | | return; |
| | | } |
| | | if (this.subscribers.size > 0) { |
| | | const target = event.target; |
| | | const form = target.form || target.closest('form'); |
| | | const target = event.target; |
| | | const form = target.form || target.closest('form');if (!form) return; |
| | | |
| | | if (!form) return; |
| | | |
| | | const formConfig = this.forms?.get(form.dataset.formId); |
| | | if (!formConfig) return; |
| | | |
| | | const formConfig = this.forms?.get(form.dataset.formId); |
| | | if (!formConfig) return; |
| | | console.log(formConfig.options); |
| | | if (formConfig.options.autosave || this.subscribers.size > 0) { |
| | | // Check conditional fields |
| | | const dependencies = formConfig.dependencies.get(target.name); |
| | | if (dependencies) { |
| | |
| | | } |
| | | |
| | | // Schedule auto-save if enabled |
| | | if (formConfig.options.autoSave && !form.dataset.noautosave) { |
| | | const delay = this.getDelayForField(target); |
| | | this.scheduleSave(formConfig, delay); |
| | | } |
| | | const delay = this.getDelayForField(target); |
| | | this.scheduleSave(formConfig, delay); |
| | | } |
| | | } |
| | | |
| | |
| | | this.validateField(input, fieldWrapper); |
| | | } |
| | | const formConfig = this.forms?.get(form.dataset.formId); |
| | | if (formConfig && formConfig.options.autoSave && !form.dataset.noautosave) { |
| | | if (formConfig) { |
| | | // Shorter delay on blur |
| | | this.scheduleSave(formConfig, { |
| | | type: 'blur', |
| | |
| | | |
| | | // All validations passed |
| | | this.showSuccess(fieldWrapper); |
| | | this.notify('field-validated', input); |
| | | return true; |
| | | } |
| | | |
| | |
| | | /** |
| | | * Show success state (green checkmark) |
| | | */ |
| | | showSuccess(fieldWrapper) { |
| | | showSuccess(fieldWrapper, textMessage = '') { |
| | | if (!fieldWrapper) return; |
| | | |
| | | // Find validation elements (they might be in field-input-wrapper or field-content) |
| | |
| | | |
| | | // Hide error message |
| | | if (message) { |
| | | message.hidden = true; |
| | | message.textContent = ''; |
| | | if (textMessage === '') { |
| | | message.hidden = true; |
| | | message.textContent = ''; |
| | | } else { |
| | | message.hidden = false; |
| | | message.textContent = textMessage; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | return this.autoSaveDefaults.delay; |
| | | } |
| | | scheduleSave(formConfig, delay = this.autoSaveDefaults.delay) { |
| | | if (!formConfig.options.autosave) { |
| | | return; |
| | | } |
| | | document.addEventListener('input', this.saveCheck, {passive: true}); |
| | | const saveKey = `autosave_${formConfig.id}`; |
| | | |
| | |
| | | |
| | | // Get only changed fields |
| | | const changes = this.getChangedFields(formConfig.data, formData); |
| | | console.log('Changes:', changes); |
| | | if (Object.keys(changes).length === 0) return; |
| | | console.log('Continuing on:'); |
| | | |
| | | // Update stored data |
| | | formConfig.data = formData; |
| | |
| | | |
| | | // Check if current data differs from snapshot |
| | | const currentData = this.collectFormData(formConfig.element); |
| | | const changes = this.getChangedFields(formConfig.lastSnapshot, currentData); |
| | | const changes = this.getChangedFields(formConfig.data, currentData); |
| | | |
| | | return Object.keys(changes).length > 0; |
| | | } |
| | | |
| | | showFormStatus(formID, status) { |
| | | showFormStatus(formID, status, message='') { |
| | | // Remove existing status |
| | | let form = this.forms.get(formID); |
| | | if (!form.options.formStatus) { |
| | | return; |
| | | } |
| | | |
| | | if (form.status === status){ |
| | | return; |
| | | } |
| | | |
| | | form.status = status; |
| | | |
| | | console.log('Setting status: ', status); |
| | | |
| | |
| | | 'offline': 'Changes will be saved when online' |
| | | }; |
| | | const icons = { |
| | | 'autosaved': 'check', |
| | | 'submitted': 'check', |
| | | 'error': 'close', |
| | | 'autosaved': 'check-circle', |
| | | 'submitted': 'check-circle', |
| | | 'error': 'close-circle', |
| | | 'offline': 'cloud-slash', |
| | | 'pending': 'exclamation-mark' |
| | | } |
| | |
| | | if (icon) { |
| | | statusWrap.prepend(icon); |
| | | } |
| | | console.log(status, messages[status]); |
| | | console.log(status, icons[status]); |
| | | statusElement.textContent = messages[status] || status; |
| | | if (message === '') { |
| | | message = messages[status] || status; |
| | | } |
| | | statusElement.textContent = message; |
| | | statusWrap.classList.toggle('loading', ['uploading', 'saving'].includes(status)); |
| | | |
| | | // Auto-hide success messages |
| | |
| | | /* ========== Form Data Methods ========== */ |
| | | |
| | | collectFormData(form) { |
| | | if (Object.hasOwn(form.dataset, 'timeline')) { |
| | | return this.collectTimeline(form); |
| | | } |
| | | const formData = new FormData(form); |
| | | let data = {}; |
| | | const repeaterData = {}; |
| | |
| | | return this.mergeRepeaterData(data, repeaterData); |
| | | } |
| | | |
| | | collectTimeline(form) { |
| | | console.log('Collecting Timeline data:'); |
| | | let data = {}; |
| | | let posts = {}; // Temporary object keyed by post ID |
| | | let postOrder = []; // Track order as encountered (preserves DOM/drag order) |
| | | let formData = new FormData(form); |
| | | |
| | | for (const [key, value] of formData.entries()) { |
| | | if (this.ignore.includes(key) || key.endsWith('_temp')) { |
| | | continue; |
| | | } |
| | | const match = key.match(/^\[(\d+)\](.+)$/); |
| | | if (match) { |
| | | // Timeline-specific field: [postId]fieldName |
| | | const [, postId, fieldName] = match; |
| | | if (!posts[postId]) { |
| | | posts[postId] = { id: parseInt(postId) }; |
| | | postOrder.push(postId); // Track first occurrence |
| | | } |
| | | const processor = this.getFieldProcessor(fieldName); |
| | | processor(fieldName, value, posts[postId], {}, {}, form); |
| | | } else { |
| | | // Shared field (post_title, taxonomies, etc.) |
| | | const processor = this.getFieldProcessor(key); |
| | | processor(key, value, data, {}, {}, form); |
| | | } |
| | | } |
| | | |
| | | // Convert to array in DOM order (matches menu_order) |
| | | data.timeline = postOrder.map(id => posts[id]); |
| | | |
| | | delete data['form-id']; |
| | | delete data['sendAll']; |
| | | delete data['timeline_temp']; |
| | | delete data['']; // Empty key |
| | | |
| | | console.log('Data: ', data); |
| | | return data; |
| | | } |
| | | |
| | | getFieldProcessor(key) { |
| | | if (key.includes('|')) return this.processTableField; |
| | | if (key.includes('::')) return this.processGroupField; |
| | |
| | | |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | window.jvbForm = FormController; |
| | | console.log('FormController in window'); |
| | | }); |
| | |
| | | * ICONS |
| | | ********************************************/ |
| | | getIcon(icon) { |
| | | console.log('Getting Icon: '+icon); |
| | | if (typeof icon === 'undefined') { |
| | | return ''; |
| | | } |
| | |
| | | /* |
| | | SITE GALLERY MANAGER |
| | | Handles two main functions: |
| | | |
| | | 1. Responsive Images |
| | | - Automatically loads appropriate image sizes based on screen width |
| | | - Listens for resize events and updates images accordingly |
| | | - Uses data-small, data-medium, and data-full attributes on images |
| | | |
| | | 2. Gallery Management |
| | | - HTML Structure: |
| | | <a class="open-gallery" data-opens="gallery-id" data-focus="image-to-show"> |
| | | <figure><img...></figure> |
| | | </a> |
| | | |
| | | <dialog class="gallery" id="gallery-id"> |
| | | - Main image display (.images with multiple <figure>s) |
| | | - Navigation bar on bottom (nav.thumbnails) with: |
| | | - Previous/Next buttons |
| | | - Thumbnail list (toggles with .open class) |
| | | - Expand button (toggles .expanded class) |
| | | - Visibility toggle (toggles .hide-info class) |
| | | - Close button |
| | | </dialog> |
| | | |
| | | Key Features: |
| | | - Opens in modal dialog with keyboard navigation |
| | | - Manages thumbnail navigation state |
| | | - Handles responsive image loading |
| | | - Uses class-based state management (open, expanded, hide-info) |
| | | - All event handling through delegation |
| | | - Built on UIHandler base class for state management |
| | | |
| | | State Management: |
| | | - Dialog: open/closed |
| | | - Thumbnails: collapsed/open/expanded |
| | | - Info panel: visible/hidden |
| | | - Active image: tracked with .focused class |
| | | */ |
| | | class Media { |
| | | constructor() { |
| | | this.currentWidth = window.innerWidth; |
| | | this.initElements(); |
| | | if (this.images.length === 0) { |
| | | return; |
| | | } |
| | | this.store = new window.jvbStore({ |
| | | name: 'images', |
| | | TTL: 604800 |
| | | }); |
| | | this.touch = { |
| | | x: null, |
| | | y: null |
| | | } |
| | | this.a11y = window.jvbA11y; |
| | | this.debouncer = window.debouncer; |
| | | this.isTouching = false; |
| | | this.images = document.querySelectorAll('.wp-site-blocks img[data-small]'); |
| | | |
| | | if (this.images.length === 0) return; |
| | | |
| | | // Immediately load visible images |
| | | this.loadVisibleImages(); |
| | | |
| | | this.initListeners(); |
| | | document.addEventListener('beforeunload', this.cleanup); |
| | | } |
| | | |
| | | initElements(){ |
| | | this.images = document.querySelectorAll('.wp-site-blocks img'); |
| | | this.gallery = document.querySelector('dialog.gallery'); |
| | | this.modal = new window.jvbModal(this.gallery, { |
| | | openMessage: 'Opened Gallery', |
| | | closeMessage: 'Closed Gallery', |
| | | open: '.open-gallery', |
| | | close: '.close' |
| | | }); |
| | | loadVisibleImages() { |
| | | // Load first image immediately, plus any in viewport |
| | | this.images.forEach((img, index) => { |
| | | const rect = img.getBoundingClientRect(); |
| | | const isVisible = rect.top < window.innerHeight && rect.bottom > 0; |
| | | |
| | | this.modal.subscribe((event, data) => { |
| | | if (event === 'modal-open') { |
| | | this.openGallery(); |
| | | } else if (event === 'modal-close') { |
| | | this.closeGallery(); |
| | | // Always load first image, or if currently visible |
| | | if (index === 0 || isVisible) { |
| | | this.loadAppropriateImage(img); |
| | | img.dataset.loaded = 'true'; // Mark so we don't observe it |
| | | } |
| | | }); |
| | | } |
| | | |
| | | initListeners() { |
| | | this.resizeHandler = this.handleResize.bind(this); |
| | | this.clickHandler = this.handleClick.bind(this); |
| | | this.keysHandler = this.handleKeys.bind(this); |
| | | this.touchStartHandler = this.handleTouchStart.bind(this); |
| | | this.touchEndHandler = this.handleTouchEnd.bind(this); |
| | | |
| | | document.addEventListener('click', this.clickHandler); |
| | | window.addEventListener('resize', this.resizeHandler); |
| | | console.log('window hash: ',window.location.hash); |
| | | let target = document.querySelector(window.location.hash); |
| | | if (target && target.tagName === 'LI') { |
| | | let trigger = target.querySelector('.open-gallery'); |
| | | if (trigger) { |
| | | this.openGallery(trigger); |
| | | } |
| | | } |
| | | |
| | | this.observer = new IntersectionObserver((entries) =>{ |
| | | // Only observe images that weren't immediately loaded |
| | | this.observer = new IntersectionObserver((entries) => { |
| | | entries.forEach(entry => { |
| | | if (entry.isIntersecting) { |
| | | let img = entry.target; |
| | | if (!img.closest('dialog')) { |
| | | this.loadAppropriateImage(img); |
| | | this.observer.unobserve(img); |
| | | } |
| | | this.loadAppropriateImage(entry.target); |
| | | this.observer.unobserve(entry.target); |
| | | } |
| | | }) |
| | | }); |
| | | }, { |
| | | root: null, |
| | | rootMargin: '50px', |
| | | threshold: .1 |
| | | threshold: 0.1 |
| | | }); |
| | | |
| | | this.images.forEach(img => { |
| | | if (!img.closest('dialog')) { |
| | | if (!img.dataset.loaded) { |
| | | this.observer.observe(img); |
| | | } |
| | | }) |
| | | }); |
| | | } |
| | | |
| | | initTouchHandling() { |
| | | this.isTouching = true; |
| | | this.touch.x = 0; |
| | | this.touch.y = 0; |
| | | |
| | | this.gallery.addEventListener('touchstart', this.touchStartHandler); |
| | | this.gallery.addEventListener('touchend', this.touchEndHandler); |
| | | } |
| | | cancelTouchHandling() { |
| | | this.isTouching = false; |
| | | this.gallery.removeEventListener('touchstart', this.touchStartHandler); |
| | | this.gallery.removeEventListener('touchend', this.touchEndHandler); |
| | | } |
| | | |
| | | handleTouchStart(e) { |
| | | this.touch.x = e.touches[0].clientX; |
| | | this.touch.y = e.touches[0].clientY; |
| | | } |
| | | handleTouchEnd(e) { |
| | | const diffX = e.changedTouches[0].clientX - this.touch.x; |
| | | const diffY = e.changedTouches[0].clientY - this.touch.y; |
| | | |
| | | if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { |
| | | this.navigateImages({target: this.gallery}, diffX < 0 ? 'next' : 'previous'); |
| | | } |
| | | } |
| | | handleResize(e) { |
| | | window.debouncer.schedule( |
| | | 'resize', |
| | | ()=> { |
| | | const currentWidth = window.innerWidth; |
| | | if (Math.abs(currentWidth - this.currentWidth) > 100) { |
| | | this.currentWidth = currentWidth; |
| | | this.handleImageResize(); |
| | | } |
| | | }, |
| | | 150 |
| | | ); |
| | | } |
| | | handleClick(e) { |
| | | |
| | | } |
| | | handleKeys(e) { |
| | | //Escape handled by Modal.js |
| | | if (e.key === 'Tab') { |
| | | if (e.shiftKey) { |
| | | |
| | | } else { |
| | | |
| | | handleResize() { |
| | | window.debouncer.schedule('image-resize', () => { |
| | | const newWidth = window.innerWidth; |
| | | if (Math.abs(newWidth - this.currentWidth) > 100) { |
| | | this.currentWidth = newWidth; |
| | | this.updateVisibleImages(); |
| | | } |
| | | }, 150); |
| | | } |
| | | |
| | | updateVisibleImages() { |
| | | this.images.forEach(img => { |
| | | const rect = img.getBoundingClientRect(); |
| | | if (rect.top < window.innerHeight && rect.bottom > 0) { |
| | | this.loadAppropriateImage(img, true); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | loadAppropriateImage(img, forceUpdate = false) { |
| | | const targetSize = this.getTargetSize(); |
| | | const newSrc = img.dataset[targetSize]; |
| | | |
| | | if (newSrc && (forceUpdate || newSrc !== img.currentSrc)) { |
| | | img.src = newSrc; |
| | | } |
| | | } |
| | | |
| | | openGallery(target = null) { |
| | | document.addEventListener('keydown', this.clickHandler); |
| | | const id = target.dataset.opens; |
| | | this.initTouchHandling(); |
| | | let focus; |
| | | if (!target) { |
| | | focus = this.images[0]; |
| | | } else { |
| | | focus = target.dataset.focus; |
| | | } |
| | | if (focus) { |
| | | this.updateImage(this.gallery.querySelector(`#${focus}`)); |
| | | } |
| | | } |
| | | |
| | | closeGallery() { |
| | | this.cancelTouchHandling(); |
| | | document.removeEventListener('keydown', this.keysHandler); |
| | | //Restore focus |
| | | if (this.lastFocusedElement) { |
| | | this.lastFocusedElement.focus(); |
| | | } |
| | | getTargetSize() { |
| | | if (this.currentWidth < 768) return 'small'; |
| | | if (this.currentWidth < 1200) return 'medium'; |
| | | return 'full'; |
| | | } |
| | | |
| | | cleanup() { |
| | | this.observer.disconnect(); |
| | | window.removeEventListener('resize', this.resizeHandler) |
| | | if (this.isTouching) { |
| | | this.cancelTouchHandling(); |
| | | document.removeEventListener('keydown', this.keysHandler); |
| | | } |
| | | document.removeEventListener('click', this.clickHandler); |
| | | this.observer?.disconnect(); |
| | | window.removeEventListener('resize', this.resizeHandler); |
| | | } |
| | | } |
| | | |
| | | window.isLoaded = false; |
| | | document.addEventListener('readystatechange', () => { |
| | | if (!window.isLoaded && document.querySelector('.wp-site-blocks img')) { |
| | | window.jvbMedia = new Media(); |
| | | window.isLoaded = true; |
| | | } |
| | | }); |
| | |
| | | **********************************************/ |
| | | class PopulateForm { |
| | | constructor(form, fieldData = {}, imageData = {}, options = {}) { |
| | | console.log('Populating field... ', form); |
| | | console.log('fieldData: ', fieldData); |
| | | console.log('imageData: ', imageData); |
| | | console.log('options: ', options); |
| | | for (let [fieldName, fieldValue] of Object.entries(fieldData)) { |
| | | let wrapper = form.querySelector(`[data-field="${fieldName}"]`); |
| | | if (wrapper) { |
| | |
| | | * Populate gallery fields |
| | | */ |
| | | populateGalleryField(fieldWrapper, fieldName, fieldValue, imagesData = {}) { |
| | | this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData); |
| | | if (fieldName === 'timeline') { |
| | | this.populateTimelineGallery(fieldWrapper, fieldName, fieldValue, imagesData); |
| | | } else { |
| | | this.populateImageField(fieldWrapper, fieldName, fieldValue, imagesData); |
| | | } |
| | | } |
| | | |
| | | populateTimelineGallery(fieldWrapper, fieldName, fieldValue, imagesData) |
| | | { |
| | | if (!fieldValue) { |
| | | return; |
| | | } |
| | | |
| | | const imageIds = Object.keys(fieldValue); |
| | | if (imageIds.length === 0) { |
| | | return; |
| | | } |
| | | |
| | | // Update image display |
| | | const grid = fieldWrapper.querySelector('.item-grid'); |
| | | const uploadContainer = fieldWrapper.querySelector('.file-upload-container'); |
| | | fieldWrapper.querySelector('.progress')?.remove(); |
| | | if (grid) { |
| | | window.removeChildren(grid); |
| | | imageIds.forEach(imageId => { |
| | | let image = window.getTemplate('timelineItem'); |
| | | let img = image.querySelector('img'); |
| | | |
| | | image.querySelector('video')?.remove(); |
| | | image.querySelector('.select-item span')?.remove(); |
| | | let data = fieldValue[imageId]; |
| | | |
| | | let imgData = imagesData[imageId]; |
| | | |
| | | img.src = imgData.medium; |
| | | |
| | | data = { ... data, ... imgData}; |
| | | |
| | | let fields = image.querySelectorAll('.field'); |
| | | fields.forEach(field => { |
| | | if (field.classList.contains('group')){ |
| | | return; |
| | | } |
| | | let input = field.querySelector('input, textarea'); |
| | | |
| | | let label = field.querySelector('label'); |
| | | let name = input.name.replace('upload_data::',''); |
| | | |
| | | let value = data[name]; |
| | | this.populateField(field, name, value, imagesData); |
| | | |
| | | let id = data.id; |
| | | let newName = `[${id}]${name}`; |
| | | [ |
| | | input.name, |
| | | input.id, |
| | | label.htmlFor |
| | | ] = [ |
| | | newName, |
| | | newName, |
| | | newName, |
| | | ] |
| | | }); |
| | | grid.append(image); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Populate repeater fields |
| | | */ |
| | | populateRepeaterField(fieldWrapper, fieldName, fieldValue, options = {}) { |
| | | console.log('fieldWrapper', fieldWrapper); |
| | | console.log('fieldName', fieldName); |
| | | console.log('fieldValue', fieldValue); |
| | | console.log('options', options); |
| | | if (!fieldValue || !Array.isArray(fieldValue)) { |
| | | return; |
| | | } |
| | |
| | | *********************************************/ |
| | | initUI() { |
| | | this.icons = { |
| | | queued: 'refresh', localProcessing: 'refresh', uploading: 'syncing', |
| | | pending: 'cloud', processing: 'syncing', completed: 'synced', |
| | | failed: 'error', failed_permanent: 'error' |
| | | queued: 'arrows-clockwise', localProcessing: 'arrows-clockwise', uploading: 'syncing', |
| | | pending: 'cloud', processing: 'syncing', completed: 'cloud-check', |
| | | failed: 'cloud-warning', failed_permanent: 'cloud-warning' |
| | | }; |
| | | |
| | | this.selectors = { |
| | |
| | | data.context = {}; |
| | | data.context['redirect_to'] = window.location.href+'?seeReferral=1'; |
| | | console.log('Making Request with: ', data); |
| | | result = await this.makeRequest('magic-link', data); |
| | | result = await this.makeRequest('magic', data); |
| | | } |
| | | |
| | | |
| | |
| | | |
| | | async makeRequest(endpoint, data) { |
| | | if (![ |
| | | 'magic-link', |
| | | 'magic', |
| | | 'referrals/register', |
| | | 'referrals/check-code' |
| | | ].includes(endpoint)) { |
| | |
| | | |
| | | if (!field) return; |
| | | |
| | | |
| | | // Store current value immediately (fixes fast typing issue) |
| | | const query = e.target.value.trim(); |
| | | field.currentAutocompleteQuery = query; |
| | | |
| | | if (query.length < 2) { |
| | | if (field.autocompleteDropdown) { |
| | |
| | | } |
| | | |
| | | this.activeField = fieldId; |
| | | this.currentConfig = field; |
| | | |
| | | |
| | | if (field.canCreate && ! this.creator) { |
| | | this.creator = new window.jvbTaxCreator(this); |
| | | } |
| | | this.isAutocompleteActive = true; |
| | | |
| | | if (field.autocompleteDropdown) { |
| | |
| | | }); |
| | | } |
| | | |
| | | // Offer to create new term if creator is available |
| | | if (this.creator) { |
| | | const createOption = this.creator.createAutocompleteOption(query, field); |
| | | // Use stored current query instead of debounced one |
| | | const currentQuery = field.currentAutocompleteQuery || query; |
| | | if (field.canCreate && currentQuery && window.jvbTaxCreator) { |
| | | const createOption = this.createNewTermOption(currentQuery); |
| | | dropdown.appendChild(createOption); |
| | | } |
| | | |
| | | dropdown.hidden = false; |
| | | } |
| | | |
| | | createNewTermOption(query) { |
| | | const button = document.createElement('button'); |
| | | button.type = 'button'; |
| | | button.className = 'autocomplete-item create-term'; |
| | | button.dataset.query = query; |
| | | button.innerHTML = `<strong>Create:</strong> "${query}"`; |
| | | |
| | | return button; |
| | | } |
| | | |
| | | createAutocompleteTermElement(field, term) { |
| | | const item = document.createElement('button'); |
| | | item.type = 'button'; |
| | |
| | | 'failed_permanent': 'Upload failed permanently' |
| | | }; |
| | | |
| | | // Sortable configuration |
| | | this.sortableInstances = new Map(); |
| | | this.sortableConfig = { |
| | | animation: 150, |
| | | draggable: '.item', |
| | | handle: '.select-item-label, img', // Can drag by image or checkbox label |
| | | ghostClass: 'sortable-ghost', |
| | | chosenClass: 'sortable-chosen', |
| | | dragClass: 'sortable-drag', |
| | | onEnd: (evt) => { |
| | | this.handleReorder(evt); |
| | | } |
| | | }; |
| | | |
| | | this.init(); |
| | | } |
| | | |
| | |
| | | if (config.destination === 'post_group' && !this.dragController) { |
| | | this.initGroupFeatures(); |
| | | } |
| | | if (config.type !== 'single') { |
| | | this.initSortable(field); |
| | | } |
| | | |
| | | return fieldId; |
| | | } |
| | |
| | | }); |
| | | } |
| | | |
| | | initSortable(field) { |
| | | if (!window.Sortable) return; |
| | | |
| | | // Main grid |
| | | const mainGrid = field.element.querySelector('.item-grid:not(.group)'); |
| | | if (mainGrid) { |
| | | this.sortableInstances.set(`${field.id}-main`, |
| | | new Sortable(mainGrid, { |
| | | ...this.sortableConfig, |
| | | group: { |
| | | name: field.id, |
| | | pull: true, |
| | | put: true |
| | | } |
| | | }) |
| | | ); |
| | | } |
| | | |
| | | // Group grids (for selection mode with grouping) |
| | | const groupGrids = field.element.querySelectorAll('.item-grid.group'); |
| | | groupGrids.forEach((grid, index) => { |
| | | this.sortableInstances.set(`${field.id}-group-${index}`, |
| | | new Sortable(grid, { |
| | | ...this.sortableConfig, |
| | | group: { |
| | | name: field.id, |
| | | pull: true, |
| | | put: true |
| | | } |
| | | }) |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | // Add reorder handler |
| | | handleReorder(evt) { |
| | | const grid = evt.to; |
| | | const fieldWrapper = grid.closest('.field, .upload'); |
| | | if (!fieldWrapper) return; |
| | | |
| | | const form = fieldWrapper.closest('form'); |
| | | if (!form) return; |
| | | |
| | | // Get form config if available |
| | | const formId = form.dataset.formId; |
| | | if (formId && window.jvbForms) { |
| | | const formConfig = window.jvbForms.forms?.get(formId); |
| | | if (formConfig?.options.autosave) { |
| | | // Trigger autosave after reordering |
| | | window.jvbForms.scheduleSave(formConfig, 1000); |
| | | } |
| | | } |
| | | |
| | | // Announce for accessibility |
| | | if (window.jvbA11y) { |
| | | window.jvbA11y.announce('Item reordered'); |
| | | } |
| | | |
| | | // Trigger custom event |
| | | fieldWrapper.dispatchEvent(new CustomEvent('jvb-items-reordered', { |
| | | detail: { |
| | | from: evt.from, |
| | | to: evt.to, |
| | | oldIndex: evt.oldIndex, |
| | | newIndex: evt.newIndex |
| | | }, |
| | | bubbles: true |
| | | })); |
| | | } |
| | | |
| | | /******************************************************************************* |
| | | * EXTERNAL FILE DROP HANDLERS (for new uploads from desktop) |
| | | *******************************************************************************/ |
| | |
| | | this.selectionHandlers.clear(); |
| | | |
| | | this.cleanupAllPreviewUrls(); |
| | | this.sortableInstances.forEach(instance => { |
| | | if (instance?.destroy) { |
| | | instance.destroy(); |
| | | } |
| | | }); |
| | | this.sortableInstances.clear(); |
| | | |
| | | // Clear data |
| | | this.fields.clear(); |
| | |
| | | this.subscribers.clear(); |
| | | } |
| | | |
| | | destroySortable(fieldName) { |
| | | // Destroy all sortable instances for this field |
| | | const instances = Array.from(this.sortableInstances.keys()) |
| | | .filter(key => key.startsWith(fieldName)); |
| | | |
| | | instances.forEach(key => { |
| | | const instance = this.sortableInstances.get(key); |
| | | if (instance?.destroy) { |
| | | instance.destroy(); |
| | | } |
| | | this.sortableInstances.delete(key); |
| | | }); |
| | | } |
| | | |
| | | cleanupRestore() { |
| | | this.restoreModal.handleClose(); |
| | | this.restoreSelection.destroy(); |
| | |
| | | console.error('No store connected to renderer'); |
| | | return; |
| | | } |
| | | |
| | | console.log(items); |
| | | |
| | | // Handle empty state |
| | | if (items.length === 0) { |
| | |
| | | item.id, |
| | | item.id |
| | | ]; |
| | | if (this.store.config.storeName === 'progress') { |
| | | [ |
| | | img.src, |
| | | img.alt, |
| | | ] = [ |
| | | item.images[item.fields['timeline'][0].post_thumbnail]?.medium??'', |
| | | item.images[item.fields['timeline'][0].post_thumbnail]?.alt??'', |
| | | ]; |
| | | } else { |
| | | // if (this.store.config.storeName === 'progress') { |
| | | // [ |
| | | // img.src, |
| | | // img.alt, |
| | | // ] = [ |
| | | // item.images[item.fields['timeline'][0].post_thumbnail]?.medium??'', |
| | | // item.images[item.fields['timeline'][0].post_thumbnail]?.alt??'', |
| | | // ]; |
| | | // } else { |
| | | [ |
| | | img.src, |
| | | img.alt, |
| | |
| | | item.images[item.fields.post_thumbnail]?.medium??'', |
| | | item.images[item.fields.post_thumbnail]?.alt??'', |
| | | ]; |
| | | } |
| | | // } |
| | | return card; |
| | | } |
| | | |
| | |
| | | toolbar.innerHTML = ` |
| | | <span class="ql-formats"> |
| | | <button type="button" class="ql-p"> |
| | | ${jvbSettings.icons.paragraph} |
| | | <i class="icon icon-paragraph"></i> |
| | | </button> |
| | | <button type="button" class="ql-h1"> |
| | | ${jvbSettings.icons.h1} |
| | | <i class="icon icon-text-h-one"></i> |
| | | </button> |
| | | <button type="button" class="ql-h2"> |
| | | ${jvbSettings.icons.h2} |
| | | <i class="icon icon-text-h-two"></i> |
| | | </button> |
| | | <button type="button" class="ql-h3"> |
| | | ${jvbSettings.icons.h3} |
| | | <i class="icon icon-text-h-three"></i> |
| | | </button> |
| | | </span> |
| | | <span class="ql-formats"> |
| | | <button type="button" class="ql-jvb_bold"> |
| | | ${jvbSettings.icons['bold']} |
| | | <i class="icon icon-text-b-fi"></i> |
| | | </button> |
| | | <button type="button" class="ql-jvb_italic"> |
| | | ${jvbSettings.icons['italic']} |
| | | <i class="icon icon-text-italic"></i> |
| | | </button> |
| | | <button type="button" class="ql-jvb_underline"> |
| | | ${jvbSettings.icons['underline']} |
| | | <i class="icon icon-text-underline"></i> |
| | | </button> |
| | | <button type="button" class="ql-jvb_strike"> |
| | | ${jvbSettings.icons['strike']} |
| | | <i class="icon icon-text-strikethrough"></i> |
| | | </button> |
| | | </span> |
| | | <span class="ql-formats"> |
| | | <button type="button" class="ql-jvb_list" value="bullet"> |
| | | ${jvbSettings.icons['list-bullets']} |
| | | <i class="icon icon-list-dashes"></i> |
| | | </button> |
| | | <button type="button" class="ql-jvb_list" value="ordered"> |
| | | ${jvbSettings.icons['list-numbers']} |
| | | <i class="icon icon-list-numbers"></i> |
| | | </button> |
| | | </span> |
| | | <span class="ql-formats"> |
| | | <button type="button" class="ql-jvb_align" value="left"> |
| | | ${jvbSettings.icons['align-left']} |
| | | <i class="icon icon-text-align-left"></i> |
| | | </button> |
| | | <button type="button" class="ql-jvb_align" value="center"> |
| | | ${jvbSettings.icons['align-center']} |
| | | <i class="icon icon-text-align-center"></i> |
| | | </button> |
| | | <button type="button" class="ql-jvb_align" value="right"> |
| | | ${jvbSettings.icons['align-right']} |
| | | <i class="icon icon-text-align-right"></i> |
| | | </button> |
| | | </span> |
| | | <span class="ql-formats"> |
| | | <button type="button" class="ql-jvb_link"> |
| | | ${jvbSettings.icons.link} |
| | | <i class="icon icon-link"></i> |
| | | </button> |
| | | ${image} |
| | | </span> |
| | |
| | | if (!this.content) { |
| | | return; |
| | | } |
| | | this.isTimeline = false; |
| | | |
| | | this.initElements(); |
| | | this.updateBulkOptions(); |
| | |
| | | this.init(); |
| | | } |
| | | handleFormChange(event, data) { |
| | | if (this.isTimeline) { |
| | | data.changes.content = this.content; |
| | | let changes = {}; |
| | | let postID = data.config.id.replace('edit-', ''); |
| | | changes[postID] = data.changes; |
| | | this.savePosts(changes, `Saving ${data.fullData.post_title}'s Changes`); |
| | | return; |
| | | } |
| | | data.changes.content = this.content; |
| | | let changes = {}; |
| | | let title = ''; |
| | |
| | | uploader: 'details.uploader' |
| | | }; |
| | | this.ui = window.uiFromSelectors(this.elements); |
| | | this.isTimeline = Object.hasOwn(this.ui.forms.edit.dataset, 'timeline'); |
| | | } |
| | | init() { |
| | | this.settings.addSetting(this.ui.uploader, 'open'); |
| | |
| | | actions.className = 'list-actions'; |
| | | actions.innerHTML = ` |
| | | <button type="button" class="share-list" title="Share List"> |
| | | <?= $icon->getIcon('share')?> |
| | | <i class="icon icon-share-fat"></i> |
| | | <span>Share</span> |
| | | </button> |
| | | <button type="button" class="duplicate-list" title="Duplicate List"> |
| | | <?= $icon->getIcon('copy')?> |
| | | <i class="icon icon-copy"></i> |
| | | <span>Duplicate</span> |
| | | </button> |
| | | <button type="button" class="delete-list" title="Delete List"> |
| | | <?= $icon->getIcon('delete')?> |
| | | <i class="icon icon-trash"></i> |
| | | <span>Delete</span> |
| | | </button> |
| | | `; |
| | |
| | | [favourite.dataset.id, favourite.dataset.artist] = [item.id, item.artist.id]; |
| | | if(window.userFavourites.news?.includes(parseInt(item.id))){ |
| | | removeChildren(favourite); |
| | | favourite.append(getIcon('star-filled')); |
| | | favourite.append(getIcon('star-fi')); |
| | | }else{ |
| | | removeChildren(favourite); |
| | | favourite.append(getIcon('star')); |
| | |
| | | if (window.targetCheck(e, '.submit-term')) { |
| | | this.handleTermCreation(e); |
| | | } |
| | | |
| | | // Handle autocomplete create button |
| | | if (window.targetCheck(e, '.create-term')) { |
| | | this.handleAutocompleteCreate(e); |
| | | } |
| | | } |
| | | |
| | | async handleTermCreation(e) { |
| | |
| | | } |
| | | } |
| | | |
| | | async handleAutocompleteCreate(e) { |
| | | const button = e.target; |
| | | const fieldId = this.selector.getFieldId(button); |
| | | const field = this.selector.fields.get(fieldId); |
| | | |
| | | if (!field) return; |
| | | |
| | | // Get the current input value (not the debounced query) |
| | | const input = field.container.querySelector('input[data-autocomplete]'); |
| | | const termName = input?.value.trim() || button.dataset.query; |
| | | |
| | | if (!termName) return; |
| | | |
| | | try { |
| | | button.disabled = true; |
| | | const originalText = button.innerHTML; |
| | | button.textContent = 'Creating...'; |
| | | |
| | | const response = await this.createTerm(termName, 0); |
| | | |
| | | if (response.success && response.term) { |
| | | const term = response.term; |
| | | |
| | | // Add term to field |
| | | field.selectedTerms.add(parseInt(term.id)); |
| | | this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name); |
| | | |
| | | // Update input |
| | | field.input.value = Array.from(field.selectedTerms).join(','); |
| | | field.input.dispatchEvent(new Event('change', { bubbles: true })); |
| | | |
| | | // Clear and hide dropdown |
| | | field.autocompleteDropdown.hidden = true; |
| | | if (input) input.value = ''; |
| | | |
| | | // Refresh the store to include the new term |
| | | await this.selector.store.setFilter('taxonomy', field.taxonomy); |
| | | } else { |
| | | button.innerHTML = originalText; |
| | | } |
| | | } catch (error) { |
| | | console.error('Error creating term:', error); |
| | | button.textContent = 'Failed - Try again'; |
| | | setTimeout(() => { |
| | | button.innerHTML = `<strong>Create:</strong> "${termName}"`; |
| | | button.disabled = false; |
| | | }, 2000); |
| | | } |
| | | } |
| | | |
| | | initTermCreation() { |
| | | if (!this.form) { |
| | | return; |
| | |
| | | return button; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Handle term creation from autocomplete |
| | | */ |
| | |
| | | * @param icon |
| | | * @returns {Node | ActiveX.IXMLDOMNode} |
| | | */ |
| | | window.icon = document.querySelector('.icon').cloneNode(true); |
| | | window.icon.className = 'icon'; |
| | | window.getIcon = function getIcon(icon){ |
| | | if (typeof icon === 'undefined') { |
| | | return ''; |
| | | } |
| | | console.log('Getting icon: '+icon); |
| | | |
| | | let theIcon = window.icon.cloneNode(true); |
| | | theIcon.classList.add('icon-'+icon); |
| | | return theIcon; |
| | | |
| | | |
| | | if(!window.jvbIcons){ |
| | |
| | | window.jvbTaxCreator=class{constructor(e){this.selector=e,this.taxonomy=e.currentConfig?.taxonomy,this.taxonomy?(this.createNew=e.modal.querySelector(".create-new-term"),this.toggle=e.modal.querySelector(".new-term-toggle"),this.form=this.createNew.querySelector(".create-new-term-section"),this.initListeners(),this.initTermCreation()):console.error("TaxonomyCreator: No active field or taxonomy found")}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){window.targetCheck(e,".create-new-term summary")&&(this.createNew.open&&this.createNew.querySelector('input[name="term_name"]').focus(),this.resetParentOptions()),window.targetCheck(e,".submit-term")&&this.handleTermCreation(e)}async handleTermCreation(e){const t=this.form.querySelector('input[name="term_name"]').value.trim(),r=parseInt(this.form.querySelector("input#select_parent")?.value)||0;if(t)try{this.form.querySelector("button").disabled=!0;const e=await this.createTerm(t,r);if(e.success&&e.term){let t=e.term;this.createNew.open=!1,await this.selector.store.invalidate({taxonomy:this.taxonomy}),this.selector.addSelectedTermToModal(t.id,t.name,t.path||t.name),(this.selector.store.filters.parent||0)===r&&await this.selector.store.setFilters({taxonomy:this.taxonomy,parent:r,page:1,search:""}),this.form.querySelector('input[name="term_name"]').value="";const n=this.createNew.querySelector(".term-suggestions");n&&(n.hidden=!0)}}catch(e){console.error("Error creating term:",e),this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleTermCreation"})||console.error("Failed to create term")}finally{this.form.querySelector("button").disabled=!1}}initTermCreation(){this.form&&this.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}resetParentOptions(){let e=this.createNew.querySelector("#select_parent");if(!e)return;let t=e.querySelector("option");if(!t)return;window.removeChildren(e),e.append(t.cloneNode(!0));const r=this.selector.store.filters.parent||0;if(0!==r){const n=this.selector.store.data.get(r);if(n){let r=t.cloneNode(!0);r.value=n.id,r.textContent=n.name,e.append(r)}}const n=[];this.selector.store.data.forEach((e=>{e.taxonomy===this.taxonomy&&e.parent===r&&n.push(e)})),n.sort(((e,t)=>e.name.localeCompare(t.name))),n.forEach((r=>{let n=t.cloneNode(!0);n.id=`select-parent-${r.id}`,n.value=r.id,n.textContent=" — "+r.name,e.append(n)}))}async createTerm(e,t=0){let r=this.createNew.querySelector(".loading-message.create-term"),n=r?.querySelector("span");try{r&&(r.hidden=!1),n&&window.typeText?window.typeText(n,"Checking term..."):n&&(n.textContent="Checking term...");const o=await this.searchExistingTerms(e),a=o.filter((t=>t.name.toLowerCase()===e.toLowerCase()));if(a.length>0)return this.showTermSuggestions(a,!0),{success:!1,reason:"exists"};if(o.length>0)return this.showTermSuggestions(o,!1),{success:!1,reason:"similar"};n&&(n.textContent="Creating term...");const s=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({taxonomy:this.taxonomy,name:e,parent:t})});if(!s.ok)throw new Error(`Server error: ${s.status}`);return await s.json()}catch(e){throw console.error("Error creating term:",e),e}finally{this.form.querySelector("button").disabled=!1,r&&(r.hidden=!0)}}async searchExistingTerms(e){return new Promise((t=>{const r=(e,n)=>{"data-loaded"===e&&(this.selector.store.unsubscribe(r),t(n.data?.items||[]))};this.selector.store.subscribe(r),this.selector.store.setFilters({taxonomy:this.taxonomy,search:e,page:1,parent:0})}))}showTermSuggestions(e,t=!1){const r=this.createNew.querySelector(".term-suggestions")||this.createSuggestionContainer();window.removeChildren(r);const n=document.createElement("h4");n.textContent=t?"This term already exists:":"Similar terms already exist:",r.appendChild(n);const o=document.createElement("ul");o.className="term-suggestion-list",e.forEach((e=>{const t=document.createElement("li"),n=document.createElement("button");n.type="button",n.className="use-existing-term",n.setAttribute("data-id",e.id),n.textContent=e.path||e.name,n.addEventListener("click",(()=>{this.selector.addSelectedTermToModal(e.id,e.name,e.path||e.name),this.createNew.open=!1,r.hidden=!0,this.form.querySelector('input[name="term_name"]').value=""})),t.appendChild(n),o.appendChild(t)})),r.appendChild(o),r.hidden=!1}createSuggestionContainer(){const e=document.createElement("div");return e.className="term-suggestions",e.hidden=!0,this.createNew.querySelector("form").after(e),e}createAutocompleteOption(e,t){const r=document.createElement("button");return r.type="button",r.className="autocomplete-item create-term",r.innerHTML=`<span>Create "${e}"</span>`,r.dataset.query=e,r.dataset.fieldId=t.id,r.addEventListener("click",(async()=>{await this.handleAutocompleteCreate(r,e,t)})),r}async handleAutocompleteCreate(e,t,r){if(!r)return;const n=e.innerHTML;try{e.disabled=!0,e.innerHTML="<span>Creating...</span>";const n=0,o=await this.createTerm(t,n);if(o.success&&o.term){const e=o.term;r.selectedTerms.add(parseInt(e.id)),this.selector.addTermToDisplay(r.id,e.id,e.name,e.path||e.name),r.input.value=Array.from(r.selectedTerms).join(","),r.input.dispatchEvent(new Event("change",{bubbles:!0})),await this.selector.store.invalidate({taxonomy:r.taxonomy}),r.autocompleteDropdown.hidden=!0;const t=r.container.querySelector("input[data-autocomplete]");t&&(t.value="")}}catch(t){console.error("Error creating term:",t),e.innerHTML=n,e.disabled=!1,this.selector.error?.log(t,{component:"TaxonomyCreator",action:"handleAutocompleteCreate"})}}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler);const e=this.createNew?.querySelector(".loading-message.create-term");e&&(e.hidden=!0);const t=this.createNew?.querySelector(".term-suggestions");t&&(t.hidden=!0)}}; |
| | | window.jvbTaxCreator=class{constructor(e){this.selector=e,this.taxonomy=e.currentConfig?.taxonomy,this.taxonomy?(this.createNew=e.modal.querySelector(".create-new-term"),this.toggle=e.modal.querySelector(".new-term-toggle"),this.form=this.createNew.querySelector(".create-new-term-section"),this.initListeners(),this.initTermCreation()):console.error("TaxonomyCreator: No active field or taxonomy found")}initListeners(){this.clickHandler=this.handleClick.bind(this),document.addEventListener("click",this.clickHandler)}handleClick(e){window.targetCheck(e,".create-new-term summary")&&(this.createNew.open&&this.createNew.querySelector('input[name="term_name"]').focus(),this.resetParentOptions()),window.targetCheck(e,".submit-term")&&this.handleTermCreation(e),window.targetCheck(e,".create-term")&&this.handleAutocompleteCreate(e)}async handleTermCreation(e){const t=this.form.querySelector('input[name="term_name"]').value.trim(),r=parseInt(this.form.querySelector("input#select_parent")?.value)||0;if(t)try{this.form.querySelector("button").disabled=!0;const e=await this.createTerm(t,r);if(e.success&&e.term){let t=e.term;this.createNew.open=!1,await this.selector.store.invalidate({taxonomy:this.taxonomy}),this.selector.addSelectedTermToModal(t.id,t.name,t.path||t.name),(this.selector.store.filters.parent||0)===r&&await this.selector.store.setFilters({taxonomy:this.taxonomy,parent:r,page:1,search:""}),this.form.querySelector('input[name="term_name"]').value="";const n=this.createNew.querySelector(".term-suggestions");n&&(n.hidden=!0)}}catch(e){console.error("Error creating term:",e),this.selector.error?.log(e,{component:"TaxonomyCreator",action:"handleTermCreation"})||console.error("Failed to create term")}finally{this.form.querySelector("button").disabled=!1}}async handleAutocompleteCreate(e){const t=e.target,r=this.selector.getFieldId(t),n=this.selector.fields.get(r);if(!n)return;const o=n.container.querySelector("input[data-autocomplete]"),a=o?.value.trim()||t.dataset.query;if(a)try{t.disabled=!0;const e=t.innerHTML;t.textContent="Creating...";const r=await this.createTerm(a,0);if(r.success&&r.term){const e=r.term;n.selectedTerms.add(parseInt(e.id)),this.selector.addTermToDisplay(n.id,e.id,e.name,e.path||e.name),n.input.value=Array.from(n.selectedTerms).join(","),n.input.dispatchEvent(new Event("change",{bubbles:!0})),n.autocompleteDropdown.hidden=!0,o&&(o.value=""),await this.selector.store.setFilter("taxonomy",n.taxonomy)}else t.innerHTML=e}catch(e){console.error("Error creating term:",e),t.textContent="Failed - Try again",setTimeout((()=>{t.innerHTML=`<strong>Create:</strong> "${a}"`,t.disabled=!1}),2e3)}}initTermCreation(){this.form&&this.form.addEventListener("change",(e=>{e.preventDefault(),e.stopPropagation()}))}resetParentOptions(){let e=this.createNew.querySelector("#select_parent");if(!e)return;let t=e.querySelector("option");if(!t)return;window.removeChildren(e),e.append(t.cloneNode(!0));const r=this.selector.store.filters.parent||0;if(0!==r){const n=this.selector.store.data.get(r);if(n){let r=t.cloneNode(!0);r.value=n.id,r.textContent=n.name,e.append(r)}}const n=[];this.selector.store.data.forEach((e=>{e.taxonomy===this.taxonomy&&e.parent===r&&n.push(e)})),n.sort(((e,t)=>e.name.localeCompare(t.name))),n.forEach((r=>{let n=t.cloneNode(!0);n.id=`select-parent-${r.id}`,n.value=r.id,n.textContent=" — "+r.name,e.append(n)}))}async createTerm(e,t=0){let r=this.createNew.querySelector(".loading-message.create-term"),n=r?.querySelector("span");try{r&&(r.hidden=!1),n&&window.typeText?window.typeText(n,"Checking term..."):n&&(n.textContent="Checking term...");const o=await this.searchExistingTerms(e),a=o.filter((t=>t.name.toLowerCase()===e.toLowerCase()));if(a.length>0)return this.showTermSuggestions(a,!0),{success:!1,reason:"exists"};if(o.length>0)return this.showTermSuggestions(o,!1),{success:!1,reason:"similar"};n&&(n.textContent="Creating term...");const s=await fetch(`${jvbSettings.api}terms`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify({taxonomy:this.taxonomy,name:e,parent:t})});if(!s.ok)throw new Error(`Server error: ${s.status}`);return await s.json()}catch(e){throw console.error("Error creating term:",e),e}finally{this.form.querySelector("button").disabled=!1,r&&(r.hidden=!0)}}async searchExistingTerms(e){return new Promise((t=>{const r=(e,n)=>{"data-loaded"===e&&(this.selector.store.unsubscribe(r),t(n.data?.items||[]))};this.selector.store.subscribe(r),this.selector.store.setFilters({taxonomy:this.taxonomy,search:e,page:1,parent:0})}))}showTermSuggestions(e,t=!1){const r=this.createNew.querySelector(".term-suggestions")||this.createSuggestionContainer();window.removeChildren(r);const n=document.createElement("h4");n.textContent=t?"This term already exists:":"Similar terms already exist:",r.appendChild(n);const o=document.createElement("ul");o.className="term-suggestion-list",e.forEach((e=>{const t=document.createElement("li"),n=document.createElement("button");n.type="button",n.className="use-existing-term",n.setAttribute("data-id",e.id),n.textContent=e.path||e.name,n.addEventListener("click",(()=>{this.selector.addSelectedTermToModal(e.id,e.name,e.path||e.name),this.createNew.open=!1,r.hidden=!0,this.form.querySelector('input[name="term_name"]').value=""})),t.appendChild(n),o.appendChild(t)})),r.appendChild(o),r.hidden=!1}createSuggestionContainer(){const e=document.createElement("div");return e.className="term-suggestions",e.hidden=!0,this.createNew.querySelector("form").after(e),e}createAutocompleteOption(e,t){const r=document.createElement("button");return r.type="button",r.className="autocomplete-item create-term",r.innerHTML=`<span>Create "${e}"</span>`,r.dataset.query=e,r.dataset.fieldId=t.id,r.addEventListener("click",(async()=>{await this.handleAutocompleteCreate(r,e,t)})),r}async handleAutocompleteCreate(e,t,r){if(!r)return;const n=e.innerHTML;try{e.disabled=!0,e.innerHTML="<span>Creating...</span>";const n=0,o=await this.createTerm(t,n);if(o.success&&o.term){const e=o.term;r.selectedTerms.add(parseInt(e.id)),this.selector.addTermToDisplay(r.id,e.id,e.name,e.path||e.name),r.input.value=Array.from(r.selectedTerms).join(","),r.input.dispatchEvent(new Event("change",{bubbles:!0})),await this.selector.store.invalidate({taxonomy:r.taxonomy}),r.autocompleteDropdown.hidden=!0;const t=r.container.querySelector("input[data-autocomplete]");t&&(t.value="")}}catch(t){console.error("Error creating term:",t),e.innerHTML=n,e.disabled=!1,this.selector.error?.log(t,{component:"TaxonomyCreator",action:"handleAutocompleteCreate"})}}destroy(){this.clickHandler&&document.removeEventListener("click",this.clickHandler);const e=this.createNew?.querySelector(".loading-message.create-term");e&&(e.hidden=!0);const t=this.createNew?.querySelector(".term-suggestions");t&&(t.hidden=!0)}}; |
| | |
| | | (()=>{class e{constructor(e){this.queue=window.jvbQueue,console.log(this.queue),this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.content&&(this.initElements(),this.updateBulkOptions(),this.store=new window.jvbStore({name:this.content,storeName:this.content,endpoint:"content",headers:{action_nonce:jvbSettings.dash},indexes:[{name:"status",keyPath:"post_status"},{name:"modified",keyPath:"modified"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all"},TTL:36e5,cacheDOM:!0}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.formController=new window.jvbForm,this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.source?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&"form"===t.source&&this.handleQueueFailure(e,t)})),this.initialized=!1,this.init())}handleFormChange(e,t){t.changes.content=this.content;let s={},i="",o=[];switch(!0){case t.config.element===this.ui.forms.edit:let l=t.config.id.replace("edit-","");console.log(l),s[l]=t.changes,i=`Saving ${t.fullData.post_title} Changes`,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(l);break;case t.config.element===this.ui.forms.bulkEdit:let n=t.config.element.querySelectorAll(".selected input:checked");n.forEach((e=>{s[e.value]=t.changes,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(e.value)})),i=`Updating ${n.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(s[t.config.data["form-id"]]=t.fullData,i=`Saving ${t.fullData.post_title} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}window.isEmptyObject(s)||this.savePosts(s,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;let s={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}handleQueueSuccess(e,t){console.log("Handling queue success..."),console.log("Event",e),console.log("Data",t)}handleQueueFailure(e,t){console.log("Handling queue failure..."),console.log("Event",e),console.log("Data",t)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements)}init(){this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{console.log(e),console.log("Is Open: ",this.ui.uploader.open),console.log(this.ui.uploader.open?"on":"off"),this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")})),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId),console.log("Data on modal close: ",s)}));this.setupEventDelegation(),this.setupFilters(),this.queue.subscribe(((e,t)=>{e})),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemID="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(console.log(t,e),e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);console.log("selected",i),i.forEach((i=>{s[i]={content:this.content},s[i][t]=e})),console.log("Taxonomy changes: ",s);let o=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,o)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;console.log(`Setting status: ${e}`);let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if(console.log(this.status),"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),window.isEmptyObject(s)||this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,s=t.dataset.filter;if("taxonomies"===s){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,s.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions.value=""}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){console.log(t);let s=this.store.get(t);console.log(s);const i=window.getTemplate("bulkItem");if(!i)return;const o=i.querySelector("input[type=checkbox]"),l=i.querySelector("img");o&&(o.id=`bulk_${s.id}`,o.value=s.id,o.checked=!0),l&&s.thumbnail&&(l.src=s.thumbnail,l.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit),console.log("Bulk Edit form registered")}populateEditForm(e){console.log(e);let t=this.store.get(parseInt(e));if(console.log(t),t){this.ui.modals.edit.dataset.itemID=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,console.log(s.dataset.formId),new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit),console.log("Edit form registered")}}setupFilters(){document.querySelectorAll("[data-filter]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("change",(e=>{this.filterTimeout&&clearTimeout(this.filterTimeout),this.filterTimeout=setTimeout((()=>{this.filterHandler(e)}),300)}))}));const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)})),this.store.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})(); |
| | | (()=>{class e{constructor(e){this.queue=window.jvbQueue,console.log(this.queue),this.config=e,this.content=e.content||!1,this.settings=window.jvbUserSettings,this.content&&(this.isTimeline=!1,this.initElements(),this.updateBulkOptions(),this.store=new window.jvbStore({name:this.content,storeName:this.content,endpoint:"content",headers:{action_nonce:jvbSettings.dash},indexes:[{name:"status",keyPath:"post_status"},{name:"modified",keyPath:"modified"}],filters:{content:this.content,user:jvbSettings.currentUser,page:1,status:"all"},TTL:36e5,cacheDOM:!0}),this.status="all",this.filterTimeout=null,this.viewController=new window.jvbViews(this.ui.container,this.store),this.formController=new window.jvbForm,this.formController.subscribe(((e,t)=>{switch(e){case"form-submit":case"form-autosave":this.handleFormChange(e,t)}})),window.jvbQueue&&window.jvbQueue.subscribe(((e,t)=>{"operation-completed"===e&&"form"===t.source?this.handleQueueSuccess(e,t):"operation-failed-permanent"===e&&"form"===t.source&&this.handleQueueFailure(e,t)})),this.initialized=!1,this.init())}handleFormChange(e,t){if(this.isTimeline){t.changes.content=this.content;let e={};return e[t.config.id.replace("edit-","")]=t.changes,void this.savePosts(e,`Saving ${t.fullData.post_title}'s Changes`)}t.changes.content=this.content;let s={},i="",o=[];switch(!0){case t.config.element===this.ui.forms.edit:let l=t.config.id.replace("edit-","");console.log(l),s[l]=t.changes,i=`Saving ${t.fullData.post_title} Changes`,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(l);break;case t.config.element===this.ui.forms.bulkEdit:let n=t.config.element.querySelectorAll(".selected input:checked");n.forEach((e=>{s[e.value]=t.changes,t.changes.post_status&&this.shouldRemoveItem(t.changes.post_status)&&o.push(e.value)})),i=`Updating ${n.length} ${this.config.plural??"posts"} Changes`;break;case t.config.element===this.ui.forms.create:"form-submit"===e&&(s[t.config.data["form-id"]]=t.fullData,i=`Saving ${t.fullData.post_title} Changes`)}if(o.length>0){let e=0;o.forEach((t=>{setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50})),t.config.element===this.ui.forms.bulkEdit&&setTimeout((()=>{this.viewController.clearSelection()}),e+100)}window.isEmptyObject(s)||this.savePosts(s,i)}shouldRemoveItem(e){return"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status}savePosts(e,t){if(window.isEmptyObject(e))return;let s={endpoint:"content",headers:{action_nonce:jvbSettings.dash},data:{posts:e},popup:"Saving changes",title:t};this.queue.addToQueue(s)}handleQueueSuccess(e,t){console.log("Handling queue success..."),console.log("Event",e),console.log("Data",t)}handleQueueFailure(e,t){console.log("Handling queue failure..."),console.log("Event",e),console.log("Data",t)}initElements(){this.elements={modals:{create:"dialog.create",edit:"dialog.edit",bulkEdit:"dialog.bulkEdit"},container:".crud[data-content]",grid:".item-grid",bulkSelectActions:".bulk-action-select",forms:{create:"dialog.create form",edit:"dialog.edit form",bulkEdit:"dialog.bulkEdit form"},uploader:"details.uploader"},this.ui=window.uiFromSelectors(this.elements),this.isTimeline=Object.hasOwn(this.ui.forms.edit.dataset,"timeline")}init(){this.settings.addSetting(this.ui.uploader,"open"),this.ui.uploader.addEventListener("toggle",(e=>{console.log(e),console.log("Is Open: ",this.ui.uploader.open),console.log(this.ui.uploader.open?"on":"off"),this.settings.saveSetting("open",this.ui.uploader.open?"on":"off")})),this.filterHandler=this.handleFilterChange.bind(this),this.changeHandler=this.handleChange.bind(this),this.modals={};for(let[e,t]of Object.entries(this.ui.modals))this.modals[e]=new window.jvbModal(t),this.modals[e].subscribe(((t,s)=>{if("modal-close"===t)this.formController.cleanupForm(this.modals[e].modal.querySelector("form").dataset.formId),console.log("Data on modal close: ",s)}));this.setupEventDelegation(),this.setupFilters(),this.queue.subscribe(((e,t)=>{e})),this.initialized=!0}setupEventDelegation(){document.addEventListener("change",this.changeHandler),document.addEventListener("click",(e=>{const t=e.target.closest("[data-action]");if(t){e.preventDefault();const s=t.dataset.action,i=t.dataset.id;switch(s){case"edit":this.populateEditForm(i),this.modals.edit.handleOpen();break;case"delete":if(confirm("Delete this item?")){let e={};e[t.dataset.id]={post_status:"delete",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`),this.store.delete(i)}break;case"trash":let e={};e[t.dataset.id]={post_status:"trash",content:this.content},window.fade(t.closest(".item"),!1),this.savePosts(e,`Sending ${this.singular} to trash...`);break;case"create":this.modals.create.dataset.itemID="new",this.modals.create.dataset.content=this.content,this.modals.create.handleOpen();break;case"bulk-edit":Array.from(this.viewController.selectedItems).length>0&&this.modals.bulkEdit.handleOpen();break;case"bulk-delete":const s=Array.from(this.viewController.selectedItems);s.length>0&&confirm(`Delete ${s.length} items?`)&&(s.forEach((e=>this.store.delete(e))),this.viewController.clearSelection());break;case"sync":break;case"refresh":this.store.fetch()}}e.target.closest(".create-item")&&(this.formController.registerForm(this.ui.forms.create),this.modals.create.handleOpen()),e.target.closest(".cancel-bulk")&&this.viewController.selectAll(!1)})),document.addEventListener("keydown",(e=>{(e.ctrlKey||e.metaKey)&&"a"===e.key&&this.ui.container&&this.ui.container.contains(document.activeElement)&&(e.preventDefault(),this.viewController.selectAll()),"Escape"===e.key&&this.viewController?.selectedItems.size>0&&0===window.jvbModal.getAllModals().length&&this.viewController.clearSelection()}))}handleChange(e){if(e.target.classList.contains("bulk-action-select")){if(e.target.value.startsWith("tax-")){const t=e.target.value.replace("tax-","");return this.openTaxonomyModal(t),void(e.target.value="")}switch(e.target.value){case"edit":this.populateBulkEdit(),this.modals.bulkEdit.handleOpen();break;case"publish":this.setBulkStatus("publish");break;case"draft":case"restore":this.setBulkStatus("draft");break;case"trash":this.setBulkStatus("trash");break;case"delete":this.setBulkStatus("delete")}}}openTaxonomyModal(e){window.jvbSelector?window.jvbSelector.openForFilter(e,((e,t)=>this.handleBulkTaxonomy(e,t))):console.error("TaxonomySelector not initialized")}handleBulkTaxonomy(e,t){if(console.log(t,e),e.length>0){e=e.join(",");let s={},i=Array.from(this.viewController.selectedItems);console.log("selected",i),i.forEach((i=>{s[i]={content:this.content},s[i][t]=e})),console.log("Taxonomy changes: ",s);let o=`Adding ${i.length} ${this.config.plural??"posts"} to ${e.length} ${jvbSettings.labels[t].plural}`;this.viewController.clearSelection(),this.savePosts(s,o)}}setBulkStatus(e){if(!["publish","draft","trash","delete"].includes(e))return;console.log(`Setting status: ${e}`);let t,s={};for(let t of this.viewController.selectedItems)s[t]={post_status:e,content:this.content};if("delete"===e)t="Deleting";else t=window.uppercaseFirst(e)+"ing";if(console.log(this.status),"all"===this.status&&!["publish","draft"].includes(e)||e!==this.status){let e=0;for(let t of this.viewController.selectedItems)setTimeout((()=>{const e=document.querySelector(`.item[data-id="${t}"]`);e&&window.fade(e,!1)}),e),e+=50}this.viewController.clearSelection(),window.isEmptyObject(s)||this.savePosts(s,`${t} ${this.viewController.selectedItems.size} ${this.plural}...`)}handleFilterChange(e){let t=e.target,s=t.dataset.filter;if("taxonomies"===s){let e=t.dataset.taxonomy;this.store.setFilter(`tax_${e}`,s.value)}else this[t.dataset.filter]=t.value,this.store.setFilter(t.dataset.filter,t.value),"status"===t.dataset.filter&&this.updateBulkOptions(t.value)}updateBulkOptions(e="all"){if("trash"===e){if(this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("trashOptions").querySelectorAll("option").forEach(((e,t)=>{0===t&&(e.checked=!0),this.ui.bulkSelectActions.append(e)}))}}else if(!this.ui.bulkSelectActions.querySelector('[value="edit"]')){window.removeChildren(this.ui.bulkSelectActions),window.getTemplate("notTrashOptions").querySelectorAll("option").forEach(((e,t)=>{this.ui.bulkSelectActions.append(e)}))}this.ui.bulkSelectActions.value=""}populateBulkEdit(){const e=this.modals.bulkEdit.modal.querySelector("form .selected");if(!e)return;window.removeChildren(e);for(let t of this.viewController.selectedItems){console.log(t);let s=this.store.get(t);console.log(s);const i=window.getTemplate("bulkItem");if(!i)return;const o=i.querySelector("input[type=checkbox]"),l=i.querySelector("img");o&&(o.id=`bulk_${s.id}`,o.value=s.id,o.checked=!0),l&&s.thumbnail&&(l.src=s.thumbnail,l.alt=s.alt||""),e.append(i)}let t=this.modals.bulkEdit.modal;[t.querySelector("h2 span").textContent]=[this.viewController.selectedItems.size],this.formController.registerForm(this.ui.forms.bulkEdit),console.log("Bulk Edit form registered")}populateEditForm(e){console.log(e);let t=this.store.get(parseInt(e));if(console.log(t),t){this.ui.modals.edit.dataset.itemID=e,this.ui.modals.edit.dataset.content=this.content;let s=this.ui.modals.edit.querySelector("form");[this.ui.modals.edit.querySelector("h2").textContent]=[`Editing ${t.fields.post_title}`],s.dataset.formId=`edit-${e}`,console.log(s.dataset.formId),new window.jvbPopulate(s,t.fields,t.images),this.formController.registerForm(this.ui.forms.edit),console.log("Edit form registered")}}setupFilters(){document.querySelectorAll("[data-filter]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("change",(e=>{this.filterTimeout&&clearTimeout(this.filterTimeout),this.filterTimeout=setTimeout((()=>{this.filterHandler(e)}),300)}))}));const e=document.querySelector('input[type="search"]');if(e){let t;e.addEventListener("input",(()=>{e.value.length>3?(clearTimeout(t),t=setTimeout((()=>{this.store.setFilter("search",e.value)}),300)):0===e.value.length&&this.store.removeFilter("search")}))}}destroy(){document.querySelectorAll("[data-filter]").forEach((e=>{e.removeEventListener("change",this.filterHandler)})),this.store.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(()=>{let t=document.querySelector("[data-content]");t&&(window.crudManager=new e({content:t.dataset.content}))}))})(); |
| | |
| | | window.favouritesManager=class{constructor(){this.queue=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.config={endpoints:{favourites:"favourites",lists:"favourites/lists",shares:"favourites/lists/shares"},selectors:{container:".favourites.container",itemsTab:'.tab-content[data-tab="items"]',listsTab:'.tab-content[data-tab="lists"]',grid:".item-grid",typeFilters:".type-filters",viewControls:".view-controls",bulkControls:".bulk-controls",selectAll:"#select-all",createListModal:".create-list-modal",addToListModal:".add-to-list-modal",shareListModal:".share-list-modal",noItems:".no-favourites",listContainer:".lists-container",listDetails:".list-details",loader:".favourites-loader"},defaultPage:1,defaultPerPage:24,defaultViewMode:"grid",refreshInterval:6e4,toastDuration:3e3},document.addEventListener("keydown",this.handleKeyDown.bind(this)),this.state={selectedItems:new Set,page:this.config.defaultPage,filter:{type:"all",order:"desc",orderBy:"date_added"},view:{mode:localStorage.getItem("favourites_view")||this.config.defaultViewMode,activeTab:"items"},pagination:{hasMore:!1,totalItems:0,totalPages:0},currentListId:null,loading:!1,initialized:!1},this.initDom(),this.initEvents(),this.loadInitialData(),this.state.initialized=!0}initDom(){this.container=document.querySelector(this.config.selectors.container),this.container?(this.grid=this.container.querySelector(this.config.selectors.grid),this.typeFilters=this.container.querySelector(this.config.selectors.typeFilters),this.viewControls=this.container.querySelector(this.config.selectors.viewControls),this.bulkControls=this.container.querySelector(this.config.selectors.bulkControls),this.listContainer=this.container.querySelector(this.config.selectors.listContainer),this.listDetails=this.container.querySelector(this.config.selectors.listDetails),this.loader=this.container.querySelector(this.config.selectors.loader),this.createListModal=document.querySelector(this.config.selectors.createListModal),this.addToListModal=document.querySelector(this.config.selectors.addToListModal),this.shareListModal=document.querySelector(this.config.selectors.shareListModal),this.grid&&this.state.view.mode&&this.grid.classList.add(`${this.state.view.mode}-view`)):console.warn("Favourites container not found")}initEvents(){if(this.typeFilters&&this.typeFilters.addEventListener("click",(t=>{const e=t.target.closest(".type-filter");e&&this.setFilterType(e.dataset.type)})),this.viewControls&&this.viewControls.addEventListener("click",(t=>{const e=t.target.closest(".view-toggle");e&&this.setView(e.dataset.view)})),this.container){const t=this.container.querySelector(this.config.selectors.selectAll);t&&t.addEventListener("change",(()=>{this.toggleSelectAll(t.checked)})),this.container.addEventListener("change",(t=>{t.target.matches(".item-select input[type=checkbox]")&&this.handleItemSelection(t.target)}));const e=this.container.querySelector(".bulk-action-select"),s=this.container.querySelector(".apply-bulk");e&&s&&s.addEventListener("click",(()=>{this.applyBulkAction(e.value)}));const i=this.container.querySelector(".cancel-bulk");i&&i.addEventListener("click",(()=>{this.clearSelection()}))}this.initModalEvents(),this.container.addEventListener("click",this.handleItemActions.bind(this)),this.grid&&this.setupInfiniteScroll()}initModalEvents(){if(this.createListModal){const t=this.createListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleCreateList(new FormData(t))}));const e=this.createListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.createListModal.close()}))}if(this.addToListModal){const t=this.addToListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleAddToList(new FormData(t))}));const e=this.addToListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.addToListModal.close()}))}if(this.shareListModal){const t=this.shareListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleShareList(new FormData(t))}));const e=this.shareListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.shareListModal.close()}));const s=this.shareListModal.querySelector(".add-email");s&&s.addEventListener("click",(()=>{const e=this.shareListModal.querySelector("#share-email");e&&e.value&&this.handleShareList(new FormData(t))}))}}setupInfiniteScroll(){let t=this.container.querySelector(".scroll-sentinel");t||(t=document.createElement("div"),t.className="scroll-sentinel",t.setAttribute("aria-hidden","true"),this.grid.parentNode.appendChild(t)),new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&this.state.pagination.hasMore&&!this.state.loading&&(this.state.page++,this.loadFavourites())}))}),{rootMargin:"200px"}).observe(t)}async loadInitialData(){this.loadingManager.show();try{await this.loadFavourites(),this.loadLists().catch((t=>{console.error("Error loading lists:",t)}))}catch(t){this.handleError(t,"loading initial data")}finally{this.loadingManager.hide()}}async loadFavourites(t=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const e=new URLSearchParams({page:this.state.page,per_page:this.config.defaultPerPage,type:"all"!==this.state.filter.type?this.state.filter.type:"",order:this.state.filter.order,orderby:this.state.filter.orderBy});t&&(this.state.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.favourites}?${e}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favouritesManager",forceRefresh:!0});return this.renderFavourites(s.favourites||[],this.state.page>1),s.counts&&this.updateTypeFilters(s.counts),s.pagination&&(this.state.pagination={hasMore:s.pagination.has_more,totalItems:s.pagination.total_items,totalPages:s.pagination.total_pages}),s}catch(t){throw this.handleError(t,"loading favourites"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderFavourites(t,e=!1){this.grid&&(0!==t.length||e?(this.hideEmptyState(),e||removeChildren(this.grid),t.forEach((t=>{const e=this.createItemElement(t);this.grid.appendChild(e),this.initItemFunctionality(e,t)})),window.jvbA11y&&window.jvbA11y.announce(`${e?"Added":"Loaded"} ${t.length} favourites`)):this.showEmptyState())}createItemElement(t){const e=document.createElement("div");e.className=`item ${t.type} favourited`,e.dataset.id=t.target_id,e.dataset.type=t.type;const s=sanitizeHtml(t.title||!1),i=sanitizeHtml(t.notes||"");let a="";return t.thumbnail&&(a=`\n <div class="item-thumbnail">\n <a href="${t.url}">${t.thumbnail}</a>\n </div>\n `),e.innerHTML=`\n <div class="item-select">\n <input type="checkbox"\n class="favourite-checkbox"\n id="select-${t.target_id}"\n value="${t.target_id}">\n <label for="select-${t.target_id}"><span class="screen-reader-text">Select this ${t.type}</span</label>\n </div>\n\n <button type="button" class="favourite-button favourited"\n onclick="toggleFavourite(this)"\n data-id="${t.target_id}"\n data-type="${t.type}"\n title="Remove from favourites">\n ${jvbSettings.icons["heart-filled"]}\n </button>\n\n ${a}\n\n <div class="item-info">\n ${s?`<h3><a href="${t.url}">${s}</a></h3>`:`<a href="${t.url}">View Item</a>`}\n\n ${t.author?`\n <div class="item-artist">\n <span>By ${t.author.name}</span>\n </div>`:""}\n\n ${t.taxonomies?.length?`\n <div class="taxonomy-lists">\n ${t.taxonomies.map((t=>`\n <div class="taxonomy-group">\n ${jvbSettings.icons[t.icon]}\n <ul>\n ${t.terms.slice(0,3).map((t=>`\n <li>\n <a href="${t.url}" ${t.umami_click}>\n ${t.title}\n </a>\n </li>\n `)).join("")}\n </ul>\n </div>\n `)).join("")}\n </div>\n `:""}\n\n <div class="notes-section">\n <button type="button" class="toggle-notes" aria-expanded="false">\n ${jvbSettings.icons.note||"Notes"}\n <span>Notes</span>\n </button>\n\n <div class="notes-content" hidden>\n <textarea class="notes-input"\n placeholder="Add notes about this item..."\n data-id="${t.target_id}"\n data-type="${t.type}">${i}</textarea>\n <button type="button" class="save-notes">Save Notes</button>\n </div>\n </div>\n </div>\n `,e}initItemFunctionality(t,e){const s=t.querySelector(".toggle-notes"),i=t.querySelector(".notes-content");s&&i&&s.addEventListener("click",(()=>{const t="true"===s.ariaExpanded;s.ariaExpanded=!t.toString(),i.hidden=t,t||i.querySelector("textarea")?.focus()}));const a=t.querySelector(".save-notes"),n=t.querySelector(".notes-input");a&&n&&(a.addEventListener("click",(()=>{this.saveNotes(n)})),n.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.saveNotes(n))})))}saveNotes(t){if(!t)return;const e=t.value.trim(),s=t.dataset.id,i=t.dataset.type;s&&i&&(this.queue.addToQueue({type:"favourite_notes",data:{type:i,target_id:parseInt(s),notes:e}}),showToast("Notes saved"),this.a11y.announce("Notes saved"))}showEmptyState(t=!1){const e=this.container.querySelector(this.config.selectors.noItems)??this.createEmptyElement;e&&(e.hidden=!1),this.grid&&this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){const t=this.container.querySelector(".no-favourites");t&&t.remove(),this.grid&&this.grid.classList.remove("empty")}createEmptyElement(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n <h3>♡ BLANK CANVAS ♡</h3>\n <p>You haven't fallen in love with any pieces... yet!</p>\n <p>Hit that heart icon when something stops your scroll.</p>\n <p>Your dream collection is waiting to start.</p>\n ",this.grid.after(e)}showEmptyListState(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n <h3>♡ FULL OF POSSIBILITY ♡</h3>\n <p>There's nothing in this list yet.</p>\n <p>Add some gap fillers from the main favourites tab.</p>\n ",this.grid.after(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}async loadMoreItems(){!this.state.loading&&this.state.pagination.hasMore&&(this.state.page+=1,await this.loadFavourites())}updateTypeFilters(t){this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{const s=e.querySelector(".count");if(!s)return;const i=e.dataset.type;if("all"===i){const e=Object.values(t).reduce(((t,e)=>t+(parseInt(e)||0)),0);s.textContent=`(${e})`}else s.textContent=`(${t[i]||0})`}))}setFilterType(t){t!==this.state.filter.type&&(this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{e.classList.toggle("active",e.dataset.type===t),e.setAttribute("aria-selected",e.dataset.type===t)})),this.state.filter.type=t,this.state.page=1,this.loadFavourites(),window.jvbA11y&&window.jvbA11y.announce(`Filtered to show ${"all"===t?"all":t} items`))}setView(t){t!==this.state.view.mode&&(this.viewControls&&this.viewControls.querySelectorAll(".view-toggle").forEach((e=>{const s=e.dataset.view===t;e.setAttribute("aria-pressed",s)})),this.grid&&(this.grid.classList.remove("grid-view","list-view"),this.grid.classList.add(`${t}-view`)),this.state.view.mode=t,localStorage.setItem("favourites_view",t),window.jvbA11y&&window.jvbA11y.announce(`Changed to ${t} view`))}toggleSelectAll(t){const e=this.getVisibleItems();e.forEach((e=>{const s=e.querySelector('.item-select input[type="checkbox"]');s&&(s.checked=t,this.toggleItemSelection(s.value,t))})),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce(t?`Selected all ${e.length} items`:"Deselected all items")}getVisibleItems(){return this.grid?Array.from(this.grid.querySelectorAll(".item:not([hidden])")):[]}toggleItemSelection(t,e){e?this.state.selectedItems.add(t):this.state.selectedItems.delete(t);const s=this.grid.querySelector(`.item[data-id="${t}"]`);s&&s.classList.toggle("selected",e)}handleItemSelection(t){const e=t.checked,s=t.value;if(this.toggleItemSelection(s,e),this.updateBulkControls(),this.updateSelectAllState(),window.jvbA11y){const s=t.closest(".item"),i=s&&s.querySelector("h3")?.textContent||"item";window.jvbA11y.announce(e?`Selected ${i}`:`Deselected ${i}`)}}updateSelectAllState(){const t=this.container.querySelector(this.config.selectors.selectAll);if(!t)return;const e=this.getVisibleItems();if(0===e.length)return t.checked=!1,void(t.indeterminate=!1);const s=e.filter((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');return e&&e.checked})).length;0===s?(t.checked=!1,t.indeterminate=!1):s===e.length?(t.checked=!0,t.indeterminate=!1):(t.checked=!1,t.indeterminate=!0)}updateBulkControls(){if(!this.bulkControls)return;const t=this.bulkControls.querySelector(".bulk-actions");if(!t)return;const e=this.state.selectedItems.size>0;t.hidden=!e;const s=this.bulkControls.querySelector(".selected-count");s&&(s.textContent=e?`${this.state.selectedItems.size} selected`:"")}handleKeyDown(t){"Escape"===t.key&&this.state.selectedItems.size>0&&(t.preventDefault(),this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared using Escape key"))}clearSelection(){this.state.selectedItems.clear(),this.getVisibleItems().forEach((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');e&&(e.checked=!1),t.classList.remove("selected")}));const t=this.container.querySelector(this.config.selectors.selectAll);t&&(t.checked=!1,t.indeterminate=!1),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")}applyBulkAction(t){if(!t||0===this.state.selectedItems.size)return;switch(t){case"unfavourite":this.bulkUnfavourite();break;case"add-to-list":this.showAddToListModal();break;case"create-list":this.showCreateListModal();break;case"add-notes":this.showBulkNotesModal()}const e=this.container.querySelector(".bulk-action-select");e&&(e.value="")}handleItemActions(t){if(t.target.closest(".toggle-notes")){const e=t.target.closest(".toggle-notes"),s="true"===e.getAttribute("aria-expanded"),i=e.closest(".notes-section").querySelector(".notes-content");return e.setAttribute("aria-expanded",!s),i.hidden=s,!s&&i&&i.querySelector("textarea")?.focus(),void t.preventDefault()}if(t.target.closest(".save-notes")){const e=t.target.closest(".save-notes").closest(".notes-content").querySelector("textarea");return e&&this.saveNotes(e),void t.preventDefault()}if(t.target.closest(".view-list")){const e=t.target.closest(".view-list").closest(".list-card");return e&&e.dataset.id&&this.viewList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".share-list")){const e=t.target.closest(".share-list").closest(".list-card");return e&&e.dataset.id&&this.showShareModal(e.dataset.id),void t.preventDefault()}if(t.target.closest(".delete-list")){const e=t.target.closest(".delete-list").closest(".list-card");return e&&e.dataset.id&&this.deleteList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".back-to-lists"))return this.exitListView(),void t.preventDefault()}async loadLists(t=!0){try{this.state.loading=!0,this.loadingManager.show("Loading lists...");const t=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favourite-lists",forceRefresh:!1});return t.lists&&this.renderLists(t.lists),t}catch(t){throw this.handleError(t,"loading lists"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderLists(t){if(!this.listContainer)return;if(removeChildren(this.listContainer),!t||0===t.length)return void(this.listContainer.innerHTML='\n <div class="no-lists">\n <h3>No Lists Yet</h3>\n <p>Select favourites from the main tab to organize into lists.</p>\n </div>\n ');const e=t.owned,s=t.shared;if(e.length>0){const t=document.createElement("details");t.className="lists-section owned-lists",t.open=!0,t.innerHTML="<summary>Your Lists:</summary>",e.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}if(s.length>0){const t=document.createElement("details");t.className="lists-section shared-lists",t.innerHTML="<summary>Lists Shared with You:</summary>",s.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}}createListCard(t){const e=document.createElement("div");e.className="list-card",e.dataset.id=t.id;const s="1"===t.is_shared;s&&e.classList.add("shared"),t.is_temp&&e.classList.add("temp"),t.is_owner;const i=sanitizeHtml(t.name||"Untitled List"),a=sanitizeHtml(t.description||"");return e.innerHTML=`\n <div class="list-header">\n <h3>${i}</h3>\n <div class="list-actions">\n <button type="button" class="view-list" title="View List">\n ${jvbSettings.icons?.show||"View"}\n </button>\n ${s?"":`\n <button type="button" class="share-list" title="Share List">\n ${jvbSettings.icons?.share||"Share"}\n </button>\n <button type="button" class="delete-list" title="Delete List">\n ${jvbSettings.icons?.delete||"Delete"}\n </button>\n `}\n </div>\n </div>\n\n ${a?`<p class="list-description">${a}</p>`:""}\n\n <div class="list-meta">\n <div class="meta-stats">\n <span class="item-count">${t.item_count||0} items</span>\n <span class="date">${formatDate(t.created_at)}</span>\n </div>\n\n\n ${s?`\n <div class="owner-info">\n Shared by ${t.owner_name||"another user"}\n </div>\n `:t.share_count>0?`\n <div class="share-info">\n Shared with ${t.share_count} ${1===t.share_count?"person":"people"}\n </div>\n `:""}\n </div>\n `,e}async viewList(t){try{this.state.loading=!0,this.loadingManager.show("Loading list..."),this.state.currentListId=t;const e=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});if(!e.list)throw new Error("List not found");this.showListDetails(e.list)}catch(t){this.handleError(t,"viewing list")}finally{this.state.loading=!1,this.loadingManager.hide()}}showListDetails(t){this.listDetails&&this.listContainer&&(console.log(t),this.listDetails.querySelector(".list-title").value=t.name||"Untitled List",this.listDetails.querySelector(".list-description").value=t.description||"",t.is_owner?this.listDetails.querySelector(".list-actions")||this.createListActions():this.listDetails.querySelector(".list-actions")?.remove(),removeChildren(this.grid),this.renderFavourites(t.items||[],!1),0===t.items.length&&this.showEmptyListState(),window.jvbA11y&&window.jvbA11y.announce(`Viewing list: ${t.name} with ${t.items?.length||0} items`))}createListActions(){const t=document.createElement("div");t.className="list-actions",t.innerHTML='\n <button type="button" class="share-list" title="Share List">\n <?= $icon->getIcon(\'share\')?>\n <span>Share</span>\n </button>\n <button type="button" class="duplicate-list" title="Duplicate List">\n <?= $icon->getIcon(\'copy\')?>\n <span>Duplicate</span>\n </button>\n <button type="button" class="delete-list" title="Delete List">\n <?= $icon->getIcon(\'delete\')?>\n <span>Delete</span>\n </button>\n ',this.listDetails.insertBefore(t,this.listDetails.querySelector(".bulk-controls"))}exitListView(){this.listDetails&&this.listContainer&&(this.listDetails.hidden=!0,this.listContainer.hidden=!1,this.container.classList.remove("viewing-list"),this.state.currentListId=null,window.jvbA11y&&window.jvbA11y.announce("Returned to lists view"))}showCreateListModal(){this.createListModal&&(this.createListModal.querySelector("form")?.reset(),this.createListModal.showModal(),setTimeout((()=>{this.createListModal.querySelector("#list-name")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Create list dialog opened"))}showAddToListModal(){this.addToListModal&&(this.populateAddToListModal(),this.addToListModal.showModal(),window.jvbA11y&&window.jvbA11y.announce("Add to list dialog opened"))}async populateAddToListModal(){if(!this.addToListModal)return;const t=this.addToListModal.querySelector(".lists-options");if(t){removeChildren(t);try{const e=(await this.loadLists()).lists.owned;if(0===e.length)return t.innerHTML='\n <div class="no-lists">\n <p>You don\'t have any lists yet.</p>\n <button type="button" class="create-list-button">Create a list</button>\n </div>\n ',void t.querySelector(".create-list-button")?.addEventListener("click",(()=>{this.addToListModal.close(),this.showCreateListModal()}));e.forEach((e=>{const s=document.createElement("div");s.className="list-option",s.innerHTML=`\n <input type="checkbox" id="${e.id}" name="list_ids[]" value="${e.id}">\n <label for="${e.id}">\n\n <span class="list-name">${sanitizeHtml(e.name)}</span>\n <span class="item-count">( ${e.item_count||0} items )</span>\n </label>\n `,t.appendChild(s)}))}catch(e){t.innerHTML='\n <div class="error-message">\n <p>Error loading lists. Please try again.</p>\n </div>\n ',console.error("Error loading lists for modal:",e)}}}async handleCreateList(t){const e=t.get("list_name"),s=t.get("list_description");if(e)try{this.showLoader("Creating list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_create",data:{name:e,description:s,items:t}}),showToast(`List "${e}" created`),this.a11y.announce(`List ${e} created with ${t.length} items`),this.createListModal.close(),this.clearSelection(),this.switchTab("lists")}catch(t){this.handleError(t,"creating list")}finally{this.hideLoader()}else showToast("Please enter a list name","error")}async handleAddToList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Adding to list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_add",data:{list_id:e.join(","),items:t}}),showToast(`Added to ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Added ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"adding to list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}async handleRemoveFromList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Removing from list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_remove",data:{list_id:e.join(","),items:t}}),showToast(`Removed from ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Removed ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"remove from list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}showShareModal(t){this.shareListModal&&(this.state.currentListId=t,this.shareListModal.querySelector("form")?.reset(),this.loadSharedUsers(t),this.shareListModal.showModal(),setTimeout((()=>{this.shareListModal.querySelector("#share-email")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Share list dialog opened"))}async loadSharedUsers(t){try{const e=this.shareListModal.querySelector(".shared-users");if(!e)return;e.innerHTML='<div class="loading">Loading shared users...</div>';const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});removeChildren(e),s.list&&s.list.shared_users&&s.list.shared_users.length>0?(s.list.shared_users.forEach((t=>{const s=document.createElement("div");s.className=`shared-user ${t.status}`,s.innerHTML=`\n <span class="user-email">${t.email}</span>\n ${"pending"===t.status?'<span class="pending-badge">Invitation sent</span>':`<span class="permission-badge">${t.permission_type||"view"}</span>`}\n <button type="button" class="remove-share" data-email="${t.email}">\n ${jvbSettings.icons?.delete||"Remove"}\n </button>\n `,e.appendChild(s)})),e.querySelectorAll(".remove-share").forEach((t=>{t.addEventListener("click",(()=>{this.unshareList(t.dataset.email)}))}))):e.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>'}catch(t){console.error("Error loading shared users:",t)}}async unshareList(t){if(confirm(`Remove ${t}'s access to this list?`))if(this.state.currentListId)try{this.showLoader("Removing access..."),this.queue.addToQueue({type:"favourite_list_unshare",data:{list_id:parseInt(this.state.currentListId),email:t}});const e=Array.from(this.shareListModal.querySelectorAll(".shared-user")).find((e=>e.querySelector(".user-email")?.textContent===t));e&&(e.classList.add("removing"),setTimeout((()=>{if(e.remove(),0===this.shareListModal.querySelectorAll(".shared-user").length){const t=this.shareListModal.querySelector(".shared-users");t&&(t.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>')}}),300)),showToast(`Removed ${t}'s access`),this.a11y.announce(`Removed ${t}'s access to list`)}catch(t){this.handleError(t,"removing share access")}finally{this.hideLoader()}else showToast("No list selected","error")}async deleteList(t){if(confirm("Are you sure you want to delete this list? This cannot be undone."))try{this.showLoader("Deleting list..."),this.queue.addToQueue({type:"favourite_list_delete",data:{list_id:parseInt(t)}});const e=this.container.querySelector(`.list-card[data-id="${t}"]`);e&&(e.classList.add("removing"),setTimeout((()=>{e.remove(),0===this.container.querySelectorAll(".list-card").length&&(this.listContainer.innerHTML='\n <div class="no-lists">\n <h3>No Lists Yet</h3>\n <p>Create your first list to organize your favourites!</p>\n </div>\n ')}),300)),showToast("List deleted"),this.a11y.announce("List deleted")}catch(t){this.handleError(t,"deleting list")}finally{this.hideLoader()}}showBulkNotesModal(){let t=document.querySelector(".bulk-notes-modal");t||(t=document.createElement("dialog"),t.className="bulk-notes-modal",t.innerHTML='\n <form method="dialog" data-save="favourites">\n <h2>Add Notes to Selected Items</h2>\n\n <div class="field">\n <label for="bulk-notes">Notes (will be applied to all selected items)</label>\n <textarea id="bulk-notes" name="bulk_notes" rows="5"></textarea>\n </div>\n\n <div class="actions">\n <button type="button" class="cancel">Cancel</button>\n <button type="submit" class="save">Save Notes</button>\n </div>\n </form>\n ',document.body.appendChild(t),t.querySelector("form").addEventListener("submit",(e=>{e.preventDefault();const s=t.querySelector("#bulk-notes").value;this.saveBulkNotes(s),t.close()})),t.querySelector(".cancel").addEventListener("click",(()=>{t.close()}))),t.querySelector("form")?.reset(),t.showModal(),setTimeout((()=>{t.querySelector("#bulk-notes")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Add notes dialog opened")}saveBulkNotes(t){if(t)try{this.showLoader("Saving notes...");let e=Array.from(this.state.selectedItems.values());this.queue.addToQueue({type:"favourite_notes",data:{target_id:e.join(","),notes:t}}),showToast(`Notes saved for ${e.length} items`),this.a11y.announce(`Notes saved for ${e.length} items`),this.clearSelection()}catch(t){this.handleError(t,"saving bulk notes")}finally{this.hideLoader()}}async bulkUnfavourite(){if(confirm("Are you sure you want to remove these items from your favourites?"))try{this.showLoader("Removing from favourites...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);if(!s)return;const i=s.dataset.type;t.push({target_id:parseInt(e),type:i,action:"remove"})})),this.queue.addToQueue({type:"favourite_toggle",data:t});const e=[];this.state.selectedItems.forEach((t=>{const s=this.grid.querySelector(`.item[data-id="${t}"]`);if(!s)return;s.style.opacity="0",s.style.transform="scale(0.9)",s.style.transition="opacity 0.3s ease, transform 0.3s ease";const i=new Promise((t=>{setTimeout((()=>{s.remove(),t()}),300)}));e.push(i)})),await Promise.all(e),0===this.grid.children.length&&this.showEmptyState(),this.clearSelection(),showToast(`Removed ${t.length} items from favourites`),this.a11y.announce(`Removed ${t.length} items from favourites`)}catch(t){this.handleError(t,"removing favourites")}finally{this.hideLoader()}}async handleShareList(t){const e=t.get("share_email");if(e)if(this.validateEmail(e))try{this.showLoader("Sharing list..."),this.queue.addToQueue({type:"favourite_list_share",data:{list_id:parseInt(this.state.currentListId),email:e,permission_type:"view"}}),this.shareListModal.querySelector("#share-email").value="",this.loadSharedUsers(this.state.currentListId),showToast(`Invitation sent to ${e}`),this.a11y.announce(`Invitation sent to ${e}`)}catch(t){this.handleError(t,"sharing list")}finally{this.hideLoader()}else showToast("Please enter a valid email address","error");else showToast("Please enter an email address","error")}showLoader(t="Loading..."){if(!this.loader)return;const e=this.loader.querySelector(".loader-message");e&&(e.textContent=t),this.loader.hidden=!1}hideLoader(){this.loader&&(this.loader.hidden=!0)}showToast(t,e){window.jvbNotifications.showToast(t,e)}handleError(t,e){console.error(`Favourites error (${e}):`,t),showToast(`Error ${e}: ${t.message||"Something went wrong"}`,"error"),window.jvbError&&window.jvbError.log(t,{component:"FavouritesManager",action:e}),window.jvbA11y&&window.jvbA11y.announce(`Error ${e}. ${t.message||"Please try again."}`)}validateEmail(t){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)}}; |
| | | window.favouritesManager=class{constructor(){this.queue=window.jvbQueue,this.loadingManager=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.tabs=new window.jvbTabs(document.querySelector(".replace")),this.config={endpoints:{favourites:"favourites",lists:"favourites/lists",shares:"favourites/lists/shares"},selectors:{container:".favourites.container",itemsTab:'.tab-content[data-tab="items"]',listsTab:'.tab-content[data-tab="lists"]',grid:".item-grid",typeFilters:".type-filters",viewControls:".view-controls",bulkControls:".bulk-controls",selectAll:"#select-all",createListModal:".create-list-modal",addToListModal:".add-to-list-modal",shareListModal:".share-list-modal",noItems:".no-favourites",listContainer:".lists-container",listDetails:".list-details",loader:".favourites-loader"},defaultPage:1,defaultPerPage:24,defaultViewMode:"grid",refreshInterval:6e4,toastDuration:3e3},document.addEventListener("keydown",this.handleKeyDown.bind(this)),this.state={selectedItems:new Set,page:this.config.defaultPage,filter:{type:"all",order:"desc",orderBy:"date_added"},view:{mode:localStorage.getItem("favourites_view")||this.config.defaultViewMode,activeTab:"items"},pagination:{hasMore:!1,totalItems:0,totalPages:0},currentListId:null,loading:!1,initialized:!1},this.initDom(),this.initEvents(),this.loadInitialData(),this.state.initialized=!0}initDom(){this.container=document.querySelector(this.config.selectors.container),this.container?(this.grid=this.container.querySelector(this.config.selectors.grid),this.typeFilters=this.container.querySelector(this.config.selectors.typeFilters),this.viewControls=this.container.querySelector(this.config.selectors.viewControls),this.bulkControls=this.container.querySelector(this.config.selectors.bulkControls),this.listContainer=this.container.querySelector(this.config.selectors.listContainer),this.listDetails=this.container.querySelector(this.config.selectors.listDetails),this.loader=this.container.querySelector(this.config.selectors.loader),this.createListModal=document.querySelector(this.config.selectors.createListModal),this.addToListModal=document.querySelector(this.config.selectors.addToListModal),this.shareListModal=document.querySelector(this.config.selectors.shareListModal),this.grid&&this.state.view.mode&&this.grid.classList.add(`${this.state.view.mode}-view`)):console.warn("Favourites container not found")}initEvents(){if(this.typeFilters&&this.typeFilters.addEventListener("click",(t=>{const e=t.target.closest(".type-filter");e&&this.setFilterType(e.dataset.type)})),this.viewControls&&this.viewControls.addEventListener("click",(t=>{const e=t.target.closest(".view-toggle");e&&this.setView(e.dataset.view)})),this.container){const t=this.container.querySelector(this.config.selectors.selectAll);t&&t.addEventListener("change",(()=>{this.toggleSelectAll(t.checked)})),this.container.addEventListener("change",(t=>{t.target.matches(".item-select input[type=checkbox]")&&this.handleItemSelection(t.target)}));const e=this.container.querySelector(".bulk-action-select"),s=this.container.querySelector(".apply-bulk");e&&s&&s.addEventListener("click",(()=>{this.applyBulkAction(e.value)}));const i=this.container.querySelector(".cancel-bulk");i&&i.addEventListener("click",(()=>{this.clearSelection()}))}this.initModalEvents(),this.container.addEventListener("click",this.handleItemActions.bind(this)),this.grid&&this.setupInfiniteScroll()}initModalEvents(){if(this.createListModal){const t=this.createListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleCreateList(new FormData(t))}));const e=this.createListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.createListModal.close()}))}if(this.addToListModal){const t=this.addToListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleAddToList(new FormData(t))}));const e=this.addToListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.addToListModal.close()}))}if(this.shareListModal){const t=this.shareListModal.querySelector("form");t&&t.addEventListener("submit",(e=>{e.preventDefault(),this.handleShareList(new FormData(t))}));const e=this.shareListModal.querySelector(".cancel");e&&e.addEventListener("click",(()=>{this.shareListModal.close()}));const s=this.shareListModal.querySelector(".add-email");s&&s.addEventListener("click",(()=>{const e=this.shareListModal.querySelector("#share-email");e&&e.value&&this.handleShareList(new FormData(t))}))}}setupInfiniteScroll(){let t=this.container.querySelector(".scroll-sentinel");t||(t=document.createElement("div"),t.className="scroll-sentinel",t.setAttribute("aria-hidden","true"),this.grid.parentNode.appendChild(t)),new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&this.state.pagination.hasMore&&!this.state.loading&&(this.state.page++,this.loadFavourites())}))}),{rootMargin:"200px"}).observe(t)}async loadInitialData(){this.loadingManager.show();try{await this.loadFavourites(),this.loadLists().catch((t=>{console.error("Error loading lists:",t)}))}catch(t){this.handleError(t,"loading initial data")}finally{this.loadingManager.hide()}}async loadFavourites(t=!0){if(!this.state.loading)try{this.state.loading=!0,this.loadingManager.show();const e=new URLSearchParams({page:this.state.page,per_page:this.config.defaultPerPage,type:"all"!==this.state.filter.type?this.state.filter.type:"",order:this.state.filter.order,orderby:this.state.filter.orderBy});t&&(this.state.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.favourites}?${e}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favouritesManager",forceRefresh:!0});return this.renderFavourites(s.favourites||[],this.state.page>1),s.counts&&this.updateTypeFilters(s.counts),s.pagination&&(this.state.pagination={hasMore:s.pagination.has_more,totalItems:s.pagination.total_items,totalPages:s.pagination.total_pages}),s}catch(t){throw this.handleError(t,"loading favourites"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderFavourites(t,e=!1){this.grid&&(0!==t.length||e?(this.hideEmptyState(),e||removeChildren(this.grid),t.forEach((t=>{const e=this.createItemElement(t);this.grid.appendChild(e),this.initItemFunctionality(e,t)})),window.jvbA11y&&window.jvbA11y.announce(`${e?"Added":"Loaded"} ${t.length} favourites`)):this.showEmptyState())}createItemElement(t){const e=document.createElement("div");e.className=`item ${t.type} favourited`,e.dataset.id=t.target_id,e.dataset.type=t.type;const s=sanitizeHtml(t.title||!1),i=sanitizeHtml(t.notes||"");let a="";return t.thumbnail&&(a=`\n <div class="item-thumbnail">\n <a href="${t.url}">${t.thumbnail}</a>\n </div>\n `),e.innerHTML=`\n <div class="item-select">\n <input type="checkbox"\n class="favourite-checkbox"\n id="select-${t.target_id}"\n value="${t.target_id}">\n <label for="select-${t.target_id}"><span class="screen-reader-text">Select this ${t.type}</span</label>\n </div>\n\n <button type="button" class="favourite-button favourited"\n onclick="toggleFavourite(this)"\n data-id="${t.target_id}"\n data-type="${t.type}"\n title="Remove from favourites">\n ${jvbSettings.icons["heart-filled"]}\n </button>\n\n ${a}\n\n <div class="item-info">\n ${s?`<h3><a href="${t.url}">${s}</a></h3>`:`<a href="${t.url}">View Item</a>`}\n\n ${t.author?`\n <div class="item-artist">\n <span>By ${t.author.name}</span>\n </div>`:""}\n\n ${t.taxonomies?.length?`\n <div class="taxonomy-lists">\n ${t.taxonomies.map((t=>`\n <div class="taxonomy-group">\n ${jvbSettings.icons[t.icon]}\n <ul>\n ${t.terms.slice(0,3).map((t=>`\n <li>\n <a href="${t.url}" ${t.umami_click}>\n ${t.title}\n </a>\n </li>\n `)).join("")}\n </ul>\n </div>\n `)).join("")}\n </div>\n `:""}\n\n <div class="notes-section">\n <button type="button" class="toggle-notes" aria-expanded="false">\n ${jvbSettings.icons.note||"Notes"}\n <span>Notes</span>\n </button>\n\n <div class="notes-content" hidden>\n <textarea class="notes-input"\n placeholder="Add notes about this item..."\n data-id="${t.target_id}"\n data-type="${t.type}">${i}</textarea>\n <button type="button" class="save-notes">Save Notes</button>\n </div>\n </div>\n </div>\n `,e}initItemFunctionality(t,e){const s=t.querySelector(".toggle-notes"),i=t.querySelector(".notes-content");s&&i&&s.addEventListener("click",(()=>{const t="true"===s.ariaExpanded;s.ariaExpanded=!t.toString(),i.hidden=t,t||i.querySelector("textarea")?.focus()}));const a=t.querySelector(".save-notes"),n=t.querySelector(".notes-input");a&&n&&(a.addEventListener("click",(()=>{this.saveNotes(n)})),n.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.ctrlKey||t.metaKey)&&(t.preventDefault(),this.saveNotes(n))})))}saveNotes(t){if(!t)return;const e=t.value.trim(),s=t.dataset.id,i=t.dataset.type;s&&i&&(this.queue.addToQueue({type:"favourite_notes",data:{type:i,target_id:parseInt(s),notes:e}}),showToast("Notes saved"),this.a11y.announce("Notes saved"))}showEmptyState(t=!1){const e=this.container.querySelector(this.config.selectors.noItems)??this.createEmptyElement;e&&(e.hidden=!1),this.grid&&this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){const t=this.container.querySelector(".no-favourites");t&&t.remove(),this.grid&&this.grid.classList.remove("empty")}createEmptyElement(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n <h3>♡ BLANK CANVAS ♡</h3>\n <p>You haven't fallen in love with any pieces... yet!</p>\n <p>Hit that heart icon when something stops your scroll.</p>\n <p>Your dream collection is waiting to start.</p>\n ",this.grid.after(e)}showEmptyListState(t=!1){const e=document.createElement("div");e.className="no-favourites",e.innerHTML="\n <h3>♡ FULL OF POSSIBILITY ♡</h3>\n <p>There's nothing in this list yet.</p>\n <p>Add some gap fillers from the main favourites tab.</p>\n ",this.grid.after(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}async loadMoreItems(){!this.state.loading&&this.state.pagination.hasMore&&(this.state.page+=1,await this.loadFavourites())}updateTypeFilters(t){this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{const s=e.querySelector(".count");if(!s)return;const i=e.dataset.type;if("all"===i){const e=Object.values(t).reduce(((t,e)=>t+(parseInt(e)||0)),0);s.textContent=`(${e})`}else s.textContent=`(${t[i]||0})`}))}setFilterType(t){t!==this.state.filter.type&&(this.typeFilters&&this.typeFilters.querySelectorAll(".type-filter").forEach((e=>{e.classList.toggle("active",e.dataset.type===t),e.setAttribute("aria-selected",e.dataset.type===t)})),this.state.filter.type=t,this.state.page=1,this.loadFavourites(),window.jvbA11y&&window.jvbA11y.announce(`Filtered to show ${"all"===t?"all":t} items`))}setView(t){t!==this.state.view.mode&&(this.viewControls&&this.viewControls.querySelectorAll(".view-toggle").forEach((e=>{const s=e.dataset.view===t;e.setAttribute("aria-pressed",s)})),this.grid&&(this.grid.classList.remove("grid-view","list-view"),this.grid.classList.add(`${t}-view`)),this.state.view.mode=t,localStorage.setItem("favourites_view",t),window.jvbA11y&&window.jvbA11y.announce(`Changed to ${t} view`))}toggleSelectAll(t){const e=this.getVisibleItems();e.forEach((e=>{const s=e.querySelector('.item-select input[type="checkbox"]');s&&(s.checked=t,this.toggleItemSelection(s.value,t))})),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce(t?`Selected all ${e.length} items`:"Deselected all items")}getVisibleItems(){return this.grid?Array.from(this.grid.querySelectorAll(".item:not([hidden])")):[]}toggleItemSelection(t,e){e?this.state.selectedItems.add(t):this.state.selectedItems.delete(t);const s=this.grid.querySelector(`.item[data-id="${t}"]`);s&&s.classList.toggle("selected",e)}handleItemSelection(t){const e=t.checked,s=t.value;if(this.toggleItemSelection(s,e),this.updateBulkControls(),this.updateSelectAllState(),window.jvbA11y){const s=t.closest(".item"),i=s&&s.querySelector("h3")?.textContent||"item";window.jvbA11y.announce(e?`Selected ${i}`:`Deselected ${i}`)}}updateSelectAllState(){const t=this.container.querySelector(this.config.selectors.selectAll);if(!t)return;const e=this.getVisibleItems();if(0===e.length)return t.checked=!1,void(t.indeterminate=!1);const s=e.filter((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');return e&&e.checked})).length;0===s?(t.checked=!1,t.indeterminate=!1):s===e.length?(t.checked=!0,t.indeterminate=!1):(t.checked=!1,t.indeterminate=!0)}updateBulkControls(){if(!this.bulkControls)return;const t=this.bulkControls.querySelector(".bulk-actions");if(!t)return;const e=this.state.selectedItems.size>0;t.hidden=!e;const s=this.bulkControls.querySelector(".selected-count");s&&(s.textContent=e?`${this.state.selectedItems.size} selected`:"")}handleKeyDown(t){"Escape"===t.key&&this.state.selectedItems.size>0&&(t.preventDefault(),this.clearSelection(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared using Escape key"))}clearSelection(){this.state.selectedItems.clear(),this.getVisibleItems().forEach((t=>{const e=t.querySelector('.item-select input[type="checkbox"]');e&&(e.checked=!1),t.classList.remove("selected")}));const t=this.container.querySelector(this.config.selectors.selectAll);t&&(t.checked=!1,t.indeterminate=!1),this.updateBulkControls(),window.jvbA11y&&window.jvbA11y.announce("Selection cleared")}applyBulkAction(t){if(!t||0===this.state.selectedItems.size)return;switch(t){case"unfavourite":this.bulkUnfavourite();break;case"add-to-list":this.showAddToListModal();break;case"create-list":this.showCreateListModal();break;case"add-notes":this.showBulkNotesModal()}const e=this.container.querySelector(".bulk-action-select");e&&(e.value="")}handleItemActions(t){if(t.target.closest(".toggle-notes")){const e=t.target.closest(".toggle-notes"),s="true"===e.getAttribute("aria-expanded"),i=e.closest(".notes-section").querySelector(".notes-content");return e.setAttribute("aria-expanded",!s),i.hidden=s,!s&&i&&i.querySelector("textarea")?.focus(),void t.preventDefault()}if(t.target.closest(".save-notes")){const e=t.target.closest(".save-notes").closest(".notes-content").querySelector("textarea");return e&&this.saveNotes(e),void t.preventDefault()}if(t.target.closest(".view-list")){const e=t.target.closest(".view-list").closest(".list-card");return e&&e.dataset.id&&this.viewList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".share-list")){const e=t.target.closest(".share-list").closest(".list-card");return e&&e.dataset.id&&this.showShareModal(e.dataset.id),void t.preventDefault()}if(t.target.closest(".delete-list")){const e=t.target.closest(".delete-list").closest(".list-card");return e&&e.dataset.id&&this.deleteList(e.dataset.id),void t.preventDefault()}if(t.target.closest(".back-to-lists"))return this.exitListView(),void t.preventDefault()}async loadLists(t=!0){try{this.state.loading=!0,this.loadingManager.show("Loading lists...");const t=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"favourite-lists",forceRefresh:!1});return t.lists&&this.renderLists(t.lists),t}catch(t){throw this.handleError(t,"loading lists"),t}finally{this.state.loading=!1,this.loadingManager.hide()}}renderLists(t){if(!this.listContainer)return;if(removeChildren(this.listContainer),!t||0===t.length)return void(this.listContainer.innerHTML='\n <div class="no-lists">\n <h3>No Lists Yet</h3>\n <p>Select favourites from the main tab to organize into lists.</p>\n </div>\n ');const e=t.owned,s=t.shared;if(e.length>0){const t=document.createElement("details");t.className="lists-section owned-lists",t.open=!0,t.innerHTML="<summary>Your Lists:</summary>",e.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}if(s.length>0){const t=document.createElement("details");t.className="lists-section shared-lists",t.innerHTML="<summary>Lists Shared with You:</summary>",s.forEach((e=>{const s=this.createListCard(e);t.appendChild(s)})),this.listContainer.appendChild(t)}}createListCard(t){const e=document.createElement("div");e.className="list-card",e.dataset.id=t.id;const s="1"===t.is_shared;s&&e.classList.add("shared"),t.is_temp&&e.classList.add("temp"),t.is_owner;const i=sanitizeHtml(t.name||"Untitled List"),a=sanitizeHtml(t.description||"");return e.innerHTML=`\n <div class="list-header">\n <h3>${i}</h3>\n <div class="list-actions">\n <button type="button" class="view-list" title="View List">\n ${jvbSettings.icons?.show||"View"}\n </button>\n ${s?"":`\n <button type="button" class="share-list" title="Share List">\n ${jvbSettings.icons?.share||"Share"}\n </button>\n <button type="button" class="delete-list" title="Delete List">\n ${jvbSettings.icons?.delete||"Delete"}\n </button>\n `}\n </div>\n </div>\n\n ${a?`<p class="list-description">${a}</p>`:""}\n\n <div class="list-meta">\n <div class="meta-stats">\n <span class="item-count">${t.item_count||0} items</span>\n <span class="date">${formatDate(t.created_at)}</span>\n </div>\n\n\n ${s?`\n <div class="owner-info">\n Shared by ${t.owner_name||"another user"}\n </div>\n `:t.share_count>0?`\n <div class="share-info">\n Shared with ${t.share_count} ${1===t.share_count?"person":"people"}\n </div>\n `:""}\n </div>\n `,e}async viewList(t){try{this.state.loading=!0,this.loadingManager.show("Loading list..."),this.state.currentListId=t;const e=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});if(!e.list)throw new Error("List not found");this.showListDetails(e.list)}catch(t){this.handleError(t,"viewing list")}finally{this.state.loading=!1,this.loadingManager.hide()}}showListDetails(t){this.listDetails&&this.listContainer&&(console.log(t),this.listDetails.querySelector(".list-title").value=t.name||"Untitled List",this.listDetails.querySelector(".list-description").value=t.description||"",t.is_owner?this.listDetails.querySelector(".list-actions")||this.createListActions():this.listDetails.querySelector(".list-actions")?.remove(),removeChildren(this.grid),this.renderFavourites(t.items||[],!1),0===t.items.length&&this.showEmptyListState(),window.jvbA11y&&window.jvbA11y.announce(`Viewing list: ${t.name} with ${t.items?.length||0} items`))}createListActions(){const t=document.createElement("div");t.className="list-actions",t.innerHTML='\n <button type="button" class="share-list" title="Share List">\n <i class="icon icon-share-fat"></i>\n <span>Share</span>\n </button>\n <button type="button" class="duplicate-list" title="Duplicate List">\n <i class="icon icon-copy"></i>\n <span>Duplicate</span>\n </button>\n <button type="button" class="delete-list" title="Delete List">\n <i class="icon icon-trash"></i>\n <span>Delete</span>\n </button>\n ',this.listDetails.insertBefore(t,this.listDetails.querySelector(".bulk-controls"))}exitListView(){this.listDetails&&this.listContainer&&(this.listDetails.hidden=!0,this.listContainer.hidden=!1,this.container.classList.remove("viewing-list"),this.state.currentListId=null,window.jvbA11y&&window.jvbA11y.announce("Returned to lists view"))}showCreateListModal(){this.createListModal&&(this.createListModal.querySelector("form")?.reset(),this.createListModal.showModal(),setTimeout((()=>{this.createListModal.querySelector("#list-name")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Create list dialog opened"))}showAddToListModal(){this.addToListModal&&(this.populateAddToListModal(),this.addToListModal.showModal(),window.jvbA11y&&window.jvbA11y.announce("Add to list dialog opened"))}async populateAddToListModal(){if(!this.addToListModal)return;const t=this.addToListModal.querySelector(".lists-options");if(t){removeChildren(t);try{const e=(await this.loadLists()).lists.owned;if(0===e.length)return t.innerHTML='\n <div class="no-lists">\n <p>You don\'t have any lists yet.</p>\n <button type="button" class="create-list-button">Create a list</button>\n </div>\n ',void t.querySelector(".create-list-button")?.addEventListener("click",(()=>{this.addToListModal.close(),this.showCreateListModal()}));e.forEach((e=>{const s=document.createElement("div");s.className="list-option",s.innerHTML=`\n <input type="checkbox" id="${e.id}" name="list_ids[]" value="${e.id}">\n <label for="${e.id}">\n\n <span class="list-name">${sanitizeHtml(e.name)}</span>\n <span class="item-count">( ${e.item_count||0} items )</span>\n </label>\n `,t.appendChild(s)}))}catch(e){t.innerHTML='\n <div class="error-message">\n <p>Error loading lists. Please try again.</p>\n </div>\n ',console.error("Error loading lists for modal:",e)}}}async handleCreateList(t){const e=t.get("list_name"),s=t.get("list_description");if(e)try{this.showLoader("Creating list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_create",data:{name:e,description:s,items:t}}),showToast(`List "${e}" created`),this.a11y.announce(`List ${e} created with ${t.length} items`),this.createListModal.close(),this.clearSelection(),this.switchTab("lists")}catch(t){this.handleError(t,"creating list")}finally{this.hideLoader()}else showToast("Please enter a list name","error")}async handleAddToList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Adding to list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_add",data:{list_id:e.join(","),items:t}}),showToast(`Added to ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Added ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"adding to list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}async handleRemoveFromList(t){const e=t.getAll("list_ids[]");if(e.length)try{this.showLoader("Removing from list...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);s&&t.push({type:s.dataset.type,target_id:parseInt(e)})})),this.queue.addToQueue({type:"favourite_list_remove",data:{list_id:e.join(","),items:t}}),showToast(`Removed from ${e.length} ${1===e.length?"list":"lists"}`),this.a11y.announce(`Removed ${t.length} items to ${e.length} ${1===e.length?"list":"lists"}`),this.addToListModal.close(),this.clearSelection()}catch(t){this.handleError(t,"remove from list")}finally{this.hideLoader()}else showToast("Please select at least one list","error")}showShareModal(t){this.shareListModal&&(this.state.currentListId=t,this.shareListModal.querySelector("form")?.reset(),this.loadSharedUsers(t),this.shareListModal.showModal(),setTimeout((()=>{this.shareListModal.querySelector("#share-email")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Share list dialog opened"))}async loadSharedUsers(t){try{const e=this.shareListModal.querySelector(".shared-users");if(!e)return;e.innerHTML='<div class="loading">Loading shared users...</div>';const s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.config.endpoints.lists}?id=${t}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.favourites}},{context:"list-item",forceRefresh:!1});removeChildren(e),s.list&&s.list.shared_users&&s.list.shared_users.length>0?(s.list.shared_users.forEach((t=>{const s=document.createElement("div");s.className=`shared-user ${t.status}`,s.innerHTML=`\n <span class="user-email">${t.email}</span>\n ${"pending"===t.status?'<span class="pending-badge">Invitation sent</span>':`<span class="permission-badge">${t.permission_type||"view"}</span>`}\n <button type="button" class="remove-share" data-email="${t.email}">\n ${jvbSettings.icons?.delete||"Remove"}\n </button>\n `,e.appendChild(s)})),e.querySelectorAll(".remove-share").forEach((t=>{t.addEventListener("click",(()=>{this.unshareList(t.dataset.email)}))}))):e.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>'}catch(t){console.error("Error loading shared users:",t)}}async unshareList(t){if(confirm(`Remove ${t}'s access to this list?`))if(this.state.currentListId)try{this.showLoader("Removing access..."),this.queue.addToQueue({type:"favourite_list_unshare",data:{list_id:parseInt(this.state.currentListId),email:t}});const e=Array.from(this.shareListModal.querySelectorAll(".shared-user")).find((e=>e.querySelector(".user-email")?.textContent===t));e&&(e.classList.add("removing"),setTimeout((()=>{if(e.remove(),0===this.shareListModal.querySelectorAll(".shared-user").length){const t=this.shareListModal.querySelector(".shared-users");t&&(t.innerHTML='<div class="no-shares">This list is not shared with anyone yet.</div>')}}),300)),showToast(`Removed ${t}'s access`),this.a11y.announce(`Removed ${t}'s access to list`)}catch(t){this.handleError(t,"removing share access")}finally{this.hideLoader()}else showToast("No list selected","error")}async deleteList(t){if(confirm("Are you sure you want to delete this list? This cannot be undone."))try{this.showLoader("Deleting list..."),this.queue.addToQueue({type:"favourite_list_delete",data:{list_id:parseInt(t)}});const e=this.container.querySelector(`.list-card[data-id="${t}"]`);e&&(e.classList.add("removing"),setTimeout((()=>{e.remove(),0===this.container.querySelectorAll(".list-card").length&&(this.listContainer.innerHTML='\n <div class="no-lists">\n <h3>No Lists Yet</h3>\n <p>Create your first list to organize your favourites!</p>\n </div>\n ')}),300)),showToast("List deleted"),this.a11y.announce("List deleted")}catch(t){this.handleError(t,"deleting list")}finally{this.hideLoader()}}showBulkNotesModal(){let t=document.querySelector(".bulk-notes-modal");t||(t=document.createElement("dialog"),t.className="bulk-notes-modal",t.innerHTML='\n <form method="dialog" data-save="favourites">\n <h2>Add Notes to Selected Items</h2>\n\n <div class="field">\n <label for="bulk-notes">Notes (will be applied to all selected items)</label>\n <textarea id="bulk-notes" name="bulk_notes" rows="5"></textarea>\n </div>\n\n <div class="actions">\n <button type="button" class="cancel">Cancel</button>\n <button type="submit" class="save">Save Notes</button>\n </div>\n </form>\n ',document.body.appendChild(t),t.querySelector("form").addEventListener("submit",(e=>{e.preventDefault();const s=t.querySelector("#bulk-notes").value;this.saveBulkNotes(s),t.close()})),t.querySelector(".cancel").addEventListener("click",(()=>{t.close()}))),t.querySelector("form")?.reset(),t.showModal(),setTimeout((()=>{t.querySelector("#bulk-notes")?.focus()}),100),window.jvbA11y&&window.jvbA11y.announce("Add notes dialog opened")}saveBulkNotes(t){if(t)try{this.showLoader("Saving notes...");let e=Array.from(this.state.selectedItems.values());this.queue.addToQueue({type:"favourite_notes",data:{target_id:e.join(","),notes:t}}),showToast(`Notes saved for ${e.length} items`),this.a11y.announce(`Notes saved for ${e.length} items`),this.clearSelection()}catch(t){this.handleError(t,"saving bulk notes")}finally{this.hideLoader()}}async bulkUnfavourite(){if(confirm("Are you sure you want to remove these items from your favourites?"))try{this.showLoader("Removing from favourites...");const t=[];this.state.selectedItems.forEach((e=>{const s=this.grid.querySelector(`.item[data-id="${e}"]`);if(!s)return;const i=s.dataset.type;t.push({target_id:parseInt(e),type:i,action:"remove"})})),this.queue.addToQueue({type:"favourite_toggle",data:t});const e=[];this.state.selectedItems.forEach((t=>{const s=this.grid.querySelector(`.item[data-id="${t}"]`);if(!s)return;s.style.opacity="0",s.style.transform="scale(0.9)",s.style.transition="opacity 0.3s ease, transform 0.3s ease";const i=new Promise((t=>{setTimeout((()=>{s.remove(),t()}),300)}));e.push(i)})),await Promise.all(e),0===this.grid.children.length&&this.showEmptyState(),this.clearSelection(),showToast(`Removed ${t.length} items from favourites`),this.a11y.announce(`Removed ${t.length} items from favourites`)}catch(t){this.handleError(t,"removing favourites")}finally{this.hideLoader()}}async handleShareList(t){const e=t.get("share_email");if(e)if(this.validateEmail(e))try{this.showLoader("Sharing list..."),this.queue.addToQueue({type:"favourite_list_share",data:{list_id:parseInt(this.state.currentListId),email:e,permission_type:"view"}}),this.shareListModal.querySelector("#share-email").value="",this.loadSharedUsers(this.state.currentListId),showToast(`Invitation sent to ${e}`),this.a11y.announce(`Invitation sent to ${e}`)}catch(t){this.handleError(t,"sharing list")}finally{this.hideLoader()}else showToast("Please enter a valid email address","error");else showToast("Please enter an email address","error")}showLoader(t="Loading..."){if(!this.loader)return;const e=this.loader.querySelector(".loader-message");e&&(e.textContent=t),this.loader.hidden=!1}hideLoader(){this.loader&&(this.loader.hidden=!0)}showToast(t,e){window.jvbNotifications.showToast(t,e)}handleError(t,e){console.error(`Favourites error (${e}):`,t),showToast(`Error ${e}: ${t.message||"Something went wrong"}`,"error"),window.jvbError&&window.jvbError.log(t,{component:"FavouritesManager",action:e}),window.jvbA11y&&window.jvbA11y.announce(`Error ${e}. ${t.message||"Please try again."}`)}validateEmail(t){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)}}; |
| | |
| | | (()=>{class e{constructor(){this.store=new window.jvbStore({name:"forms",storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:6048e5}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.scanForms(),this.init()}async init(){await this.checkPendingOperations(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners()}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){(await this.store.query("status","draft")).forEach((e=>{let t=this.forms.get(e.formId);t&&t.element&&(t.element.querySelector(".restore-form").hidden=!1,new this.populateForm(t.element,e.data))}))}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(r,t.firstChild),r.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),r.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}scanForms(){document.querySelectorAll("form").forEach((e=>{console.log("Registering form...",e),this.registerForm(e)}))}registerForm(e,t={}){const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,options:{autoSave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",cache:!0,...t},dependencies:new Map,data:this.collectFormData(e),isDirty:!1};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.formData&&this.showPendingNotification(e)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=s.switchTab.bind(s);s.switchTab=(e,t)=>{c(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&t.options.autoSave&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s==a;case"!=":return s!=a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}handleSubmit(e){if(this.subscribers.size>0){const t=e.target;if(!t.dataset.formId)return;e.preventDefault();const r=this.forms.get(t.dataset.formId);if(!r)return;const s=this.collectFormData(t);this.notify("form-submit",{formId:r.id,data:s,config:r})}}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]");switch(t=t.dataset.action,t){case"clear-form":let t=e.target.closest("form");this.store.delete(t.dataset.formId),t?.reset(),e.target.closest(".restore-form").hidden=!0;break;case"dismiss-restore":e.target.closest(".restore-form").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(!e.target.closest("[data-ignore]")&&this.subscribers.size>0){const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(!s)return;const a=s.dependencies.get(t.name);if(a&&a.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)})),s.options.autoSave&&!r.dataset.noautosave){const e=this.getDelayForField(t);this.scheduleSave(s,e)}}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&a.options.autoSave&&!r.dataset.noautosave&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form"))return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,((e,t)=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with http:// or https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),!0}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}showSuccess(e){if(!e)return;const t=e.querySelector(".validation-icon.success"),r=e.querySelector(".validation-icon.error"),s=e.querySelector(".validation-message"),a=e.querySelector("input, textarea, select");e.classList.remove("has-error"),a?.classList.remove("error"),e.classList.add("has-success"),t&&(t.hidden=!1),r&&(r.hidden=!0),s&&(s.hidden=!0,s.textContent="")}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let r=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const s=e.dataset.field;s&&this.touchedFields.add(s);this.validateField(t,e)||(r=!1,!1===r&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),r}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const r=t.target.closest("input, select");if(!r)return;const s=r.name;if(!s)return;e.querySelectorAll(`[data-show-if*="${s}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const r=e.dataset.field,s=e.querySelector(".validation-message");r&&s&&(t[r]=s.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")}));const r=this.getChangedFields(e.data,t);if(0!==Object.keys(r).length){e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.lastSnapshot,r);return Object.keys(s).length>0}showFormStatus(e,t){let r=this.forms.get(e);console.log("Setting status: ",t);const s=r.element.querySelector(".fstatus");s.hidden=!1;const a=s.querySelector(".message");a.textContent="",s.querySelector(".icon")?.remove();const i={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"},n={autosaved:"check",submitted:"check",error:"close",offline:"cloud-slash",pending:"exclamation-mark"};let o=window.getIcon(n[t]);o&&s.prepend(o),console.log(t,i[t]),console.log(t,n[t]),a.textContent=i[t]||t,s.classList.toggle("loading",["uploading","saving"].includes(t)),"submitted"===t&&setTimeout((()=>s.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){const t=new FormData(e);let r={};const s={},a={};for(let[i,n]of t.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;this.getFieldProcessor(i)(i,n,r,s,a,e)}return window.isEmptyObject(a)?this.mergeRepeaterData(r,s):(r=this.mergeRepeaterData(r,s),this.mergePostData(r,a))}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[t,r]in Object.entries(r))e[t]=r;return e}processTableField(e,t,r,s,a,i){let[n,o]=e.split("|");!n in a&&(a[n]={});this.getFieldProcessor(o)(o,t,a,s,a,i)}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),c||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary"),[i,n,o]=[a.querySelector("h2"),a.querySelector(".summary"),a.querySelector(".result")],l=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;const a=this.createResultElement(o,r,t,s);a&&n.appendChild(a)}o.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=null,a=null;if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}createResultElement(e,t,r,s){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(r,t.type,s);return this.isHtmlContent(o)?n.innerHTML=o:n.textContent=o,a}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e,console.log("FormController in window")}))})(); |
| | | (()=>{class e{constructor(){this.store=new window.jvbStore({name:"forms",storeName:"forms",keyPath:"formId",indexes:[{name:"status",keyPath:"status"},{name:"operationId",keyPath:"operationId"},{name:"timestamp",keyPath:"timestamp"},{name:"formType",keyPath:"type"}],TTL:6048e5}),this.debouncer=window.debouncer,this.ignore=[],this.populateForm=window.jvbPopulate,this.subscribers=new Set,this.forms=new Map,this.specialFields=new Map,this.dependencies=new Map,this.validators=this.initValidators(),this.touchedFields=new Set,this.autoSaveDefaults={delay:3e3,typingDelay:1500,enabled:!0},this.activeRepeaters=new Map,this.repeaterDelays={change:6e3,typing:3e3,blur:1500,add:500,remove:800,reorder:1e3},this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),this.submitHandler=this.handleSubmit.bind(this),this.inputHandler=this.handleInput.bind(this),this.focusHandler=this.handleFocus.bind(this),this.blurHandler=this.handleBlur.bind(this),this.init()}async init(){await this.checkPendingOperations(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.initListeners()}handleStoreEvent(e,t){switch(e){case"item-saved":t.item.status;break;case"data-loaded":this.checkPendingForms()}}async checkPendingForms(){(await this.store.query("status","draft")).forEach((e=>{let t=this.forms.get(e.formId);t&&t.element&&(t.element.querySelector(".restore-form").hidden=!1,new this.populateForm(t.element,e.data))}))}async checkPendingOperations(){const e=await this.store.query("status","pending");if(0===e.length)return;const t=this.groupPendingForms(e);this.showPendingNotification(t)}showPendingNotification(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);if(!t)return;const r=document.createElement("div");r.className="pending-changes-notification",r.innerHTML=`\n\t\t\t<p>We noticed unsaved changes from last time. Would you like to restore them?</p>\n\t\t\t<button class="restore-changes" data-form-id="${e.formId}">Restore</button>\n\t\t\t<button class="discard-changes" data-form-id="${e.formId}">Discard</button>\n\t\t`,t.insertBefore(r,t.firstChild),r.querySelector(".restore-changes").addEventListener("click",(()=>{this.restorePendingForm(e),r.remove()})),r.querySelector(".discard-changes").addEventListener("click",(()=>{this.discardPendingForm(e.formId),r.remove()}))}restorePendingForm(e){const t=document.querySelector(`[data-form-id="${e.formId}"]`);t&&(new this.populateForm(t,e.formData),e.status="restored",this.pendingForms.set(e.formId,e),window.jvbA11y&&window.jvbA11y.announce("Previous changes restored"))}async discardPendingForm(e){this.store.delete(e),window.jvbA11y&&window.jvbA11y.announce("Previous changes discarded")}initListeners(){this.globalHandlersAdded||(document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),document.addEventListener("focus",this.focusHandler,!0),document.addEventListener("blur",this.blurHandler,!0),document.addEventListener("input",this.inputHandler),this.globalHandlersAdded=!0)}registerForm(e,t={}){if(!e)return;const r=e.dataset.formId||`form_${Date.now()}`;e.dataset.formId=r,e.addEventListener("submit",this.submitHandler);const s={element:e,id:r,status:"",options:{autosave:"autosave"in e.dataset,saveDelay:this.autoSaveDefaults.delay,endpoint:e.dataset.save??"",formStatus:!0,cache:!0,...t},dependencies:new Map,data:this.collectFormData(e)};if(this.initializeFormFields(e,s),this.forms.set(r,s),this.store&&s.options.cache){const e=this.store.get(r);e&&e.formData&&this.showPendingNotification(e)}return s}initializeFormFields(e,t=null){this.initQuillEditors(e),this.initRepeaterFields(e,t),t&&this.initConditionalFields(e,t),this.initCharacterLimits(e),this.initImageUploadFields(e),window.jvbTabs&&e.querySelector("nav.tabs")&&(t.tabs=new window.jvbTabs(e),this.forms.set(t.formId,t),this.initSteppedForm(t.formId)),window.jvbSelector&&window.jvbSelector.scanExistingFields(e)}initSteppedForm(e){const t=this.forms.get(e),r=t.element,s=t.tabs,a=r.querySelectorAll(".tab-content").length,i=r.querySelector(".form-progress .fill"),n=r.querySelector(".step-text .current"),o=r.querySelectorAll("nav.tabs button"),l=e=>{const t=e/a*100;i&&(i.style.width=t+"%"),n&&(n.textContent=e),o.forEach(((t,r)=>{const s=r+1;t.classList.remove("current","completed","pending"),s<e?t.classList.add("completed"):s===e?t.classList.add("current"):t.classList.add("pending")}))};r.addEventListener("click",(e=>{const t=e.target.closest('[data-action="next-step"]'),a=e.target.closest('[data-action="prev-step"]');if(t){e.preventDefault();const a=t.closest(".tab-content"),i=parseInt(a.dataset.step),n=r.querySelector(`.tab-content[data-step="${i+1}"]`);if(n&&this.validateStep(a)){const e=n.dataset.tab;s.switchTab(e,!0),l(i+1),r.scrollIntoView({behavior:"smooth",block:"start"})}}if(a){e.preventDefault();const t=a.closest(".tab-content"),i=parseInt(t.dataset.step),n=r.querySelector(`.tab-content[data-step="${i-1}"]`);if(n){const e=n.dataset.tab;s.switchTab(e,!0),l(i-1),r.scrollIntoView({behavior:"smooth",block:"start"})}}}));const c=s.switchTab.bind(s);s.switchTab=(e,t)=>{c(e,t);const s=r.querySelector(`.tab-content[data-tab="${e}"]`);if(s){const e=parseInt(s.dataset.step);l(e)}},l(1)}validateStep(e){const t=e.querySelectorAll(".field");let r=!0;return t.forEach((e=>{const t=e.querySelector("input, textarea, select");if(t&&!t.closest("[hidden]")){this.validateField(t,e)||(r=!1)}})),r}initQuillEditors(e){window.jvbQuill(e)}initRepeaterFields(e,t){e.querySelectorAll(".repeater").forEach((e=>{const r=e.querySelector(".add-repeater-row"),s=e.querySelector(".repeater-items"),a=e.querySelector("template");r&&a&&s&&(window.Sortable&&new Sortable(s,{handle:".repeater-row-header",animation:150,onEnd:()=>{this.updateRepeaterOrder(e,t)}}),r.addEventListener("click",(()=>{this.addRepeaterRow(e,t)})),s.addEventListener("click",(e=>{e.target.closest(".remove-row")&&this.removeRepeaterRow(e.target.closest(".repeater-row"),t)})))}))}addRepeaterRow(e,t){const r=e.querySelector(".repeater-items"),s=e.querySelector("template"),a=r.children.length,i=e.dataset.field,n=s.content.cloneNode(!0).firstElementChild;n.dataset.index=a,n.querySelectorAll("input, select, textarea").forEach((e=>{const t=e.name;e.name=`${i}:${a}:${t}`,e.id=`${i}-${a}-${t}`;const r=e.nextElementSibling;r&&"LABEL"===r.tagName&&(r.htmlFor=e.id)})),r.appendChild(n),t&&this.scheduleSave(t,{type:"repeater",action:"add",fieldName:i,delay:this.repeaterDelays.add}),window.jvbA11y&&window.jvbA11y.announce("Row added")}removeRepeaterRow(e,t){const r=e.closest(".repeater"),s=r.dataset.field;e.remove(),this.updateRepeaterOrder(r,t),t&&this.scheduleSave(t,{type:"repeater",action:"remove",fieldName:s,delay:this.repeaterDelays.remove}),window.jvbA11y&&window.jvbA11y.announce("Row removed")}updateRepeaterOrder(e,t){const r=e.querySelector(".repeater-items"),s=e.dataset.field;Array.from(r.children).forEach(((e,t)=>{e.dataset.index=t,e.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name.split(":");if(3===r.length){const a=r[2];e.name=`${s}:${t}:${a}`,e.id=`${s}-${t}-${a}`;const i=e.nextElementSibling;i&&"LABEL"===i.tagName&&(i.htmlFor=e.id)}}))})),t&&this.scheduleSave(t,{type:"repeater",action:"reorder",fieldName:s,delay:this.repeaterDelays.reorder})}initConditionalFields(e,t){e.querySelectorAll("[data-depends-on]").forEach((r=>{const s=r.dataset.dependsOn,a=r.dataset.dependsValue,i=r.dataset.dependsOperator||"==";t.dependencies.has(s)||t.dependencies.set(s,[]),t.dependencies.get(s).push({field:r,requiredValue:a,operator:i}),this.checkFieldDependency(e,r,s,a,i)}))}checkFieldDependency(e,t,r,s,a){const i=e.querySelector(`[name="${r}"]`);if(!i)return;const n=this.getFieldValue(i),o=this.evaluateCondition(n,s,a);this.toggleFieldVisibility(t,o)}evaluateCondition(e,t,r){const s=String(e||""),a=String(t||"");switch(r){case"==":default:return s==a;case"!=":return s!=a;case">":return parseFloat(s)>parseFloat(a);case"<":return parseFloat(s)<parseFloat(a);case">=":return parseFloat(s)>=parseFloat(a);case"<=":return parseFloat(s)<=parseFloat(a);case"contains":return s.includes(a);case"empty":return""===s;case"not_empty":return""!==s}}toggleFieldVisibility(e,t){const r=e.closest(".field, fieldset");r&&(r.hidden=!t,r.querySelectorAll("input, select, textarea").forEach((e=>{e.disabled=!t,!t&&e.hasAttribute("required")?(e.dataset.wasRequired="true",e.removeAttribute("required")):t&&"true"===e.dataset.wasRequired&&(e.setAttribute("required",""),delete e.dataset.wasRequired)})))}initCharacterLimits(e){e.querySelectorAll("[data-limit]").forEach((e=>{const t=parseInt(e.dataset.limit,10),r=e.closest(".field");let s=r?.querySelector(".char-count");!s&&r&&(s=document.createElement("div"),s.className="char-count",s.innerHTML=`<span class="current">0</span> / <span class="limit">${t}</span>`,r.appendChild(s));const a=()=>{const r=e.value.length;s&&(s.querySelector(".current").textContent=r,s.classList.toggle("exceeded",r>t)),r>t&&(e.value=e.value.substring(0,t),s&&(s.querySelector(".current").textContent=t))};e.addEventListener("input",a),a()}))}initImageUploadFields(e){window.jvbUploads.scanFields(e)}async handleSubmit(e){const t=e.target;if(!t.dataset.formId)return;const r=this.forms.get(t.dataset.formId);if(this.subscribers.size>0){e.preventDefault();const s=this.collectFormData(t);this.notify("form-submit",{formId:r.id,data:s,config:r})}}handleFormSuccess(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error").forEach((e=>e.classList.remove("field-error"))),e.classList.add("form-success"),t.message){const r=document.createElement("div");r.className="form-success-message success-message",r.textContent=t.message,e.insertBefore(r,e.firstChild);const s=window.getIcon?.("check-circle");s&&(s.classList.add("success-icon"),r.prepend(s))}if(t.title||t.description){const r=document.createElement("div");if(r.className="success-box",t.title){const e=document.createElement("h3");e.textContent=t.title,r.appendChild(e)}if(t.description){(Array.isArray(t.description)?t.description:[t.description]).forEach((e=>{const t=document.createElement("p");t.textContent=e,r.appendChild(t)}))}e.insertBefore(r,e.firstChild)}window.jvbA11y&&window.jvbA11y.announce(t.message||"Form submitted successfully"),e.dispatchEvent(new CustomEvent("jvb-form-success",{detail:t}))}handleFormError(e,t){if(e.querySelectorAll(".error-message").forEach((e=>e.remove())),e.querySelectorAll(".field-error, .has-error").forEach((e=>{e.classList.remove("field-error","has-error")})),e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)})),t.field){const r=e.querySelector(`[data-field="${t.field}"]`);if(r){this.showError(r,t.message),this.touchedFields.add(t.field),r.scrollIntoView({behavior:"smooth",block:"center"});const e=r.querySelector("input, textarea, select");e&&e.focus()}}else{const r=document.createElement("div");r.className="form-error error-message",r.textContent=t.message;const s=window.getIcon?.("close-circle");s&&(s.classList.add("error-icon"),r.prepend(s)),e.insertBefore(r,e.firstChild),e.scrollIntoView({behavior:"smooth",block:"start"})}if(window.jvbA11y){const e=t.field?`Error in ${t.field}: ${t.message}`:`Form error: ${t.message}`;window.jvbA11y.announce(e)}e.dispatchEvent(new CustomEvent("jvb-form-error",{detail:t}))}handleClick(e){if(window.targetCheck(e,"div.quantity")){let t=window.targetCheck(e,"div.quantity");this.handleNumberClick(e,t.querySelector("input"))}else if(window.targetCheck(e,"[data-action]")){let t=window.targetCheck(e,"[data-action]");switch(t=t.dataset.action,t){case"clear-form":let t=e.target.closest("form");this.store.delete(t.dataset.formId),t?.reset(),e.target.closest(".restore-form").hidden=!0;break;case"dismiss-restore":e.target.closest(".restore-form").hidden=!0}}}handleNumberClick(e,t){let r=0;if(e.target.closest(".increase")?r+=1:e.target.closest(".decrease")&&(r-=1),0!==r){let s=parseFloat(t.step);s=Math.max(s,1),e.ctrlKey&&e.shiftKey?s*=50:e.ctrlKey?s*=5:e.shiftKey&&(s*=10);let a=""===t.value?0:parseFloat(t.value);t.value=a+s*r,this.handleNumberLimits(t)}}handleNumberLimits(e){let[t,r,s,a]=[e.min,e.max,e.closest(".quantity")?.querySelector(".increase"),e.closest(".quantity")?.querySelector(".decrease")],i=parseFloat(e.value);i<t?(e.value=t,a.disabled=!0):i>r?(e.value=r,s.disabled=!1):s.disabled?s.disabled=!1:a.disabled&&(a.disabled=!1)}handleChange(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=this.forms?.get(r.dataset.formId);if(s&&(console.log(s.options),s.options.autosave||this.subscribers.size>0)){const e=s.dependencies.get(t.name);e&&e.forEach((e=>{this.checkFieldDependency(r,e.field,t.name,e.requiredValue,e.operator)}));const a=this.getDelayForField(t);this.scheduleSave(s,a)}}handleFocus(e){const t=e.target;t.matches("input, textarea, select")&&(this.currentFocus=t)}handleBlur(e){if(e.target.closest("[data-ignore]"))return;const t=e.target,r=t.form||t.closest("form");if(!r)return;const s=e.target.closest("input, textarea, select");if(s){const e=this.findFieldWrapper(s);if(e){const t=e.dataset.field;t&&(this.shouldDebounce(s)&&window.debouncer.cancel(`validate_${t}`),this.touchedFields.add(t)),this.validateField(s,e)}const a=this.forms?.get(r.dataset.formId);a&&this.scheduleSave(a,{type:"blur",fieldName:t.name,delay:1500})}}handleInput(e){if(e.target.closest("[data-ignore]")||!e.target.closest("form"))return;const t=e.target.closest("input, textarea, select");if(!t)return;let r=t.closest("form");this.showFormStatus(r.dataset.formId,"pending");const s=this.findFieldWrapper(t);if(!s)return;const a=s.dataset.field;a&&this.touchedFields.add(a),this.shouldDebounce(t)&&window.debouncer.schedule(`validate_${a}`,((e,t)=>this.validateField.bind(this)),500)}initValidators(){return{email:{pattern:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,message:"Please enter a valid email address"},url:{pattern:/^https?:\/\/.+\..+/,message:"Please enter a valid URL starting with http:// or https://"},phone:{pattern:/^[\d\s\-\+\(\)\.]+$/,message:"Please enter a valid phone number"},number:{test:(e,t)=>{const r=parseFloat(e);if(isNaN(r))return"Please enter a valid number";const s=t.dataset.min,a=t.dataset.max;return void 0!==s&&r<parseFloat(s)?`Value must be at least ${s}`:!(void 0!==a&&r>parseFloat(a))||`Value must be at most ${a}`}},text:{test:(e,t)=>{const r=t.dataset.minlength,s=t.dataset.maxlength;return r&&e.length<parseInt(r)?`Must be at least ${r} characters`:!(s&&e.length>parseInt(s))||`Must be no more than ${s} characters`}}}}findFieldWrapper(e){let t=e.closest(".field");return t||(t=e.closest("[data-field]")),t}shouldDebounce(e){return["text","email","url","tel","search"].includes(e.type)||"TEXTAREA"===e.tagName}validateField(e,t){const r=this.getFieldValue(e),s=t.dataset.field;if(!this.touchedFields.has(s)&&!e.required)return!0;if(!r&&!e.required)return this.clearValidation(t),!0;if(e.required&&!r)return this.showError(t,"This field is required"),!1;if(e.checkValidity&&!e.checkValidity())return this.showError(t,e.validationMessage),!1;const a=t.dataset.pattern;if(a&&r){if(!new RegExp(a).test(r)){const e=t.dataset.validationMessage||"Invalid format";return this.showError(t,e),!1}}const i=t.dataset.validate||e.type;if(i&&this.validators[i]){const e=this.validators[i];if(e.pattern&&!e.pattern.test(r))return this.showError(t,e.message),!1;if(e.test){const s=e.test(r,t);if(!0!==s)return this.showError(t,s),!1}}return this.showSuccess(t),this.notify("field-validated",e),!0}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form?.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value?.trim()||""}showSuccess(e,t=""){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-error"),i?.classList.remove("error"),e.classList.add("has-success"),r&&(r.hidden=!1),s&&(s.hidden=!0),a&&(""===t?(a.hidden=!0,a.textContent=""):(a.hidden=!1,a.textContent=t))}showError(e,t){if(!e)return;const r=e.querySelector(".validation-icon.success"),s=e.querySelector(".validation-icon.error"),a=e.querySelector(".validation-message"),i=e.querySelector("input, textarea, select");e.classList.remove("has-success"),e.classList.add("has-error"),i?.classList.add("error"),r&&(r.hidden=!0),s&&(s.hidden=!1),a&&(a.hidden=!1,a.textContent=t)}clearValidation(e){if(!e)return;const t=e.querySelector(".validation-icon"),r=e.querySelector(".validation-message"),s=e.querySelector("input, textarea, select");e.classList.remove("has-error","has-success"),s?.classList.remove("error"),t&&(t.hidden=!0),r&&(r.hidden=!0,r.textContent="")}validateAllFields(e){if(!e)return!0;const t=e.querySelectorAll(".field:not([hidden])");let r=!0;return t.forEach((e=>{if(this.isComplexFieldWrapper(e))return;const t=e.querySelector('input:not([type="hidden"]), textarea, select');if(t&&!t.closest("[hidden]")){const s=e.dataset.field;s&&this.touchedFields.add(s);this.validateField(t,e)||(r=!1,!1===r&&(t.scrollIntoView({behavior:"smooth",block:"center"}),t.focus()))}})),r}isComplexFieldWrapper(e){return e.classList.contains("repeater")||e.classList.contains("group")||e.classList.contains("upload")}attachRepeaterValidation(e){e.addEventListener("click",(t=>{t.target.closest(".add-repeater-row")&&setTimeout((()=>{e.querySelectorAll(".repeater-row").forEach((e=>{e.querySelectorAll("input, textarea, select").forEach((e=>{const t=this.findFieldWrapper(e);t&&this.clearValidation(t)}))}))}),100)}))}attachGroupValidation(e){e.addEventListener("change",(t=>{const r=t.target.closest("input, select");if(!r)return;const s=r.name;if(!s)return;e.querySelectorAll(`[data-show-if*="${s}"]`).forEach((e=>{e.hidden&&this.clearValidation(e)}))}))}resetForm(e){if(!e)return;this.touchedFields.clear();e.querySelectorAll(".field").forEach((e=>{this.clearValidation(e)}))}getFormErrors(e){const t={};return e.querySelectorAll(".field.has-error").forEach((e=>{const r=e.dataset.field,s=e.querySelector(".validation-message");r&&s&&(t[r]=s.textContent)})),t}addValidator(e,t){this.validators[e]=t}getDelayForField(e){return"text"===e.type||"textarea"===e.type?this.autoSaveDefaults.typingDelay:["checkbox","radio","select-one","select-multiple"].includes(e.type)?1e3:this.autoSaveDefaults.delay}scheduleSave(e,t=this.autoSaveDefaults.delay){if(!e.options.autosave)return;document.addEventListener("input",this.saveCheck,{passive:!0});const r=`autosave_${e.id}`;this.debouncer.schedule(r,(()=>this.autosave(e)),t)}saveCheck(e){let t=e.target.closest("form[data-id]");t&&this.scheduleSave(this.forms.get(t.dataset.id))}async autosave(e){const t=this.collectFormData(e.element);this.showFormStatus(e.id,"saving"),await this.store.save({formId:e.id,data:t,status:"draft",timestamp:Date.now()}).then((()=>{this.showFormStatus(e.id,"autosaved")}));const r=this.getChangedFields(e.data,t);if(console.log("Changes:",r),0!==Object.keys(r).length){console.log("Continuing on:"),e.data=t,this.forms.set(e.id,e),document.removeEventListener("input",this.handleInput);for(let[e,s]of Object.entries(t))"object"==typeof s&&(r[e]=s);this.notify("form-autosave",{formId:e.id,changes:r,fullData:t,config:e})}}hasUnsavedChanges(e){const t=this.forms.get(e);if(!t)return!1;if(t.operations?.size>0)return!0;const r=this.collectFormData(t.element),s=this.getChangedFields(t.data,r);return Object.keys(s).length>0}showFormStatus(e,t,r=""){let s=this.forms.get(e);if(!s.options.formStatus)return;if(s.status===t)return;s.status=t,console.log("Setting status: ",t);const a=s.element.querySelector(".fstatus");a.hidden=!1;const i=a.querySelector(".message");i.textContent="",a.querySelector(".icon")?.remove();const n={saving:"Saving changes...",autosaved:"Changes saved locally. Submit form to send to server.",uploading:"Uploading your form to server",submitted:"Successfully sent to server",pending:"Unsaved changes",error:"Failed to save changes. Refresh and try again?",offline:"Changes will be saved when online"};let o=window.getIcon({autosaved:"check-circle",submitted:"check-circle",error:"close-circle",offline:"cloud-slash",pending:"exclamation-mark"}[t]);o&&a.prepend(o),""===r&&(r=n[t]||t),i.textContent=r,a.classList.toggle("loading",["uploading","saving"].includes(t)),"submitted"===t&&setTimeout((()=>a.hidden=!0),3e3)}cleanupSpecialFields(){this.specialFields.forEach((e=>{if("quill"===e.type&&e.instance){const t=e.instance.container.previousSibling;t?.classList.contains("ql-toolbar")&&t.remove()}})),this.uploader?.destroy(),this.specialFields.clear()}collectFormData(e){if(Object.hasOwn(e.dataset,"timeline"))return this.collectTimeline(e);const t=new FormData(e);let r={};const s={},a={};for(let[i,n]of t.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;this.getFieldProcessor(i)(i,n,r,s,a,e)}return window.isEmptyObject(a)?this.mergeRepeaterData(r,s):(r=this.mergeRepeaterData(r,s),this.mergePostData(r,a))}collectTimeline(e){console.log("Collecting Timeline data:");let t={},r={},s=[],a=new FormData(e);for(const[i,n]of a.entries()){if(this.ignore.includes(i)||i.endsWith("_temp"))continue;const a=i.match(/^\[(\d+)\](.+)$/);if(a){const[,t,i]=a;r[t]||(r[t]={id:parseInt(t)},s.push(t));this.getFieldProcessor(i)(i,n,r[t],{},{},e)}else{this.getFieldProcessor(i)(i,n,t,{},{},e)}}return t.timeline=s.map((e=>r[e])),delete t["form-id"],delete t.sendAll,delete t.timeline_temp,delete t[""],console.log("Data: ",t),t}getFieldProcessor(e){return e.includes("|")?this.processTableField:e.includes("::")?this.processGroupField:e.includes(":")?this.processRepeaterField:/\[[^\]]+\]/.test(e)?this.processLocationField:this.processRegularField}mergeRepeaterData(e,t){return Object.keys(t).forEach((r=>{const s={};Object.keys(t[r]).forEach((e=>{const a=t[r][e];Object.keys(a).length>0&&(s[e]=a)})),e[r]=Object.values(s)})),e}mergePostData(e,t){for(let[t,r]in Object.entries(r))e[t]=r;return e}processTableField(e,t,r,s,a,i){let[n,o]=e.split("|");!n in a&&(a[n]={});this.getFieldProcessor(o)(o,t,a,s,a,i)}processRepeaterField(e,t,r,s,a,i){let[n,o,l]=e.split(":");const c=l.endsWith("[]");l=l.replace("[]",""),s[n]||(s[n]={}),s[n][o]||(s[n][o]={}),c||s[n][o][l]?(s[n][o][l]?Array.isArray(s[n][o][l])||(s[n][o][l]=[s[n][o][l]]):s[n][o][l]=[],s[n][o][l].push(t)):s[n][o][l]=t}processGroupField(e,t,r,s,a,i){const n=e.split("::"),o=n[0];r[o]||(r[o]={});let l=r[o];for(let e=1;e<n.length-1;e++){const t=n[e];l[t]||(l[t]={}),l=l[t]}const c=n[n.length-1];void 0!==l[c]?(Array.isArray(l[c])||(l[c]=[l[c]]),l[c].push(t)):l[c]=t}processLocationField(e,t,r,s,a,i){let[n,o]=e.split("[");o=o.replace("]",""),Object.hasOwn(r,n)||(r[n]={},Object.hasOwn(r,"sendAll")?r.sendAll.includes(n)||r.sendAll.push(n):r.sendAll=[n]),r[n][o]=t}processRegularField(e,t,r,s,a,i){r[e=e.replace("[]","")]?(Array.isArray(r[e])||(r[e]=[r[e]]),r[e].push(t)):r[e]=t}getFieldValue(e){if(!e)return"";if("checkbox"===e.type)return e.checked?e.value||"1":"";if("radio"===e.type){const t=e.form.querySelector(`[name="${e.name}"]:checked`);return t?t.value:""}return"select-multiple"===e.type?Array.from(e.selectedOptions).map((e=>e.value)):e.value}getChangedFields(e,t){return window.getDifferences?.map(e,t)||{}}showSummary(e,t="form"){const r=this.forms.get(e);if(!r)return;const s=r.element||document.querySelector(`[data-form-id="${e}"]`),a=window.getTemplate("formSummary"),[i,n,o]=[a.querySelector("h2"),a.querySelector(".summary"),a.querySelector(".result")],l=["sendAll",...this.ignore];for(const[e,t]of Object.entries(r.data)){if(l.includes(e)||this.isEmptyValue(t))continue;const r=this.getFieldInfo(s,e);if(!r.label)continue;const a=this.createResultElement(o,r,t,s);a&&n.appendChild(a)}o.remove(),(t="form"!==t?s.closest(t)??s:s).after(a),window.fade(t,!1)}isEmptyValue(e){return null==e||""===e||(!(!Array.isArray(e)||0!==e.length)||"object"==typeof e&&0===Object.keys(e).length)}getFieldInfo(e,t){let r=e.querySelector(`label[for="${t}"]`),s=null,a=null;if(s||(s=e.querySelector(`[name="${t}"]`)),s||(s=e.querySelector(`[name="${t}[]"]`)),!s){const a=e.querySelector(`fieldset[data-field="${t}"]`);a&&(r=a.querySelector("legend"),s=a.querySelector("input, select, textarea"))}if(!r&&s){const e=s.closest(".field, fieldset");e&&(r=e.querySelector("label, legend"))}a=e.querySelector(`.field[data-field="${t}"], fieldset[data-field="${t}"]`);let i="text";return a?.dataset.type?i=a.dataset.type:s&&(i="checkbox"===s.type&&s.name.endsWith("[]")?"checkbox":"checkbox"===s.type?"true_false":"SELECT"===s.tagName&&s.multiple?"select":s.type||"text"),{label:r?.textContent.replace("*","").trim()||null,type:i,wrapper:a,input:s}}createResultElement(e,t,r,s){const a=e.cloneNode(!0),i=a.querySelector("h4"),n=a.querySelector("p");i.textContent=t.label;const o=this.formatFieldValue(r,t.type,s);return this.isHtmlContent(o)?n.innerHTML=o:n.textContent=o,a}isHtmlContent(e){return"string"==typeof e&&(e.includes("<br>")||e.includes("<p>")||e.includes("<ul>")||e.includes("<ol>")||e.includes("<a ")||e.includes("<strong>")||e.includes("<em>")||e.includes("<div"))}formatFieldValue(e,t,r){switch(t){case"textarea":case"wysiwyg":return this.formatTextareaValue(e,t);case"true_false":return"1"===e||1===e||!0===e?"Yes":"No";case"checkbox":return Array.isArray(e)?this.formatArrayValue(e):"1"===e||1===e||!0===e?"Yes":"No";case"select":return Array.isArray(e)?this.formatArrayValue(e):this.getSelectLabel(e,r,t);case"date":case"datetime":case"time":return window.formatDate?window.formatDate(e):e;case"radio":return this.getSelectLabel(e,r,t);case"repeater":return this.formatRepeaterValue(e);case"group":return this.formatGroupValue(e);case"location":return this.formatLocationValue(e);case"file":case"image":return this.formatFileValue(e);case"number":return this.formatNumber(e);case"email":return`<a href="mailto:${e}">${e}</a>`;case"url":return`<a href="${e}" target="_blank" rel="noopener">${e}</a>`;case"phone":return`<a href="tel:${e.replace(/\D/g,"")}">${e}</a>`;default:return Array.isArray(e)?this.formatArrayValue(e):e}}formatRepeaterValue(e){if(!Array.isArray(e)||0===e.length)return"<em>No entries</em>";let t='<div class="repeater-summary">';return e.forEach(((e,r)=>{t+='<div class="repeater-row">',t+=`<strong>Entry ${r+1}:</strong><ul>`;for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));t+=`<li><strong>${e}:</strong> ${s}</li>`}t+="</ul></div>"})),t+="</div>",t}formatGroupValue(e){if("object"!=typeof e||0===Object.keys(e).length)return"<em>No data</em>";let t='<div class="group-summary"><ul>';for(const[r,s]of Object.entries(e))if(!this.isEmptyValue(s)){const e=r.replace(/_/g," ").replace(/\b\w/g,(e=>e.toUpperCase()));"object"!=typeof s||Array.isArray(s)?t+=`<li><strong>${e}:</strong> ${s}</li>`:t+=`<li><strong>${e}:</strong> ${this.formatGroupValue(s)}</li>`}return t+="</ul></div>",t}formatLocationValue(e){if("object"!=typeof e)return e;const t=[];return["address","city","state","zip","country"].forEach((r=>{e[r]&&t.push(e[r])})),t.join(", ")}formatFileValue(e){return"string"==typeof e?e.startsWith("http")?`<a href="${e}" target="_blank">View file</a>`:e:Array.isArray(e)?e.map((e=>"string"==typeof e?`<a href="${e}" target="_blank">View file</a>`:e.name||"File")).join(", "):"File uploaded"}formatNumber(e){const t=parseFloat(e);return isNaN(t)?e:e.toString().includes(".")&&2===e.toString().split(".")[1].length?new Intl.NumberFormat("en-CA",{style:"currency",currency:"USD"}).format(t):new Intl.NumberFormat("en-CA").format(t)}formatArrayValue(e,t=null,r=null){if(0===e.length)return"<em>None selected</em>";if(t&&r&&r.input){return"<ul><li>"+e.map((e=>this.getSelectLabel(e,t,r.type))).join("</li><li>")+"</li></ul>"}return"<ul><li>"+e.join("</li><li>")+"</li></ul>"}getSelectLabel(e,t,r){if("select"===r){const r=t.querySelector(`option[value="${e}"]`);return r?.textContent||e}if("radio"===r){const r=t.querySelector(`input[type="radio"][value="${e}"]`),s=r?.nextElementSibling;return s?.textContent||e}if("checkbox"===r){const r=t.querySelector(`input[type="checkbox"][value="${e}"]`);if(r){const e=t.querySelector(`label[for="${r.id}"]`);if(e)return e.textContent.trim();const s=r.nextElementSibling;if("LABEL"===s?.tagName)return s.textContent.trim()}}return e}formatTextareaValue(e,t){return e?"wysiwyg"===t||this.containsHtml(e)?e:this.formatPlainText(e):"<em>Empty</em>"}containsHtml(e){return/<(p|strong|em|u|s|ol|ul|li|blockquote|h[1-6]|a|br|span)\b[^>]*>/i.test(e)}formatPlainText(e){if(!e)return"";const t=(e=e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")).split(/\n\n+/);return t.length>1?t.map((e=>`<p>${e.replace(/\n/g,"<br>")}</p>`)).join(""):e.replace(/\n/g,"<br>")}nl2br(e){return this.formatPlainText(e)}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((r=>r(e,t)))}cleanupForm(e){const t=this.forms.get(e);t&&(this.hasUnsavedChanges(e)&&this.autosave(t),this.cleanupSpecialFields(),this.forms.delete(e))}destroy(){this.globalHandlersAdded&&(document.removeEventListener("change",this.changeHandler),document.removeEventListener("focus",this.focusHandler,!0),document.removeEventListener("blur",this.blurHandler,!0),document.removeEventListener("input",this.inputHandler,!0)),this.forms.forEach((e=>{let t=e.element;t&&t.removeEventListener("submit",this.submitHandler)})),this.specialFields.clear(),this.forms.clear(),this.activeRepeaters.clear(),this.forms&&this.forms.clear()}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbForm=e}))})(); |
| | |
| | | window.jvbGallery=class{constructor(e,t){this.imageWrapper=t.imageWrapper??null,this.container=t.container?document.querySelector(t.container):document.querySelector("main"),this.modal=new window.jvbModal(e,{onOpen:!1,onSave:!1,onClose:()=>this.closeGallery(!1)}),this.items=this.getGalleryItems()||[],this.loadMore=t.loadMore??!1,this.modalElement="string"==typeof e?document.querySelector(e)??!1:e,this.modal&&(this.a11y=window.jvbA11y,this.openWhenReady=!1,this.initElements(),this.imageWrapper="string"==typeof this.imageWrapper?this.imageWrapper:"."+this.imageWrapper.classList.join("."),this.index=0,this.swipe={touchStart:null,touchEnd:null,minSwipe:50},this.isLoading=!1)}initElements(){this.prevBtn=this.modalElement.querySelector(".prev"),this.nextBtn=this.modalElement.querySelector(".next"),this.favourite=this.modalElement.querySelector(".favourite"),this.image=this.modalElement.querySelector(".image"),this.counter=this.modalElement.querySelector(".counter"),this.extra=this.modalElement.querySelector(".item-info"),this.imageWrapper||(this.favourite.remove(),this.extra.remove())}getGalleryItems(){let e=this.imageWrapper?this.imageWrapper:"img";return Array.from(this.container.querySelectorAll(e)).map((e=>{const t=this.imageWrapper?e.querySelector("img"):e;return t?{id:this.imageWrapper?e.querySelector("button.favourite").dataset.id:"",small:t.dataset.small||t.src,large:t.dataset.large||t.src,full:t.dataset.full||t.src,alt:t.alt||"",fav:this.imageWrapper?e.querySelector("button.favourite")?.cloneNode(!0):"",info:this.imageWrapper?e.querySelector(".item-info")?.cloneNode(!0):""}:null})).filter(Boolean)}initListeners(){this.container.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("keydown",this.handleKeys.bind(this)),document.addEventListener("touchstart",this.handleTouchStart.bind(this)),document.addEventListener("touchend",this.handleTouchEnd.bind(this)),document.addEventListener("touchmove",this.handleTouchMove.bind(this))}destroyListeners(){this.container.removeEventListener("click",this.handleClick.bind(this)),document.removeEventListener("keydown",this.handleKeys.bind(this)),document.removeEventListener("touchstart",this.handleTouchStart.bind(this)),document.removeEventListener("touchend",this.handleTouchEnd.bind(this)),document.removeEventListener("touchmove",this.handleTouchMove.bind(this))}handleKeys(e){switch(e.key){case"ArrowLeft":this.navigate(-1);break;case"ArrowRight":this.navigate(1)}}handleTouchStart(e){this.swipe.touchStart=e.touches[0].clientX}handleTouchMove(e){this.swipe.touchEnd=e.touches[0].clientX}handleTouchEnd(e){if(!this.swipe.touchStart||!this.swipe.touchEnd)return;const t=this.swipe.touchStart-this.swipe.touchEnd,i=t>this.swipe.minSwipe,s=t<-this.swipe.minSwipe;i?this.navigate(1):s&&this.navigate(-1),this.swipe.touchStart=null,this.swipe.touchEnd=null}handleClick(e){window.targetCheck(e,".feed-images")&&this.handleGalleryOpen(e),window.targetCheck(e,".nav")&&(window.targetCheck(e,".next")?this.navigate(1):this.navigate(-1)),window.targetCheck(e,"button.cancel")&&this.closeGallery()}async navigate(e){let t=this.index+e;(t<0||t>=this.items.length)&&(this.a11y.announceNavigation(t,this.items.length,e<0,e>0),t=t<0?this.items.length-1:0),this.index=t,this.updateDisplay(),this.preloadImages(),this.a11y.announceNavigation(this.index,this.items.length),"function"==typeof this.loadMore&&e>0&&t>=this.items.length-3&&window.feedBlock.hasMore&&(await this.loadMore(),this.updateGalleryItems(this.getGalleryItems()))}updateGalleryItems(e){const t=this.items[this.index];if(this.items=e,t){const e=this.items.findIndex((e=>e.full===t.full||e.large===t.large));-1!==e&&(this.index=e)}if(this.updateNavigationButtons(),this.items.length>0&&this.openWhenReady){let e=this.findIndex("id",this.openWhenReady);this.openGallery(e)}}preloadImages(){[-1,0,1].forEach((e=>{const t=this.index+e;if(t>=0&&t<this.items.length){const e=new Image,i=this.items[t];window.innerWidth<1e3?e.src=i.large||i.src:e.src=i.full||i.src}}))}updateDisplay(){const e=this.items[this.index];e&&([this.image.src,this.image.alt,this.counter.textContent]=[window.innerWidth<1e3?e.large||e.src:e.full||e.src,e.alt||"",`${this.index+1} / ${this.items.length}`],this.imageWrapper&&(e.fav&&(window.removeChildren(this.favourite),this.favourite.appendChild(e.fav.cloneNode(!0))),e.info&&(window.removeChildren(this.extra),this.extra.appendChild(e.info.cloneNode(!0)))),this.updateNavigationButtons())}updateNavigationButtons(){this.prevBtn.classList.toggle("end",this.index>0?"":"none"),this.nextBtn.classList.toggle("end",this.index<this.items.length-1?"":"none")}handleGalleryOpen(e){let t=this.imageWrapper?e.target.closest(this.imageWrapper):e.target.closest("img"),i=this.imageWrapper?"id":"small",s=t.dataset[i],n=this.findIndex(i,s);this.openGallery(n)}findIndex(e,t){return this.items.findIndex((i=>i[e]===t))}openGallery(e){this.initListeners(),this.index=e,this.modal.handleOpen(),this.updateDisplay(),this.preloadImages(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}. Use arrow keys to navigate.`)}closeGallery(e=!0){this.destroyListeners(),e&&this.modal.handleClose()}}; |
| | | (()=>{class e{constructor(e,t={}){this.container=t.container?document.querySelector(t.container):document.querySelector("main"),this.gallerySelector=t.gallerySelector||"img[data-small]",this.modal=new window.jvbModal(e,{onOpen:!1,onSave:!1,onClose:()=>this.closeGallery(!1)}),this.modalElement="string"==typeof e?document.querySelector(e):e,this.modalElement&&(this.a11y=window.jvbA11y,this.index=0,this.items=[],this.swipe={touchStart:null,touchEnd:null,minSwipe:50},this.initElements(),this.initListeners())}initElements(){this.prevBtn=this.modalElement.querySelector(".prev"),this.nextBtn=this.modalElement.querySelector(".next"),this.image=this.modalElement.querySelector(".image"),this.counter=this.modalElement.querySelector(".counter")}initListeners(){this.container.addEventListener("click",this.handleClick.bind(this)),this.boundKeyHandler=this.handleKeys.bind(this),this.boundTouchStart=this.handleTouchStart.bind(this),this.boundTouchMove=this.handleTouchMove.bind(this),this.boundTouchEnd=this.handleTouchEnd.bind(this)}handleClick(e){const t=e.target.closest(this.gallerySelector);if(t&&!this.modal.isOpen)return e.preventDefault(),void this.openGallery(t);this.modal.isOpen&&(e.target.closest(".next")?this.navigate(1):e.target.closest(".prev")&&this.navigate(-1))}handleKeys(e){if(this.modal.isOpen)switch(e.key){case"ArrowLeft":e.preventDefault(),this.navigate(-1);break;case"ArrowRight":e.preventDefault(),this.navigate(1)}}handleTouchStart(e){this.modal.isOpen&&(this.swipe.touchStart=e.touches[0].clientX)}handleTouchMove(e){this.modal.isOpen&&(this.swipe.touchEnd=e.touches[0].clientX)}handleTouchEnd(e){if(!this.modal.isOpen||!this.swipe.touchStart||!this.swipe.touchEnd)return;const t=this.swipe.touchStart-this.swipe.touchEnd;Math.abs(t)>this.swipe.minSwipe&&this.navigate(t>0?1:-1),this.swipe.touchStart=null,this.swipe.touchEnd=null}buildGalleryItems(){return Array.from(this.container.querySelectorAll(this.gallerySelector)).map(((e,t)=>({id:e.dataset.id||t,small:e.dataset.small||e.src,medium:e.dataset.medium||e.src,full:e.dataset.full||e.src,alt:e.alt||"",element:e})))}openGallery(e){this.items=this.buildGalleryItems(),this.index=this.items.findIndex((t=>t.element===e)),-1===this.index&&(this.index=0),document.addEventListener("keydown",this.boundKeyHandler),document.addEventListener("touchstart",this.boundTouchStart,{passive:!0}),document.addEventListener("touchmove",this.boundTouchMove,{passive:!0}),document.addEventListener("touchend",this.boundTouchEnd,{passive:!0}),this.modal.handleOpen(),this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Gallery opened. Image ${this.index+1} of ${this.items.length}. Use arrow keys to navigate.`)}closeGallery(e=!0){document.removeEventListener("keydown",this.boundKeyHandler),document.removeEventListener("touchstart",this.boundTouchStart),document.removeEventListener("touchmove",this.boundTouchMove),document.removeEventListener("touchend",this.boundTouchEnd),e&&this.modal.handleClose()}navigate(e){let t=this.index+e;t<0?t=this.items.length-1:t>=this.items.length&&(t=0),this.index=t,this.updateDisplay(),this.preloadAdjacent(),this.a11y.announce(`Image ${this.index+1} of ${this.items.length}`)}updateDisplay(){const e=this.items[this.index];if(!e)return;const t=window.innerWidth<1e3?e.medium||e.src:e.full||e.src;this.image.src=t,this.image.alt=e.alt,this.counter.textContent=`${this.index+1} / ${this.items.length}`,this.prevBtn.classList.toggle("disabled",this.items.length<=1),this.nextBtn.classList.toggle("disabled",this.items.length<=1)}preloadAdjacent(){[-1,1].forEach((e=>{const t=this.index+e;if(t>=0&&t<this.items.length){const e=this.items[t];(new Image).src=window.innerWidth<1e3?e.medium||e.src:e.full||e.src}}))}cleanup(){this.container.removeEventListener("click",this.handleClick),this.closeGallery(!1)}}document.addEventListener("DOMContentLoaded",(function(){let t=document.querySelectorAll("dialog.gallery");t.length>0&&(window.galleries=new Map,t.forEach((t=>{let i=t.id;window.galleries.set(i,new e(t))})))}))})(); |
| | |
| | | (()=>{class e{constructor(){this.currentWidth=window.innerWidth,this.images=document.querySelectorAll(".wp-site-blocks img[data-small]"),0!==this.images.length&&(this.loadVisibleImages(),this.initListeners())}loadVisibleImages(){this.images.forEach(((e,t)=>{const i=e.getBoundingClientRect(),s=i.top<window.innerHeight&&i.bottom>0;(0===t||s)&&(this.loadAppropriateImage(e),e.dataset.loaded="true")}))}initListeners(){this.resizeHandler=this.handleResize.bind(this),window.addEventListener("resize",this.resizeHandler),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&(this.loadAppropriateImage(e.target),this.observer.unobserve(e.target))}))}),{rootMargin:"50px",threshold:.1}),this.images.forEach((e=>{e.dataset.loaded||this.observer.observe(e)}))}handleResize(){window.debouncer.schedule("image-resize",(()=>{const e=window.innerWidth;Math.abs(e-this.currentWidth)>100&&(this.currentWidth=e,this.updateVisibleImages())}),150)}updateVisibleImages(){this.images.forEach((e=>{const t=e.getBoundingClientRect();t.top<window.innerHeight&&t.bottom>0&&this.loadAppropriateImage(e,!0)}))}loadAppropriateImage(e,t=!1){const i=this.getTargetSize(),s=e.dataset[i];s&&(t||s!==e.currentSrc)&&(e.src=s)}getTargetSize(){return this.currentWidth<768?"small":this.currentWidth<1200?"medium":"full"}cleanup(){this.observer?.disconnect(),window.removeEventListener("resize",this.resizeHandler)}}window.isLoaded=!1,document.addEventListener("readystatechange",(()=>{!window.isLoaded&&document.querySelector(".wp-site-blocks img")&&(window.jvbMedia=new e,window.isLoaded=!0)}))})(); |
| | |
| | | window.newsManager=class{constructor(){this.queue=window.jvbQueue,this.loading=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.activeTab="all",this.tabs=new window.jvbTabs(document.querySelector(".replace"),{news:()=>{this.activeTab="all",this.resetFilters(),this.loadItems(!0).then((()=>{}))},mine:()=>{console.log("switching to mine tab"),this.activeTab="own",this.resetFilters(),this.filters.artist=jvbSettings.currentUser,this.loadItems(!0).then((()=>{}))},watching:()=>{this.activeTab="watching",this.resetFilters(),this.filters.watched=!0,this.loadItems(!0).then((()=>{}))}}),this.isLoading=!1,this.alreadyHandling=!1,this.template=new Map,this.endpoints={news:"news",vote:"news/vote"},this.resetFilters(),this.state={hasMore:!0,pages:1,items:0},this.initElements(),this.initEvents(),this.loadItems()}resetFilters(){this.filters={page:1,order:"DESC",orderby:"date",shop:null,type:null,artist:null,watched:!1}}initElements(){this.container=document.querySelector(".replace"),this.grid=this.container.querySelector(".item-grid"),this.addButton=this.container.querySelector(".add-item-btn"),this.addModal=new window.jvbModal(this.container.querySelector(".create-modal"),{render:this.renderModal.bind(this),open:this.addButton,content:"news",openMessage:"Opened modal to create a news post.",onSave:this.saveModal.bind(this)}),this.filterForm=this.container.querySelector("form"),this.dateRangeFilter=new window.jvbModal(this.container.querySelector("dialog.date-range"),{open:!1}),this.clearFilters=this.container.querySelector(".clear-filters"),this.replyModal=new window.jvbModal(this.container.querySelector(".create-response"),{open:!1,content:"response",openMessage:"Opened Response modal",onSave:this.saveCreatedResponse.bind(this)})}initEvents(){this.filterForm.addEventListener("change",(e=>{let t=e.target.value;if(!e.target.closest(".date-range"))if("custom"===t)this.handleCustomDateRange();else{let s=e.target.name;s?this.filters[s]=t:this.resetFilters(),this.loadItems(!0)}})),document.addEventListener("click",(e=>{if(e.target===this.clearFilters&&(this.filterForm.reset(),this.resetFilters(),this.loadItems(!0)),e.target.closest("button.reply")){let t=e.target.closest("button"),s=t.closest(".item").dataset.id,n="";"news"===t.dataset.type?n=t.closest(".item").querySelector(".item-info").innerHTML:(n=t.closest(".response").querySelector(".content").innerHTML,this.replyModal.modal.dataset.parent_id=t.id.replace("reply-to","")),this.replyModal.modal.dataset.id=s,this.replyModal.modal.dataset.type=t.dataset.type,this.replyModal.modal.querySelector(".original").innerHTML="<h5>Replying to:</h5>"+n,this.replyModal.handleOpen()}}))}renderModal(){}handleCustomDateRange(){this.dateRangeFilter.handleOpen();let e=this.dateRangeFilter.modal.querySelector("input.date-start"),t=this.dateRangeFilter.modal.querySelector("input.date-end"),s=this.dateRangeFilter.modal.querySelector("select");this.dateRangeFilter.modal.querySelectorAll("input, select").forEach((n=>{n.addEventListener("change",(i=>{n===e&&""!==t.value||n===t&&""!==e.value?(this.filters.dateFrom=e.value,this.filters.dateTo=t.value,this.dateRangeFilter.handleClose(),this.loadItems(!0)):n===s&&(this.filters.customDate=s.value,this.dateRangeFilter.handleClose(),this.loadItems(!0))}))}))}async saveModal(e){const t=new FormData(this.addModal.modal.querySelector("form"));t.append("user",jvbSettings.currentUser),this.queue.addToQueue({type:"new_news",data:t})}async loadItems(e=!0){if(!this.isLoading)try{this.isLoading=!0,this.loading.show(),e&&(this.filters.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const t=this.buildFilters(),s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.endpoints.news}?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash}},{context:"news",forceRefresh:!0});return this.renderItems(s.items||[],this.filters.page>1),s.pagination&&(this.state={hasMore:s.has_more,items:s.items,pages:s.pages}),s}catch(e){throw this.handleError(e,"loading news"),e}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const e=JSON.parse(JSON.stringify(this.filters));let t={};for(var[s,n]of Object.entries(e))!1!==n&&null!==n&&(t[s]=n);return new URLSearchParams(t)}renderItems(e,t=!1){if(t||removeChildren(this.grid),0===e.length)return this.a11y.announceItems(0,t),void this.showEmptyState();const s=document.createDocumentFragment(),n=i=>{const a=Math.min(i+10,e.length);for(let t=i;t<a;t++){const n=e[t],i=this.createItemElement(n);s.appendChild(i)}a<e.length?requestAnimationFrame((()=>{n(a)})):(this.grid.appendChild(s),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.state.hasMore))};e.length>0?n(0):this.a11y.announceItems(0,t)}createItemElement(e){const t=window.getTemplate(`template-${this.activeTab}`);t.id=`news-${e.id}`,t.dataset.id=e.id;const[s]=t.getElementsByTagName("h3"),[n]=t.getElementsByClassName("published"),[i]=t.getElementsByClassName("artist"),[a]=t.getElementsByClassName("shop"),[r]=t.getElementsByClassName("tldr"),[o]=t.getElementsByClassName("item-info"),[l]=t.getElementsByClassName("image");[s.textContent,n.textContent,i.href,i.textContent,r.textContent,o.innerHTML]=[e.title,formatTimeAgo(e.date),e.artist.url,e.artist.name,e.tldr,e.post_content],e.shop?[a.href,a.innerHTML]=[e.shop.url,jvbSettings.icons.shop+e.shop.name]:a.hidden=!0;const[d]=t.getElementsByClassName("favourite-button");if("own"!==this.activeTab)[d.dataset.id,d.dataset.artist]=[e.id,e.artist.id],window.userFavourites.news?.includes(parseInt(e.id))?(removeChildren(d),d.append(getIcon("star-filled"))):(removeChildren(d),d.append(getIcon("star")));else{d.hidden=!0;const[s]=t.getElementsByClassName("select-checkbox"),[n]=t.getElementsByTagName("label");[s.id,s.value,n.for]=[`item-${e.id}`,e.id,`item-${e.id}`]}let h="";window.userVotes?.news?.has(e.id)&&(h=window.userVotes.news.get(e.id)),console.log(e),t.querySelector(".summary").appendChild(formatVote(e,h));let c=window.getTemplate("commentsButton");c.href=`#responses-to-${e.id}`,c.querySelector(".count").textContent=e.comments.items.length;let m=window.getTemplate("responses");m.id=`responses-to-${e.id}`,m.querySelector("summary").textContent+=" { "+e.comments.items.length+" }";let p=window.getTemplate("replyButton");return p.id="reply-to-"+e.id,p.dataset.type="news",p.dataset.action="reply",t.appendChild(p),e.comments.items.length>0&&e.comments.items.forEach((e=>{m.appendChild(this.formatComment(e))})),t.appendChild(m),t.querySelector(".vote").prepend(c,t.querySelector(".vote button")),e.image&&e.image.replace(/src="([^"]+)"/,'data-src="$1"'),t}formatComment(e,t=null){let s=window.getTemplate("response");s.id="response-"+e.id;let n=s.querySelector("summary");n.querySelector(".content").innerHTML=e.response,n.querySelector(".created").textContent=formatTimeAgo(e.created_at);let i=checkVoteStatus("response",e.id);e.content="response",s.querySelector(".footer").appendChild(formatVote(e,i)),console.log(e);let a=window.getTemplate("replyButton");a.id="reply-to-"+e.id,t&&(a.dataset.parent_id=t),a.dataset.action="reply",a.dataset.type=e.content,n.querySelector(".vote").prepend(a,n.querySelector(".vote").firstElementChild);let r=n.querySelector(".artist"),o=n.querySelector(".shop");if(console.log(e),e.artist?(e.artist.shop||o.remove(),[r.href,r.textContent,o.href,o.textContent]=[e.artist.url,e.artist.name,e.artist.shop.url,e.artist.shop.name]):(r.remove(),o.remove()),e.children.items.length>0){let t=window.getTemplate("responses");t.id="replies-to-"+e.id,t.querySelector("summary").textContent="See Responses {"+e.children.items.length+"}",e.children.items.forEach((s=>{t.appendChild(this.formatComment(s,e.id))})),s.appendChild(t)}return s}renderResponseCreate(){}saveCreatedResponse(){console.log("Saving create response"),console.log(this.replyModal.modal.id);const e=this.replyModal.modal;let t={user:jvbSettings.currentUser,item_id:e.dataset.id,response:e.querySelector(".ql-editor").innerHTML,content:e.dataset.type,action:"create"};e.dataset.parent_id&&(t.parent_id=e.dataset.parent_id),console.log(t),this.queue.addToQueue({type:"new_response",data:t})}showEmptyState(){const e=document.createElement("div");e.className="no-news",e.innerHTML="\n <h3>Nothing here</h3>\n <p>No updates here.</p>\n <p>Add some gap fillers from the main favourites tab.</p>\n ",this.grid.appendChild(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){let e=this.grid.querySelector(".no-news");e&&e.remove()}handleError(e,t){console.error(`News error (${t}):`,e),window.jvbError&&window.jvbError.log(e,{component:"NewsManager",action:t}),window.jvbA11y&&window.jvbA11y.announce(`Error ${t}. ${e.message||"Please try again."}`)}}; |
| | | window.newsManager=class{constructor(){this.queue=window.jvbQueue,this.loading=window.jvbLoading,this.cache=window.jvbCache,this.a11y=window.jvbA11y,this.error=window.jvbError,this.activeTab="all",this.tabs=new window.jvbTabs(document.querySelector(".replace"),{news:()=>{this.activeTab="all",this.resetFilters(),this.loadItems(!0).then((()=>{}))},mine:()=>{console.log("switching to mine tab"),this.activeTab="own",this.resetFilters(),this.filters.artist=jvbSettings.currentUser,this.loadItems(!0).then((()=>{}))},watching:()=>{this.activeTab="watching",this.resetFilters(),this.filters.watched=!0,this.loadItems(!0).then((()=>{}))}}),this.isLoading=!1,this.alreadyHandling=!1,this.template=new Map,this.endpoints={news:"news",vote:"news/vote"},this.resetFilters(),this.state={hasMore:!0,pages:1,items:0},this.initElements(),this.initEvents(),this.loadItems()}resetFilters(){this.filters={page:1,order:"DESC",orderby:"date",shop:null,type:null,artist:null,watched:!1}}initElements(){this.container=document.querySelector(".replace"),this.grid=this.container.querySelector(".item-grid"),this.addButton=this.container.querySelector(".add-item-btn"),this.addModal=new window.jvbModal(this.container.querySelector(".create-modal"),{render:this.renderModal.bind(this),open:this.addButton,content:"news",openMessage:"Opened modal to create a news post.",onSave:this.saveModal.bind(this)}),this.filterForm=this.container.querySelector("form"),this.dateRangeFilter=new window.jvbModal(this.container.querySelector("dialog.date-range"),{open:!1}),this.clearFilters=this.container.querySelector(".clear-filters"),this.replyModal=new window.jvbModal(this.container.querySelector(".create-response"),{open:!1,content:"response",openMessage:"Opened Response modal",onSave:this.saveCreatedResponse.bind(this)})}initEvents(){this.filterForm.addEventListener("change",(e=>{let t=e.target.value;if(!e.target.closest(".date-range"))if("custom"===t)this.handleCustomDateRange();else{let s=e.target.name;s?this.filters[s]=t:this.resetFilters(),this.loadItems(!0)}})),document.addEventListener("click",(e=>{if(e.target===this.clearFilters&&(this.filterForm.reset(),this.resetFilters(),this.loadItems(!0)),e.target.closest("button.reply")){let t=e.target.closest("button"),s=t.closest(".item").dataset.id,n="";"news"===t.dataset.type?n=t.closest(".item").querySelector(".item-info").innerHTML:(n=t.closest(".response").querySelector(".content").innerHTML,this.replyModal.modal.dataset.parent_id=t.id.replace("reply-to","")),this.replyModal.modal.dataset.id=s,this.replyModal.modal.dataset.type=t.dataset.type,this.replyModal.modal.querySelector(".original").innerHTML="<h5>Replying to:</h5>"+n,this.replyModal.handleOpen()}}))}renderModal(){}handleCustomDateRange(){this.dateRangeFilter.handleOpen();let e=this.dateRangeFilter.modal.querySelector("input.date-start"),t=this.dateRangeFilter.modal.querySelector("input.date-end"),s=this.dateRangeFilter.modal.querySelector("select");this.dateRangeFilter.modal.querySelectorAll("input, select").forEach((n=>{n.addEventListener("change",(i=>{n===e&&""!==t.value||n===t&&""!==e.value?(this.filters.dateFrom=e.value,this.filters.dateTo=t.value,this.dateRangeFilter.handleClose(),this.loadItems(!0)):n===s&&(this.filters.customDate=s.value,this.dateRangeFilter.handleClose(),this.loadItems(!0))}))}))}async saveModal(e){const t=new FormData(this.addModal.modal.querySelector("form"));t.append("user",jvbSettings.currentUser),this.queue.addToQueue({type:"new_news",data:t})}async loadItems(e=!0){if(!this.isLoading)try{this.isLoading=!0,this.loading.show(),e&&(this.filters.page=1,removeChildren(this.grid),this.grid.classList.remove("empty"));const t=this.buildFilters(),s=await this.cache.fetchWithCache(`${jvbSettings.api}${this.endpoints.news}?${t.toString()}`,{method:"GET",headers:{"X-WP-Nonce":jvbSettings.nonce,action_nonce:jvbSettings.dash}},{context:"news",forceRefresh:!0});return this.renderItems(s.items||[],this.filters.page>1),s.pagination&&(this.state={hasMore:s.has_more,items:s.items,pages:s.pages}),s}catch(e){throw this.handleError(e,"loading news"),e}finally{this.isLoading=!1,this.loading.hide()}}buildFilters(){const e=JSON.parse(JSON.stringify(this.filters));let t={};for(var[s,n]of Object.entries(e))!1!==n&&null!==n&&(t[s]=n);return new URLSearchParams(t)}renderItems(e,t=!1){if(t||removeChildren(this.grid),0===e.length)return this.a11y.announceItems(0,t),void this.showEmptyState();const s=document.createDocumentFragment(),n=i=>{const a=Math.min(i+10,e.length);for(let t=i;t<a;t++){const n=e[t],i=this.createItemElement(n);s.appendChild(i)}a<e.length?requestAnimationFrame((()=>{n(a)})):(this.grid.appendChild(s),this.a11y.makeNavigable(this.grid.querySelectorAll(".item:not([data-keyboard-nav])")),this.a11y.announceItems(e.length,t,this.state.hasMore))};e.length>0?n(0):this.a11y.announceItems(0,t)}createItemElement(e){const t=window.getTemplate(`template-${this.activeTab}`);t.id=`news-${e.id}`,t.dataset.id=e.id;const[s]=t.getElementsByTagName("h3"),[n]=t.getElementsByClassName("published"),[i]=t.getElementsByClassName("artist"),[a]=t.getElementsByClassName("shop"),[r]=t.getElementsByClassName("tldr"),[o]=t.getElementsByClassName("item-info"),[l]=t.getElementsByClassName("image");[s.textContent,n.textContent,i.href,i.textContent,r.textContent,o.innerHTML]=[e.title,formatTimeAgo(e.date),e.artist.url,e.artist.name,e.tldr,e.post_content],e.shop?[a.href,a.innerHTML]=[e.shop.url,jvbSettings.icons.shop+e.shop.name]:a.hidden=!0;const[d]=t.getElementsByClassName("favourite-button");if("own"!==this.activeTab)[d.dataset.id,d.dataset.artist]=[e.id,e.artist.id],window.userFavourites.news?.includes(parseInt(e.id))?(removeChildren(d),d.append(getIcon("star-fi"))):(removeChildren(d),d.append(getIcon("star")));else{d.hidden=!0;const[s]=t.getElementsByClassName("select-checkbox"),[n]=t.getElementsByTagName("label");[s.id,s.value,n.for]=[`item-${e.id}`,e.id,`item-${e.id}`]}let h="";window.userVotes?.news?.has(e.id)&&(h=window.userVotes.news.get(e.id)),console.log(e),t.querySelector(".summary").appendChild(formatVote(e,h));let c=window.getTemplate("commentsButton");c.href=`#responses-to-${e.id}`,c.querySelector(".count").textContent=e.comments.items.length;let m=window.getTemplate("responses");m.id=`responses-to-${e.id}`,m.querySelector("summary").textContent+=" { "+e.comments.items.length+" }";let p=window.getTemplate("replyButton");return p.id="reply-to-"+e.id,p.dataset.type="news",p.dataset.action="reply",t.appendChild(p),e.comments.items.length>0&&e.comments.items.forEach((e=>{m.appendChild(this.formatComment(e))})),t.appendChild(m),t.querySelector(".vote").prepend(c,t.querySelector(".vote button")),e.image&&e.image.replace(/src="([^"]+)"/,'data-src="$1"'),t}formatComment(e,t=null){let s=window.getTemplate("response");s.id="response-"+e.id;let n=s.querySelector("summary");n.querySelector(".content").innerHTML=e.response,n.querySelector(".created").textContent=formatTimeAgo(e.created_at);let i=checkVoteStatus("response",e.id);e.content="response",s.querySelector(".footer").appendChild(formatVote(e,i)),console.log(e);let a=window.getTemplate("replyButton");a.id="reply-to-"+e.id,t&&(a.dataset.parent_id=t),a.dataset.action="reply",a.dataset.type=e.content,n.querySelector(".vote").prepend(a,n.querySelector(".vote").firstElementChild);let r=n.querySelector(".artist"),o=n.querySelector(".shop");if(console.log(e),e.artist?(e.artist.shop||o.remove(),[r.href,r.textContent,o.href,o.textContent]=[e.artist.url,e.artist.name,e.artist.shop.url,e.artist.shop.name]):(r.remove(),o.remove()),e.children.items.length>0){let t=window.getTemplate("responses");t.id="replies-to-"+e.id,t.querySelector("summary").textContent="See Responses {"+e.children.items.length+"}",e.children.items.forEach((s=>{t.appendChild(this.formatComment(s,e.id))})),s.appendChild(t)}return s}renderResponseCreate(){}saveCreatedResponse(){console.log("Saving create response"),console.log(this.replyModal.modal.id);const e=this.replyModal.modal;let t={user:jvbSettings.currentUser,item_id:e.dataset.id,response:e.querySelector(".ql-editor").innerHTML,content:e.dataset.type,action:"create"};e.dataset.parent_id&&(t.parent_id=e.dataset.parent_id),console.log(t),this.queue.addToQueue({type:"new_response",data:t})}showEmptyState(){const e=document.createElement("div");e.className="no-news",e.innerHTML="\n <h3>Nothing here</h3>\n <p>No updates here.</p>\n <p>Add some gap fillers from the main favourites tab.</p>\n ",this.grid.appendChild(e),this.grid.classList.add("empty"),this.a11y.announce("No favourites to show!")}hideEmptyState(){let e=this.grid.querySelector(".no-news");e&&e.remove()}handleError(e,t){console.error(`News error (${t}):`,e),window.jvbError&&window.jvbError.log(e,{component:"NewsManager",action:t}),window.jvbA11y&&window.jvbA11y.announce(`Error ${t}. ${e.message||"Please try again."}`)}}; |
| | |
| | | window.jvbPopulate=class{constructor(e,t={},a={},l={}){console.log("Populating field... ",e),console.log("fieldData: ",t),console.log("imageData: ",a),console.log("options: ",l);for(let[r,o]of Object.entries(t)){let t=e.querySelector(`[data-field="${r}"]`);t&&this.populateField(t,r,o,a,l)}}populateField(e,t,a,l={},r={}){if(e&&null!=a)switch(this.getFieldType(e)){case"image":this.populateImageField(e,t,a,l);break;case"gallery":this.populateGalleryField(e,t,a,l);break;case"repeater":this.populateRepeaterField(e,t,a,r);break;case"taxonomy":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}getFieldType(e){const t=["image","gallery","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;if(e.dataset.type)return e.dataset.type;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateTextField(e,t,a){const l=e.querySelector(`[name="${t}"], input, textarea`);if(l&&(l.value=String(a||""),l.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=l.value.length)}}populateTextareaField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(l){if(l.value,l.value=String(a||""),l.dispatchEvent(new Event("change",{bubbles:!0})),l.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=l.value.length;const a=parseInt(l.dataset.limit,10);l.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="number"]`);l&&(l.value=Number(a)||0)}populateBooleanField(e,t,a){const l=e.querySelector(`[name="${t}"], input[type="checkbox"]`);l&&(l.checked=Boolean(a))}populateSelectField(e,t,a){const l=String(a||""),r=e.querySelector(`select[name="${t}"]`);if(r)return void(r.value=l);const o=e.querySelector(`input[type="radio"][name="${t}"][value="${l}"]`);o&&(o.checked=!0)}populateSetField(e,t,a){let l=a;if("string"==typeof a)try{l=JSON.parse(a)}catch(e){l=a.split(",").map((e=>e.trim()))}Array.isArray(l)||(l=[String(l)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=l.includes(e.value)}))}populateDateField(e,t,a){const l=e.querySelector(`[name="${t}"], input`);if(l&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(l.type){case"date":l.value=t.toISOString().split("T")[0];break;case"time":l.value=t.toTimeString().slice(0,5);break;case"datetime-local":l.value=t.toISOString().slice(0,16);break;default:l.value=e}}catch(t){l.value=e}}}populateEditorField(e,t,a){const l=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!l)return void console.warn(`Editor field ${t}: textarea not found`);const r=String(a||"");l.value=r;const o=e.querySelector(".editor");if(o){let e=null;if(o.__quill)e=o.__quill;else if(o.quill)e=o.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(o);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===o){e=t;break}e?(e.root.innerHTML=r,o.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),o.innerHTML=r)}else console.warn(`Editor container not found for ${t}`);l.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((l=>{if(void 0!==a[l]){const r=e.querySelector(`[name="${t}_${l}"], [name="${l}"]`);r&&(r.value=String(a[l]||""))}}))}populateTaxonomyField(e,t,a){let l=[];if(Array.isArray(a))l=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);l=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){l=a.split(",").map((e=>e.trim()))}else a&&(l=[String(a)]);if(0===l.length)return;const r=e.querySelector(`input[type="hidden"][name="${t}"]`);r&&(r.value=l.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateImageField(e,t,a,l={}){if(!a)return;const r=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===r.length)return;const o=e.querySelector(`input[type="hidden"][name="${t}"]`);o&&(o.value=r.join(","));const i=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),i&&(window.removeChildren(i),r.forEach((e=>{let t=window.getTemplate("uploadItem"),a=t.querySelector("img"),r=t.querySelector("details"),o=window.getTemplate("uploadMeta");r.append(o),[a.src,a.alt,t.querySelector('[name="image-title"]').value,t.querySelector('[name="image-alt-text"]').value,t.querySelector('[name="image-caption"]').value]=[l[e].medium,l[e].alt,l[e].title,l[e].alt,l[e].caption],r.querySelector(".upload-meta > .hint")?.remove(),i.append(t)})),r.length>0&&n&&(n.hidden=!0))}populateGalleryField(e,t,a,l={}){this.populateImageField(e,t,a,l)}populateRepeaterField(e,t,a,l={}){if(console.log("fieldWrapper",e),console.log("fieldName",t),console.log("fieldValue",a),console.log("options",l),!a||!Array.isArray(a))return;const r=e.querySelector(".repeater-items"),o=e.querySelector("template");r&&o?(window.removeChildren(r),a.forEach(((a,l)=>{if(!a||"object"!=typeof a)return;const i=window.getTemplate(o.className);if(!i)return void console.warn(`Repeater field ${t}: template not found`);i.id=`${e.closest("form").id}-${t}-row-${l}`,i.dataset.index=l;const n=i.querySelector(".row-number");n&&(n.textContent=`#${l+1}`),i.querySelectorAll("input, select, textarea").forEach((e=>{const r=e.name,o=`${t}:${l}:${r}`,i=`${t}-${l}-${r}-${e.value}`;e.name=o,e.id=i;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=i),void 0!==a[r]&&this.populateRepeaterFieldValue(e,r,a[r])})),r.appendChild(i)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}}; |
| | | window.jvbPopulate=class{constructor(e,t={},a={},r={}){for(let[l,i]of Object.entries(t)){let t=e.querySelector(`[data-field="${l}"]`);t&&this.populateField(t,l,i,a,r)}}populateField(e,t,a,r={},l={}){if(e&&null!=a)switch(this.getFieldType(e)){case"image":this.populateImageField(e,t,a,r);break;case"gallery":this.populateGalleryField(e,t,a,r);break;case"repeater":this.populateRepeaterField(e,t,a,l);break;case"taxonomy":this.populateTaxonomyField(e,t,a);break;case"user":this.populateUserField(e,t,a);break;case"location":this.populateLocationField(e,t,a);break;case"set":case"checkbox":this.populateSetField(e,t,a);break;case"select":case"radio":this.populateSelectField(e,t,a);break;case"true_false":this.populateBooleanField(e,t,a);break;case"date":case"time":case"datetime":this.populateDateField(e,t,a);break;case"number":this.populateNumberField(e,t,a);break;case"textarea":e.querySelector(".editor-container")?this.populateEditorField(e,t,a):this.populateTextareaField(e,t,a);break;default:this.populateTextField(e,t,a)}}getFieldType(e){const t=["image","gallery","repeater","taxonomy","user","location","set","checkbox","select","radio","true_false","date","time","datetime","editor","number","text","textarea","email","url","tel","phone"];for(const a of t)if(e.classList.contains(a))return a;if(e.dataset.type)return e.dataset.type;const a=e.querySelector("input, select, textarea");if(a){if("TEXTAREA"===a.tagName)return"true"===a.dataset.editor?"editor":"textarea";if(a.type)return"checkbox"!==a.type||e.classList.contains("true_false")?a.type:"set"}return"text"}populateTextField(e,t,a){const r=e.querySelector(`[name="${t}"], input, textarea`);if(r&&(r.value=String(a||""),r.dataset.limit)){const t=e.querySelector(".char-count .current");t&&(t.textContent=r.value.length)}}populateTextareaField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea:not([data-editor="true"])');if(r){if(r.value,r.value=String(a||""),r.dispatchEvent(new Event("change",{bubbles:!0})),r.dataset.limit){const t=e.querySelector(".char-count .current");if(t){t.textContent=r.value.length;const a=parseInt(r.dataset.limit,10);r.value.length>=a?e.classList.add("reached"):e.classList.remove("reached")}}}else console.warn(`No textarea found for field ${t} in wrapper:`,e)}populateNumberField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="number"]`);r&&(r.value=Number(a)||0)}populateBooleanField(e,t,a){const r=e.querySelector(`[name="${t}"], input[type="checkbox"]`);r&&(r.checked=Boolean(a))}populateSelectField(e,t,a){const r=String(a||""),l=e.querySelector(`select[name="${t}"]`);if(l)return void(l.value=r);const i=e.querySelector(`input[type="radio"][name="${t}"][value="${r}"]`);i&&(i.checked=!0)}populateSetField(e,t,a){let r=a;if("string"==typeof a)try{r=JSON.parse(a)}catch(e){r=a.split(",").map((e=>e.trim()))}Array.isArray(r)||(r=[String(r)]),e.querySelectorAll(`input[type="checkbox"][name*="${t}"]`).forEach((e=>{e.checked=r.includes(e.value)}))}populateDateField(e,t,a){const r=e.querySelector(`[name="${t}"], input`);if(r&&a){let e=a;"object"==typeof a&&a.date&&(e=a.date);try{const t=new Date(e);if(!isNaN(t.getTime()))switch(r.type){case"date":r.value=t.toISOString().split("T")[0];break;case"time":r.value=t.toTimeString().slice(0,5);break;case"datetime-local":r.value=t.toISOString().slice(0,16);break;default:r.value=e}}catch(t){r.value=e}}}populateEditorField(e,t,a){const r=e.querySelector(`textarea[name="${t}"]`)||e.querySelector('textarea[data-editor="true"]')||e.querySelector("textarea");if(!r)return void console.warn(`Editor field ${t}: textarea not found`);const l=String(a||"");r.value=l;const i=e.querySelector(".editor");if(i){let e=null;if(i.__quill)e=i.__quill;else if(i.quill)e=i.quill;else if(window.Quill&&window.Quill.find)e=window.Quill.find(i);else if(window.Quill&&window.Quill.instances)for(let t of window.Quill.instances)if(t.container===i){e=t;break}e?(e.root.innerHTML=l,i.__quill=e):(console.warn(`Quill instance not found for ${t}, setting HTML directly`),i.innerHTML=l)}else console.warn(`Editor container not found for ${t}`);r.dispatchEvent(new Event("change",{bubbles:!0}))}populateLocationField(e,t,a){a&&"object"==typeof a&&["address","lat","lng","street","city","province","postal_code","country"].forEach((r=>{if(void 0!==a[r]){const l=e.querySelector(`[name="${t}_${r}"], [name="${r}"]`);l&&(l.value=String(a[r]||""))}}))}populateTaxonomyField(e,t,a){let r=[];if(Array.isArray(a))r=a.map((e=>String(e)));else if("string"==typeof a)try{const e=JSON.parse(a);r=Array.isArray(e)?e.map((e=>String(e))):[String(e)]}catch(e){r=a.split(",").map((e=>e.trim()))}else a&&(r=[String(a)]);if(0===r.length)return;const l=e.querySelector(`input[type="hidden"][name="${t}"]`);l&&(l.value=r.join(","))}populateUserField(e,t,a){this.populateTaxonomyField(e,t,a)}populateImageField(e,t,a,r={}){if(!a)return;const l=String(a).split(",").filter((e=>parseInt(e.trim())));if(0===l.length)return;const i=e.querySelector(`input[type="hidden"][name="${t}"]`);i&&(i.value=l.join(","));const o=e.querySelector(".item-grid"),n=e.querySelector(".file-upload-container");e.querySelector(".progress")?.remove(),o&&(window.removeChildren(o),l.forEach((e=>{let t=window.getTemplate("uploadItem"),a=t.querySelector("img"),l=t.querySelector("details"),i=window.getTemplate("uploadMeta");l.append(i),[a.src,a.alt,t.querySelector('[name="image-title"]').value,t.querySelector('[name="image-alt-text"]').value,t.querySelector('[name="image-caption"]').value]=[r[e].medium,r[e].alt,r[e].title,r[e].alt,r[e].caption],l.querySelector(".upload-meta > .hint")?.remove(),o.append(t)})),l.length>0&&n&&(n.hidden=!0))}populateGalleryField(e,t,a,r={}){"timeline"===t?this.populateTimelineGallery(e,t,a,r):this.populateImageField(e,t,a,r)}populateTimelineGallery(e,t,a,r){if(!a)return;const l=Object.keys(a);if(0===l.length)return;const i=e.querySelector(".item-grid");e.querySelector(".file-upload-container"),e.querySelector(".progress")?.remove(),i&&(window.removeChildren(i),l.forEach((e=>{let t=window.getTemplate("timelineItem"),l=t.querySelector("img");t.querySelector("video")?.remove(),t.querySelector(".select-item span")?.remove();let o=a[e],n=r[e];l.src=n.medium,o={...o,...n},t.querySelectorAll(".field").forEach((e=>{if(e.classList.contains("group"))return;let t=e.querySelector("input, textarea"),a=e.querySelector("label"),l=t.name.replace("upload_data::",""),i=o[l];this.populateField(e,l,i,r);let n=`[${o.id}]${l}`;[t.name,t.id,a.htmlFor]=[n,n,n]})),i.append(t)})))}populateRepeaterField(e,t,a,r={}){if(!a||!Array.isArray(a))return;const l=e.querySelector(".repeater-items"),i=e.querySelector("template");l&&i?(window.removeChildren(l),a.forEach(((a,r)=>{if(!a||"object"!=typeof a)return;const o=window.getTemplate(i.className);if(!o)return void console.warn(`Repeater field ${t}: template not found`);o.id=`${e.closest("form").id}-${t}-row-${r}`,o.dataset.index=r;const n=o.querySelector(".row-number");n&&(n.textContent=`#${r+1}`),o.querySelectorAll("input, select, textarea").forEach((e=>{const l=e.name,i=`${t}:${r}:${l}`,o=`${t}-${r}-${l}-${e.value}`;e.name=i,e.id=o;const n=e.nextElementSibling;n&&"LABEL"===n.tagName&&(n.htmlFor=o),void 0!==a[l]&&this.populateRepeaterFieldValue(e,l,a[l])})),l.appendChild(o)}))):console.warn(`Repeater field ${t}: missing container or template`)}populateRepeaterFieldValue(e,t,a){switch(e.type){case"checkbox":e.checked=Boolean(a);break;case"radio":e.checked=e.value===String(a);break;default:e.value=String(a||"")}}}; |
| | |
| | | (()=>{class t{constructor(t={}){this.canUpdateUI=!0,console.log("jvbSettings",jvbSettings),this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...t},this.user=jvbSettings.currentUser,console.log(this.user),this.headers={"X-WP-Nonce":jvbSettings.nonce,...t.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=new window.jvbStore({name:"queue",storeName:"operations",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,getBlobs:async t=>{if(window.jvbUploadBlobs){Array.isArray(t)||"string"!=typeof t||(t=[t]);return(await Promise.all(t.map((t=>window.jvbUploadBlobs.getBlob(t))))).filter(Boolean)}return null}}),this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),console.log(this.ui),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const t=this.getOperationsByStatus(["completed","failed_permanent"],!1);t.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((t,e)=>{switch(t){case"data-loaded":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":this.hasQueuedOperations()&&this.startPolling();default:this.updateUI()}})),this.notify("queue-initialized",{operations:t})}addToQueue(t){const e={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...t};if(e.headers={...this.headers,...e.headers},!e.endpoint||!e.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((t=>"queued"===t.status&&t.endpoint===e.endpoint&&t.canMerge));if(s.length>0){const t=s[0];return t.data=window.deepMerge(t.data,e.data),t.timestamp=Date.now(),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}return console.log("Added to Queue: ",e),this.setQueue(e),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}setQueue(t){this.store.save(t)}updateOperationStatus(t,e){let s=this.store.get(t);s&&(s.status=e,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(t){return this.store.get(t)}clearQueue(t){this.store.delete(t)}startActivityTracking(){if(!this.activityListeners){const t=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=t.map((t=>{const e=()=>this.resetActivityTimer();return document.addEventListener(t,e,{passive:!0}),{event:t,handler:e}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:t,handler:e})=>{document.removeEventListener(t,e)})),this.activityListeners=null)}setProcessing(t){this.isProcessing=t,this.ui.toggle.classList.toggle("saving",t)}async processQueue(){if(this.isProcessing)return;const t=this.getOperationsByStatus("queued");if(0===t.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const e of t)await this.processOperation(e);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(t){try{this.updateOperationStatus(t.id,"uploading"),t=this.getQueue(t.id);const i=`${this.config.apiBase}${t.endpoint}`;let a;if(console.log(t.data),t.data instanceof FormData)for(var[e,s]of(t.data.append("id",t.id),t.data.append("user",this.user),a=t.data,console.log("Sending to server:"),a.entries()))console.log(e,s);else a=JSON.stringify({...t.data,id:t.id,user:this.user}),t.headers["Content-Type"]="application/json";const n=await fetch(i,{method:t.method,headers:t.headers,body:a}),r=await n.json();if(!n.ok||!1===r.success)throw new Error(r.message||`HTTP ${n.status}`);if(r.id&&t.id!==r.id){const e=this.getQueue(r.id);e?(e.data=window.deepMerge(e.data,t.data),e.status="pending",e.serverData=r,this.updateOperationStatus(e.id,e.status),this.setQueue(e),this.removeOperationFromUI(t.id),t=e):(this.clearQueue(t.id),t.id=r.id,t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,t.status),this.setQueue(t))}else t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,"pending"),this.setQueue(t);this.a11y.announce(`${t.title} sent to server for processing.`)}catch(e){console.error("Operation failed:",e),t.retries++,t.lastError=e.message,t.retries>=this.config.maxRetries?t.status="failed_permanent":(t.status="failed",t.nextRetry=Date.now()+1e3*Math.pow(2,t.retries)),this.updateOperationStatus(t.id,t.status),this.setQueue(t)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(t){console.error("Polling error:",t)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(t,e){if(0!==(t=(t=Array.isArray(t)?t:t.includes(",")?t.split(","):[t]).filter((t=>{let s=this.getQueue(t);return this.getAllowedActions(s.status).includes(e)}))).length){["cancel","dismiss"].includes(e)&&t.forEach((t=>{this.removeOperationFromUI(t)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:t,action:e})});if(!i.ok){const t=await i.json().catch((()=>{}));throw new Error(t.message||`${e} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${e} operation failed`);return["cancel","dismiss"].includes(e)?t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),this.clearQueue(t)})):(t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),a}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:e,operationIds:t,itemCount:t.length},(()=>this.updateServerOperations(t,e)));if(i.retried)return i;throw s}}}getAllowedActions(t){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[t]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=t=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return t.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(t){if(t.target.closest(this.selectors.panel,this.selectors.toggle))if(t.target.closest(this.selectors.refreshButton))this.store.fetch();else if(t.target.closest(this.selectors.clearButton)){const t=this.getOperationsByStatus("completed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"dismiss")}}else if(t.target.closest(this.selectors.retryButton)){const t=this.getOperationsByStatus("failed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"retry")}}else if(t.target.closest("[data-action]")){const e=t.target.closest("[data-action]"),s=e.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,e.dataset.action)}else if(t.target.closest(".filters [data-filter]")){const e=t.target.closest("[data-filter]").dataset.filter;this.setFilter(e)}}handleChange(t){}initUI(){if(this.icons={queued:"refresh",localProcessing:"refresh",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"synced",failed:"error",failed_permanent:"error"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[t,e]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(t))if("object"==typeof e){this.ui[t]={};for(let[s,i]of Object.entries(e))this.ui[t][s]=this.ui.panel.querySelector(i)}else this.ui[t]=this.ui.panel.querySelector(e)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const t=this.getQueueStats();if(this.ui.count){const e=t.total-t.completed;this.ui.count.textContent=e>0?e:"",this.ui.count.style.display=e>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}let e=this.getOperationsByStatus("failed"),s=this.getOperationsByStatus("completed");this.ui.clearButton.disabled=0===s.length,this.ui.retryButton.disabled=0===e.length,Object.entries(this.ui.filters).forEach((([e,s])=>{const i="all"===e?t.total:t[e]||0,a=s.querySelector(".count");a&&(a.textContent=i>0?i:""),s.setAttribute("data-count",i)})),this.renderOperations()}getStatusLabel(t){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[t]||t}getItemMessage(t){if(t.message)return t.message;if(t.error_message)return t.error_message;switch(t.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return t.position?`Position ${t.position} in queue`:"In server queue";case"processing":return t.progress?`${t.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${t.lastError||"Unknown error"} (Retry ${t.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${t.lastError||"Unknown error"}`;default:return""}}calculateProgress(t){if(t.progress)return t.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[t.status]||0}getQueueStats(){const t={};return this.statuses.forEach((e=>{t[e]=0})),Array.from(this.store.data.values()).forEach((e=>{t.hasOwnProperty(e.status)&&t[e.status]++})),t.total=Array.from(this.store.data.values()).length,t}renderOperations(){if(!this.ui.itemsContainer)return;const t=this.getActiveFilter(),e=this.getFilteredOperations(t);if(window.removeChildren(this.ui.itemsContainer),0===e.length){let t=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(t),this.a11y.announce("Nothing queued.")}else{let t=this.ui.itemsContainer.querySelector(".emptyQueue");t&&t.remove(),e.forEach((t=>{const e=this.createOperationUI(t);this.ui.itemsContainer.appendChild(e)}))}}createOperationUI(t){const e=window.getTemplate("queueItem");return e.dataset.id=t.id,this.updateOperationUI(t,e),e}updateOperationUI(t,e=null){e||(e=this.ui.itemsContainer?.querySelector(`[data-id="${t.id}"]`)),e||(e=this.createOperationUI(t)),this.statuses.forEach((t=>e.classList.remove(t))),e.classList.add(t.status);let s="";t.updated_at?s=window.formatTimeAgo(new Date(t.updated_at)):t.created_at&&(s=window.formatTimeAgo(new Date(t.created_at)));const i=this.calculateProgress(t),a=e.querySelector(".type"),n=e.querySelector(".status"),r=e.querySelector(".info .details"),o=e.querySelector(".info .time"),l=e.querySelector(".progress .fill");if(a&&(a.textContent=t.title),n){n.querySelector(".icon")?.remove();let e=this.getStatusLabel(t.status);n.title=e,n.prepend(window.getIcon(this.icons[t.status])),n.querySelector("span").textContent=e}r&&(r.textContent=this.getItemMessage(t)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const u=e.querySelector(".actions");u&&this.updateActionButtons(t,u)}updateActionButtons(t,e){switch(window.removeChildren(e),t.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",e.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=t.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",e.appendChild(i),e.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",e.appendChild(n)}}removeOperationFromUI(t){const e=this.ui.itemsContainer?.querySelector(`[data-id="${t}"]`);e&&(e.style.opacity="0",e.style.transform="scale(0.9)",setTimeout((()=>e.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let t=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{t--,this.ui.countdown.textContent=t,t<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(t){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(t)&&this.ui.panel?.classList.add(t)}setFilter(t){Object.values(this.ui.filters).forEach((e=>{e&&e.classList.toggle("active",e.dataset.filter===t)})),this.activeFilter=t,this.renderOperations()}getActiveFilter(){const t=this.ui.panel?.querySelector(".filter.active");return t?.dataset.filter||"all"}getFilteredOperations(t){const e=Array.from(this.store.data.values());return"all"===t?e:e.filter((e=>e.status===t))}showPopup(t,e="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=t),this.ui.popup.className=`popup ${e} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(t,e=!0){return Array.isArray(t)||"string"!=typeof t||(t=[t]),e?Array.from(this.store.data.values()).filter((e=>t.includes(e.status))):Array.from(this.store.data.values()).filter((e=>!t.includes(e.status)))}async hasQueuedOperations(){return(await this.store.query("status","queued")).length>0}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((s=>s(t,e)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new t}))})(); |
| | | (()=>{class t{constructor(t={}){this.canUpdateUI=!0,console.log("jvbSettings",jvbSettings),this.config={apiBase:jvbSettings.api,maxRetries:3,pollInterval:5e3,activityDelay:2e3,autosync:!0,endpoint:"queue",...t},this.user=jvbSettings.currentUser,console.log(this.user),this.headers={"X-WP-Nonce":jvbSettings.nonce,...t.headers},this.a11y=window.jvbA11y,this.errors=window.jvbError,this.store=new window.jvbStore({name:"queue",storeName:"operations",keyPath:"id",endpoint:this.config.endpoint,TTL:1/0,indexes:[{name:"status",keyPath:"status"},{name:"type",keyPath:"type"}],showLoading:!1,getBlobs:async t=>{if(window.jvbUploadBlobs){Array.isArray(t)||"string"!=typeof t||(t=[t]);return(await Promise.all(t.map((t=>window.jvbUploadBlobs.getBlob(t))))).filter(Boolean)}return null}}),this.classes=["offline","synced","pending"],this.isProcessing=!1,this.isPolling=!1,this.subscribers=new Set,this.statuses=["queued","localProcessing","uploading","pending","processing","completed","failed","failed_permanent"],this.initUI(),this.initListeners(),console.log(this.ui),this.ui.panel&&(this.popup=new window.jvbPopup({popup:this.ui.panel,toggle:this.ui.toggle,name:"Queue Panel"})),this.initQueue(),this.user&&(this.ui.toggle.hidden=!1,this.ui.panel.hidden=!1)}async initQueue(){const t=this.getOperationsByStatus(["completed","failed_permanent"],!1);t.length>0?this.startPolling():this.updateStatusPanel("synced"),this.store.subscribe(((t,e)=>{switch(t){case"data-loaded":this.getOperationsByStatus(["completed","failed_permanent"],!1).length>0&&this.startPolling(),this.updateUI();break;case"item-saved":this.hasQueuedOperations()&&this.startPolling();default:this.updateUI()}})),this.notify("queue-initialized",{operations:t})}addToQueue(t){const e={id:`u${this.user}_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,endpoint:null,method:"POST",headers:{},data:{},canMerge:!0,popup:"Saving changes...",title:"Operation",status:"queued",timestamp:Date.now(),retries:0,user:this.user,...t};if(e.headers={...this.headers,...e.headers},!e.endpoint||!e.data)return console.error("Invalid operation queued: missing endpoint or data"),null;const s=Array.from(this.store.data.values()).filter((t=>"queued"===t.status&&t.endpoint===e.endpoint&&t.canMerge));if(s.length>0){const t=s[0];return t.data=window.deepMerge(t.data,e.data),t.timestamp=Date.now(),this.updateOperationStatus(t.id,t.status),this.updateUI(),this.startActivityTracking(),t.id}return console.log("Added to Queue: ",e),this.setQueue(e),this.updateOperationStatus(e.id,e.status),this.updateUI(),this.startActivityTracking(),e.id}setQueue(t){this.store.save(t)}updateOperationStatus(t,e){let s=this.store.get(t);s&&(s.status=e,this.notify("operation-status",s),this.updateOperationUI(s))}getQueue(t){return this.store.get(t)}clearQueue(t){this.store.delete(t)}startActivityTracking(){if(!this.activityListeners){const t=["mousedown","mousemove","keypress","scroll","touchstart"];this.activityListeners=t.map((t=>{const e=()=>this.resetActivityTimer();return document.addEventListener(t,e,{passive:!0}),{event:t,handler:e}}))}this.resetActivityTimer()}resetActivityTimer(){this.lastActivity=Date.now(),this.activityTimer&&clearTimeout(this.activityTimer),this.activityTimer=setTimeout((()=>{this.processQueue()}),this.config.activityDelay)}stopActivityTracking(){this.activityTimer&&(clearTimeout(this.activityTimer),this.activityTimer=null),this.activityListeners&&(this.activityListeners.forEach((({event:t,handler:e})=>{document.removeEventListener(t,e)})),this.activityListeners=null)}setProcessing(t){this.isProcessing=t,this.ui.toggle.classList.toggle("saving",t)}async processQueue(){if(this.isProcessing)return;const t=this.getOperationsByStatus("queued");if(0===t.length)return void this.stopActivityTracking();this.setProcessing(!0);for(const e of t)await this.processOperation(e);this.setProcessing(!1),this.stopActivityTracking();this.getOperationsByStatus(["queued","completed","failed_permanent"],!1).length>0&&this.startPolling()}async processOperation(t){try{this.updateOperationStatus(t.id,"uploading"),t=this.getQueue(t.id);const i=`${this.config.apiBase}${t.endpoint}`;let a;if(console.log(t.data),t.data instanceof FormData)for(var[e,s]of(t.data.append("id",t.id),t.data.append("user",this.user),a=t.data,console.log("Sending to server:"),a.entries()))console.log(e,s);else a=JSON.stringify({...t.data,id:t.id,user:this.user}),t.headers["Content-Type"]="application/json";const n=await fetch(i,{method:t.method,headers:t.headers,body:a}),r=await n.json();if(!n.ok||!1===r.success)throw new Error(r.message||`HTTP ${n.status}`);if(r.id&&t.id!==r.id){const e=this.getQueue(r.id);e?(e.data=window.deepMerge(e.data,t.data),e.status="pending",e.serverData=r,this.updateOperationStatus(e.id,e.status),this.setQueue(e),this.removeOperationFromUI(t.id),t=e):(this.clearQueue(t.id),t.id=r.id,t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,t.status),this.setQueue(t))}else t.status="pending",t.serverData=r,this.updateOperationStatus(t.id,"pending"),this.setQueue(t);this.a11y.announce(`${t.title} sent to server for processing.`)}catch(e){console.error("Operation failed:",e),t.retries++,t.lastError=e.message,t.retries>=this.config.maxRetries?t.status="failed_permanent":(t.status="failed",t.nextRetry=Date.now()+1e3*Math.pow(2,t.retries)),this.updateOperationStatus(t.id,t.status),this.setQueue(t)}}startPolling(){this.isPolling||(this.isPolling=!0,this.updateStatusPanel("pending"),this.pollTimer=setInterval((async()=>{try{await this.store.fetch();0===this.getOperationsByStatus(["completed","failed_permanent"],!1).length&&(this.stopPolling(),this.updateStatusPanel("synced"))}catch(t){console.error("Polling error:",t)}}),this.config.pollInterval))}stopPolling(){this.isPolling&&(this.isPolling=!1,this.pollTimer&&(clearInterval(this.pollTimer),this.pollTimer=null),this.countdownTimer&&(clearInterval(this.countdownTimer),this.countdownTimer=null))}async updateServerOperations(t,e){if(0!==(t=(t=Array.isArray(t)?t:t.includes(",")?t.split(","):[t]).filter((t=>{let s=this.getQueue(t);return this.getAllowedActions(s.status).includes(e)}))).length){["cancel","dismiss"].includes(e)&&t.forEach((t=>{this.removeOperationFromUI(t)}));try{const s=`${this.config.apiBase}${this.config.endpoint}`,i=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...this.headers},body:JSON.stringify({ids:t,action:e})});if(!i.ok){const t=await i.json().catch((()=>{}));throw new Error(t.message||`${e} failed: ${i.status}`)}const a=await i.json();if(!a.success)throw new Error(a.message||`${e} operation failed`);return["cancel","dismiss"].includes(e)?t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),this.clearQueue(t)})):(t.forEach((t=>{let s=this.getQueue(t);this.notify(`${e}-operation`,s),s.status="queued",s.retries=0,this.setQueue(s),this.updateOperationStatus(s.id,s.status)})),this.startActivityTracking()),this.updateUI(),a}catch(s){const i=await window.jvbError.log(s,{component:"QueueManager",operation:"performQueueAction",action:e,operationIds:t,itemCount:t.length},(()=>this.updateServerOperations(t,e)));if(i.retried)return i;throw s}}}getAllowedActions(t){return{queued:["cancel"],localProcessing:["cancel"],pending:["cancel"],processing:[],completed:["dismiss"],failed:["retry","dismiss"],failed_permanent:["dismiss"]}[t]||[]}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),this.ui.panel?.addEventListener("change",this.changeHandler),this.handleOnline=()=>{this.updateStatusPanel(),this.hasQueuedOperations()&&this.processQueue()},this.handleOffline=()=>this.updateStatusPanel("offline"),this.handleBeforeUnload=t=>{if(this.getOperationsByStatus(["queued","uploading"]).length>0)return t.preventDefault(),"You have unsaved changes in the queue."},window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline),window.addEventListener("beforeunload",this.handleBeforeUnload)}handleClick(t){if(t.target.closest(this.selectors.panel,this.selectors.toggle))if(t.target.closest(this.selectors.refreshButton))this.store.fetch();else if(t.target.closest(this.selectors.clearButton)){const t=this.getOperationsByStatus("completed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"dismiss")}}else if(t.target.closest(this.selectors.retryButton)){const t=this.getOperationsByStatus("failed");if(t.length>0){const e=t.map((t=>t.id));this.updateServerOperations(e,"retry")}}else if(t.target.closest("[data-action]")){const e=t.target.closest("[data-action]"),s=e.closest("[data-id]")?.dataset.id;s&&this.updateServerOperations(s,e.dataset.action)}else if(t.target.closest(".filters [data-filter]")){const e=t.target.closest("[data-filter]").dataset.filter;this.setFilter(e)}}handleChange(t){}initUI(){if(this.icons={queued:"arrows-clockwise",localProcessing:"arrows-clockwise",uploading:"syncing",pending:"cloud",processing:"syncing",completed:"cloud-check",failed:"cloud-warning",failed_permanent:"cloud-warning"},this.selectors={panel:"aside#queue",toggle:"button.qtoggle",refreshButton:"button.refreshNow",countdown:".countdown",indicator:".qtoggle .indicator",count:".qtoggle .count",popup:".popup",itemsContainer:".qitems",clearButton:".dismiss-all",retryButton:".retry-all",filters:{all:'.filters [data-filter="all"]',received:'.filters [data-filter="queued"]',localProcessing:'.filters [data-filter="localProcessing"]',uploading:'.filters [data-filter="uploading"]',pending:'.filters [data-filter="pending"]',processing:'.filters [data-filter="processing"]',completed:'.filters [data-filter="completed"]',failed:'.filters [data-filter="failed"]'}},this.ui={panel:document.querySelector(this.selectors.panel),toggle:document.querySelector(this.selectors.toggle),count:document.querySelector(this.selectors.count),indicator:document.querySelector(this.selectors.indicator)},this.ui.panel){for(let[t,e]of Object.entries(this.selectors))if(!["panel","toggle","count","indicator"].includes(t))if("object"==typeof e){this.ui[t]={};for(let[s,i]of Object.entries(e))this.ui[t][s]=this.ui.panel.querySelector(i)}else this.ui[t]=this.ui.panel.querySelector(e)}else this.canUpdateUI=!1}updateUI(){if(!this.canUpdateUI)return;const t=this.getQueueStats();if(this.ui.count){const e=t.total-t.completed;this.ui.count.textContent=e>0?e:"",this.ui.count.style.display=e>0?"":"none"}if(this.ui.indicator){const e=t.queued>0||t.uploading>0||t.pending>0||t.processing>0;this.ui.indicator.classList.toggle("active",e)}let e=this.getOperationsByStatus("failed"),s=this.getOperationsByStatus("completed");this.ui.clearButton.disabled=0===s.length,this.ui.retryButton.disabled=0===e.length,Object.entries(this.ui.filters).forEach((([e,s])=>{const i="all"===e?t.total:t[e]||0,a=s.querySelector(".count");a&&(a.textContent=i>0?i:""),s.setAttribute("data-count",i)})),this.renderOperations()}getStatusLabel(t){return{queued:"Queued",localProcessing:"Processing locally",uploading:"Uploading",pending:"Waiting on server",processing:"Processing",completed:"Completed",failed:"Failed (will retry)",failed_permanent:"Failed permanently"}[t]||t}getItemMessage(t){if(t.message)return t.message;if(t.error_message)return t.error_message;switch(t.status){case"queued":return"Waiting to send...";case"uploading":return"Sending to server...";case"pending":return t.position?`Position ${t.position} in queue`:"In server queue";case"processing":return t.progress?`${t.progress}% complete`:"Processing...";case"completed":return"Successfully completed";case"failed":return`Failed: ${t.lastError||"Unknown error"} (Retry ${t.retries}/${this.config.maxRetries})`;case"failed_permanent":return`Failed: ${t.lastError||"Unknown error"}`;default:return""}}calculateProgress(t){if(t.progress)return t.progress;return{queued:10,uploading:25,pending:40,processing:70,completed:100,failed:0,failed_permanent:0}[t.status]||0}getQueueStats(){const t={};return this.statuses.forEach((e=>{t[e]=0})),Array.from(this.store.data.values()).forEach((e=>{t.hasOwnProperty(e.status)&&t[e.status]++})),t.total=Array.from(this.store.data.values()).length,t}renderOperations(){if(!this.ui.itemsContainer)return;const t=this.getActiveFilter(),e=this.getFilteredOperations(t);if(window.removeChildren(this.ui.itemsContainer),0===e.length){let t=window.getTemplate("emptyQueue");this.ui.itemsContainer.append(t),this.a11y.announce("Nothing queued.")}else{let t=this.ui.itemsContainer.querySelector(".emptyQueue");t&&t.remove(),e.forEach((t=>{const e=this.createOperationUI(t);this.ui.itemsContainer.appendChild(e)}))}}createOperationUI(t){const e=window.getTemplate("queueItem");return e.dataset.id=t.id,this.updateOperationUI(t,e),e}updateOperationUI(t,e=null){e||(e=this.ui.itemsContainer?.querySelector(`[data-id="${t.id}"]`)),e||(e=this.createOperationUI(t)),this.statuses.forEach((t=>e.classList.remove(t))),e.classList.add(t.status);let s="";t.updated_at?s=window.formatTimeAgo(new Date(t.updated_at)):t.created_at&&(s=window.formatTimeAgo(new Date(t.created_at)));const i=this.calculateProgress(t),a=e.querySelector(".type"),n=e.querySelector(".status"),r=e.querySelector(".info .details"),o=e.querySelector(".info .time"),l=e.querySelector(".progress .fill");if(a&&(a.textContent=t.title),n){n.querySelector(".icon")?.remove();let e=this.getStatusLabel(t.status);n.title=e,n.prepend(window.getIcon(this.icons[t.status])),n.querySelector("span").textContent=e}r&&(r.textContent=this.getItemMessage(t)),o&&(o.textContent=s),l&&(l.style.width=`${i}%`);const u=e.querySelector(".actions");u&&this.updateActionButtons(t,u)}updateActionButtons(t,e){switch(window.removeChildren(e),t.status){case"queued":case"localProcessing":case"pending":const s=window.getTemplate("button");s.classList.add("cancel"),s.dataset.action="cancel",s.textContent="Cancel",e.appendChild(s);break;case"failed":case"failed_permanent":const i=window.getTemplate("button"),a=window.getTemplate("button");i.classList.add("retry"),i.textContent="Retry",i.disabled=t.retries>=this.maxRetries,i.dataset.action="retry",a.classList.add("dismiss"),a.textContent="Dismiss",a.dataset.action="dismiss",e.appendChild(i),e.appendChild(a);break;case"completed":const n=window.getTemplate("button");n.dataset.action="dismiss",n.classList.add("dismiss"),n.textContent="Dismiss",e.appendChild(n)}}removeOperationFromUI(t){const e=this.ui.itemsContainer?.querySelector(`[data-id="${t}"]`);e&&(e.style.opacity="0",e.style.transform="scale(0.9)",setTimeout((()=>e.remove()),300))}updateCountdown(){if(!this.ui.countdown||!this.isPolling)return;let t=this.config.pollInterval/1e3;this.countdownTimer=setInterval((()=>{t--,this.ui.countdown.textContent=t,t<=0&&(clearInterval(this.countdownTimer),this.isPolling&&setTimeout((()=>this.updateCountdown()),100))}),1e3)}updateStatusPanel(t){this.ui.panel?.classList.remove(...this.classes),this.classes.includes(t)&&this.ui.panel?.classList.add(t)}setFilter(t){Object.values(this.ui.filters).forEach((e=>{e&&e.classList.toggle("active",e.dataset.filter===t)})),this.activeFilter=t,this.renderOperations()}getActiveFilter(){const t=this.ui.panel?.querySelector(".filter.active");return t?.dataset.filter||"all"}getFilteredOperations(t){const e=Array.from(this.store.data.values());return"all"===t?e:e.filter((e=>e.status===t))}showPopup(t,e="success"){if(!this.ui.popup)return;const s=this.ui.popup.querySelector("span");s&&(s.textContent=t),this.ui.popup.className=`popup ${e} show`,setTimeout((()=>{this.ui.popup.classList.remove("show")}),3e3)}getOperationsByStatus(t,e=!0){return Array.isArray(t)||"string"!=typeof t||(t=[t]),e?Array.from(this.store.data.values()).filter((e=>t.includes(e.status))):Array.from(this.store.data.values()).filter((e=>!t.includes(e.status)))}async hasQueuedOperations(){return(await this.store.query("status","queued")).length>0}subscribe(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((s=>s(t,e)))}destroy(){this.stopPolling(),this.stopActivityTracking(),this.clickHandler&&document.removeEventListener("click",this.clickHandler),this.keyHandler&&document.removeEventListener("keydown",this.keyHandler),this.subscribers.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbQueue=new t}))})(); |
| | |
| | | window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const l=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n ${dashboardSettings.icons.image}\n </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n <span class="ql-formats">\n <button type="button" class="ql-p">\n ${jvbSettings.icons.paragraph}\n </button>\n <button type="button" class="ql-h1">\n ${jvbSettings.icons.h1}\n </button>\n <button type="button" class="ql-h2">\n ${jvbSettings.icons.h2}\n </button>\n <button type="button" class="ql-h3">\n ${jvbSettings.icons.h3}\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_bold">\n ${jvbSettings.icons.bold}\n </button>\n <button type="button" class="ql-jvb_italic">\n ${jvbSettings.icons.italic}\n </button>\n <button type="button" class="ql-jvb_underline">\n ${jvbSettings.icons.underline}\n </button>\n <button type="button" class="ql-jvb_strike">\n ${jvbSettings.icons.strike}\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_list" value="bullet">\n ${jvbSettings.icons["list-bullets"]}\n </button>\n <button type="button" class="ql-jvb_list" value="ordered">\n ${jvbSettings.icons["list-numbers"]}\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_align" value="left">\n ${jvbSettings.icons["align-left"]}\n </button>\n <button type="button" class="ql-jvb_align" value="center">\n ${jvbSettings.icons["align-center"]}\n </button>\n <button type="button" class="ql-jvb_align" value="right">\n ${jvbSettings.icons["align-right"]}\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_link">\n ${jvbSettings.icons.link}\n </button>\n ${l}\n </span>\n `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const l=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;this.quill.getText(t.index,t.length);const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n <div class="quill-link-modal-content ">\n <label for="link">Enter URL</label>\n <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n <div class="buttons">\n <button type="button" class="save">Save</button>\n ${n?'<button type="button" class="remove">Remove</button>':""}\n <button type="button" class="cancel">Cancel</button>\n </div>\n </div>\n `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const l=e.querySelector(".remove");l&&l.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),l=new FormData;l.append("image",e),objectID&&l.append("post_id",objectID),window.jvbLoading&&window.jvbLoading.showLoading("Uploading image...","Processing Upload");try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":jvbSettings.nonce},body:l});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{window.jvbLoading&&window.jvbLoading.hide(),t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});l.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),l.on("text-change",(()=>{t.value=l.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))}; |
| | | window.jvbQuill=function(t){t.querySelectorAll("textarea[data-editor=true]").forEach((t=>{let n,e,i;if(t.parentNode.querySelector(".editor-container"))n=t.parentNode.querySelector(".editor-container"),e=n.querySelector(".editor"),i=n.querySelector(".toolbar");else{n=document.createElement("div"),n.className="editor-container",e=document.createElement("div"),e.className="editor",i=document.createElement("div"),i.className="toolbar";const o=!0===t.dataset.allowimage?`<button type="button" class="ql-jvb_image">\n ${dashboardSettings.icons.image}\n </button>`:"";i.id=`toolbar-${t.id}`,i.innerHTML=`\n <span class="ql-formats">\n <button type="button" class="ql-p">\n <i class="icon icon-paragraph"></i>\n </button>\n <button type="button" class="ql-h1">\n <i class="icon icon-text-h-one"></i>\n </button>\n <button type="button" class="ql-h2">\n <i class="icon icon-text-h-two"></i>\n </button>\n <button type="button" class="ql-h3">\n <i class="icon icon-text-h-three"></i>\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_bold">\n <i class="icon icon-text-b-fi"></i>\n </button>\n <button type="button" class="ql-jvb_italic">\n <i class="icon icon-text-italic"></i>\n </button>\n <button type="button" class="ql-jvb_underline">\n <i class="icon icon-text-underline"></i>\n </button>\n <button type="button" class="ql-jvb_strike">\n <i class="icon icon-text-strikethrough"></i>\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_list" value="bullet">\n <i class="icon icon-list-dashes"></i>\n </button>\n <button type="button" class="ql-jvb_list" value="ordered">\n <i class="icon icon-list-numbers"></i>\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_align" value="left">\n <i class="icon icon-text-align-left"></i>\n </button>\n <button type="button" class="ql-jvb_align" value="center">\n <i class="icon icon-text-align-center"></i>\n </button>\n <button type="button" class="ql-jvb_align" value="right">\n <i class="icon icon-text-align-right"></i>\n </button>\n </span>\n <span class="ql-formats">\n <button type="button" class="ql-jvb_link">\n <i class="icon icon-link"></i>\n </button>\n ${o}\n </span>\n `,n.appendChild(i),n.appendChild(e),t.parentNode.insertBefore(n,t),t.style.display="none",e.innerHTML=t.value}const o=new Quill(e,{theme:"snow",modules:{toolbar:{container:i,handlers:{p:function(){this.quill.format("header",!1)},h1:function(){this.quill.format("header",1)},h2:function(){this.quill.format("header",2)},h3:function(){this.quill.format("header",3)},jvb_bold:function(){this.quill.format("bold",!0)},jvb_italic:function(){this.quill.format("italic",!0)},jvb_strike:function(){this.quill.format("strike",!0)},jvb_underline:function(){this.quill.format("underline",!0)},jvb_align:function(t){this.quill.format("align",t!==this.quill.getFormat().list&&t)},jvb_list:function(t){this.quill.format("list",t!==this.quill.getFormat().list&&t)},jvb_link:function(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;this.quill.getText(t.index,t.length);const n=this.quill.getFormat(t).link,e=document.createElement("dialog");e.className="quill-link-modal",e.innerHTML=`\n <div class="quill-link-modal-content ">\n <label for="link">Enter URL</label>\n <input type="url" id="link" placeholder="Enter URL" value="${n||""}" />\n <div class="buttons">\n <button type="button" class="save">Save</button>\n ${n?'<button type="button" class="remove">Remove</button>':""}\n <button type="button" class="cancel">Cancel</button>\n </div>\n </div>\n `,document.body.appendChild(e),e.showModal();const i=e.querySelector("input");i.focus(),e.querySelector(".save").addEventListener("click",(()=>{const t=i.value;t&&this.quill.format("link",t),e.remove()}));const o=e.querySelector(".remove");o&&o.addEventListener("click",(()=>{this.quill.format("link",!1),e.remove()})),e.querySelector(".cancel").addEventListener("click",(()=>{e.remove()})),i.addEventListener("keyup",(t=>{if("Enter"===t.key){const t=i.value;t&&this.quill.format("link",t),e.remove()}}))}},jvb_image:function(){const t=document.createElement("input");t.setAttribute("type","file"),t.setAttribute("accept","image/jpeg,image/png,image/gif,image/webp"),t.style.display="none",document.body.appendChild(t),t.onchange=async n=>{const e=n.target.files?.[0];if(!e)return;if(e.size>5242880)return this.quill.insertText(i.index,"File too large. Maximum size is 5MB",{color:"#f00",italic:!0},!0),void t.remove();const i=this.quill.getSelection(!0),o=new FormData;o.append("image",e),objectID&&o.append("post_id",objectID),window.jvbLoading&&window.jvbLoading.showLoading("Uploading image...","Processing Upload");try{const t=await fetch(`${jvbSettings.api}uploads/`,{method:"POST",headers:{"X-WP-Nonce":jvbSettings.nonce},body:o});if(!t.ok)throw new Error("Upload failed");const n=await t.json();this.quill.insertEmbed(i.index,"image",n.url)}catch(t){this.handleError("Upload error:",t),this.quill.insertText(i.index,"Failed to upload image. Please try again.",{color:"#f00",italic:!0},!0)}finally{window.jvbLoading&&window.jvbLoading.hide(),t.remove()}},t.click()}}},history:{delay:2e3,maxStack:500},clipboard:{matchVisual:!1}}});o.on("selection-change",(function(t){const n=i.querySelector(".ql-align");if(n){if(t&&0===t.length){const[e]=this.quill.getLeaf(t.index);if(e&&e.domNode&&"IMG"===e.domNode.tagName)return void(n.style.display="inline-block")}n.style.display="none"}})),o.on("text-change",(()=>{t.value=o.root.innerHTML,t.dispatchEvent(new Event("change",{bubbles:!0}))}))}))}; |
| | |
| | | (()=>{class e{constructor(){this.container=document.querySelector(".jvb-referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.initElements(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copy:"button.copy",login:".login",submit:"[type=submit]"},this.forms=this.container.querySelectorAll("form"),console.log(this.forms),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.forms.forEach((e=>{e.addEventListener("submit",this.submitHandler)})),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("input",this.inputHandler)},onClose:()=>{this.forms.forEach((e=>{e.removeEventListener("submit",this.submitHandler)})),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("input",this.inputHandler)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this),this.changeHandler=this.handleChange.bind(this)}handleClick(e){if(e.target.classList.contains(".copy")){let t=e.target.dataset.target,r=this.container.querySelector(`#${t}`);r=!!r&&r.textContent,r&&this.handleCopy(e.target,r)}}handleChange(e){"referral-code"===e.target.id&&window.debouncer.schedule("check-referral",(()=>this.makeRequest("referrals/check-code",{code:e.target.value})))}handleInput(e){"referral-code"===e.target.id&&(e.target.value=e.target.value.toUpperCase())}initShareWidget(){this.initCopyButton(),this.loadStats()}async checkForReferral(){const e=this.getUrlParameter("seeReferral"),t=this.getUrlParameter("ref");if(!e&&!t)return;if(!t)return void this.popup.openPopup();const r=this.container.querySelector("#referral-code-input");if(!r)return;const n=t.toUpperCase();r.value=n,r.readOnly=!0,this.popup.togglePopup();try{const e=await this.validateCodeOnly(n);if(e.success){this.showReferrerBanner(e.referrer_name,n);const t=this.container.querySelector("#referral-name");t&&t.focus()}else r.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),r.readOnly=!1}this.removeUrlParameter("ref")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}/referrals/check-code`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});return await t.json()}showReferrerBanner(e,t){const r=this.container.querySelector(".referral-header");if(!r)return;const n=document.createElement("div");n.className="referrer-banner",n.innerHTML=`\n <div class="banner-icon">🎉</div>\n <div class="banner-content">\n <strong>${window.escapeHtml(e)}</strong> referred you!\n <div class="banner-code">Code: <code>${window.escapeHtml(t)}</code></div>\n </div>\n `,r.parentNode.insertBefore(n,r.nextSibling);const a=r.querySelector("h3");a&&(a.textContent="Complete Your Registration");const o=r.querySelector("p");o&&(o.textContent="Enter your details below to claim your welcome reward!")}handleCopy(e,t=""){if(""===t||"string"!=typeof t)return;let r=e.textContent;(navigator.clipboard||navigator.clipboard.writeText)&&navigator.clipboard.writeText(t).then((()=>{e.textContent="Copied!",e.style.background="#00a32a",setTimeout((()=>{e.textContent=r,e.style.background=""}),2e3)}))}async loadStats(){if(this.container.querySelector(".referral-stats"))try{const e=await fetch(`${jvbSettings.api}/referrals/stats`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}async handleFormSubmit(e){console.log("Form Submission!"),window.debouncer.cancel("check-referral"),e.preventDefault(),console.log("Still working?");const t=e.target,r=new FormData(t);let n={};this.setFormLoading(!0,t);try{let e={success:!1,message:""};console.log(t),console.log(t.id),"referral-code-form"===t.id?(r.get("name")||(e.message+="We need your name to know who you are."),r.get("email")||(e.message+="We need your email to confirm you have access to it."),r.get("referral_code")||(e.message+="We need the referral code to know who sent you."),r.get("name")&&r.get("email")&&r.get("referral_code")&&(n.name=r.get("name"),n.email=r.get("email"),n.code=r.get("referral_code"),e=await this.makeRequest("referrals/register",n))):"login-form"===t.id&&r.get("login-email")&&(n.type="login",n.email=r.get("login-email"),n.context={},n.context.redirect_to=window.location.href+"?seeReferral=1",console.log("Making Request with: ",n),e=await this.makeRequest("magic-link",n)),e.success?this.handleSuccess(e):(this.showMessage(e.message||"Something went wrong. Please try again.","error"),this.setFormLoading(!1,t))}catch(e){console.error("Error registering:",e),this.showMessage("Something went wrong. Please try again.","error"),this.setFormLoading(!1,t)}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic-link","referrals/register","referrals/check-code"].includes(e))return{success:!1,message:"Something went wrong (Invalid endpoint)."};console.log("Endpoint: ",e),console.log("Data: ",t);const r=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(t)});return await r.json()}handleSuccess(e){this.container.querySelectorAll("form").forEach((e=>{window.fade(e,!1)}));const t=this.container.querySelector(".success-message");t&&(t.hidden=!1,t.scrollIntoView({behavior:"smooth",block:"center"}),this.dispatchEvent("emailSent",{email:e.email}))}setFormLoading(e,t){t.querySelectorAll("input").forEach((t=>t.disabled=e));let r=t.querySelector(".status"),n=r.querySelector(".message");r.hidden=e,r.classList.toggle("loading",e),n.textContent=e?"Checking with server...":""}showMessage(e,t="success"){const r=this.container.querySelector("#referral-message");r&&(r.textContent=e,r.className="message "+t,r.style.display="block","error"===t&&setTimeout((()=>{r.style.display="none"}),5e3))}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbReferral=new e}))})(); |
| | | (()=>{class e{constructor(){this.container=document.querySelector(".jvb-referral"),this.container&&(this.a11y=window.jvbA11y,this.toggle=document.querySelector('button[data-action="toggle-referral"]'),this.initElements(),this.initListeners(),this.checkForReferral())}initElements(){this.selectors={copy:"button.copy",login:".login",submit:"[type=submit]"},this.forms=this.container.querySelectorAll("form"),console.log(this.forms),this.popup=new window.jvbPopup({toggle:this.toggle,popup:this.container,name:"Referral Box",onOpen:()=>{this.forms.forEach((e=>{e.addEventListener("submit",this.submitHandler)})),this.container.addEventListener("click",this.clickHandler),this.container.addEventListener("input",this.inputHandler)},onClose:()=>{this.forms.forEach((e=>{e.removeEventListener("submit",this.submitHandler)})),this.container.removeEventListener("click",this.clickHandler),this.container.removeEventListener("input",this.inputHandler)}}),this.tabs=null,this.container.querySelector("nav.tabs")&&(this.tabs=new window.jvbTabs(this.container,{updateURL:!1})),this.ui=window.uiFromSelectors(this.selectors,this.container)}initListeners(){this.clickHandler=this.handleClick.bind(this),this.inputHandler=this.handleInput.bind(this),this.submitHandler=this.handleFormSubmit.bind(this),this.changeHandler=this.handleChange.bind(this)}handleClick(e){if(e.target.classList.contains(".copy")){let t=e.target.dataset.target,r=this.container.querySelector(`#${t}`);r=!!r&&r.textContent,r&&this.handleCopy(e.target,r)}}handleChange(e){"referral-code"===e.target.id&&window.debouncer.schedule("check-referral",(()=>this.makeRequest("referrals/check-code",{code:e.target.value})))}handleInput(e){"referral-code"===e.target.id&&(e.target.value=e.target.value.toUpperCase())}initShareWidget(){this.initCopyButton(),this.loadStats()}async checkForReferral(){const e=this.getUrlParameter("seeReferral"),t=this.getUrlParameter("ref");if(!e&&!t)return;if(!t)return void this.popup.openPopup();const r=this.container.querySelector("#referral-code-input");if(!r)return;const n=t.toUpperCase();r.value=n,r.readOnly=!0,this.popup.togglePopup();try{const e=await this.validateCodeOnly(n);if(e.success){this.showReferrerBanner(e.referrer_name,n);const t=this.container.querySelector("#referral-name");t&&t.focus()}else r.readOnly=!1,this.showMessage("This referral link is invalid. Please enter a valid code.","error")}catch(e){console.error("Error validating code:",e),r.readOnly=!1}this.removeUrlParameter("ref")}getUrlParameter(e){return new URLSearchParams(window.location.search).get(e)}removeUrlParameter(e){const t=new URL(window.location);t.searchParams.delete(e),window.history.replaceState({},document.title,t.toString())}async validateCodeOnly(e){const t=await fetch(`${jvbSettings.api}/referrals/check-code`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});return await t.json()}showReferrerBanner(e,t){const r=this.container.querySelector(".referral-header");if(!r)return;const n=document.createElement("div");n.className="referrer-banner",n.innerHTML=`\n <div class="banner-icon">🎉</div>\n <div class="banner-content">\n <strong>${window.escapeHtml(e)}</strong> referred you!\n <div class="banner-code">Code: <code>${window.escapeHtml(t)}</code></div>\n </div>\n `,r.parentNode.insertBefore(n,r.nextSibling);const a=r.querySelector("h3");a&&(a.textContent="Complete Your Registration");const o=r.querySelector("p");o&&(o.textContent="Enter your details below to claim your welcome reward!")}handleCopy(e,t=""){if(""===t||"string"!=typeof t)return;let r=e.textContent;(navigator.clipboard||navigator.clipboard.writeText)&&navigator.clipboard.writeText(t).then((()=>{e.textContent="Copied!",e.style.background="#00a32a",setTimeout((()=>{e.textContent=r,e.style.background=""}),2e3)}))}async loadStats(){if(this.container.querySelector(".referral-stats"))try{const e=await fetch(`${jvbSettings.api}/referrals/stats`,{headers:{"X-WP-Nonce":jvbSettings.nonce}}),t=await e.json();t.success&&t.stats&&this.updateStats(t.stats)}catch(e){console.error("Error loading stats:",e)}}updateStats(e){const t={total:this.container.querySelector('[data-stat="total"]'),treated:this.container.querySelector('[data-stat="treated"]'),pending:this.container.querySelector('[data-stat="pending"]'),rewards:this.container.querySelector('[data-stat="rewards"]')};t.total&&(t.total.textContent=e.total_referrals||0),t.treated&&(t.treated.textContent=e.treated_count||0),t.pending&&(t.pending.textContent=e.pending_count||0),t.rewards&&(t.rewards.textContent="$"+parseFloat(e.available_rewards||0).toFixed(2))}async handleFormSubmit(e){console.log("Form Submission!"),window.debouncer.cancel("check-referral"),e.preventDefault(),console.log("Still working?");const t=e.target,r=new FormData(t);let n={};this.setFormLoading(!0,t);try{let e={success:!1,message:""};console.log(t),console.log(t.id),"referral-code-form"===t.id?(r.get("name")||(e.message+="We need your name to know who you are."),r.get("email")||(e.message+="We need your email to confirm you have access to it."),r.get("referral_code")||(e.message+="We need the referral code to know who sent you."),r.get("name")&&r.get("email")&&r.get("referral_code")&&(n.name=r.get("name"),n.email=r.get("email"),n.code=r.get("referral_code"),e=await this.makeRequest("referrals/register",n))):"login-form"===t.id&&r.get("login-email")&&(n.type="login",n.email=r.get("login-email"),n.context={},n.context.redirect_to=window.location.href+"?seeReferral=1",console.log("Making Request with: ",n),e=await this.makeRequest("magic",n)),e.success?this.handleSuccess(e):(this.showMessage(e.message||"Something went wrong. Please try again.","error"),this.setFormLoading(!1,t))}catch(e){console.error("Error registering:",e),this.showMessage("Something went wrong. Please try again.","error"),this.setFormLoading(!1,t)}finally{this.setFormLoading(!1,t)}}async makeRequest(e,t){if(!["magic","referrals/register","referrals/check-code"].includes(e))return{success:!1,message:"Something went wrong (Invalid endpoint)."};console.log("Endpoint: ",e),console.log("Data: ",t);const r=await fetch(`${jvbSettings.api}${e}`,{method:"POST",headers:{"Content-Type":"application/json","X-WP-Nonce":jvbSettings.nonce},body:JSON.stringify(t)});return await r.json()}handleSuccess(e){this.container.querySelectorAll("form").forEach((e=>{window.fade(e,!1)}));const t=this.container.querySelector(".success-message");t&&(t.hidden=!1,t.scrollIntoView({behavior:"smooth",block:"center"}),this.dispatchEvent("emailSent",{email:e.email}))}setFormLoading(e,t){t.querySelectorAll("input").forEach((t=>t.disabled=e));let r=t.querySelector(".status"),n=r.querySelector(".message");r.hidden=e,r.classList.toggle("loading",e),n.textContent=e?"Checking with server...":""}showMessage(e,t="success"){const r=this.container.querySelector("#referral-message");r&&(r.textContent=e,r.className="message "+t,r.style.display="block","error"===t&&setTimeout((()=>{r.style.display="none"}),5e3))}dispatchEvent(e,t){const r=new CustomEvent("referralWidget:"+e,{detail:t,bubbles:!0});this.container.dispatchEvent(r)}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbReferral=new e}))})(); |
| | |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.hasAutocomplete=!1,this.isInitializing=!0,this.taxonomiesToFetch=new Set,this.store=new window.jvbStore({name:"taxonomies",storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:72e5,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy"}),this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.activeStore=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":if(this.modal?.open&&this.handleTermsLoaded(t),this.isAutocompleteActive&&this.activeField){const e=this.fields.get(this.activeField),i=t.data?.items||[],s=t.filters?.search||"";this.showAutocompleteResults(e,i,s),this.isAutocompleteActive=!1}break;case"filters-changed":this.modal?.open&&this.showLoading();break;case"fetch-error":this.isAutocompleteActive&&this.activeField&&(this.showAutocompleteError(this.activeField),this.isAutocompleteActive=!1),this.handleFetchError(t.error)}}handleTermsLoaded(e){this.hideLoading();const t=e.data?.items||[],i=e.data?.pagination||{},s=e.filters?.search&&e.filters.search.length>0,o=e.filters?.page>1;0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),this.currentTerms=t,i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,o)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldsForTaxonomy(e,t){this.fields.forEach((i=>{i.taxonomy===e&&i.selectedTerms.size>0&&i.selectedTerms.forEach((e=>{const s=t.find((t=>t.id===e));if(s){const t=i.selectedContainer.querySelector(`[data-id="${e}"]`);t&&(t.dataset.path=s.path,t.querySelector("span").textContent=s.path)}}))}))}scanExistingFields(e=null){e||(e=document.body);e.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=e.querySelector("button.taxonomy-toggle"),n={id:s,input:i,container:e,taxonomy:o.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(o.dataset.max)||0,canSearch:"search"in o.dataset,hasAutocomplete:"autocomplete"in o.dataset,autocompleteDropdown:e.querySelector(".autocomplete-dropdown")??!1,canCreate:"creatable"in o.dataset,isRequired:"required"in o.dataset,selectedTerms:new Set,toggle:o,selectedContainer:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&n.hasAutocomplete&&(this.hasAutocomplete=!0,this.initAutocomplete());const a=i.value.trim();if(""!==a){a.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>n.selectedTerms.add(e)))}return this.fields.set(s,n),this.isInitializing?this.taxonomiesToFetch.add(n.taxonomy):this.store.setFilter("taxonomy",n.taxonomy),n.selectedTerms.size>0&&this.initFieldDisplay(s),s}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),console.log(`Batch fetching ${e.length} unique taxonomies:`,e);for(const t of e)await this.store.setFilters({taxonomy:t,page:1,search:"",parent:0});this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;const i=Array.from(t.selectedTerms),s=[];i.forEach((e=>{const t=this.store.data.get(e);t&&s.push(t)})),s.forEach((t=>{this.addTermToDisplay(e,t.id,t.name,t.path)}))}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":console.log(t),this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.activeStore&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),this.hasAutocomplete&&this.initAutocomplete()}initAutocomplete(){console.log("Autocomplete init"),this.autocompleteHandler=window.debounce((e=>this.handleAutocomplete(e)),300),document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this))}handleClick(e){const t=window.targetCheck(e,".taxonomy-toggle");if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t),this.modalInstance.handleOpen()}catch(e){console.error("Error handling toggle click:",e),this.error?.handleError(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e){if(this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=jvbSettings.labels[this.currentConfig.taxonomy].single,this.currentPlural=jvbSettings.labels[this.currentConfig.taxonomy].plural,this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms){let e=[];if(this.currentConfig.selectedTerms.forEach((t=>{const i=this.store.getItem(t);i?this.selectedTerms.set(t,{id:t,name:i.name,path:i.path}):e.push(t)})),e.length>0){this.fetchSpecificTerms(e).forEach((e=>{this.selectedTerms.set(e.id,{id:e.id,name:e.name,path:e.path})}))}}}fetchSpecificTerms(e){return[]}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,hasAutocomplete:!1,autocompleteDropdown:document.querySelector(".autocomplete-dropdown")??!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s),this.modalInstance.handleOpen()}openModal(e){this.activeField=e.fieldId,this.currentConfig=e,e.canCreate&&"jvbTaxCreator"in window?this.creator=new window.jvbTaxCreator(this):this.creator&&delete this.creator,this.selectedTerms=new Set(e.selectedTerms);this.store.filters.taxonomy!==e.taxonomy&&this.store.setFilters({taxonomy:e.taxonomy,page:1,search:"",parent:0}),window.removeChildren(this.ui.termsList),this.ui.search.value="",this.updateSelectionCount(),this.modalInstance.open()}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}this.fields.delete(this.activeField)}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),this.creator&&delete this.creator,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const o=i.selectedContainer.querySelector(`[data-id="${s}"]`);o&&o.remove();const n=Array.from(i.selectedTerms);i.input.value=n.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const o=this.fields.get(e);if(!o||o.selectedContainer.querySelector(`[data-id="${t}"]`))return;const n=window.getTemplate("selectedTerm").cloneNode(!0);n.dataset.id=t,n.dataset.path=s,n.dataset.name=i,n.dataset.taxonomy=o.taxonomy,n.querySelector("span").textContent=s,n.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(n)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(e){const t=e.target.value.trim();this.searchHandler&&clearTimeout(this.searchHandler),this.searchHandler=setTimeout((()=>{this.store.setFilters({search:t,page:1,parent:t?0:this.store.filters.parent||0}),window.removeChildren(this.ui.termsList)}),300)}async handleAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);if(!i)return;const s=e.target.value.trim();if(s.length<2)return i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!0),void(this.isAutocompleteActive=!1);this.activeField=t,this.currentConfig=i,i.canCreate&&!this.creator&&(this.creator=new window.jvbTaxCreator(this)),this.isAutocompleteActive=!0,i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!1),this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1})}cleanupAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target);this.fields.get(t)&&this.creator&&delete this.creator}showAutocompleteError(e){const t=this.fields.get(e);if(!t)return;t.config.autocompleteDropdown||(t.config.autocompleteDropdown=t.element.querySelector(".autocomplete-dropdown"));const i=t.config.autocompleteDropdown;i&&(window.removeChildren(i),this.showEmptyState("Hmmm... something went wrong",i))}showAutocompleteResults(e,t,i){if(!e||!e.autocompleteDropdown)return;const s=e.autocompleteDropdown;if(window.removeChildren(s),0===t.length?this.showEmptyState("No items found.",s):t.forEach((t=>{const i=this.createAutocompleteTermElement(e,t);i&&s.appendChild(i)})),this.creator){const t=this.creator.createAutocompleteOption(i,e);s.appendChild(t)}s.hidden=!1}createAutocompleteTermElement(e,t){const i=document.createElement("button");return i.type="button",i.className="autocomplete-item",i.dataset.id=t.id,i.dataset.name=t.name,i.dataset.path=t.path||t.name,i.textContent=t.path||t.name,i.addEventListener("click",(()=>{e.selectedTerms.add(parseInt(t.id)),this.addTermToDisplay(e.id,t.id,t.name,t.path),e.input.value=Array.from(e.selectedTerms).join(","),e.input.dispatchEvent(new Event("change",{bubbles:!0})),e.autocompleteDropdown.hidden=!0;const i=e.container.querySelector("input[data-autocomplete]");i&&(i.value="")})),i}navigateToParent(){this.store.setFilters({parent:0,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.termsList),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.store.setFilters({parent:t,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=0===t}loadMoreTerms(){if(!this.activeStore)return;const e=this.activeStore.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e,t=!1,i=!1){if(t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.store.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s,e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&this.ui.termsList.appendChild(t)}))}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),o=t.querySelector("label"),n=t.querySelector("span, .term-name");if(s&&o&&n&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,o.htmlFor=s.id,o.title=e.path||e.name,o.dataset.path=e.path,n.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.store?.filters?.search||"",t=this.store?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found.",t=null){t||(t=this.ui.termsList);const i=window.getTemplate("noResults").cloneNode(!0);e&&i.querySelector("span")&&(i.querySelector("span").textContent=e),t.appendChild(i)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector=new e}))})(); |
| | | (()=>{class e{constructor(){this.a11y=window.jvbA11y,this.error=window.jvbError,this.index=-1,this.hasAutocomplete=!1,this.isInitializing=!0,this.taxonomiesToFetch=new Set,this.store=new window.jvbStore({name:"taxonomies",storeName:"terms",keyPath:"id",showLoading:!1,indexes:[{name:"taxonomy",keyPath:"taxonomy"},{name:"parent",keyPath:"parent"},{name:"slug",keyPath:"slug",unique:!0},{name:"count",keyPath:"count"}],endpoint:"terms",TTL:72e5,filters:{taxonomy:"",page:1,search:"",parent:0},required:"taxonomy"}),this.fields=new Map,this.selectedTerms=new Map,this.activeField=null,this.currentConfig=null,this.currentSingular=null,this.currentPlural=null,this.activeStore=null,this.disabled=!1,this.searchHandler=null,this.autocompleteHandler=null,this.isAutocompleteActive=!1,this.init()}init(){this.initModal(),this.scanExistingFields(),this.initGlobalListeners(),this.store.subscribe(this.handleStoreEvent.bind(this)),this.isInitializing=!1,this.batchFetchTaxonomies()}handleStoreEvent(e,t){switch(e){case"data-loaded":if(this.modal?.open&&this.handleTermsLoaded(t),this.isAutocompleteActive&&this.activeField){const e=this.fields.get(this.activeField),i=t.data?.items||[],s=t.filters?.search||"";this.showAutocompleteResults(e,i,s),this.isAutocompleteActive=!1}break;case"filters-changed":this.modal?.open&&this.showLoading();break;case"fetch-error":this.isAutocompleteActive&&this.activeField&&(this.showAutocompleteError(this.activeField),this.isAutocompleteActive=!1),this.handleFetchError(t.error)}}handleTermsLoaded(e){this.hideLoading();const t=e.data?.items||[],i=e.data?.pagination||{},s=e.filters?.search&&e.filters.search.length>0,o=e.filters?.page>1;0===t.length?(o||this.showEmptyState(s?"No results found.":"No items available."),this.observer.unobserve(this.ui.sentinel)):(this.renderTerms(t,o,s),this.currentTerms=t,i.has_more?this.observer.observe(this.ui.sentinel):this.observer.unobserve(this.ui.sentinel)),this.a11y?.announce(t.length,o)}handleFetchError(e){console.error("Taxonomy fetch error:",e),this.hideLoading(),this.error?.log?this.error.log(e,{component:"TaxonomySelector",action:"fetchTerms"},(()=>this.fetchCurrentTerms())):this.showEmptyState("Error loading terms. Please try again.")}updateFieldsForTaxonomy(e,t){this.fields.forEach((i=>{i.taxonomy===e&&i.selectedTerms.size>0&&i.selectedTerms.forEach((e=>{const s=t.find((t=>t.id===e));if(s){const t=i.selectedContainer.querySelector(`[data-id="${e}"]`);t&&(t.dataset.path=s.path,t.querySelector("span").textContent=s.path)}}))}))}scanExistingFields(e=null){e||(e=document.body);e.querySelectorAll(".field.taxonomy, .field.post").forEach((e=>{try{this.registerField(e)}catch(t){this.error.log(t,{component:"TaxonomySelector",action:"scanExistingFields",container:e.dataset.name})}}))}registerField(e,t={}){let i=e.querySelector("input[type=hidden]");if(!i)return;"fieldId"in e.dataset||(e.dataset.fieldId=this.createFieldId(e));let s=e.dataset.fieldId,o=e.querySelector("button.taxonomy-toggle"),n={id:s,input:i,container:e,taxonomy:o.dataset.taxonomy,name:e.dataset.field,maxSelection:parseInt(o.dataset.max)||0,canSearch:"search"in o.dataset,hasAutocomplete:"autocomplete"in o.dataset,autocompleteDropdown:e.querySelector(".autocomplete-dropdown")??!1,canCreate:"creatable"in o.dataset,isRequired:"required"in o.dataset,selectedTerms:new Set,toggle:o,selectedContainer:e.querySelector(".selected-items"),...t};!this.hasAutocomplete&&n.hasAutocomplete&&(this.hasAutocomplete=!0,this.initAutocomplete());const a=i.value.trim();if(""!==a){a.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>n.selectedTerms.add(e)))}return this.fields.set(s,n),this.isInitializing?this.taxonomiesToFetch.add(n.taxonomy):this.store.setFilter("taxonomy",n.taxonomy),n.selectedTerms.size>0&&this.initFieldDisplay(s),s}async batchFetchTaxonomies(){if(0===this.taxonomiesToFetch.size)return;const e=Array.from(this.taxonomiesToFetch);this.taxonomiesToFetch.clear(),console.log(`Batch fetching ${e.length} unique taxonomies:`,e);for(const t of e)await this.store.setFilters({taxonomy:t,page:1,search:"",parent:0});this.fields.forEach(((e,t)=>{e.selectedTerms.size>0&&this.initFieldDisplay(t)}))}createFieldId(e){return this.index++,"selector-"+this.index}async initFieldDisplay(e){const t=this.fields.get(e);if(!t||0===t.selectedTerms.size)return;const i=Array.from(t.selectedTerms),s=[];i.forEach((e=>{const t=this.store.data.get(e);t&&s.push(t)})),s.forEach((t=>{this.addTermToDisplay(e,t.id,t.name,t.path)}))}initModal(){this.modalID="dialog#jvb-selector",this.modal=document.querySelector(this.modalID),this.modal?(this.initModalElements(),this.modalInstance=new window.jvbModal(this.modal,{handleForm:!1,save:null,open:null}),this.modalInstance.subscribe(((e,t)=>{switch(e){case"modal-open":console.log(t),this.openModal(t);break;case"modal-close":this.closeModal(t)}}))):console.warn("Taxonomy selector modal not found")}initModalElements(){this.selectors={search:{input:"[type=search]",clear:".clear-search",container:".search-wrapper"},termsList:".items-container",termsWrap:".items-wrap",breadcrumbs:{nav:"nav.term-navigation",back:".back-to-parent"},loading:{loading:".loading",text:".loading span"},selectedTerms:".selected-items",sentinel:".scroll-sentinel",modal:{title:"#modal-title",content:".modal-content"},create:{details:".create-new-term",parent:"#select_parent",summary:".create-new-term summary",name:"#term_name",button:".submit-term",label:{name:"[for=term_name]",parent:"[for=select_parent]"}},favouriteTerms:".favourite-terms"},this.ui=window.uiFromSelectors(this.selectors),this.observer=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting&&this.activeStore&&this.loadMoreTerms()}))}),{root:this.ui.termsWrap,threshold:.5})}initGlobalListeners(){document.addEventListener("click",this.handleClick.bind(this)),document.addEventListener("change",this.handleChange.bind(this)),this.hasAutocomplete&&this.initAutocomplete()}initAutocomplete(){console.log("Autocomplete init"),this.autocompleteHandler=window.debounce((e=>this.handleAutocomplete(e)),300),document.addEventListener("input",this.autocompleteHandler),document.addEventListener("blur",this.cleanupAutocomplete.bind(this))}handleClick(e){const t=window.targetCheck(e,".taxonomy-toggle");if(t)return e.preventDefault(),void this.handleToggleClick(t);const i=window.targetCheck(e,"button.remove-item");if(i&&e.target.closest(".jvb-selector")){const e=this.getFieldId(i),t=i.closest(".selected-item").dataset.id;this.removeSelectedTerm(e,t)}else e.target.matches(".modal-close")?this.modalInstance&&this.modalInstance.handleClose():this.modal&&this.modal.contains(e.target)&&this.handleModalClick(e)}handleChange(e){if(window.targetCheck(e,".taxonomy.field, .post.field")&&"hidden"===e.target.type){const t=this.getFieldId(e.target);this.updateFieldFromInput(t)}else this.modal&&this.modal.contains(e.target)&&this.handleModalChange(e)}handleToggleClick(e){try{const t=this.getFieldId(e);if(!this.fields.get(t))return void console.error("Field not found for toggle:",t);this.setActiveField(t),this.modalInstance.handleOpen()}catch(e){console.error("Error handling toggle click:",e),this.error?.handleError(e,{component:"TaxonomySelector",action:"handleToggleClick"})}}setActiveField(e){if(this.activeField=e,this.currentConfig=this.fields.get(e),this.currentSingular=jvbSettings.labels[this.currentConfig.taxonomy].single,this.currentPlural=jvbSettings.labels[this.currentConfig.taxonomy].plural,this.store.setFilter("taxonomy",this.currentConfig.taxonomy),this.selectedTerms.clear(),this.currentConfig.selectedTerms){let e=[];if(this.currentConfig.selectedTerms.forEach((t=>{const i=this.store.getItem(t);i?this.selectedTerms.set(t,{id:t,name:i.name,path:i.path}):e.push(t)})),e.length>0){this.fetchSpecificTerms(e).forEach((e=>{this.selectedTerms.set(e.id,{id:e.id,name:e.name,path:e.path})}))}}}fetchSpecificTerms(e){return[]}handleModalClick(e){if(window.targetCheck(e,".remove-item")){let t=window.targetCheck(e,".selected-item");t&&this.removeSelectedTermFromModal(t.dataset.id)}else if(window.targetCheck(e,".back-to-parent"))this.navigateToParent();else if(window.targetCheck(e,".toggle-children")){let t=e.target.closest("li");this.navigateToChild(parseInt(t.dataset.id),t.querySelector(".term-name").textContent)}else if(window.targetCheck(e,".path-level")){let t=window.targetCheck(e,".path-level");this.navigateToPath(t)}}handleModalChange(e){if(window.targetCheck(e,this.modalID)&&"checkbox"===e.target.type){e.preventDefault(),e.stopPropagation();const t=parseInt(e.target.closest("li").dataset.id),i=e.target.closest("li").querySelector("label");e.target.checked?this.addSelectedTermToModal(t,i.title,i.dataset.path):this.removeSelectedTermFromModal(t)}}openForFilter(e,t,i=[]){const s=`filter-${e}-${Date.now()}`;this.fields.set(s,{id:s,input:null,container:null,taxonomy:e,name:`filter_${e}`,maxSelection:0,canSearch:!0,hasAutocomplete:!1,autocompleteDropdown:document.querySelector(".autocomplete-dropdown")??!1,canCreate:!1,isRequired:!1,selectedTerms:new Set(i),toggle:null,selectedContainer:null,isFilterMode:!0,filterCallback:t}),this.setActiveField(s),this.modalInstance.handleOpen()}openModal(e){this.activeField=e.fieldId,this.currentConfig=e,e.canCreate&&"jvbTaxCreator"in window?this.creator=new window.jvbTaxCreator(this):this.creator&&delete this.creator,this.selectedTerms=new Set(e.selectedTerms);this.store.filters.taxonomy!==e.taxonomy&&this.store.setFilters({taxonomy:e.taxonomy,page:1,search:"",parent:0}),window.removeChildren(this.ui.termsList),this.ui.search.value="",this.updateSelectionCount(),this.modalInstance.open()}closeModal(){if(this.observer.unobserve(this.ui.sentinel),window.removeChildren(this.ui.termsList),this.currentConfig?.isFilterMode){if(this.currentConfig.filterCallback){const e=Array.from(this.selectedTerms.keys());this.currentConfig.filterCallback(e,this.currentConfig.taxonomy)}this.fields.delete(this.activeField)}else this.activeField&&this.saveSelectionsToField(this.activeField);this.currentConfig?.canSearch&&this.searchHandler&&this.ui.search.input.removeEventListener("input",this.searchHandler),this.creator&&delete this.creator,this.activeField=null,this.currentConfig=null}resetModalState(){this.disabled=!1,window.removeChildren(this.ui.termsList),window.removeChildren(this.ui.selectedTerms),this.ui.search.input.value="",window.removeChildren(this.ui.breadcrumbs.nav),this.ui.breadcrumbs.nav.appendChild(this.ui.breadcrumbs.back),this.ui.breadcrumbs.back.hidden=!0}updateModalForTaxonomy(){if(!this.currentConfig)return;this.ui.modal.title.textContent=`Select ${this.currentPlural}`,this.ui.search.container&&(this.ui.search.container.style.display=this.currentConfig.canSearch?"block":"none"),this.ui.create.details&&(this.ui.create.details.style.display=this.currentConfig.canCreate?"block":"none",this.ui.create.details.hidden=!this.currentConfig.canCreate,this.ui.create.summary&&(this.ui.create.summary.textContent=`Add new ${this.currentSingular}`),this.ui.create.label.name&&(this.ui.create.label.name.textContent=`Name this ${this.currentSingular}`),this.ui.create.label.parent&&(this.ui.create.label.parent.textContent="Nest it under"),this.ui.create.parent);const e=`Opened ${this.currentSingular} selection. Choose from checkboxes or search to filter results.`;this.a11y?.announce(e)}updateModalSelections(){window.removeChildren(this.ui.selectedTerms),this.selectedTerms.forEach(((e,t)=>{this.addTermToModalDisplay(t,e.name,e.path)})),this.checkSelectionLimits()}addSelectedTermToModal(e,t,i){this.selectedTerms.set(e,{id:e,name:t,path:i}),this.addTermToModalDisplay(e,t,i),this.checkSelectionLimits();const s=this.ui.termsList.querySelector(`input[value="${e}"]`);s&&(s.checked=!0)}removeSelectedTermFromModal(e){this.selectedTerms.delete(parseInt(e));const t=this.ui.selectedTerms.querySelector(`[data-id="${e}"]`);t&&t.remove();const i=this.ui.termsList.querySelector(`input[value="${e}"]`);i&&(i.checked=!1),this.checkSelectionLimits()}addTermToModalDisplay(e,t,i){const s=window.getTemplate("selectedTerm").cloneNode(!0);s.dataset.id=e,s.dataset.path=i,s.dataset.name=t,s.dataset.taxonomy=this.currentConfig.taxonomy,s.querySelector("span").textContent=i,s.querySelector("button").title=`Remove ${t}`,this.ui.selectedTerms.appendChild(s)}checkSelectionLimits(){this.currentConfig&&0!==this.currentConfig.maxSelection&&(this.disabled=this.selectedTerms.size>=this.currentConfig.maxSelection,this.setCheckboxes(this.disabled))}setCheckboxes(e){this.ui.termsList.querySelectorAll('input[type="checkbox"]').forEach((t=>{t.checked||(t.disabled=e)}))}saveSelectionsToField(e){const t=this.fields.get(e);if(!t)return;t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),this.selectedTerms.forEach(((i,s)=>{t.selectedTerms.add(s),this.addTermToDisplay(e,s,i.name,i.path)}));const i=Array.from(t.selectedTerms);t.input.value=i.join(","),t.input.dispatchEvent(new Event("change",{bubbles:!0}))}removeSelectedTerm(e,t){const i=this.fields.get(e);if(!i)return;const s=parseInt(t);i.selectedTerms.delete(s);const o=i.selectedContainer.querySelector(`[data-id="${s}"]`);o&&o.remove();const n=Array.from(i.selectedTerms);i.input.value=n.join(","),i.input.dispatchEvent(new Event("change",{bubbles:!0}))}addTermToDisplay(e,t,i,s){const o=this.fields.get(e);if(!o||o.selectedContainer.querySelector(`[data-id="${t}"]`))return;const n=window.getTemplate("selectedTerm").cloneNode(!0);n.dataset.id=t,n.dataset.path=s,n.dataset.name=i,n.dataset.taxonomy=o.taxonomy,n.querySelector("span").textContent=s,n.querySelector("button").title=`Remove ${i}`,o.selectedContainer.appendChild(n)}updateFieldFromInput(e){const t=this.fields.get(e);if(!t)return;const i=t.input.value.trim();if(t.selectedTerms.clear(),window.removeChildren(t.selectedContainer),""!==i){i.split(",").map((e=>parseInt(e.trim()))).filter((e=>!isNaN(e))).forEach((e=>t.selectedTerms.add(e))),this.initFieldDisplay(e)}}handleSearch(e){const t=e.target.value.trim();this.searchHandler&&clearTimeout(this.searchHandler),this.searchHandler=setTimeout((()=>{this.store.setFilters({search:t,page:1,parent:t?0:this.store.filters.parent||0}),window.removeChildren(this.ui.termsList)}),300)}async handleAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target),i=this.fields.get(t);if(!i)return;const s=e.target.value.trim();if(i.currentAutocompleteQuery=s,s.length<2)return i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!0),void(this.isAutocompleteActive=!1);this.activeField=t,this.isAutocompleteActive=!0,i.autocompleteDropdown&&(i.autocompleteDropdown.hidden=!1),this.store.setFilters({taxonomy:i.taxonomy,search:s,page:1})}cleanupAutocomplete(e){if(!("autocomplete"in e.target.dataset))return;const t=this.getFieldId(e.target);this.fields.get(t)&&this.creator&&delete this.creator}showAutocompleteError(e){const t=this.fields.get(e);if(!t)return;t.config.autocompleteDropdown||(t.config.autocompleteDropdown=t.element.querySelector(".autocomplete-dropdown"));const i=t.config.autocompleteDropdown;i&&(window.removeChildren(i),this.showEmptyState("Hmmm... something went wrong",i))}showAutocompleteResults(e,t,i){if(!e||!e.autocompleteDropdown)return;const s=e.autocompleteDropdown;window.removeChildren(s),0===t.length?this.showEmptyState("No items found.",s):t.forEach((t=>{const i=this.createAutocompleteTermElement(e,t);i&&s.appendChild(i)}));const o=e.currentAutocompleteQuery||i;if(e.canCreate&&o&&window.jvbTaxCreator){const e=this.createNewTermOption(o);s.appendChild(e)}s.hidden=!1}createNewTermOption(e){const t=document.createElement("button");return t.type="button",t.className="autocomplete-item create-term",t.dataset.query=e,t.innerHTML=`<strong>Create:</strong> "${e}"`,t}createAutocompleteTermElement(e,t){const i=document.createElement("button");return i.type="button",i.className="autocomplete-item",i.dataset.id=t.id,i.dataset.name=t.name,i.dataset.path=t.path||t.name,i.textContent=t.path||t.name,i.addEventListener("click",(()=>{e.selectedTerms.add(parseInt(t.id)),this.addTermToDisplay(e.id,t.id,t.name,t.path),e.input.value=Array.from(e.selectedTerms).join(","),e.input.dispatchEvent(new Event("change",{bubbles:!0})),e.autocompleteDropdown.hidden=!0;const i=e.container.querySelector("input[data-autocomplete]");i&&(i.value="")})),i}navigateToParent(){this.store.setFilters({parent:0,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=!0}navigateToChild(e,t){this.store.setFilters({parent:e,page:1}),window.removeChildren(this.ui.termsList),this.updateBreadcrumbs(e,t),this.ui.breadcrumbs.back.hidden=!1}navigateToPath(e){const t=parseInt(e.dataset.id)||0;this.store.setFilters({parent:t,page:1}),window.removeChildren(this.ui.termsList),this.ui.breadcrumbs.back.hidden=0===t}loadMoreTerms(){if(!this.activeStore)return;const e=this.activeStore.filters.page||1;this.store.setFilter("page",e+1)}renderTerms(e,t=!1,i=!1){if(t||window.removeChildren(this.ui.termsList),0===e.length)return void(t||this.showEmptyState());const s=this.store.filters.parent||0;this.ui.breadcrumbs.back.hidden=0===s,e.forEach((e=>{const t=this.createTermElement({id:parseInt(e.id),name:e.name,hasChildren:e.hasChildren,path:e.path||null,show:i});t&&this.ui.termsList.appendChild(t)}))}createTermElement(e){if(!e||!e.name)return null;const t=window.getTemplate("termListItem").cloneNode(!0);t.dataset.id=e.id;const i=this.selectedTerms.has(e.id),s=t.querySelector("input"),o=t.querySelector("label"),n=t.querySelector("span, .term-name");if(s&&o&&n&&(s.id=`${this.currentConfig.container.id}${e.id}`,s.name=`${this.currentConfig.container.id}${this.currentConfig.taxonomy}-select`,s.value=e.id,s.disabled=!i&&this.disabled,s.checked=i,o.htmlFor=s.id,o.title=e.path||e.name,o.dataset.path=e.path,n.textContent=e.show?e.path:e.name),e.hasChildren){const i=window.getTemplate?window.getTemplate("termChildrenToggle"):this.createChildrenToggle();i&&(i.ariaLabel=`View sub-terms of ${e.name}`,t.appendChild(i))}return t}createChildrenToggle(){const e=document.createElement("button");return e.type="button",e.className="toggle-children",e.innerHTML="→",e}updateBreadcrumbs(e,t){const i=window.getTemplate("termBreadcrumb").cloneNode(!0);i.dataset.id=e,i.textContent=t,i.title=t;const s=this.ui.breadcrumbs.nav.querySelector(`[data-id="${e}"]`);if(s)for(;s.nextElementSibling;)s.nextElementSibling.remove();else this.ui.breadcrumbs.nav.appendChild(i)}showLoading(){this.ui.loading.loading.hidden=!1,this.modal.classList.add("loading");const e=this.store?.filters?.search||"",t=this.store?.filters?.parent||0;let i=""!==e?`searching for "${e}" items`:0===t?"loading items":"loading child items";window.typeLoop?this.stopTyping=window.typeLoop(this.ui.loading.text,i):this.ui.loading.text.textContent=i}hideLoading(){this.ui.loading.loading.hidden=!0,this.modal.classList.remove("loading"),this.stopTyping&&this.stopTyping()}showEmptyState(e="No items found.",t=null){t||(t=this.ui.termsList);const i=window.getTemplate("noResults").cloneNode(!0);e&&i.querySelector("span")&&(i.querySelector("span").textContent=e),t.appendChild(i)}getFieldId(e){if(e.dataset.fieldId)return e.dataset.fieldId;const t=e.closest("[data-field-id]");return t?t.dataset.fieldId:null}destroy(){document.removeEventListener("click",this.handleClick),document.removeEventListener("change",this.handleChange),this.observer?.disconnect(),this.store.destroy(),this.fields.clear(),this.selectedTerms.clear()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbSelector=new e}))})(); |
| | |
| | | (()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStore=new window.jvbStore({name:"upload_fields",storeName:"fieldStates",keyPath:"id",version:2,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],stripDOMReferences:!0,TTL:6048e5}),this.uploadStore=new window.jvbStore({name:"uploads",storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}]}),window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.subscribers=new Set,this.dragController=null,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',hiddenValue:'input[type="hidden"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.init()}async init(){await this.loadFields(),await this.loadUploads(),this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if("uploads"!==t.endpoint&&"uploads/meta"!==t.endpoint)return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data.fieldId;switch(e){case"cancel-operation":s&&this.clearField(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":(t.result?.data||[]).forEach((e=>{const t=this.uploads.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploads.set(t.id,t))})),s&&this.cleanField(s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),r={id:t,config:s,element:e,ui:this.buildFieldUI(e),uploads:new Set,groups:new Set,state:"ready"};return this.fields.set(t,r),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"post_group"!==s.destination||this.dragController||this.initGroupFeatures(),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}initGroupFeatures(){this.dragController=new window.jvbDragHandler({draggableSelector:this.selectors.items.item,dropTargetSelector:`${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,ignoreSelector:"input:not(.upload-select), button, select, textarea, details, summary, a",previewElement:"img, video, .icon",getItemId:e=>e.dataset.uploadId,getSelectedItems:e=>{const t=this.getFieldIdFromElement(e),s=e.dataset.uploadId,r=this.getCurrentSelection(t);return r&&r.includes(s)?r:[s]},validateDrop:(e,t)=>{const s=this.getFieldIdFromElement(t),r=document.querySelector(`[data-upload-id="${e[0]}"]`);return s===this.getFieldIdFromElement(r)},onDrop:(e,t)=>{this.handleItemDrop(e,t),t.scrollIntoView({behavior:"smooth",block:"center"})},onDragStart:e=>{},onDragEnd:(e,t)=>{if(t){const t=document.querySelector(`[data-upload-id="${e[0]}"]`),s=this.getFieldIdFromElement(t),r=this.selectionHandlers.get(s);r?.clearSelection()}},previewOptions:{multiOffset:{x:-60,y:-80},singleOffset:{x:-50,y:-60},showCount:!0}})}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const r=this.getFieldIdFromElement(t);r?(this.processFiles(r,s),this.a11y.announce(`${s.length} file(s) dropped for upload`)):console.error("No field ID found for drop zone")}handleItemDrop(e,t){const s=t.classList.contains("preview");let r=t;if(t.classList.contains("empty-group")){const e=this.getFieldIdFromElement(t),s=this.createGroup(e);if(!s)return void console.error("Failed to create group");r=s.grid}e.forEach((e=>{s?this.removeFromGroup(e):this.addToGroup(e,r)}));const o=this.getFieldIdFromElement(t);this.schedulePersistance(o);const i=e.length>1?`Moved ${e.length} items`:"Moved item";this.a11y.announce(i)}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const t=this.getFieldIdFromElement(e.target),s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}t&&("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e))}getCurrentSelection(e){let t=[];for(let[s,r]of this.selectionHandlers)(e===s||s.includes(e))&&r.selectedItems.size>0&&(t=t.concat([...r.selectedItems]));return t}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){switch(e){case"local_processing":return 28;case"queued":return 50;case"uploading":return 66;case"pending":return 75;case"processing":return 89;case"completed":return 100;default:return 0}}getModalType(e){if(void 0!==e._cachedModalType)return e._cachedModalType;if(!e||!e.element)return e._cachedModalType=null,null;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":this.fields.get(s).element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s);break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),s=t?.dataset.uploader;if(!s)return;const r=this.selected.get(s);if(r&&0!==r.size){const e=this.createGroup(s);if(!e)return;r.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${r.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,r=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(r)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,r=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(r,s),this.a11y.announce("Item removed"),this.schedulePersistance(r))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fields.get(e);if(!t)return;const s=t.ui.field;if(!s)return;const r=new window.jvbHandleSelection({container:s,ui:{selectAll:s.querySelector('[name="select-all-uploads"]'),bulkControls:s.querySelector(".selection-actions"),count:s.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return r.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,r),r}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const r=this.groups.get(t);if(!r)return;const o=new window.jvbHandleSelection({container:r.element,ui:{selectAll:r.element.querySelector(this.selectors.groups.selectAll),bulkControls:r.element.querySelector(this.selectors.groups.actions),count:r.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return o.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,o),o}handleSelectAll(e,t){}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",store:this.fields},upload:{selector:this.selectors.items.item,key:"uploadId",store:this.uploads},group:{selector:this.selectors.groups.container,key:"groupId",store:this.groups}}[t];if(!s)return null;const r=e.closest(s.selector);if(!r)return null;const o=r.dataset[s.key];return s.store.get(o)}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){return this.getFromElement(e,"field")?.id??null}getUploadIdFromElement(e){return this.getFromElement(e,"upload")?.id??null}getGroupIdFromElement(e){return this.getFromElement(e,"group")?.id??null}async processFiles(e,t){const s=this.fields.get(e);if(!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups.display&&(s.ui.groups.display.hidden=!1);const r=t.length;let o=0;this.updateUploadProgress(e,0,r,"Processing files..."),s.uploads||(s.uploads=new Set);const i=Array.from(t).map((async(t,i)=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,a={id:i,attachment_id:null,fieldId:e,originalFile:t,processedFile:null,preview:null,status:"local_processing",element:null,location:null,meta:{originalName:t.name,size:t.size,type:t.type}};a.preview=this.createPreviewUrl(t),t.type.startsWith("image/")?a.processedFile=await this.processImage(t,s.subtype):a.processedFile=t,await this.uploadStore.saveBlob(i,a.processedFile||t);const l=this.getSubtypeFromMime(t.type);return a.element=this.createUploadElement({...a,subtype:l},"post_group"===s.config.destination),this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),s.ui.preview&&(s.ui.preview.appendChild(a.element),a.location=s.ui.preview),this.uploads.set(i,a),s.uploads.add(i),o++,this.updateUploadProgress(e,o,r,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),a.status="processed",setTimeout((()=>{this.showUploadProgress(i,!1)}),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),o++,this.updateUploadProgress(e,o,r,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),await this.schedulePersistance(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}updateFieldState(e){const t=this.fields.get(e);if(!t||!t.ui.field)return;const s=t.ui.field,r=t.uploads?.size||0,o=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=r>0?"true":"false",s.dataset.uploadCount=r.toString(),s.dataset.hasGroups=o?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${r} item${1!==r?"s":""}`)}updateUploadProgress(e,t,s,r){const o=this.fields.get(e);if(!o?.ui?.progress?.progress)return;const i=o.ui.progress,a=s>0?t/s*100:0;i.fill&&(i.fill.style.width=`${a}%`),i.text&&(i.text.textContent=r),i.count&&(i.count.textContent=`${t}/${s}`),i.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.fields.get(e);s&&(s.state=t)}updateUploadStatus(e,t){const s=this.uploads.get(e);s&&(s.status=t,this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploads.get(e);if(!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${t.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(t.status),t.status)}showUploadProgress(e,t=!0){const s=this.uploads.get(e);if(!s||!s.element)return;const r=s.element.querySelector(".progress");r&&(t?(r.style.removeProperty("animation"),r.hidden=!1):(r.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{r.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const r=this.uploads.get(e);if(!r||!r.element)return;const o=r.element.querySelector(".progress");if(!o)return;const i=o.querySelector(".fill"),a=o.querySelector(".details"),l=o.querySelector(".icon");i&&(i.style.width=`${t}%`),s&&a&&(a.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}checkFieldLimits(e,t){const s=this.fields.get(e);if(!s)return!1;return(s.uploads?.size||0)+t<=s.maxFiles}validateFile(e,t){return this.settings.allowedTypes.includes(e.type)?!(e.size>this.settings.maxFileSize)||(this.notify(`File too large: ${this.formatBytes(e.size)}`,"error"),!1):(this.notify(`Invalid file type: ${e.type}`,"error"),!1)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,r=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,r)).toFixed(s))+" "+["Bytes","KB","MB","GB"][r]}shouldProcessClientSide(e,t){return!("image"!==t||!e.type.startsWith("image/"))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((r,o)=>{let i,a=!1;i=setTimeout((()=>{a||(a=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),o(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:i}),this.handleProcess(e,t).then((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),r(e))})).catch((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),o(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((r,o)=>{const i=new Image,a=document.createElement("canvas"),l=a.getContext("2d");let n=null;const d=()=>{i.onload=null,i.onerror=null,n&&(URL.revokeObjectURL(n),n=null),a.width=1,a.height=1,l.clearRect(0,0,1,1)};i.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(i,t);a.width=n,a.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(i,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);a.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});r(s)}else o(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),o(new Error(`Canvas processing failed: ${e.message}`))}},i.onerror=()=>{d(),o(new Error(`Failed to load image: ${e.name}`))};try{n=this.createPreviewUrl(e),i.src=n}catch(e){d(),o(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,r){return new Promise(((o,i)=>{if(!this.worker.worker)return void i(new Error("Worker not available"));const a=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===a)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});o(s)}else i(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),i(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:a,file:e,maxDimension:s,quality:r,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n self.onmessage = async function(e) {\n const { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\n try {\n // Create ImageBitmap from file\n const bitmap = await createImageBitmap(file);\n\n // Calculate dimensions\n const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n const width = Math.round(bitmap.width * scale);\n const height = Math.round(bitmap.height * scale);\n\n // Create OffscreenCanvas\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d');\n\n // Draw and resize\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = 'high';\n ctx.drawImage(bitmap, 0, 0, width, height);\n\n // Clean up bitmap\n bitmap.close();\n\n // Convert to blob\n const blob = await canvas.convertToBlob({\n type: outputFormat,\n quality: quality\n });\n\n self.postMessage({\n messageId,\n success: true,\n blob: blob,\n format: outputFormat\n });\n\n } catch (error) {\n self.postMessage({\n messageId,\n success: false,\n error: error.message\n });\n }\n };\n "],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:r}=e;if(s<=t&&r<=t)return{width:s,height:r};const o=Math.min(t/s,t/r);return{width:Math.round(s*o),height:Math.round(r*o)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}maybeLockUploads(e){const t=this.fields.get(e);if(!t?.ui?.dropZone)return;if("post_group"===t.config.destination)return;const s=t.uploads?.size||0,r=t.config?.maxFiles||999;t.ui.dropZone.hidden=s>=r,t.element.classList.toggle("at-max-uploads",s>=r)}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return void console.error("Image template not found");s.dataset.uploadId=e.id,e.originalFile&&(s.dataset.subtype=this.getSubtypeFromMime(e.originalFile.type)),s.querySelector('[name="featured"]').value=e.id;let[r,o,i,a,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch([r.value,o.src,o.alt]=[e.id,e.preview,e.originalFile?.name??e.meta?.originalName??""],s.dataset.subtype){case"image":[o.src,o.alt]=[e.preview,e.originalFile?.name??e.meta?.originalName??""],i.remove(),a.remove();break;case"video":i.src=e.preview,o.remove(),a.remove();break;case"document":const t=e.originalFile?.name??e.meta?.originalName??"",s=t.split(".").pop()?.toLowerCase()??"",r={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(r[s]||"file");a.innerText=e.originalFile.name,a.prepend(l),o.remove(),i.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e.id,o=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,o&&(o.htmlFor=r)}})),s}async submitUploads(e){const t=this.fields.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const r=this.getFieldGroups(e);if(0===r.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=[],i=new FormData;let a=[];s=s.map((e=>this.uploads.get(e))),r.forEach(((e,t)=>{const r={images:[],fields:{}};for(let[t,s]of Object.entries(e.changes))r.fields[t]=s;s.filter((t=>t.groupId===e.id)).forEach((e=>{if(e){const t=e.processedFile||e.originalFile;if(t){i.append("files[]",t);const s={upload_id:e.id,index:a.length};r.images.push(s),a.push(e.id)}}})),o.push(r)})),s.filter((e=>!Object.hasOwn(e,"groupId"))).forEach((e=>{if(e){const t={images:[],fields:{}},s=e.processedFile||e.originalFile;if(s){i.append("files[]",s);const r={upload_id:e.id,index:a.length};t.images.push(r),a.push(e.id)}o.push(t)}})),i.append("content",t.config.content),i.append("user",t.config.itemID),i.append("posts",JSON.stringify(o)),i.append("upload_ids",JSON.stringify(a));for(const[e,t]of i.entries())console.log(e,t);const l={endpoint:"uploads/groups",method:"POST",data:i,title:`Creating ${o.length} ${t.config.content}${o.length>1?"s":""} from uploads...`,popup:`Creating ${o.length} post${o.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(l);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,this.a11y.announce(`Creating ${o.length} post${o.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}finally{this.schedulePersistance(t.id)}}async queueUpload(e){const t=this.fields.get(e);if(!t?.uploads)return;const s=Array.from(t.uploads);if(0===s.length)return;const r=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");let o=1===s.length?"file":"files";const i={endpoint:"uploads",method:"POST",data:r,title:`Uploading ${s.length} ${o} to server...`,popup:`Uploading ${s.length} ${o}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(i);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,e}catch(e){throw e}finally{this.schedulePersistance(t.id)}}prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let r=[];const o=this.getFieldGroups(e.id);if("post_group"===e.config.destination&&o.length>0){let e=[],t=[],i=[];o.forEach((o=>{let a=[],l=null;o.uploads.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;if(e){s.append("files[]",e);r.length;r.push(t.id),a.push(t.id);const o=t.element?.querySelector('[name="featured"]');o?.checked&&(l=t.id)}}})),e.push(a),t.push(o.title||""),i.push(l)})),s.append("groups",JSON.stringify(e)),s.append("group_titles",JSON.stringify(t)),s.append("featured_images",JSON.stringify(i))}else t.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;e&&(s.append("files[]",e),r.push(t.id))}}));return s.append("upload_ids",JSON.stringify(r)),s}getFieldGroups(e){const t=[];return this.groups.forEach(((s,r)=>{if(s.fieldId===e){const o=this.fields.get(e),i=o?.ui?.groups?.groups?.get(r);t.push({id:r,uploads:Array.from(s.uploads||new Set),changes:s.changes||{},element:i||null})}})),t}async queueUploadMeta(e){const t=this.getUploadFromElement(e.target);if(!t)return;if(!this.fields.get(t.fieldId))return;if(!e.target.closest(".upload-meta"))return;let s={};s[e.target.name]=e.target.value,t.meta={...t.meta,...s};let r={};r[t.attachmentId??t.id]=t.meta;const o={endpoint:"uploads/meta",method:"POST",data:r,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(o)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:t.id})}}createGroup(e,t=null){const s=this.fields.get(e);if(!s)return console.error("Field not found:",e),null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const r=this.createGroupElement(t,e);if(!r)return console.error("Failed to create group element"),null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,r),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(r,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(r);const o={id:t,fieldId:e,element:r,grid:r.querySelector(".item-grid.group"),uploads:new Set,changes:{}};return this.groups.set(t,o),this.addGroupSelectionHandler(e,t),this.schedulePersistance(e),o}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let r=window.getTemplate("groupMetadata");const o=s.querySelector(".fields");if(o&&r){o.append(r);const i=o.querySelector('[name="post_title"]'),a=o.querySelector('[name="post_excerpt"]');i&&(i.id=`${e}_title`,i.name=`${e}[post_title]`),a&&(a.id=`${e}_excerpt`,a.name=`${e}[post_excerpt]`);let l=this.fields.get(t);if(""!==l.config.content){s.querySelector("summary").textContent=l.config.content+" Fields"}}else s.querySelector("details").remove();const i=s.querySelector(".item-grid.group");return i&&(i.dataset.groupId=e),s}deleteGroup(e,t=!0){let s=this.groups.get(e);if(!s)return;let r=!0;t&&s.uploads&&s.uploads.size>0&&(r=!window.confirm("Delete uploads in group?")),t&&r&&s.uploads&&s.uploads.size>0&&Array.from(s.uploads).forEach((e=>{this.addImageToGroup(e,null,!1)})),this.groups.delete(e);let o=s.element;o&&(o.remove(),this.a11y.announce("Group removed")),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){let r=this.uploads.get(e);if(!r)return;let o=this.fields.get(r.fieldId);if(!o)return;if(!t&&r.location===o.ui.preview||t===r.location)return;if(r.location){let t=r.location.dataset.groupId;if(t){let s=this.groups.get(t);s&&s.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t))}}const i=r.element.querySelector('[name*="select-item"]');i&&(i.checked=!1);let a=r.element.querySelector('[name="featured"]');if(a.hidden=!t,t){if(!t.classList.contains("item-grid")||!t.classList.contains("preview")){let s=t.dataset.groupId;a.name=s+"_"+a.name;let o=this.groups.get(s);o||(o=this.createGroup(r.fieldId),t=o.grid,s=o.id),o&&(o.uploads.add(e),r.groupId=s)}}else t=o.ui.preview,r.groupId=null;r.location=t,t.append(r.element),s&&this.schedulePersistance(o.id)}removeFromGroup(e){const t=this.uploads.get(e);if(!t)return;const s=this.fields.get(t.fieldId);if(!s)return;if(t.groupId){const s=this.groups.get(t.groupId);s?.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t.groupId,!1)),t.groupId=null}s.ui?.preview&&(s.ui.preview.appendChild(t.element),t.location=s.ui.preview);const r=t.element.querySelector('[name="featured"]');r&&(r.hidden=!0,r.checked=!1)}removeUpload(e,t){const s=this.fields.get(e),r=this.uploads.get(t);if(!s||!r)return;if(s.uploads?.delete(t),r.groupId){const e=this.groups.get(r.groupId);e&&e.uploads&&(e.uploads.delete(t),0===e.uploads.size&&this.removeGroup(r.groupId))}r.element?.remove(),this.clearUpload(t),this.updateFieldState(e),this.maybeLockUploads(e);const o=this.selectionHandlers.get(s.id);o&&o.deselect(t),this.a11y.announce("Upload removed")}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),1e3)}async persistFieldState(e){const t=this.fields.get(e);if(!t)return;const s={...t,id:e,fieldId:e,uploads:Array.from(t.uploads||[]).map((e=>this.uploads.get(e))),groups:Array.from(this.groups.entries()).filter((([t,s])=>s.fieldId===e&&s.uploads&&s.uploads.size>0)).map((([e,t])=>({id:t.id,uploads:Array.from(t.uploads),changes:t.changes||{}}))),context:{url:this.normalizeUrl(window.location.href),fullUrl:window.location.href,modalType:this.getModalType(t),formId:t.formId,fieldSelector:`.field.upload[data-field="${t.config.name}"]`},timestamp:Date.now()};await this.fieldStore.save(s)}normalizeUrl(e){try{const t=new URL(e);return t.origin+t.pathname}catch(t){return e}}getFieldUploads(e,t=!1){const s=this.fields.get(e);return s&&s.uploads?Array.from(s.uploads).map((e=>{const s=this.uploads.get(e);return s?t?{id:s.id,fieldId:s.fieldId,status:s.status,attachmentId:s.attachmentId,operationId:s.operationId,groupId:s.groupId||null,changes:s.changes||{},meta:{originalName:s.meta?.originalName||s.originalFile?.name,size:s.meta?.size||s.originalFile?.size,type:s.meta?.type||s.originalFile?.type,title:s.meta?.title,alt:s.meta?.alt,caption:s.meta?.caption}}:s:null})).filter(Boolean):[]}async checkForStoredUploads(){if(!this.db)return;const e=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates"),t=(await new Promise((t=>{const s=e.getAll();s.onsuccess=()=>t(s.result)}))).filter((e=>e.uploads.some((e=>!e.operationId&&("completed"===e.status||"processed"===e.status||"local_processing"===e.status||"processed-original"===e.status)))));0!==t.length&&this.showRecoveryNotification(t)}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}handleGroupMetaChange(e){let t=this.getGroupFromElement(e);if(!t)return;Object.hasOwn(t,"changes")||(t.changes={});let s=e.name;if(s.includes("group")){let e=t.id+"_",r=t.id+"[";s=s.replace(e,"").replace(r,"").replace("]","")}t.changes[`${s}`]=e.value,this.groups.set(t.id,t),this.schedulePersistance(t.fieldId)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let r,o=window.getTemplate("restoreNotification");if(!o)return void console.error("Restore notification template not found");if(s>0){r=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else r=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const i=o.querySelector(".restore-details");i&&(i.textContent=r);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const r=e.querySelector(".item-grid.restore");for(const e of t.uploads){let s=window.getTemplate("uploadItem");if(!s)continue;const o=await this.uploadStore.getBlob(e.id);if(o)try{const r=new Blob([o.data],{type:o.type}),i=this.createPreviewUrl(r);let[a,l,n,d,c]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];s.dataset.uploadId=e.id,s.dataset.fieldId=t.id;let u=this.getSubtypeFromMime(o.type);switch(s.dataset.subtype=u,u){case"image":[l.src,l.alt]=[i,e.originalFile?.name??e.meta?.originalName??""],n.remove(),d.remove();break;case"video":n.src=i,l.remove(),d.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}d.innerText=e.originalFile.name,d.prepend(t),l.remove(),n.remove()}s.dataset.previewUrl=i}catch(t){console.warn("Failed to create preview for upload:",e.id,t)}const i=s.querySelector("summary span");i&&(i.textContent=e.meta?.originalName||"Unknown file");const a=s.querySelector("details");a&&e.meta&&(a.textContent=`${this.formatBytes(e.meta.size)} • ${e.meta.type}`),s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let r=s+e.id,o=t.parentNode.querySelector(`label[for="${s}"]`);t.id=r,o&&(o.htmlFor=r)}})),r&&r.appendChild(s)}o.querySelector(".wrap").appendChild(r)}document.querySelector(".field.upload").appendChild(o),o=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(o),this.restoreSelection=new window.jvbHandleSelection({container:o,ui:{selectAll:o.querySelector("#select-all-restore"),count:o.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async restoreSelectedUploads(e){const t=new Map;if(e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)})),!this.db)return;const s=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates");for(const[e,r]of t.entries()){const t=s.get(e),o=await new Promise((e=>{t.onsuccess=()=>e(t.result),t.onerror=()=>e(null)}));o&&(o.uploads=o.uploads.filter((e=>r.includes(e.id))),await this.restoreField(o))}}async restoreField(e){const{config:t,context:s,uploads:r,groups:o,id:i}=e;s.modalType&&await this.openModalForRestore(s);let a=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!a){const e=`${t.content}_${t.itemID}_${t.name}`;a=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!a)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=a.dataset.uploader;l&&this.fields.has(l)||(l=this.registerUploader(a,t));const n=this.fields.get(l);if(n){n.state=e.state||"ready",n.ui=this.buildFieldUI(a),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),o&&o.length>0&&await this.restoreGroups(l,o);for(const e of r)await this.restoreUpload(n,e);this.updateFieldState(l),this.maybeLockUploads(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}else console.error("Failed to register field for restoration")}async restoreUpload(e,t){const s=await this.uploadStore.getBlob(t.id);if(!s)return void console.warn("Blob data not found for upload:",t.id);{const e=s.data instanceof File?s.data:new File([s.data],s.name,{type:s.type,lastModified:s.lastModified});t.originalFile=e,t.processedFile=e,t.preview=this.createPreviewUrl(e)}e.uploads||(e.uploads=new Set),e.uploads.add(t.id);const r=this.getSubtypeFromMime(t.originalFile.type);let o;if(t.element=this.createUploadElement({...t,subtype:r},"post_group"===e.config.destination),o=t.groupId&&e.ui.groups.groups.has(t.groupId)?e.ui.groups.groups.get(t.groupId).querySelector(".item-grid"):e.ui.preview,o&&(o.appendChild(t.element),t.location=o),this.uploads.set(t.id,t),t.groupId){const e=this.groups.get(t.groupId);e&&e.uploads&&e.uploads.add(t.id)}}async restoreGroups(e,t){for(const s of t){const t=this.createGroup(e,s.id);if(t&&(s.meta&&(t.meta={...s.meta}),s.changes&&(t.changes={...s.changes}),s.title)){const e=t.element.querySelector('[name*="post_title"]');e&&(e.value=s.title)}}}async openModalForRestore(e){const{modalType:t,formId:s}=e;let r=null;switch(t){case"create":r=document.querySelector('[data-action="create"]');break;case"edit":r=document.querySelector(`[data-action="edit"][data-id="${e.itemId}"]`);break;case"bulkEdit":r=document.querySelector('[data-action="bulk-edit"]')}r&&(r.click(),await new Promise((e=>setTimeout(e,300))))}handleFieldStoreEvent(e,t){switch(e){case"data-loaded":break;case"item-saved":console.log(`Field state saved: ${t.key}`)}}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.checkForStoredUploads();break;case"item-saved":this.showSaveIndicator(t.key)}}async saveUpload(e){const t=e.processedFile||e.originalFile||e.file;if(t instanceof File||t instanceof Blob){await this.uploadStore.saveBlob(e.id,t);const{file:s,originalFile:r,processedFile:o,...i}=e;await this.uploadStore.save(i)}else await this.uploadStore.save(e)}async loadFields(){(await this.fieldStore.getAll()).forEach((e=>{e.uploads&&Array.isArray(e.uploads)&&(e.uploads=new Set(e.uploads.map((e=>e.id)))),this.fields.set(e.fieldId,e)}))}async loadUploads(){(await this.uploadStore.getAll()).forEach((e=>{this.uploads.set(e.id,e)}))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.fields.clear(),this.uploads.clear(),this.groups.clear(),this.selected.clear(),this.subscribers.clear()}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){this.fieldStore.clear(),this.uploadStore.clear()}async clearField(e){await this.fieldStore.delete(e);const t=this.fields.get(e);if(t?.uploads)for(const e of t.uploads)await this.uploadStore.delete(e);this.fields.delete(e)}async clearUpload(e,t=!0){const s=this.uploads.get(e);if(s){if(this.revokePreviewUrl(s.preview),s.element){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}t&&await this.schedulePersistance(s.fieldId),this.uploads.delete(e),this.uploadStore.delete(e),this.uploadStore.delete(e,"blobs")}}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})(); |
| | | (()=>{class e{constructor(){this.queue=window.jvbQueue,this.a11y=window.jvbA11y,this.error=window.jvbError,this.fieldStore=new window.jvbStore({name:"upload_fields",storeName:"fieldStates",keyPath:"id",version:2,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"timestamp",keyPath:"timestamp"},{name:"content",keyPath:"content"},{name:"itemId",keyPath:"itemId"},{name:"status",keyPath:"status"}],stripDOMReferences:!0,TTL:6048e5}),this.uploadStore=new window.jvbStore({name:"uploads",storeName:"uploads",keyPath:"id",storeBlobs:!0,indexes:[{name:"fieldId",keyPath:"fieldId"},{name:"status",keyPath:"status"},{name:"groupId",keyPath:"groupId"},{name:"attachmentId",keyPath:"attachmentId"}]}),window.jvbUploadBlobs=this.uploadStore,this.fieldStore.subscribe(this.handleFieldStoreEvent.bind(this)),this.uploadStore.subscribe(this.handleUploadStoreEvent.bind(this)),this.initWorker(),this.fields=new Map,this.uploads=new Map,this.groups=new Map,this.selected=new Map,this.selectionHandlers=new Map,this.previewUrls=new Set,this.subscribers=new Set,this.dragController=null,this.selectors={field:{field:"[data-upload-field]",input:'input[type="file"]',hiddenValue:'input[type="hidden"]',dropZone:".file-upload-container",preview:".item-grid.preview",progress:".image-progress"},groups:{container:".upload-group",grid:".item-grid.group",header:".group-header",selectAll:'[name="select-all-group"]',actions:".group-actions",count:".selection-controls .info"},items:{item:"[data-upload-id]",checkbox:'[name*="select-item"]',featured:'[name="featured"]',details:"details"}},this.statusMapping={received:"Image Received",local_processing:"Processing Image...",queued:"Waiting to upload...",uploading:"Uploading to Server",pending:"Successfully sent to server. In line for further processing.",processing:"Processing on server...",completed:"Upload complete!",failed:"Upload failed (will retry)",failed_permanent:"Upload failed permanently"},this.sortableInstances=new Map,this.sortableConfig={animation:150,draggable:".item",handle:".select-item-label, img",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",onEnd:e=>{this.handleReorder(e)}},this.init()}async init(){await this.loadFields(),await this.loadUploads(),this.initializeFields(),this.initListeners(),this.queue.subscribe(((e,t)=>{if("uploads"!==t.endpoint&&"uploads/meta"!==t.endpoint)return;const s=t.data instanceof FormData?t.data.get("fieldId"):t.data.fieldId;switch(e){case"cancel-operation":s&&this.clearField(s);break;case"operation-status":s&&this.updateFieldStatus(s,t.status);break;case"operation-complete":(t.result?.data||[]).forEach((e=>{const t=this.uploads.get(e.upload_id);t&&(t.attachmentId=e.attachment_id,t.status="completed",this.uploads.set(t.id,t))})),s&&this.cleanField(s)}})),window.addEventListener("beforeunload",(()=>{this.cleanupAllPreviewUrls()}))}initWorker(){this.worker={worker:null,timeout:null,tasks:new Map,restart:{count:0,max:3},settings:{timeout:1e4,batchSize:1,maxConcurrent:3,restartAfterTimeout:!0}}}initializeFields(){document.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}scanFields(e){e.querySelectorAll(this.selectors.field.field).forEach((e=>{this.registerUploader(e)}))}registerUploader(e){const t=this.determineFieldId(e),s=this.extractFieldConfig(e),o={id:t,config:s,element:e,ui:this.buildFieldUI(e),uploads:new Set,groups:new Set,state:"ready"};return this.fields.set(t,o),e.dataset.uploader=t,this.addFieldSelectionHandler(t),"post_group"!==s.destination||this.dragController||this.initGroupFeatures(),"single"!==s.type&&this.initSortable(o),t}extractFieldConfig(e){return{destination:e.dataset.destination||"meta",content:e.dataset.content||null,mode:e.dataset.mode||"direct",type:e.dataset.type||"single",name:e.dataset.field,itemID:e.dataset.itemId||0,maxFiles:parseInt(e.dataset.maxFiles)||999,subtype:e.dataset.subtype||"image"}}buildFieldUI(e){let t={field:e,input:e.querySelector(this.selectors.field.input),dropZone:e.querySelector(this.selectors.field.dropZone),preview:e.querySelector(this.selectors.field.preview),progress:{progress:e.querySelector(this.selectors.field.progress),bar:e.querySelector(".bar"),fill:e.querySelector(".fill"),details:e.querySelector(".details"),text:e.querySelector(".details .text"),count:e.querySelector(".details .count")}},s=e.querySelector(".group-display");return s&&(t.groups={display:s,container:e.querySelector(".item-grid.groups"),empty:e.querySelector(".empty-group"),groups:new Map}),t}initListeners(){this.clickHandler=this.handleClick.bind(this),this.changeHandler=this.handleChange.bind(this),document.addEventListener("click",this.clickHandler),document.addEventListener("change",this.changeHandler),this.dragEnterHandler=this.handleExternalDragEnter.bind(this),this.dragLeaveHandler=this.handleExternalDragLeave.bind(this),this.dragOverHandler=this.handleExternalDragOver.bind(this),this.dropHandler=this.handleExternalDrop.bind(this),document.addEventListener("dragenter",this.dragEnterHandler),document.addEventListener("dragleave",this.dragLeaveHandler),document.addEventListener("dragover",this.dragOverHandler),document.addEventListener("drop",this.dropHandler)}initGroupFeatures(){this.dragController=new window.jvbDragHandler({draggableSelector:this.selectors.items.item,dropTargetSelector:`${this.selectors.field.preview}, ${this.selectors.groups.grid}, .empty-group`,ignoreSelector:"input:not(.upload-select), button, select, textarea, details, summary, a",previewElement:"img, video, .icon",getItemId:e=>e.dataset.uploadId,getSelectedItems:e=>{const t=this.getFieldIdFromElement(e),s=e.dataset.uploadId,o=this.getCurrentSelection(t);return o&&o.includes(s)?o:[s]},validateDrop:(e,t)=>{const s=this.getFieldIdFromElement(t),o=document.querySelector(`[data-upload-id="${e[0]}"]`);return s===this.getFieldIdFromElement(o)},onDrop:(e,t)=>{this.handleItemDrop(e,t),t.scrollIntoView({behavior:"smooth",block:"center"})},onDragStart:e=>{},onDragEnd:(e,t)=>{if(t){const t=document.querySelector(`[data-upload-id="${e[0]}"]`),s=this.getFieldIdFromElement(t),o=this.selectionHandlers.get(s);o?.clearSelection()}},previewOptions:{multiOffset:{x:-60,y:-80},singleOffset:{x:-50,y:-60},showCount:!0}})}initSortable(e){if(!window.Sortable)return;const t=e.element.querySelector(".item-grid:not(.group)");t&&this.sortableInstances.set(`${e.id}-main`,new Sortable(t,{...this.sortableConfig,group:{name:e.id,pull:!0,put:!0}}));e.element.querySelectorAll(".item-grid.group").forEach(((t,s)=>{this.sortableInstances.set(`${e.id}-group-${s}`,new Sortable(t,{...this.sortableConfig,group:{name:e.id,pull:!0,put:!0}}))}))}handleReorder(e){const t=e.to.closest(".field, .upload");if(!t)return;const s=t.closest("form");if(!s)return;const o=s.dataset.formId;if(o&&window.jvbForms){const e=window.jvbForms.forms?.get(o);e?.options.autosave&&window.jvbForms.scheduleSave(e,1e3)}window.jvbA11y&&window.jvbA11y.announce("Item reordered"),t.dispatchEvent(new CustomEvent("jvb-items-reordered",{detail:{from:e.from,to:e.to,oldIndex:e.oldIndex,newIndex:e.newIndex},bubbles:!0}))}handleExternalDragLeave(e){const t=e.target.closest(this.selectors.field.dropZone);t&&!t.contains(e.relatedTarget)&&t.classList.remove("dragover")}handleExternalDragEnter(e){if(!e.dataTransfer.types.includes("Files"))return;const t=e.target.closest(this.selectors.field.dropZone);t&&(e.preventDefault(),t.classList.add("dragover"))}handleExternalDragOver(e){if(!e.dataTransfer.types.includes("Files"))return;e.target.closest(this.selectors.field.dropZone)&&(e.preventDefault(),e.dataTransfer.dropEffect="copy")}handleExternalDrop(e){const t=e.target.closest(this.selectors.field.dropZone);if(!t)return;e.preventDefault(),t.classList.remove("dragover");const s=Array.from(e.dataTransfer.files);if(0===s.length)return;const o=this.getFieldIdFromElement(t);o?(this.processFiles(o,s),this.a11y.announce(`${s.length} file(s) dropped for upload`)):console.error("No field ID found for drop zone")}handleItemDrop(e,t){const s=t.classList.contains("preview");let o=t;if(t.classList.contains("empty-group")){const e=this.getFieldIdFromElement(t),s=this.createGroup(e);if(!s)return void console.error("Failed to create group");o=s.grid}e.forEach((e=>{s?this.removeFromGroup(e):this.addToGroup(e,o)}));const r=this.getFieldIdFromElement(t);this.schedulePersistance(r);const i=e.length>1?`Moved ${e.length} items`:"Moved item";this.a11y.announce(i)}handleClick(e){if(e.target.matches(this.selectors.field.dropZone)||e.target.closest(this.selectors.field.dropZone)){const t=e.target.closest(this.selectors.field.dropZone);if(t&&!e.target.matches("input, button, a")){const e=t.querySelector(this.selectors.field.input);e?.click()}}const t=e.target.closest("[data-action]");t&&this.handleAction(t)}handleChange(e){const t=this.getFieldIdFromElement(e.target);if(e.target.matches(this.selectors.field.input)){const t=this.getFieldIdFromElement(e.target),s=Array.from(e.target.files);s.length>0&&t&&this.processFiles(t,s)}t&&("post_group"===this.fields.get(t).config.destination?this.handleGroupMetaChange(e.target):this.queueUploadMeta(e))}getCurrentSelection(e){let t=[];for(let[s,o]of this.selectionHandlers)(e===s||s.includes(e))&&o.selectedItems.size>0&&(t=t.concat([...o.selectedItems]));return t}getSubtypeFromMime(e){return e.startsWith("image/")?"image":e.startsWith("video/")?"video":"document"}getStatusText(e){return this.statusMapping[e]||e}getStatusIcon(e){return window.getIcon(this.queue.icons[e])}getStatusProgress(e){switch(e){case"local_processing":return 28;case"queued":return 50;case"uploading":return 66;case"pending":return 75;case"processing":return 89;case"completed":return 100;default:return 0}}getModalType(e){if(void 0!==e._cachedModalType)return e._cachedModalType;if(!e||!e.element)return e._cachedModalType=null,null;const t=e.element.closest("dialog");if(!t)return e._cachedModalType=null,null;let s=null;return s=t.classList.contains("edit")?"edit":t.classList.contains("create")?"create":t.classList.contains("bulkEdit")?"bulkEdit":t.className,e._cachedModalType=s,s}handleAction(e){const t=e.dataset.action,s=this.getFieldIdFromElement(e);switch(t){case"add-to-group":this.handleAddToGroup(e);break;case"delete-group":this.handleDeleteGroup(e);break;case"delete-upload":case"remove-from-group":this.handleRemoveItem(e);break;case"upload":this.fields.get(s).element.closest("details").open=!1,document.body.classList.add("uploading"),this.submitUploads(s);break;case"restore":this.handleRestoreUploads().then((()=>{}));break;case"clear-cache":confirm("Save these uploads for later?")||this.cleanupStoredUploads(),this.cleanupRestore()}}handleAddToGroup(e){const t=e.closest(this.selectors.field.field),s=t?.dataset.uploader;if(!s)return;const o=this.selected.get(s);if(o&&0!==o.size){const e=this.createGroup(s);if(!e)return;o.forEach((t=>{this.addToGroup(t,e.grid)}));const t=this.selectionHandlers.get(s);t?.clearSelection(),this.a11y.announce(`Created group with ${o.size} items`)}else this.createGroup(s);this.schedulePersistance(s)}handleDeleteGroup(e){const t=e.closest(this.selectors.groups.container);if(!t)return;const s=t.dataset.groupId,o=this.getFieldIdFromElement(t);if(!confirm("Delete this group? Items will be moved back to the upload area."))return;t.querySelectorAll(this.selectors.items.item).forEach((e=>{const t=e.dataset.uploadId;this.removeFromGroup(t)})),this.deleteGroup(s),this.a11y.announce("Group deleted, items returned to upload area"),this.schedulePersistance(o)}handleRemoveItem(e){const t=e.closest(this.selectors.items.item);if(!t)return;const s=t.dataset.uploadId,o=this.getFieldIdFromElement(t);confirm("Remove this item?")&&(this.removeUpload(o,s),this.a11y.announce("Item removed"),this.schedulePersistance(o))}addFieldSelectionHandler(e){if(this.selectionHandlers.has(e))return this.selectionHandlers.get(e);const t=this.fields.get(e);if(!t)return;const s=t.ui.field;if(!s)return;const o=new window.jvbHandleSelection({container:s,ui:{selectAll:s.querySelector('[name="select-all-uploads"]'),bulkControls:s.querySelector(".selection-actions"),count:s.querySelector(".selection-count")},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return o.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(e,o),o}addGroupSelectionHandler(e,t){const s=`${e}_${t}`;if(this.selectionHandlers.has(s))return this.selectionHandlers.get(s);const o=this.groups.get(t);if(!o)return;const r=new window.jvbHandleSelection({container:o.element,ui:{selectAll:o.element.querySelector(this.selectors.groups.selectAll),bulkControls:o.element.querySelector(this.selectors.groups.actions),count:o.element.querySelector(this.selectors.groups.count)},itemSelector:"[data-upload-id]",checkboxSelector:'[name*="select-item"]'});return r.subscribe(((t,s)=>{switch(t){case"item-selected":case"item-deselected":case"range-selected":this.selected.set(e,s.selectedItems);break;case"select-all":this.handleSelectAll(s.container,s.selected)}})),this.selectionHandlers.set(s,r),r}handleSelectAll(e,t){}determineFieldId(e){return`${e.dataset.content||e.closest("dialog")?.dataset.content||e.closest("form")?.dataset.save||""}_${e.dataset.itemId||e.closest("dialog")?.dataset.itemId||""}_${e.dataset.field||""}`}getFromElement(e,t){const s={field:{selector:this.selectors.field.field,key:"uploader",store:this.fields},upload:{selector:this.selectors.items.item,key:"uploadId",store:this.uploads},group:{selector:this.selectors.groups.container,key:"groupId",store:this.groups}}[t];if(!s)return null;const o=e.closest(s.selector);if(!o)return null;const r=o.dataset[s.key];return s.store.get(r)}getFieldFromElement(e){return this.getFromElement(e,"field")}getUploadFromElement(e){return this.getFromElement(e,"upload")}getGroupFromElement(e){return this.getFromElement(e,"group")}getFieldIdFromElement(e){return this.getFromElement(e,"field")?.id??null}getUploadIdFromElement(e){return this.getFromElement(e,"upload")?.id??null}getGroupIdFromElement(e){return this.getFromElement(e,"group")?.id??null}async processFiles(e,t){const s=this.fields.get(e);if(!s)return;s.ui.dropZone&&(s.ui.dropZone.hidden=!0),s.ui.groups.display&&(s.ui.groups.display.hidden=!1);const o=t.length;let r=0;this.updateUploadProgress(e,0,o,"Processing files..."),s.uploads||(s.uploads=new Set);const i=Array.from(t).map((async(t,i)=>{try{const i=`upload_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,a={id:i,attachment_id:null,fieldId:e,originalFile:t,processedFile:null,preview:null,status:"local_processing",element:null,location:null,meta:{originalName:t.name,size:t.size,type:t.type}};a.preview=this.createPreviewUrl(t),t.type.startsWith("image/")?a.processedFile=await this.processImage(t,s.subtype):a.processedFile=t,await this.uploadStore.saveBlob(i,a.processedFile||t);const l=this.getSubtypeFromMime(t.type);return a.element=this.createUploadElement({...a,subtype:l},"post_group"===s.config.destination),this.showUploadProgress(i,!0),this.updateUploadItemProgress(i,50,"local_processing"),s.ui.preview&&(s.ui.preview.appendChild(a.element),a.location=s.ui.preview),this.uploads.set(i,a),s.uploads.add(i),r++,this.updateUploadProgress(e,r,o,"Processing files..."),this.updateUploadItemProgress(i,100,"processed"),a.status="processed",setTimeout((()=>{this.showUploadProgress(i,!1)}),1e3),i}catch(s){return console.error("Error processing file:",t.name,s),r++,this.updateUploadProgress(e,r,o,"Processing files..."),null}}));await Promise.all(i),this.updateFieldState(e),await this.schedulePersistance(e),"post_group"!==s.config.destination&&(await this.queueUpload(e),this.maybeLockUploads(e))}updateFieldState(e){const t=this.fields.get(e);if(!t||!t.ui.field)return;const s=t.ui.field,o=t.uploads?.size||0,r=t.ui.groups?.container?.querySelectorAll(".upload-group").length>0;s.dataset.hasUploads=o>0?"true":"false",s.dataset.uploadCount=o.toString(),s.dataset.hasGroups=r?"true":"false",t.ui.preview&&t.ui.preview.setAttribute("aria-label",`Upload preview area with ${o} item${1!==o?"s":""}`)}updateUploadProgress(e,t,s,o){const r=this.fields.get(e);if(!r?.ui?.progress?.progress)return;const i=r.ui.progress,a=s>0?t/s*100:0;i.fill&&(i.fill.style.width=`${a}%`),i.text&&(i.text.textContent=o),i.count&&(i.count.textContent=`${t}/${s}`),i.progress.hidden=t===s}updateFieldStatus(e,t){const s=this.fields.get(e);s&&(s.state=t)}updateUploadStatus(e,t){const s=this.uploads.get(e);s&&(s.status=t,this.updateUploadUI(e))}updateUploadUI(e){const t=this.uploads.get(e);if(!t?.element)return;t.element.className=t.element.className.replace(/status-[\w-]+/g,""),t.element.classList.add(`status-${t.status}`);t.element.querySelector(".progress")&&this.updateUploadItemProgress(e,this.getStatusProgress(t.status),t.status)}showUploadProgress(e,t=!0){const s=this.uploads.get(e);if(!s||!s.element)return;const o=s.element.querySelector(".progress");o&&(t?(o.style.removeProperty("animation"),o.hidden=!1):(o.style.animation="fadeOut var(--transition-base)",setTimeout((()=>{o.hidden=!0}),300)))}updateUploadItemProgress(e,t,s=null){const o=this.uploads.get(e);if(!o||!o.element)return;const r=o.element.querySelector(".progress");if(!r)return;const i=r.querySelector(".fill"),a=r.querySelector(".details"),l=r.querySelector(".icon");i&&(i.style.width=`${t}%`),s&&a&&(a.textContent=this.getStatusText(s)),s&&l&&(l.innerHTML=this.getStatusIcon(s).outerHTML)}checkFieldLimits(e,t){const s=this.fields.get(e);if(!s)return!1;return(s.uploads?.size||0)+t<=s.maxFiles}validateFile(e,t){return this.settings.allowedTypes.includes(e.type)?!(e.size>this.settings.maxFileSize)||(this.notify(`File too large: ${this.formatBytes(e.size)}`,"error"),!1):(this.notify(`Invalid file type: ${e.type}`,"error"),!1)}formatBytes(e,t=2){if(0===e)return"0 Bytes";const s=t<0?0:t,o=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,o)).toFixed(s))+" "+["Bytes","KB","MB","GB"][o]}shouldProcessClientSide(e,t){return!("image"!==t||!e.type.startsWith("image/"))}async processImage(e,t){const s=this.worker.settings.timeout;return new Promise(((o,r)=>{let i,a=!1;i=setTimeout((()=>{a||(a=!0,this.worker.tasks.delete(t),this.worker.settings.restartAfterTimeout&&this.restartCompressionWorker(),r(new Error(`Processing timeout for ${e.name}`)))}),s),this.worker.tasks.set(t,{file:e,timeoutId:i}),this.handleProcess(e,t).then((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),o(e))})).catch((e=>{a||(a=!0,clearTimeout(i),this.worker.tasks.delete(t),r(e))}))}))}async handleProcess(e,t){if(!e.type.startsWith("image/"))return e;const s=this.getMaxDimension();if(this.shouldUseWorker(e))try{if(this.worker.worker||this.initCompressionWorker(),this.worker.worker)return await this.processWithWorker(e,t,s,.85)}catch(e){console.warn("Worker processing failed, falling back to main thread:",e)}return await this.processOnMainThread(e,s,.85)}async processOnMainThread(e,t,s){return new Promise(((o,r)=>{const i=new Image,a=document.createElement("canvas"),l=a.getContext("2d");let n=null;const d=()=>{i.onload=null,i.onerror=null,n&&(URL.revokeObjectURL(n),n=null),a.width=1,a.height=1,l.clearRect(0,0,1,1)};i.onload=()=>{try{const{width:n,height:c}=this.calculateOptimalDimensions(i,t);a.width=n,a.height=c,l.imageSmoothingEnabled=!0,l.imageSmoothingQuality="high",l.drawImage(i,0,0,n,c);const u=this.getOptimalFormat(e),p=this.getOptimalQuality(e,s);a.toBlob((t=>{if(d(),t){const s=new File([t],this.getProcessedFileName(e,u),{type:u,lastModified:Date.now()});o(s)}else r(new Error("Canvas toBlob failed"))}),u,p)}catch(e){d(),r(new Error(`Canvas processing failed: ${e.message}`))}},i.onerror=()=>{d(),r(new Error(`Failed to load image: ${e.name}`))};try{n=this.createPreviewUrl(e),i.src=n}catch(e){d(),r(new Error(`Failed to create object URL: ${e.message}`))}}))}getOptimalFormat(e){return"image/gif"===e.type||"image/svg+xml"===e.type?e.type:this.supportsWebP()?"image/webp":"image/jpeg"}getOptimalQuality(e,t){return e.size<512e3?Math.max(t,.9):e.size<2097152?t:Math.min(t,.8)}getProcessedFileName(e,t){return e.name.replace(/\.[^/.]+$/,"")+({"image/webp":".webp","image/jpeg":".jpg","image/png":".png","image/gif":".gif"}[t]||".jpg")}getMaxDimension(){const e=window.screen.width,t=window.devicePixelRatio||1;return e*t>2560?2400:e*t>1920?1920:1200}shouldUseWorker(e){return this.worker.worker&&e.size>1048576&&"undefined"!=typeof OffscreenCanvas}async processWithWorker(e,t,s,o){return new Promise(((r,i)=>{if(!this.worker.worker)return void i(new Error("Worker not available"));const a=`${t}_${Date.now()}`,l=t=>{if(t.data.messageId===a)if(this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),t.data.success){const s=new File([t.data.blob],this.getProcessedFileName(e,t.data.format||"image/webp"),{type:t.data.format||"image/webp",lastModified:Date.now()});r(s)}else i(new Error(t.data.error||"Worker processing failed"))},n=e=>{this.worker.worker.removeEventListener("message",l),this.worker.worker.removeEventListener("error",n),i(new Error(`Worker error: ${e.message}`))};this.worker.worker.addEventListener("message",l),this.worker.worker.addEventListener("error",n),this.worker.worker.postMessage({messageId:a,file:e,maxDimension:s,quality:o,outputFormat:this.getOptimalFormat(e)})}))}restartCompressionWorker(){this.worker.worker&&(this.worker.worker.terminate(),this.worker.worker=null),this.worker.tasks.clear(),this.worker.restart.count>=this.worker.restart.max?console.error("Max worker restarts reached, disabling worker"):(this.worker.restart.count++,this.initCompressionWorker())}initCompressionWorker(){if(!this.worker.worker&&"undefined"!=typeof Worker)try{const e=new Blob(["\n self.onmessage = async function(e) {\n const { messageId, file, maxDimension, quality, outputFormat } = e.data;\n\n try {\n // Create ImageBitmap from file\n const bitmap = await createImageBitmap(file);\n\n // Calculate dimensions\n const scale = Math.min(maxDimension / bitmap.width, maxDimension / bitmap.height, 1);\n const width = Math.round(bitmap.width * scale);\n const height = Math.round(bitmap.height * scale);\n\n // Create OffscreenCanvas\n const canvas = new OffscreenCanvas(width, height);\n const ctx = canvas.getContext('2d');\n\n // Draw and resize\n ctx.imageSmoothingEnabled = true;\n ctx.imageSmoothingQuality = 'high';\n ctx.drawImage(bitmap, 0, 0, width, height);\n\n // Clean up bitmap\n bitmap.close();\n\n // Convert to blob\n const blob = await canvas.convertToBlob({\n type: outputFormat,\n quality: quality\n });\n\n self.postMessage({\n messageId,\n success: true,\n blob: blob,\n format: outputFormat\n });\n\n } catch (error) {\n self.postMessage({\n messageId,\n success: false,\n error: error.message\n });\n }\n };\n "],{type:"application/javascript"});this.worker.worker=new Worker(this.createPreviewUrl(e))}catch(e){console.warn("Failed to initialize compression worker:",e),this.worker.worker=null}}calculateOptimalDimensions(e,t){let{width:s,height:o}=e;if(s<=t&&o<=t)return{width:s,height:o};const r=Math.min(t/s,t/o);return{width:Math.round(s*r),height:Math.round(o*r)}}supportsWebP(){return 0===document.createElement("canvas").toDataURL("image/webp").indexOf("data:image/webp")}createPreviewUrl(e){const t=URL.createObjectURL(e);return this.previewUrls||(this.previewUrls=new Set),this.previewUrls.add(t),t}revokePreviewUrl(e){e?.startsWith("blob:")&&(URL.revokeObjectURL(e),this.previewUrls?.delete(e))}maybeLockUploads(e){const t=this.fields.get(e);if(!t?.ui?.dropZone)return;if("post_group"===t.config.destination)return;const s=t.uploads?.size||0,o=t.config?.maxFiles||999;t.ui.dropZone.hidden=s>=o,t.element.classList.toggle("at-max-uploads",s>=o)}createUploadElement(e,t=!1){let s=window.getTemplate("uploadItem");if(!s)return void console.error("Image template not found");s.dataset.uploadId=e.id,e.originalFile&&(s.dataset.subtype=this.getSubtypeFromMime(e.originalFile.type)),s.querySelector('[name="featured"]').value=e.id;let[o,r,i,a,l]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];switch([o.value,r.src,r.alt]=[e.id,e.preview,e.originalFile?.name??e.meta?.originalName??""],s.dataset.subtype){case"image":[r.src,r.alt]=[e.preview,e.originalFile?.name??e.meta?.originalName??""],i.remove(),a.remove();break;case"video":i.src=e.preview,r.remove(),a.remove();break;case"document":const t=e.originalFile?.name??e.meta?.originalName??"",s=t.split(".").pop()?.toLowerCase()??"",o={pdf:"file-pdf",csv:"file-csv",doc:"file-doc",docx:"file-doc",txt:"file-txt",xls:"file-xls",xlsx:"file-xls"},l=window.getIcon(o[s]||"file");a.innerText=e.originalFile.name,a.prepend(l),r.remove(),i.remove()}if(l){let e=window.getTemplate("uploadMeta");e&&l.append(e)}return s.draggable=t,s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,r=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,r&&(r.htmlFor=o)}})),s}async submitUploads(e){const t=this.fields.get(e);if(!t?.uploads||0===t.uploads.size)return;let s=Array.from(t.uploads);if(0===s.length)return void this.error.log("No uploads to upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const o=this.getFieldGroups(e);if(0===o.length)return void this.error.log("No groups created for post_group upload",{component:"UploadManager",action:"submitGroupedUploads",fieldId:e});const r=[],i=new FormData;let a=[];s=s.map((e=>this.uploads.get(e))),o.forEach(((e,t)=>{const o={images:[],fields:{}};for(let[t,s]of Object.entries(e.changes))o.fields[t]=s;s.filter((t=>t.groupId===e.id)).forEach((e=>{if(e){const t=e.processedFile||e.originalFile;if(t){i.append("files[]",t);const s={upload_id:e.id,index:a.length};o.images.push(s),a.push(e.id)}}})),r.push(o)})),s.filter((e=>!Object.hasOwn(e,"groupId"))).forEach((e=>{if(e){const t={images:[],fields:{}},s=e.processedFile||e.originalFile;if(s){i.append("files[]",s);const o={upload_id:e.id,index:a.length};t.images.push(o),a.push(e.id)}r.push(t)}})),i.append("content",t.config.content),i.append("user",t.config.itemID),i.append("posts",JSON.stringify(r)),i.append("upload_ids",JSON.stringify(a));for(const[e,t]of i.entries())console.log(e,t);const l={endpoint:"uploads/groups",method:"POST",data:i,title:`Creating ${r.length} ${t.config.content}${r.length>1?"s":""} from uploads...`,popup:`Creating ${r.length} post${r.length>1?"s":""}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(l);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,this.a11y.announce(`Creating ${r.length} post${r.length>1?"s":""} from your uploads`),e}catch(t){throw this.error.log(t,{component:"UploadManager",action:"submitGroupedUploads",fieldId:e}),t}finally{this.schedulePersistance(t.id)}}async queueUpload(e){const t=this.fields.get(e);if(!t?.uploads)return;const s=Array.from(t.uploads);if(0===s.length)return;const o=this.prepareUploadData(t,s);this.a11y.announce("Queuing for upload");let r=1===s.length?"file":"files";const i={endpoint:"uploads",method:"POST",data:o,title:`Uploading ${s.length} ${r} to server...`,popup:`Uploading ${s.length} ${r}...`,canMerge:!1,headers:{action_nonce:jvbSettings.dash},append:"_upload"};try{const e=await this.queue.addToQueue(i);return s.forEach((t=>{let s=this.uploads.get(t);s&&(s.operationId=e,this.updateUploadStatus(t,"queued"))})),t.operationId=e,e}catch(e){throw e}finally{this.schedulePersistance(t.id)}}prepareUploadData(e,t){const s=new FormData;s.append("content",e.config.content),s.append("mode",e.config.mode),s.append("field_name",e.config.name),s.append("fieldId",e.id),s.append("field_type",e.config.type),s.append("subtype",e.config.subtype),s.append("item_id",e.config.itemID),s.append("destination",e.config.destination||"meta");let o=[];const r=this.getFieldGroups(e.id);if("post_group"===e.config.destination&&r.length>0){let e=[],t=[],i=[];r.forEach((r=>{let a=[],l=null;r.uploads.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;if(e){s.append("files[]",e);o.length;o.push(t.id),a.push(t.id);const r=t.element?.querySelector('[name="featured"]');r?.checked&&(l=t.id)}}})),e.push(a),t.push(r.title||""),i.push(l)})),s.append("groups",JSON.stringify(e)),s.append("group_titles",JSON.stringify(t)),s.append("featured_images",JSON.stringify(i))}else t.forEach((e=>{let t=this.uploads.get(e);if(t){const e=t.processedFile||t.originalFile;e&&(s.append("files[]",e),o.push(t.id))}}));return s.append("upload_ids",JSON.stringify(o)),s}getFieldGroups(e){const t=[];return this.groups.forEach(((s,o)=>{if(s.fieldId===e){const r=this.fields.get(e),i=r?.ui?.groups?.groups?.get(o);t.push({id:o,uploads:Array.from(s.uploads||new Set),changes:s.changes||{},element:i||null})}})),t}async queueUploadMeta(e){const t=this.getUploadFromElement(e.target);if(!t)return;if(!this.fields.get(t.fieldId))return;if(!e.target.closest(".upload-meta"))return;let s={};s[e.target.name]=e.target.value,t.meta={...t.meta,...s};let o={};o[t.attachmentId??t.id]=t.meta;const r={endpoint:"uploads/meta",method:"POST",data:o,title:"Updating meta",canMerge:!0,headers:{action_nonce:jvbSettings.dash}};try{await this.queue.addToQueue(r)}catch(e){this.error.log(e,{component:"UploadManager",action:"sendMetaUpdate",uploadId:t.id})}}createGroup(e,t=null){const s=this.fields.get(e);if(!s)return console.error("Field not found:",e),null;t||(t=`group_${Date.now()}_${Math.random().toString(36).substr(2,9)}`);const o=this.createGroupElement(t,e);if(!o)return console.error("Failed to create group element"),null;s.ui.groups||(s.ui.groups={groups:new Map,container:null,empty:null,display:null}),s.ui.groups.groups.set(t,o),s.ui.groups.container&&s.ui.groups.empty?s.ui.groups.container.insertBefore(o,s.ui.groups.empty):s.ui.groups.container&&s.ui.groups.container.appendChild(o);const r={id:t,fieldId:e,element:o,grid:o.querySelector(".item-grid.group"),uploads:new Set,changes:{}};return this.groups.set(t,r),this.addGroupSelectionHandler(e,t),this.schedulePersistance(e),r}createGroupElement(e,t){let s=window.getTemplate("imageGroup");if(!s)return;s.dataset.groupId=e,s.dataset.fieldId=t;let o=window.getTemplate("groupMetadata");const r=s.querySelector(".fields");if(r&&o){r.append(o);const i=r.querySelector('[name="post_title"]'),a=r.querySelector('[name="post_excerpt"]');i&&(i.id=`${e}_title`,i.name=`${e}[post_title]`),a&&(a.id=`${e}_excerpt`,a.name=`${e}[post_excerpt]`);let l=this.fields.get(t);if(""!==l.config.content){s.querySelector("summary").textContent=l.config.content+" Fields"}}else s.querySelector("details").remove();const i=s.querySelector(".item-grid.group");return i&&(i.dataset.groupId=e),s}deleteGroup(e,t=!0){let s=this.groups.get(e);if(!s)return;let o=!0;t&&s.uploads&&s.uploads.size>0&&(o=!window.confirm("Delete uploads in group?")),t&&o&&s.uploads&&s.uploads.size>0&&Array.from(s.uploads).forEach((e=>{this.addImageToGroup(e,null,!1)})),this.groups.delete(e);let r=s.element;r&&(r.remove(),this.a11y.announce("Group removed")),this.schedulePersistance(s.fieldId)}addToGroup(e,t=null,s=!0){let o=this.uploads.get(e);if(!o)return;let r=this.fields.get(o.fieldId);if(!r)return;if(!t&&o.location===r.ui.preview||t===o.location)return;if(o.location){let t=o.location.dataset.groupId;if(t){let s=this.groups.get(t);s&&s.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t))}}const i=o.element.querySelector('[name*="select-item"]');i&&(i.checked=!1);let a=o.element.querySelector('[name="featured"]');if(a.hidden=!t,t){if(!t.classList.contains("item-grid")||!t.classList.contains("preview")){let s=t.dataset.groupId;a.name=s+"_"+a.name;let r=this.groups.get(s);r||(r=this.createGroup(o.fieldId),t=r.grid,s=r.id),r&&(r.uploads.add(e),o.groupId=s)}}else t=r.ui.preview,o.groupId=null;o.location=t,t.append(o.element),s&&this.schedulePersistance(r.id)}removeFromGroup(e){const t=this.uploads.get(e);if(!t)return;const s=this.fields.get(t.fieldId);if(!s)return;if(t.groupId){const s=this.groups.get(t.groupId);s?.uploads&&(s.uploads.delete(e),0===s.uploads.size&&this.deleteGroup(t.groupId,!1)),t.groupId=null}s.ui?.preview&&(s.ui.preview.appendChild(t.element),t.location=s.ui.preview);const o=t.element.querySelector('[name="featured"]');o&&(o.hidden=!0,o.checked=!1)}removeUpload(e,t){const s=this.fields.get(e),o=this.uploads.get(t);if(!s||!o)return;if(s.uploads?.delete(t),o.groupId){const e=this.groups.get(o.groupId);e&&e.uploads&&(e.uploads.delete(t),0===e.uploads.size&&this.removeGroup(o.groupId))}o.element?.remove(),this.clearUpload(t),this.updateFieldState(e),this.maybeLockUploads(e);const r=this.selectionHandlers.get(s.id);r&&r.deselect(t),this.a11y.announce("Upload removed")}schedulePersistance(e){const t=`persist_${e}`;window.debouncer.schedule(t,(()=>this.persistFieldState(e)),1e3)}async persistFieldState(e){const t=this.fields.get(e);if(!t)return;const s={...t,id:e,fieldId:e,uploads:Array.from(t.uploads||[]).map((e=>this.uploads.get(e))),groups:Array.from(this.groups.entries()).filter((([t,s])=>s.fieldId===e&&s.uploads&&s.uploads.size>0)).map((([e,t])=>({id:t.id,uploads:Array.from(t.uploads),changes:t.changes||{}}))),context:{url:this.normalizeUrl(window.location.href),fullUrl:window.location.href,modalType:this.getModalType(t),formId:t.formId,fieldSelector:`.field.upload[data-field="${t.config.name}"]`},timestamp:Date.now()};await this.fieldStore.save(s)}normalizeUrl(e){try{const t=new URL(e);return t.origin+t.pathname}catch(t){return e}}getFieldUploads(e,t=!1){const s=this.fields.get(e);return s&&s.uploads?Array.from(s.uploads).map((e=>{const s=this.uploads.get(e);return s?t?{id:s.id,fieldId:s.fieldId,status:s.status,attachmentId:s.attachmentId,operationId:s.operationId,groupId:s.groupId||null,changes:s.changes||{},meta:{originalName:s.meta?.originalName||s.originalFile?.name,size:s.meta?.size||s.originalFile?.size,type:s.meta?.type||s.originalFile?.type,title:s.meta?.title,alt:s.meta?.alt,caption:s.meta?.caption}}:s:null})).filter(Boolean):[]}async checkForStoredUploads(){if(!this.db)return;const e=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates"),t=(await new Promise((t=>{const s=e.getAll();s.onsuccess=()=>t(s.result)}))).filter((e=>e.uploads.some((e=>!e.operationId&&("completed"===e.status||"processed"===e.status||"local_processing"===e.status||"processed-original"===e.status)))));0!==t.length&&this.showRecoveryNotification(t)}async handleRestoreUploads(){let e=document.querySelector("dialog.restore-uploads");if(!e)return;const t=this.getSelectedRestorationUploads(e);0!==t.length&&(await this.restoreSelectedUploads(t),this.cleanupRestore())}getSelectedRestorationUploads(e){let t=[];return e.querySelectorAll("[type=checkbox]:checked").forEach((e=>{const s=e.closest(".item");s&&t.push({uploadId:s.dataset.uploadId,fieldId:s.dataset.fieldId})})),t}handleGroupMetaChange(e){let t=this.getGroupFromElement(e);if(!t)return;Object.hasOwn(t,"changes")||(t.changes={});let s=e.name;if(s.includes("group")){let e=t.id+"_",o=t.id+"[";s=s.replace(e,"").replace(o,"").replace("]","")}t.changes[`${s}`]=e.value,this.groups.set(t.id,t),this.schedulePersistance(t.fieldId)}async showRecoveryNotification(e){const t=e.reduce(((e,t)=>e+t.uploads.length),0),s=e.reduce(((e,t)=>e+(t.groups?.length||0)),0);let o,r=window.getTemplate("restoreNotification");if(!r)return void console.error("Restore notification template not found");if(s>0){o=`${s} ${s>1?"groups":"group"} with ${t} ${t>1?"uploads":"upload"} can be restored.`}else o=`${t} upload(s) from ${e.length} field(s) can be recovered.`;const i=r.querySelector(".restore-details");i&&(i.textContent=o);for(const t of e){let e=window.getTemplate("restoreField");if(!e)continue;const s=e.querySelector("h3");s&&(s.textContent=t.config.name||"Unnamed Field");const o=e.querySelector(".item-grid.restore");for(const e of t.uploads){let s=window.getTemplate("uploadItem");if(!s)continue;const r=await this.uploadStore.getBlob(e.id);if(r)try{const o=new Blob([r.data],{type:r.type}),i=this.createPreviewUrl(o);let[a,l,n,d,c]=[s.querySelector('[name="featured"]'),s.querySelector("img"),s.querySelector("video"),s.querySelector("label > span"),s.querySelector("details")];s.dataset.uploadId=e.id,s.dataset.fieldId=t.id;let u=this.getSubtypeFromMime(r.type);switch(s.dataset.subtype=u,u){case"image":[l.src,l.alt]=[i,e.originalFile?.name??e.meta?.originalName??""],n.remove(),d.remove();break;case"video":n.src=i,l.remove(),d.remove();break;case"document":let t;switch(""){case"pdf":t=window.getIcon("file-pdf");break;case"csv":t=window.getIcon("file-csv");break;case"doc":t=window.getIcon("file-doc");break;case"txt":t=window.getIcon("file-txt");break;case"xls":t=window.getIcon("file-xls");break;default:t=window.getIcon("file")}d.innerText=e.originalFile.name,d.prepend(t),l.remove(),n.remove()}s.dataset.previewUrl=i}catch(t){console.warn("Failed to create preview for upload:",e.id,t)}const i=s.querySelector("summary span");i&&(i.textContent=e.meta?.originalName||"Unknown file");const a=s.querySelector("details");a&&e.meta&&(a.textContent=`${this.formatBytes(e.meta.size)} • ${e.meta.type}`),s.querySelectorAll("input").forEach((t=>{let s=t.id;if(s){let o=s+e.id,r=t.parentNode.querySelector(`label[for="${s}"]`);t.id=o,r&&(r.htmlFor=o)}})),o&&o.appendChild(s)}r.querySelector(".wrap").appendChild(o)}document.querySelector(".field.upload").appendChild(r),r=document.querySelector("dialog.restore-uploads"),this.restoreModal=new window.jvbModal(r),this.restoreSelection=new window.jvbHandleSelection({container:r,ui:{selectAll:r.querySelector("#select-all-restore"),count:r.querySelector(".selection-count")}}),this.restoreModal.handleOpen()}async restoreSelectedUploads(e){const t=new Map;if(e.forEach((e=>{t.has(e.fieldId)||t.set(e.fieldId,[]),t.get(e.fieldId).push(e.uploadId)})),!this.db)return;const s=this.db.transaction(["fieldStates"],"readonly").objectStore("fieldStates");for(const[e,o]of t.entries()){const t=s.get(e),r=await new Promise((e=>{t.onsuccess=()=>e(t.result),t.onerror=()=>e(null)}));r&&(r.uploads=r.uploads.filter((e=>o.includes(e.id))),await this.restoreField(r))}}async restoreField(e){const{config:t,context:s,uploads:o,groups:r,id:i}=e;s.modalType&&await this.openModalForRestore(s);let a=document.querySelector(`.field.upload[data-field="${t.name}"]`);if(!a){const e=`${t.content}_${t.itemID}_${t.name}`;a=document.querySelector(`.field.upload[data-uploader="${e}"]`)}if(!a)return void console.warn(`Field ${t.name} not found for restoration`,t);let l=a.dataset.uploader;l&&this.fields.has(l)||(l=this.registerUploader(a,t));const n=this.fields.get(l);if(n){n.state=e.state||"ready",n.ui=this.buildFieldUI(a),n.ui.groups?.display&&(n.ui.groups.display.hidden=!1),r&&r.length>0&&await this.restoreGroups(l,r);for(const e of o)await this.restoreUpload(n,e);this.updateFieldState(l),this.maybeLockUploads(l),"direct"===t.mode&&"post_group"!==t.destination&&await this.queueUpload(l)}else console.error("Failed to register field for restoration")}async restoreUpload(e,t){const s=await this.uploadStore.getBlob(t.id);if(!s)return void console.warn("Blob data not found for upload:",t.id);{const e=s.data instanceof File?s.data:new File([s.data],s.name,{type:s.type,lastModified:s.lastModified});t.originalFile=e,t.processedFile=e,t.preview=this.createPreviewUrl(e)}e.uploads||(e.uploads=new Set),e.uploads.add(t.id);const o=this.getSubtypeFromMime(t.originalFile.type);let r;if(t.element=this.createUploadElement({...t,subtype:o},"post_group"===e.config.destination),r=t.groupId&&e.ui.groups.groups.has(t.groupId)?e.ui.groups.groups.get(t.groupId).querySelector(".item-grid"):e.ui.preview,r&&(r.appendChild(t.element),t.location=r),this.uploads.set(t.id,t),t.groupId){const e=this.groups.get(t.groupId);e&&e.uploads&&e.uploads.add(t.id)}}async restoreGroups(e,t){for(const s of t){const t=this.createGroup(e,s.id);if(t&&(s.meta&&(t.meta={...s.meta}),s.changes&&(t.changes={...s.changes}),s.title)){const e=t.element.querySelector('[name*="post_title"]');e&&(e.value=s.title)}}}async openModalForRestore(e){const{modalType:t,formId:s}=e;let o=null;switch(t){case"create":o=document.querySelector('[data-action="create"]');break;case"edit":o=document.querySelector(`[data-action="edit"][data-id="${e.itemId}"]`);break;case"bulkEdit":o=document.querySelector('[data-action="bulk-edit"]')}o&&(o.click(),await new Promise((e=>setTimeout(e,300))))}handleFieldStoreEvent(e,t){switch(e){case"data-loaded":break;case"item-saved":console.log(`Field state saved: ${t.key}`)}}handleUploadStoreEvent(e,t){switch(e){case"data-loaded":this.checkForStoredUploads();break;case"item-saved":this.showSaveIndicator(t.key)}}async saveUpload(e){const t=e.processedFile||e.originalFile||e.file;if(t instanceof File||t instanceof Blob){await this.uploadStore.saveBlob(e.id,t);const{file:s,originalFile:o,processedFile:r,...i}=e;await this.uploadStore.save(i)}else await this.uploadStore.save(e)}async loadFields(){(await this.fieldStore.getAll()).forEach((e=>{e.uploads&&Array.isArray(e.uploads)&&(e.uploads=new Set(e.uploads.map((e=>e.id)))),this.fields.set(e.fieldId,e)}))}async loadUploads(){(await this.uploadStore.getAll()).forEach((e=>{this.uploads.set(e.id,e)}))}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((s=>s(e,t)))}destroy(){document.removeEventListener("click",this.clickHandler),document.removeEventListener("change",this.changeHandler),document.removeEventListener("dragenter",this.dragEnterHandler),document.removeEventListener("dragleave",this.dragLeaveHandler),document.removeEventListener("dragover",this.dragOverHandler),document.removeEventListener("drop",this.dropHandler),this.dragController&&this.dragController.destroy(),this.selectionHandlers.forEach((e=>e.destroy())),this.selectionHandlers.clear(),this.cleanupAllPreviewUrls(),this.sortableInstances.forEach((e=>{e?.destroy&&e.destroy()})),this.sortableInstances.clear(),this.fields.clear(),this.uploads.clear(),this.groups.clear(),this.selected.clear(),this.subscribers.clear()}destroySortable(e){Array.from(this.sortableInstances.keys()).filter((t=>t.startsWith(e))).forEach((e=>{const t=this.sortableInstances.get(e);t?.destroy&&t.destroy(),this.sortableInstances.delete(e)}))}cleanupRestore(){this.restoreModal.handleClose(),this.restoreSelection.destroy(),this.restoreSelection=null,this.restoreModal.destroy(),this.restoreModal.modal.remove(),this.restoreModal=null}async cleanupStoredUploads(){this.fieldStore.clear(),this.uploadStore.clear()}async clearField(e){await this.fieldStore.delete(e);const t=this.fields.get(e);if(t?.uploads)for(const e of t.uploads)await this.uploadStore.delete(e);this.fields.delete(e)}async clearUpload(e,t=!0){const s=this.uploads.get(e);if(s){if(this.revokePreviewUrl(s.preview),s.element){const e=s.element.dataset.previewUrl;this.revokePreviewUrl(e),delete s.element.dataset.previewUrl}t&&await this.schedulePersistance(s.fieldId),this.uploads.delete(e),this.uploadStore.delete(e),this.uploadStore.delete(e,"blobs")}}cleanupAllPreviewUrls(){this.previewUrls&&(this.previewUrls.forEach((e=>{try{URL.revokeObjectURL(e)}catch(e){}})),this.previewUrls.clear())}}document.addEventListener("DOMContentLoaded",(()=>{window.jvbUploads=new e}))})(); |
| | |
| | | (()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),a=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:a<7?`${a} ${1===a?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.getIcon=function(t){if(void 0===t)return"";if(window.jvbIcons||(window.jvbIcons=new Map),!window.jvbIcons.has(t)&&jvbSettings.icons[t]){let e=document.createElement("div");e.innerHTML=jvbSettings.icons[t],window.jvbIcons.set(t,e.firstElementChild.cloneNode(!0)),e.remove()}return window.jvbIcons.get(t)?.cloneNode(!0)},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let a=!0;return async function(){for(;a;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){a=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}}})(); |
| | | (()=>{window.fade=function(t,e=!0){e?t.style.animation="fadeIn var(--transition-base)":(t.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${t.dataset.id??t.id??t.className.replace(" ","-")}`,(()=>{t.remove()}),500))},window.formatTimeAgo=function(t){const e=t instanceof Date?t:new Date(t),n=new Date,i=Math.floor((n-e)/1e3),o=Math.floor(i/60),r=Math.floor(o/60),a=Math.floor(r/24);return r<24?0===r?0===o?"Just now":`${o} ${1===o?"minute":"minutes"} ago`:`${r} ${1===r?"hour":"hours"} ago`:a<7?`${a} ${1===a?"day":"days"} ago`:e.toLocaleDateString()},window.formatTimeSoon=function(t){const e=t instanceof Date?t:new Date(t),n=new Date;if(e<=n)return"Just now";const i=Math.floor((e-n)/1e3),o=Math.floor(i/60);return i<60?"In a moment":o<5?"In a few minutes":o<20?"Coming up soon":o<60?"In about half an hour":"Later today"},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((t=>{const e=Array.from(t.classList);if(e.length>0){const n=t.content.cloneNode(!0).firstElementChild;e.forEach((t=>{window.templates.has(t)||window.templates.set(t,n)}))}}))},window.getTemplate=function(t){return 0===window.templates.size&&loadTemplates(),!!window.templates.has(t)&&window.templates.get(t).cloneNode(!0)},window.formatVote=function(t,e){let n=window.getTemplate("voteButton");n.dataset.itemId=t.id,n.dataset.content=t.content;let i=n.querySelector("button.up"),o=n.querySelector("button.down");return"up"===e&&i.classList.add("voted"),"down"===e&&o.classList.add("voted"),t.upvotes>0&&(i.querySelector(".count").textContent=t.upvotes),t.downvotes>0&&(o.querySelector(".count").textContent="-"+t.downvotes),n},window.checkVoteStatus=function(t,e){if(!jvbSettings.currentUser)return"";let n="";return window.userVotes&&window.userVotes[t]?.has(e)&&(n=window.userVotes[t].get(e)),n},window.icon=document.querySelector(".icon").cloneNode(!0),window.icon.className="icon",window.getIcon=function(t){if(void 0===t)return"";console.log("Getting icon: "+t);let e=window.icon.cloneNode(!0);return e.classList.add("icon-"+t),e},window.isEmptyObject=function(t){return 0===Object.keys(t).length},window.formatNumber=function(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(t,e="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:e}).format(t)},window.escapeHtml=function(t){return t?("string"==typeof t||t instanceof String||(t=String(t)),t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.truncateText=function(t,e=100){return!t||t.length<=e?t:t.substring(0,e)+"..."},window.removeChildren=function(t){if(0!==t.children.length)for(;t.firstChild;)t.removeChild(t.firstChild)},window.formatDateRange=function(t,e){const n=new Date(t),i=new Date(e);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-US",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-US",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`},window.debounce=function(t,e=300){let n;return function(...i){clearTimeout(n),n=setTimeout((()=>t.apply(this,i)),e)}},window.throttle=function(t,e){let n;return function(){const i=arguments,o=this;n||(t.apply(o,i),n=!0,setTimeout((()=>n=!1),e))}},window.throttle=function(t,e=300){let n;return function(...i){n||(t.apply(this,i),n=!0,setTimeout((()=>n=!1),e))}},window.uppercaseFirst=function(t){return t.charAt(0).toUpperCase()+t.slice(1)},window.sanitizeHtml=function(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML},window.formatDate=function(t){if(!t)return"";const e=new Date(t),n=new Date,i=Math.floor((n-e)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:e.toLocaleDateString()},window.getPluralContent=function(t){return"artwork"===t?"artwork":t+"s"},window.showToast=function(t,e="success",n={}){window.jvbNotifications.showToast(t,e,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(t){return t instanceof Date&&!isNaN(t)||(t=new Date(t)),window.dateFormatter.format(t)},window.typeText=function(t,e,n=50){return t.classList.add("typeText"),new Promise((i=>{let o=0;t.textContent="";const r=setInterval((()=>{o<e.length?(t.textContent+=e.charAt(o),o++):(clearInterval(r),i())}),n)}))},window.eraseText=function(t,e=10){return new Promise((n=>{let i=t.textContent,o=i.length;const r=setInterval((()=>{o>0?(o--,t.textContent=i.substring(0,o)):(clearInterval(r),n())}),e)}))},window.typeLoop=function(t,e,n=50,i=10,o=1e3,r=250){let a=!0;return async function(){for(;a;)await window.typeText(t,e,n),await new Promise((t=>setTimeout(t,o))),await window.eraseText(t,i),await new Promise((t=>setTimeout(t,r)))}(),function(){a=!1}},window.toCamelCase=function(t){return t.replace(/-([a-z])/g,(function(t){return t[1].toUpperCase()}))},window.targetCheck=function(t,e){return"string"==typeof e&&(t.target.closest(e)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(t,e){if(this.isFunction(t)||this.isFunction(e))throw"Invalid argument. Function given, object expected.";if(this.isFile(t)||this.isFile(e)){const n=this.compareFiles(t,e);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===t?e:t}}if(this.isValue(t)||this.isValue(e)){const n=this.compareValues(t,e);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=e;break;case this.VALUE_DELETED:i=this.getEmptyValue(t);break;case this.VALUE_UPDATED:default:i=e}return{type:n,data:i}}let n={},i=!1;for(let o in t)if(!this.isFunction(t[o])){let r;e&&void 0!==e[o]&&(r=e[o]);const a=this.map(t[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(e)for(let o in e)if(!this.isFunction(e[o])&&(void 0===t||void 0===t[o])){const t=this.map(void 0,e[o]);null!==t&&(t.hasOwnProperty("type")&&t.hasOwnProperty("data")?n[o]=t.data:n[o]=t,i=!0)}return i?n:null},getEmptyValue:function(t){return this.isArray(t)?[]:this.isObject(t)?{}:"number"==typeof t?0:"boolean"!=typeof t&&""},compareValues:function(t,e){return t===e||this.isDate(t)&&this.isDate(e)&&t.getTime()===e.getTime()?this.VALUE_UNCHANGED:void 0===t?this.VALUE_CREATED:void 0===e?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(t){return"[object Function]"===Object.prototype.toString.call(t)},isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},isDate:function(t){return"[object Date]"===Object.prototype.toString.call(t)},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isFile:function(t){return t instanceof File},isValue:function(t){return!this.isObject(t)&&!this.isArray(t)},compareFiles:function(t,e){return!this.isFile(t)&&this.isFile(e)?this.VALUE_CREATED:this.isFile(t)&&!this.isFile(e)?this.VALUE_DELETED:this.isFile(t)&&this.isFile(e)?t.name===e.name&&t.size===e.size&&t.type===e.type&&t.lastModified===e.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(t,e){if(null==t)return e;if(null==e)return t;if(this.isFunction(t)||this.isFunction(e))return e;if(this.isFile(t)||this.isFile(e))return e;if(this.isValue(t)||this.isValue(e)||this.isArray(t)||this.isArray(e))return e;if(this.isObject(t)&&this.isObject(e)){let n={};for(let e in t)this.isFunction(t[e])||(n[e]=t[e]);for(let i in e)this.isFunction(e[i])||(void 0!==t[i]?n[i]=this.merge(t[i],e[i]):n[i]=e[i]);return n}return e}},window.deepMerge=function(t,e){return window.getDifferences.merge(t,e)},window.isInt=function(t){return!isNaN(parseInt(t))&&isFinite(t)},window.isNumeric=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},window.handleListField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("li");e.forEach((e=>{let i=n.cloneNode(!0);i.textContent=e,t.append(i)})),n.remove()},window.handleTextField=function(t,e){"string"==typeof e?t.textContent=e:t.remove()},window.handleImageField=function(t,e){if(!Array.isArray(e)||0===e)return void t.remove();let n="IMG"===t.tagName?t:t.querySelector("img");n?(n.alt=e.alt,n.src=e.thumbnail,n.dataset.small=e.small,n.dataset.medium=e.medium,n.dataset.large=e.full):t.remove()},window.handleGalleryField=function(t,e){if(!Array.isArray(e))return void t.remove();let n=t.querySelector("img");e.forEach((e=>{let i=n.cloneNode(!0);window.handleImageField(i,e),t.append(i)})),n.remove()},window.uiFromSelectors=function(t,e=null){let n={};for(let[i,o]of Object.entries(t))n[i]="object"==typeof o?window.uiFromSelectors(o,e):e?e.querySelector(o):document.querySelector(o);return n};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(t,e,n=1e3){this.cancel(t),this.timeouts.set(t,setTimeout((()=>{e(),this.timeouts.delete(t)}),n))}cancel(t){this.timeouts.has(t)&&(clearTimeout(this.timeouts.get(t)),this.timeouts.delete(t))}cleanup(){for(let t of this.timeouts.values())clearTimeout(t);this.timeouts.clear()}}})(); |
| | |
| | | window.jvbViews=class{constructor(e,t){this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.settings=window.jvbUserSettings,this.store=t,this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"table",body:"table body",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container)}init(){this.store.subscribe(((e,t)=>{switch(e){case"items-saved":case"item-saved":case"item-deleted":break;case"data-loaded":this.handleItemsUpdate()}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const a=Math.min(s,l),r=Math.max(s,l);let d=0;for(let e=a;e<=r;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){this.ui.table.columns.filter((t=>t.className===e))}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}))}handleDataUpdate(e){console.log(e);const t=e.data?.items||e.items||[];this.render(t)}handleItemsUpdate(){console.log(this.store.data),this.render(this.store.data)}render(e=[]){if(this.store)if(0!==e.length){switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}else this.renderEmpty();else console.error("No store connected to renderer")}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.getTemplate("emptyState");e&&(this.ui.grid.appendChild(e),this.a11y?.announce("No items found"))}renderGrid(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i;this.store.renderOrRetrieve?i=this.store.renderOrRetrieve(e,"grid",this.renderGridItem.bind(this)):this.items.grid.has(e.id)?i=this.items.grid.get(e.id):(i=this.renderGridItem(e),this.items.grid.set(e.id,i)),t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,a,r]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,a.dataset.id,r.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.id,e.id],"progress"===this.store.config.storeName?[l.src,l.alt]=[e.images[e.fields.timeline[0].post_thumbnail]?.medium??"",e.images[e.fields.timeline[0].post_thumbnail]?.alt??""]:[l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""],t}toggleTable(e){if(this.ui.table.selectedColumns.hidden=!e,e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.body=this.ui.table.table.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns.hidden=!e}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t;this.items.table.has(e.id)?t=this.items.table.get(e.id):(t=this.store.renderOrRetrieve(e,"table",this.renderTableItem.bind(this)),this.items.table.set(e.id,t)),this.ui.table.body.append(t)})),window.jvbSelector.scanExistingFields()}renderTableItem(e){let t=["",0];const i=window.getTemplate("tableView");return i.dataset.id=e.id,[i.querySelector(".select-item").id,i.querySelector(".select-item").value,i.querySelector(".select-item").checked,i.querySelector(".select-item + label").htmlFor,i.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],i.querySelectorAll("td[data-field]").forEach((i=>{let s,l=e.fields[i.dataset.field],a=i.querySelector("label"),r=t.includes(l);switch(i.dataset.fieldType){case"text":case"number":case"url":case"tel":case"email":r||(i.querySelector("input").value=l),a.remove();break;case"textarea":r||(i.querySelector("textarea").value=l),a.remove();break;case"taxonomy":a.remove(),r||(s=i.querySelector("input[type=hidden]"),s.value=l);break;case"image":if(!r){let t=window.getTemplate("uploadItem"),s=t.querySelector("img");[s.src,s.alt]=[e.images[l].medium??"",e.images[l].alt??""],i.querySelector(".item-grid").append(t),i.querySelector("input[type=hidden]").value=l}i.querySelectorAll(".progress,label,.upload-select,.status,details").forEach((e=>{e.remove()}));break;case"true_false":r||(i.querySelector("input").checked=1===parseInt(l)),i.querySelector(".toggle-label")?.remove();break;case"select":a.remove();case"radio":case"checkbox":i.querySelector(".label")?.remove(),r||(l=l.split(","),l.forEach((e=>{s=i.querySelector(`[value="${e}"]`),s&&(s.checked=!0)})));break;default:r||console.log(l)}})),i}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t;this.items.list.has(e.id)?t=this.items.list.get(e.id):(t=this.store.renderOrRetrieve(e,"list",this.renderListItem.bind(this)),this.items.list.set(e.id,t)),this.ui.grid.appendChild(t)}))}renderListItem(e){const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}}; |
| | | window.jvbViews=class{constructor(e,t){this.a11y=window.jvbA11y,this.error=window.jvbError,this.container=e,this.initElements(),this.settings=window.jvbUserSettings,this.store=t,this.items={list:new Map,grid:new Map,table:new Map},this.currentView="grid",this.selectedItems=new Set,this.init()}initElements(){this.selectors={grid:".item-grid",table:{table:"table",body:"table body",selectedColumns:".all-filters .multi-select",columns:"thead th"},bulk:{count:".bulk-controls .selected-count",control:".bulk-controls .bulk-actions",select:".bulk-controls select",selectAll:".select-all"}},this.ui=window.uiFromSelectors(this.selectors,this.container)}init(){this.store.subscribe(((e,t)=>{switch(e){case"items-saved":case"item-saved":case"item-deleted":break;case"data-loaded":this.handleItemsUpdate()}})),this.setupViewSwitcher(),this.changeHandler=this.handleChange.bind(this),this.clickHandler=this.handleClick.bind(this),this.lastSelected=null,document.addEventListener("change",this.changeHandler),document.addEventListener("click",this.clickHandler)}handleClick(e){e.target.closest(".select-item-label")&&(e.shiftKey?(e.preventDefault(),this.handleRangeSelection(e.target)):this.lastSelected=e.target.closest(".item"))}handleRangeSelection(e){if(!this.lastSelected)return void(this.lastSelected=e.closest(".item"));const t=e.closest(".item"),i=Array.from(this.container.querySelectorAll(".item")),s=i.indexOf(this.lastSelected),l=i.indexOf(t);if(-1===s||-1===l)return void(this.lastSelected=t);const a=Math.min(s,l),r=Math.max(s,l);let d=0;for(let e=a;e<=r;e++){let t=i[e];this.selectedItems.add(t.dataset.id);let s=t.querySelector(".select-item");s&&!s.checked&&(s.checked=!0,d++)}this.updateSelectionUI(),window.jvbA11y.announce(`Selected ${d} items in range.`)}handleChange(e){e.target.closest(".select-all")?this.selectAll(e.target.checked):e.target.closest(".select-item")?this.toggleSelection(e.target.closest(".item").dataset.id):e.target.closest("details.multi-select")&&this.toggleColumns(e.target.id,e.target.checked)}toggleColumns(e,t){this.ui.table.columns.filter((t=>t.className===e))}setupViewSwitcher(){document.querySelectorAll("[data-view]").forEach((e=>{this.settings.addSetting(e),e.addEventListener("click",(()=>{this.currentView=e.dataset.view,this.render()}))}))}handleDataUpdate(e){console.log(e);const t=e.data?.items||e.items||[];this.render(t)}handleItemsUpdate(){console.log(this.store.data),this.render(this.store.data)}render(e=[]){if(this.store)if(console.log(e),0!==e.length){switch(this.currentView){case"grid":this.renderGrid(e);break;case"table":this.renderTable(e);break;case"list":this.renderList(e)}this.updateSelectionUI()}else this.renderEmpty();else console.error("No store connected to renderer")}renderEmpty(){this.toggleTable(!1),window.removeChildren(this.ui.grid);const e=window.getTemplate("emptyState");e&&(this.ui.grid.appendChild(e),this.a11y?.announce("No items found"))}renderGrid(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("list-view"),this.ui.grid.classList.add("grid-view");const t=document.createDocumentFragment();e.forEach((e=>{let i;this.store.renderOrRetrieve?i=this.store.renderOrRetrieve(e,"grid",this.renderGridItem.bind(this)):this.items.grid.has(e.id)?i=this.items.grid.get(e.id):(i=this.renderGridItem(e),this.items.grid.set(e.id,i)),t.appendChild(i)})),this.ui.grid.appendChild(t)}renderGridItem(e){const t=window.getTemplate("gridView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let[i,s,l,a,r]=[t.querySelector("input"),t.querySelector("label"),t.querySelector("img"),t.querySelector('[data-action="edit"]'),t.querySelector('[data-action="trash"]')];return[i.value,i.id,i.checked,s.htmlFor,a.dataset.id,r.dataset.id]=[e.id,`select-${e.id}`,this.selectedItems.has(`${e.id}`),`select-${e.id}`,e.id,e.id],[l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""],t}toggleTable(e){if(this.ui.table.selectedColumns.hidden=!e,e&&!this.ui.table.table){let e=window.getTemplate("contentTable");this.container.append(e),this.ui.table.table=this.container.querySelector("form.table"),this.ui.table.body=this.ui.table.table.querySelector("tbody"),this.ui.table.columns=this.container.querySelectorAll(this.selectors.table.columns)}this.ui.table.table&&(this.ui.table.table.hidden=!e,window.removeChildren(this.ui.table.body)),this.ui.table.selectedColumns.hidden=!e}toggleGrid(){window.removeChildren(this.ui.grid)}renderTable(e){this.toggleTable(!0),this.toggleGrid(),e.forEach((e=>{let t;this.items.table.has(e.id)?t=this.items.table.get(e.id):(t=this.store.renderOrRetrieve(e,"table",this.renderTableItem.bind(this)),this.items.table.set(e.id,t)),this.ui.table.body.append(t)})),window.jvbSelector.scanExistingFields()}renderTableItem(e){let t=["",0];const i=window.getTemplate("tableView");return i.dataset.id=e.id,[i.querySelector(".select-item").id,i.querySelector(".select-item").value,i.querySelector(".select-item").checked,i.querySelector(".select-item + label").htmlFor,i.querySelector(`input[name="post_status"][value="${e.status}"]`).checked]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id,e.status],i.querySelectorAll("td[data-field]").forEach((i=>{let s,l=e.fields[i.dataset.field],a=i.querySelector("label"),r=t.includes(l);switch(i.dataset.fieldType){case"text":case"number":case"url":case"tel":case"email":r||(i.querySelector("input").value=l),a.remove();break;case"textarea":r||(i.querySelector("textarea").value=l),a.remove();break;case"taxonomy":a.remove(),r||(s=i.querySelector("input[type=hidden]"),s.value=l);break;case"image":if(!r){let t=window.getTemplate("uploadItem"),s=t.querySelector("img");[s.src,s.alt]=[e.images[l].medium??"",e.images[l].alt??""],i.querySelector(".item-grid").append(t),i.querySelector("input[type=hidden]").value=l}i.querySelectorAll(".progress,label,.upload-select,.status,details").forEach((e=>{e.remove()}));break;case"true_false":r||(i.querySelector("input").checked=1===parseInt(l)),i.querySelector(".toggle-label")?.remove();break;case"select":a.remove();case"radio":case"checkbox":i.querySelector(".label")?.remove(),r||(l=l.split(","),l.forEach((e=>{s=i.querySelector(`[value="${e}"]`),s&&(s.checked=!0)})));break;default:r||console.log(l)}})),i}renderList(e){this.toggleGrid(),this.toggleTable(!1),this.ui.grid.classList.remove("grid-view"),this.ui.grid.classList.add("list-view"),e.forEach((e=>{let t;this.items.list.has(e.id)?t=this.items.list.get(e.id):(t=this.store.renderOrRetrieve(e,"list",this.renderListItem.bind(this)),this.items.list.set(e.id,t)),this.ui.grid.appendChild(t)}))}renderListItem(e){const t=window.getTemplate("listView");t.dataset.id=e.id,e._pending&&t.classList.add("pending");let i=t.querySelector(".select-item"),s=t.querySelector(".select-item + label");[i.id,i.value,i.checked,s.htmlFor]=[e.id,e.id,this.selectedItems.has(`${e.id}`),e.id],t.querySelectorAll("[data-attr]").forEach((t=>{""!==e[t.dataset.attr]?t.textContent=e[t.dataset.attr]:t.remove()})),t.querySelectorAll("[data-field]").forEach((t=>{let i=e.fields[t.dataset.field];""!==i?"DIV"===t.tagName?t.innerHTML=i:t.textContent=i:t.remove()}));let l=t.querySelector("img");return l&&([l.src,l.alt]=[e.images[e.fields.post_thumbnail]?.medium??"",e.images[e.fields.post_thumbnail]?.alt??""]),t}toggleSelection(e){this.selectedItems.has(e)?this.selectedItems.delete(e):this.selectedItems.add(e),this.updateSelectionUI()}selectAll(e){const t=this.container.querySelectorAll(".item");e||(this.selectedItems.clear(),this.ui.bulk.selectAll.checked=!1,this.ui.bulk.select.value=""),t.forEach((t=>{e&&this.selectedItems.add(t.dataset.id),t.querySelector(".select-item").checked=e})),this.updateSelectionUI()}clearSelection(){this.selectAll(!1),this.ui.bulk.select.value=""}updateSelectionUI(){const e=this.selectedItems.size;if(this.ui.bulk.control&&(this.ui.bulk.control.hidden=0===e),this.ui.bulk.count){let t=1===e?"item":"items";this.ui.bulk.count.hidden=0===e,this.ui.bulk.count.textContent=0===e?"":`${e} ${t} selected`}}}; |
| | |
| | | |
| | | require(JVB_DIR.'/base/site.php'); |
| | | require(JVB_DIR.'/base/content.php'); |
| | | require(JVB_DIR.'/base/email.php'); |
| | | require(JVB_DIR.'/base/taxonomies.php'); |
| | | require(JVB_DIR.'/base/users.php'); |
| | | require(JVB_DIR.'/base/login.php'); |
| New file |
| | |
| | | <?php |
| | | $defaults = [ |
| | | 'colours' => [ |
| | | 'action-0' => '#ff0080', |
| | | 'action-50' => '#ff2492', |
| | | 'action-100' => '#ff47a4', |
| | | 'action-200' => '#ff6bb5', |
| | | 'secondary-0' => '#D69121', |
| | | 'secondary-50' => '#ffc421', |
| | | 'secondary-100' => '#ffcd44', |
| | | 'secondary-200' => '#ffd768', |
| | | 'light' => '#efefef', |
| | | 'light-50' => '#e2e2e2', |
| | | 'light-100' => '#d5d5d5', |
| | | 'light-200' => '#c9c9c9', |
| | | 'dark' => '#151515', |
| | | 'dark-50' => '#222222', |
| | | 'dark-100' => '#2e2e2e', |
| | | 'dark-200' => '#3b3b3b', |
| | | 'action-contrast'=> '', |
| | | 'secondary-contrast'=> '', |
| | | ], |
| | | 'content' => [ |
| | | 'title' => get_bloginfo('name'), |
| | | 'subjectPrefix' => '['.get_bloginfo('name').']', |
| | | 'signature' => '<p>  — ♡ the edmonton.ink crew</p>', |
| | | 'footer' => [ |
| | | '<p>© ' . date('Y') . ' edmonton.ink — Your tattoo scene on your screen.</p>', |
| | | '<p><a href="' . get_home_url() . '" class="text-link">edmonton.ink</a></p>' |
| | | ] |
| | | ], |
| | | 'types' => [ |
| | | 'newUser' => [ |
| | | 'subject' => 'Welcome to Legacy! Finish creating your account.', |
| | | 'showPrefix' => true, |
| | | ], |
| | | 'resetPass' => [ |
| | | 'subject' => 'Reset your Password' |
| | | ], |
| | | 'emailChange' => [ |
| | | 'subject' => 'Successfully Changed Email' |
| | | ], |
| | | 'passwordChange' => [ |
| | | 'subject' => 'Successfully Changed Password', |
| | | ] |
| | | ] |
| | | ]; |
| | | |
| | | |
| | | /* |
| | | * EXTEND WITH: |
| | | * {string} $message: email content |
| | | * {WP_User} $user: user |
| | | * apply_filters |
| | | * jvbNewUserEmail -> New user email content |
| | | * add_filter('jvbNewUserEmail', 'customFunction', 10, 2); |
| | | * jvbNewUserAdminEmail -> New User notification for admin |
| | | * add_filter('jvbNewUserAdminEmail', 'customFunction', 10, 2); |
| | | * jvbPasswordResetEmail -> Password Reset Email |
| | | * {string} $message: email content |
| | | * {string} $key |
| | | * {string} $user_login, |
| | | * {WP_User} $user_data |
| | | * add_filter('jvbPasswordResetEmail', 'customFunction', 10, 4); |
| | | * jvbEmailChangeRequestEmail -> Request for email change |
| | | * {string} $message |
| | | * {array} $oldUser |
| | | * {array} newUser |
| | | * add_filter('jvbEmailChangeRequestEmail', 'customFunction', 10, 3); |
| | | * jvbEmailChangedEmail -> Notification that email successfully changed |
| | | * {string} $message |
| | | * {string} $confirm_url |
| | | add_filter('jvbEmailChangedEmail', 'customFunction', 10, 2); |
| | | * jvbPasswordChangeEmail -> Request for password change |
| | | * {string} $message |
| | | * {array} $oldUser |
| | | * {array} $newUser |
| | | * add_filter('jvbPasswordChangeEmail', 'customFunction', 10, 3); |
| | | * jvbPersonalDataExport -> Request for User Data |
| | | * {string} $message |
| | | * {string} $request_type |
| | | * {string} $confirmation_url |
| | | * {array} $emailData, from the original filter |
| | | * add_filter('jvbPersonalDataExport', 'customFunction', 10, 4); |
| | | * jvbPersonalDataExported -> Notification that data is ready for download |
| | | * {string} $message |
| | | * {string} $downloadURL |
| | | * {string} $expiresAt |
| | | * {array} $emailData, from the original filter |
| | | * add_filter('jvbPersonalDataExported', 'customFunction', 10, ); |
| | | * |
| | | */ |
| | | |
| | | |
| | | $jvbEmail = apply_filters('jvb_email', $defaults); |
| | | define('JVB_EMAIL', $jvbEmail); |
| | | |
| | |
| | | <div class="filter-actions"> |
| | | <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', true) ?> |
| | | <button type="button" class="clear-filters row" hidden> |
| | | <?= jvbIcon('close', ['title' => 'Clear']) ?> |
| | | <?= jvbIcon('x', ['title' => 'Clear']) ?> |
| | | Clear All Filters |
| | | </button> |
| | | </div> |
| | |
| | | |
| | | <input type="radio" id="order-random" class="btn" name="orderby" value="random"> |
| | | <label for="order-random" title="Random Order"> |
| | | <?= jvbIcon('random') ?> |
| | | <?= jvbIcon('shuffle') ?> |
| | | <span class="label">Random</span> |
| | | </label> |
| | | </div> |
| | |
| | | <div class="order-direction radio-group-label" data-for-order="date,title"> |
| | | <input type="radio" id="order-desc" class="btn" name="order" value="desc" checked> |
| | | <label for="order-desc" title="Newest First"> |
| | | <?= jvbIcon('desc') ?> |
| | | <?= jvbIcon('sort-descending') ?> |
| | | </label> |
| | | |
| | | <input type="radio" id="order-asc" class="btn" name="order" value="asc"> |
| | | <label for="order-asc" title="Oldest First"> |
| | | <?= jvbIcon('asc') ?> |
| | | <?= jvbIcon('sort-ascending') ?> |
| | | </label> |
| | | </div> |
| | | </div> |
| | |
| | | <?php return array('dependencies' => array(), 'version' => '90747b206e7cdf47035c'); |
| | | <?php return array('dependencies' => array(), 'version' => '2f66c620b1540459b263'); |
| | |
| | | (()=>{class t{constructor(t="dl.glossary",e="nav.glossary-index"){this.glossary=document.querySelector(t),this.nav=document.querySelector(e),this.glossary&&this.nav&&(this.terms=this.glossary.querySelectorAll("dt[id]"),this.navList=this.nav.querySelector("ul"),this.activeClass="active",this.currentActive=null,this.breakpoint=768,this.init(),this.setupResizeHandler())}init(){const t={root:null,rootMargin:this.getRootMargin(),threshold:0};this.observer=new IntersectionObserver((t=>this.handleIntersection(t)),t),this.terms.forEach((t=>this.observer.observe(t))),this.handleScroll=this.debounce((()=>this.checkActiveTerm()),100),window.addEventListener("scroll",this.handleScroll,{passive:!0})}getRootMargin(){if(window.innerWidth<this.breakpoint){const t=parseFloat(getComputedStyle(document.documentElement).fontSize),e=Math.round(5*t);return`-${e}px 0px -${e}px 0px`}return"-50% 0px -50% 0px"}setupResizeHandler(){let t;window.addEventListener("resize",(()=>{clearTimeout(t),t=setTimeout((()=>{this.reinitialize()}),250)}))}reinitialize(){this.observer&&this.observer.disconnect(),this.init()}handleIntersection(t){const e=t.find((t=>t.isIntersecting));e&&this.setActive(e.target)}checkActiveTerm(){const t=4*parseFloat(getComputedStyle(document.documentElement).fontSize);let e=null,i=1/0;this.terms.forEach((s=>{const n=s.getBoundingClientRect();if(window.innerWidth<this.breakpoint?n.top>=t&&n.top<=window.innerHeight-t:n.top+n.height/2>=0&&n.top+n.height/2<=window.innerHeight){const o=window.innerWidth<this.breakpoint?t:window.innerHeight/2,r=Math.abs(n.top-o);r<i&&(i=r,e=s)}})),e&&this.setActive(e)}setActive(t){this.currentActive!==t&&(this.currentActive&&this.currentActive.classList.remove(this.activeClass),t.classList.add(this.activeClass),this.currentActive=t,this.updateNavigation(t.id))}updateNavigation(t){this.nav.querySelectorAll("a").forEach((t=>t.classList.remove(this.activeClass)));const e=this.nav.querySelector(`a[href="#${t}"]`);e&&(e.classList.add(this.activeClass),this.centerNavItem(e))}centerNavItem(t){const e=this.navList.getBoundingClientRect(),i=t.getBoundingClientRect(),s=this.navList.scrollTop,n=i.top-e.top,o=e.height/2-i.height/2;this.navList.scrollTo({top:s+n-o,behavior:"smooth"})}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout((()=>{clearTimeout(i),t(...s)}),e)}}destroy(){this.observer&&this.observer.disconnect(),window.removeEventListener("scroll",this.handleScroll)}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>{new t})):new t})(); |
| | | (()=>{class t{constructor(t="dl.glossary",e="nav.glossary-index"){this.glossary=document.querySelector(t),this.nav=document.querySelector(e),this.glossary&&this.nav&&(this.terms=this.glossary.querySelectorAll("dt[id]"),this.navList=this.nav.querySelector("ul"),this.activeClass="active",this.currentActive=null,this.init(),this.setupResizeHandler())}init(){const t={root:null,rootMargin:this.getRootMargin(),threshold:0};this.observer=new IntersectionObserver((t=>this.handleIntersection(t)),t),this.terms.forEach((t=>this.observer.observe(t))),this.handleScroll=this.debounce((()=>this.checkActiveTerm()),100),window.addEventListener("scroll",this.handleScroll,{passive:!0})}getRootMargin(){return"-50% 0px -50% 0px"}setupResizeHandler(){let t;window.addEventListener("resize",(()=>{clearTimeout(t),t=setTimeout((()=>{this.reinitialize()}),250)}))}reinitialize(){this.observer&&this.observer.disconnect(),this.init()}handleIntersection(t){const e=t.find((t=>t.isIntersecting));e&&this.setActive(e.target)}checkActiveTerm(){parseFloat(getComputedStyle(document.documentElement).fontSize);let t=null,e=1/0;this.terms.forEach((i=>{const s=i.getBoundingClientRect();if(s.top+s.height/2>=0&&s.top+s.height/2<=window.innerHeight){const n=window.innerHeight/2,r=Math.abs(s.top-n);r<e&&(e=r,t=i)}})),t&&this.setActive(t)}setActive(t){this.currentActive!==t&&(this.currentActive&&this.currentActive.classList.remove(this.activeClass),t.classList.add(this.activeClass),this.currentActive=t,this.updateNavigation(t.id))}updateNavigation(t){this.nav.querySelectorAll("a").forEach((t=>t.classList.remove(this.activeClass)));const e=this.nav.querySelector(`a[href="#${t}"]`);e&&(e.classList.add(this.activeClass),this.centerNavItem(e))}centerNavItem(t){const e=this.navList.getBoundingClientRect(),i=t.getBoundingClientRect(),s=this.navList.scrollTop,n=i.top-e.top,r=e.height/2-i.height/2;this.navList.scrollTo({top:s+n-r,behavior:"smooth"})}debounce(t,e){let i;return function(...s){clearTimeout(i),i=setTimeout((()=>{clearTimeout(i),t(...s)}),e)}}destroy(){this.observer&&this.observer.disconnect(),window.removeEventListener("scroll",this.handleScroll)}}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>{new t})):new t})(); |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\CacheManager; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | |
| | | function jvbRenderArtistSummary():string |
| | | { |
| | | $current = get_queried_object(); |
| | | $cache = new JVBase\managers\CacheManager('artists', WEEK_IN_SECONDS); |
| | | $cache = CacheManager::for('artists', WEEK_IN_SECONDS); |
| | | $key = 'artist-bio-'.$current->ID; |
| | | $cached = $cache->get($key); |
| | | $cached = false; |
| | |
| | | <nav id="artist" class="on-this-page index"> |
| | | <label>Jump to: |
| | | <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false"> |
| | | <?= jvbIcon('add')?> |
| | | <?= jvbIcon('plus-square')?> |
| | | </button> |
| | | </label> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> |
| | | <li><a href="#about">About</a></li> |
| | | <li><a href="#styles">Styles</a></li> |
| | | <li><a href="#contact">Contact</a></li> |
| | |
| | | { |
| | | $current = get_queried_object(); |
| | | |
| | | $cache = new JVBase\managers\CacheManager('shops', WEEK_IN_SECONDS); |
| | | $cache = CacheManager::for('shops', WEEK_IN_SECONDS); |
| | | $key = 'shop-bio-'.$current->term_id; |
| | | $cached = $cache->get($key); |
| | | $cached = false; |
| | |
| | | <nav id="shop" class="on-this-page index"> |
| | | <label>Jump to: |
| | | <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false"> |
| | | <?= jvbIcon('add')?> |
| | | <?= jvbIcon('plus-square')?> |
| | | </button> |
| | | </label> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li> <?php |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php |
| | | if ($rating !== 'none') { |
| | | ?> |
| | | <li><a href="#rating">Rating</a></li> |
| | |
| | | function jvbRenderTermSummary() |
| | | { |
| | | $current = get_queried_object(); |
| | | $cache = new JVBase\managers\CacheManager($current->taxonomy, WEEK_IN_SECONDS); |
| | | $key = $current->taxonomy.'-'.$current->ID; |
| | | $cache = CacheManager::for(jvbNoBase($current->taxonomy), WEEK_IN_SECONDS); |
| | | $key = $current->ID; |
| | | $cached = $cache->get($key); |
| | | $cached = false; |
| | | if ($cached !== false) { |
| | |
| | | } |
| | | |
| | | // Remove block-specific scripts and modules |
| | | wp_deregister_script('heartbeat'); |
| | | if (!is_admin()) { |
| | | wp_deregister_script('heartbeat'); |
| | | } |
| | | wp_dequeue_script('wp-block-template-skip-link'); |
| | | |
| | | // Remove WordPress 6.5+ script modules |
| New file |
| | |
| | | <?php |
| | | namespace JVBase; |
| | | |
| | | use JVBase\managers\CacheManager; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | class JVBIcons |
| | | { |
| | | static array $processing = ['map' => [],'label' => []]; |
| | | protected string $style; |
| | | protected array $styles = [ |
| | | 'regular', |
| | | 'bold', |
| | | 'duotone', |
| | | 'fill', |
| | | 'light', |
| | | 'thin' |
| | | ]; |
| | | |
| | | protected array $labels = [ |
| | | 'submenu' => 'Toggle Menu', |
| | | 'dashboard' => 'Dashboard', |
| | | 'arrow-fat-down' => 'Downvote', |
| | | 'arrow-fat-up' => 'Upvote', |
| | | 'hamburger' => 'Menu', |
| | | 'check-square-offset'=> 'Approval Requests', |
| | | 'h1' => 'Heading 1', |
| | | 'h2' => 'Heading 2', |
| | | 'h3' => 'Heading 3', |
| | | 'note-pencil' => 'Notes', |
| | | 'sun-dim' => 'Light Mode', |
| | | 'moon' => 'Dark Mode', |
| | | 'grid' => 'Grid View', |
| | | 'list' => 'List View', |
| | | 'clock-clockwise' => 'Upcoming Events', |
| | | 'clock-counter-clockwise'=>'Past Events', |
| | | 'repeat' => 'Recurring Events', |
| | | 'floppy-disk' => 'Save', |
| | | ]; |
| | | |
| | | protected array $map = [ |
| | | 'time' => 'clock', |
| | | 'back' => 'arrow-u-up-left', |
| | | 'logo' => 'logo.svg', |
| | | 'logo-basic' => 'logo-basic.svg', |
| | | 'save' => 'floppy-disk', |
| | | 'restore' => 'arrow-counter-clockwise', |
| | | 'help' => 'question', |
| | | 'upload' => 'cloud-arrow-up', |
| | | 'download' => 'cloud-arrow-down', |
| | | 'synced' => 'cloud-check', |
| | | 'syncing' => 'cloud-sync-thin.svg', |
| | | 'offline' => 'cloud-slash', |
| | | 'error' => 'cloud-warning', |
| | | 'cart' => 'shopping-cart', |
| | | 'delete' => 'trash', |
| | | 'edit' => 'pencil-simple', |
| | | 'bio' => 'user', |
| | | 'login' => 'sign-in', |
| | | 'logout' => 'sign-out', |
| | | 'future' => 'clock-clockwise', |
| | | 'past' => 'clock-counter-clockwise', |
| | | 'light' => 'sun-dim', |
| | | 'dark' => 'moon', |
| | | 'h1' => 'text-h-one', |
| | | 'h2' => 'text-h-two', |
| | | 'h3' => 'text-h-three', |
| | | 'bold' => 'text-b', |
| | | 'italic' => 'text-italic', |
| | | 'underline' => 'text-underline', |
| | | 'strike' => 'text-strikethrough', |
| | | 'image' => 'image-square', |
| | | 'reply' => 'arrow-bend-up-left', |
| | | 'approval' => 'check-square-offset', |
| | | 'response' => 'chat-teardrop', |
| | | 'karma' => 'scales', |
| | | 'show' => 'eye', |
| | | 'publish' => 'eye', |
| | | 'hide' => 'eye-closed', |
| | | 'draft' => 'eye-closed', |
| | | 'asc' => 'sort-ascending', |
| | | 'desc' => 'sort-descending', |
| | | 'all' => 'infinity', |
| | | 'random' => 'shuffle', |
| | | 'location' => 'map-pin', |
| | | 'hours' => 'clock', |
| | | 'favourite' => 'heart', |
| | | 'email' => 'envelope', |
| | | 'text' => 'chat', |
| | | 'integrations' => 'plugs-connected', |
| | | 'connected' => 'plugs-connected', |
| | | 'disconnected' => 'plugs', |
| | | 'umami' => 'chart-line', |
| | | 'square-up' => 'square-logo', |
| | | 'tiktok' => 'tiktok-logo', |
| | | 'threads' => 'threads-logo', |
| | | 'twitch' => 'twitch-logo', |
| | | 'u' => 'snapchat-logo', |
| | | 'linktree' => 'linktree-logo', |
| | | 'fediverse' => 'fediverse-logo', |
| | | 'mastadon' => 'mastadon-logo', |
| | | 'youtube' => 'youtube-logo', |
| | | 'twitter' => 'twitter-logo', |
| | | 'messenger' => 'messenger-logo', |
| | | 'facebook' => 'facebook-logo', |
| | | 'instagram' => 'instagram-logo', |
| | | 'dash' => 'door', |
| | | 'event' => 'calendar', |
| | | 'events' => 'calendar', |
| | | 'settings' => 'gear-six', |
| | | 'grid' => 'squares-four', |
| | | 'list' => 'rows', |
| | | 'pinned' => 'push-pin', |
| | | 'search' => 'magnifying-glass', |
| | | 'add' => 'plus-square', |
| | | 'minus' => 'minus-square', |
| | | 'support' => 'question', |
| | | 'grab' => 'dots-six-vertical', |
| | | 'checkout' => 'receipt', |
| | | 'home' => 'house', |
| | | 'elbow-right-down' => 'arrow-elbow-right-down', |
| | | 'elbow-right-up'=> 'arrow-elbow-right-up', |
| | | 'elbow-left-down'=> 'arrow-elbow-left-down', |
| | | 'elbow-left-up' => 'arrow-elbow-left-up', |
| | | 'menu' => 'list', |
| | | 'submenu' => 'caret-down', |
| | | 'close' => 'x', |
| | | 'close-square' => 'x-square', |
| | | 'dashboard' => 'door', |
| | | 'system' => 'gear-six', |
| | | 'options' => 'gear-six', |
| | | 'news' => 'newspaper', |
| | | 'metrics' => 'chart-line', |
| | | 'prev' => 'caret-left', |
| | | 'up' => 'caret-circle-up', |
| | | 'right' => 'caret-double-right', |
| | | 'left' => 'caret-double-left', |
| | | 'next' => 'caret-right', |
| | | 'refresh' => 'arrows-clockwise', |
| | | 'pending' => 'arrows-clockwise', |
| | | 'copy' => 'copy-simple', |
| | | 'align-center' => 'text-align-center', |
| | | 'align-left' => 'text-align-left', |
| | | 'align-right' => 'text-align-right', |
| | | // 'alphabetical' => 'alphabetical.svg', |
| | | 'date' => 'calendar', |
| | | 'down' => 'caret-double-down', |
| | | 'tattoo' => 'drop-simple', |
| | | 'tattoos' => 'drop-simple', |
| | | 'theme' => 'folder-open', |
| | | 'arttheme' => 'folder-open', |
| | | 'style' => 'hash', |
| | | 'artstyle' => 'hash', |
| | | 'colour' => 'drop', |
| | | 'placement' => 'person-arms-spread', |
| | | 'artmedia' => 'squares-four', |
| | | 'artist' => 'user', |
| | | 'client' => 'user', |
| | | 'artists' => 'users-three', |
| | | 'partner' => 'currency-circle-dollar', |
| | | 'shop' => 'storefront', |
| | | 'piercing' => 'needle', |
| | | 'piercings' => 'needle', |
| | | 'artwork' => 'palette', |
| | | 'artform' => 'shapes', |
| | | 'city' => 'map-pin', |
| | | 'type' => 'users-three', |
| | | 'pstyle' => 'nut', |
| | | 'project' => 'code', |
| | | 'map' => 'map-trifold', |
| | | 'offer' => 'gift', |
| | | 'referrals' => 'hand-heart' |
| | | ]; |
| | | |
| | | |
| | | private const ICON_GROUPS = [ |
| | | 'navigation' => [ |
| | | 'back' => ['filename' => 'back', 'label' => 'Back', 'size' => 24], |
| | | 'home' => ['filename' => 'house', 'label' => 'Home'], |
| | | 'right' => ['filename' => 'arrow-fat-right', 'label' => ''], |
| | | 'elbow-right-down'=> ['filename' => 'arrow-elbow-right-down', 'label' => ''], |
| | | 'elbow-right-up'=> ['filename' => 'arrow-elbow-right-up', 'label' => ''], |
| | | 'elbow-left-down'=> ['filename' => 'arrow-elbow-left-down', 'label' => ''], |
| | | 'elbow-left-up'=> ['filename' => 'arrow-elbow-left-up', 'label' => ''], |
| | | 'menu' => ['filename' => 'list', 'label' => 'Toggle Menu', 'size' => 32], |
| | | 'submenu' => ['filename' => 'caret-down', 'label' => 'Toggle Submenu'], |
| | | 'close' => ['filename' => 'x', 'label' => 'Close', 'size' => 32], |
| | | 'close-square' => ['filename' => 'x-square', 'label' => 'Close'], |
| | | 'dashboard' => ['filename' => 'door', 'label'=> 'Behind the Scenes', 'size' => 24], |
| | | 'dash' => ['filename' => 'door', 'label'=> 'Behind the Scenes', 'size' => 24], |
| | | 'settings' => ['filename' => 'gear-six', 'label'=> 'Settings', 'size' => 24], |
| | | 'system' => ['filename' => 'gear-six', 'label'=> 'Settings', 'size' => 24], |
| | | 'options' => ['filename' => 'gear-six', 'label'=> 'Options', 'size' => 24], |
| | | 'news' => ['filename' => 'newspaper', 'label' => 'News', 'size' => 24], |
| | | 'metrics' => ['filename' => 'chart-line', 'label' => 'Metrics', 'size' => 24], |
| | | 'prev' => ['filename' => 'caret-left', 'label' => 'Previous', 'size' => 24], |
| | | 'up' => ['filename' => 'caret-circle-up', 'label' => 'Up', 'size' => 24], |
| | | 'down' => ['filename' => 'caret-double-down', 'label' => 'Down', 'size' => 24], |
| | | 'right' => ['filename' => 'caret-double-right', 'label' => 'Right', 'size' => 24], |
| | | 'next' => ['filename' => 'caret-right', 'label' => 'Next', 'size' => 24], |
| | | 'refresh' => ['filename' => 'arrows-clockwise', 'label' => 'Refresh', 'size' => 24], |
| | | 'pending' => ['filename' => 'arrows-clockwise', 'label' => 'Refresh', 'size' => 24], |
| | | 'clock' => ['filename' => 'clock', 'label' => 'Hours'], |
| | | 'copy' => ['filename' => 'copy-simple', 'label' => 'Duplicate'], |
| | | 'list-heart' => ['filename' => 'list-heart', 'label'=> 'Lists'], |
| | | 'downvoted' => ['filename' => 'arrow-fat-down-fill.svg', 'label'=> 'Downvote'], |
| | | 'upvoted' => ['filename' => 'arrow-fat-up-fill.svg', 'label'=> 'Upvote'], |
| | | 'downvote' => ['filename' => 'arrow-fat-down', 'label'=> 'Downvote'], |
| | | 'upvote' => ['filename' => 'arrow-fat-up', 'label'=> 'Upvote'], |
| | | 'karma' => ['filename' => 'scales', 'label'=> 'Karma'], |
| | | 'response' => ['filename' => 'chat-teardrop', 'label' => 'Responses'], |
| | | 'reply' => ['filename' => 'arrow-bend-up-left', 'label' => 'Reply'], |
| | | 'approval' => ['filename' => 'check-square-offset', 'label' => 'Approval Requests'], |
| | | 'check' => ['filename' => 'check-circle', 'label' => 'done'], |
| | | 'hamburger' => ['filename' => 'hamburger', 'label' => 'menu'] |
| | | ], |
| | | 'formatting' => [ |
| | | 'paragraph' => ['filename' => 'paragraph', 'label' => 'Paragraph', 'size' => 24], |
| | | 'h1' => ['filename' => 'text-h-one', 'label' => 'Heading 1', 'size' => 24], |
| | | 'h2' => ['filename' => 'text-h-two', 'label' => 'Heading 2', 'size' => 24], |
| | | 'h3' => ['filename' => 'text-h-three', 'label' => 'Heading 3', 'size' => 24], |
| | | 'bold' => ['filename' => 'text-b-bold.svg', 'label' => 'Bold', 'size' => 24], |
| | | 'italic' => ['filename' => 'text-italic', 'label' => 'Italic', 'size' => 24], |
| | | 'underline' => ['filename' => 'text-underline', 'label' => 'Underline', 'size' => 24], |
| | | 'strike' => ['filename' => 'text-strikethrough', 'label' => 'Strikethrough', 'size' => 24], |
| | | 'list-bullets' => ['filename' => 'list-bullets', 'label' => 'Bulleted List', 'size' => 24], |
| | | 'list-numbers' => ['filename' => 'list-numbers', 'label' => 'Numbered List', 'size' => 24], |
| | | 'image' => ['filename' => 'image-square', 'label' => 'Image', 'size' => 24], |
| | | 'align-left' => ['filename' => 'text-align-left', 'label' => 'Align Left', 'size' => 24], |
| | | 'align-right' => ['filename' => 'text-align-right', 'label' => 'Align Right', 'size' => 24], |
| | | 'align-center' => ['filename' => 'text-align-center', 'label' => 'Align Center', 'size' => 24], |
| | | 'link' =>['filename' => 'link', 'label' => 'Link', 'size' => 24], |
| | | 'note' => ['filename' => 'note-pencil', 'label' => 'Notes', 'size' => 24], |
| | | 'password' => ['filename' => 'password', 'label' => 'Password'] |
| | | ], |
| | | 'theme' => [ |
| | | 'light' => ['filename' => 'sun-dim', 'label' => 'Light Mode'], |
| | | 'dark' => ['filename' => 'moon', 'label' => 'Dark Mode'], |
| | | 'grid' => ['filename' => 'squares-four', 'label' => 'Grid View'], |
| | | 'list' => ['filename' => 'rows', 'label' => 'List View'], |
| | | ], |
| | | 'content' => [ |
| | | 'offer' => ['filename'=>'gift', 'label'=>'Offer'], |
| | | 'logo' => ['filename' => 'logo.svg', 'label' => 'North\'eh', 'size' => 20], |
| | | 'logo-basic' => ['filename' => 'logo-basic.svg', 'label' => 'North\'eh', 'size' => 20], |
| | | 'theme' => ['filename' => 'folder-open', 'label' => 'Theme', 'size' => 20], |
| | | 'arttheme' => ['filename' => 'folder-open', 'label' => 'Art Theme', 'size' => 20], |
| | | 'style' => ['filename' => 'hash', 'label' => 'Style', 'size' => 20], |
| | | 'artstyle' => ['filename' => 'hash', 'label' => 'Art Style', 'size' => 20], |
| | | 'colour' => ['filename' => 'drop', 'label' => 'Colour', 'size' => 20], |
| | | 'placement' => ['filename' => 'person-arms-spread', 'label' => 'Placement', 'size' => 20], |
| | | 'artmedia' => ['filename' => 'squares-four', 'label' => 'Media', 'size' => 20], |
| | | 'artist' => ['filename' => 'user', 'label'=> 'Artist', 'size' => 24], |
| | | 'client' => ['filename' => 'user', 'label'=> 'Artist', 'size' => 24], |
| | | 'artists' => ['filename' => 'users-three', 'label' => 'Artists'], |
| | | 'partner' => ['filename' => 'currency-circle-dollar', 'label' => 'Partner'], |
| | | 'shop' => ['filename' => 'storefront', 'label' => 'Shop', 'size' => 20], |
| | | 'tattoo' => ['filename' => 'drop-simple', 'label' => 'Tattoo', 'size' => 20], |
| | | 'tattoos' => ['filename' => 'drop-simple', 'label' => 'Your Tattoos', 'size' => 20], |
| | | 'event' => ['filename' => 'calendar', 'label' => 'Event', 'size' => 20], |
| | | 'events' => ['filename' => 'calendar', 'label' => 'Your Events', 'size' => 20], |
| | | 'piercing' => ['filename' => 'needle', 'label' => 'Piercings', 'size' => 20], |
| | | 'piercings' => ['filename' => 'needle', 'label' => 'Your Piercings', 'size' => 20], |
| | | 'artwork' => ['filename' => 'palette', 'label' => 'Artwork', 'size' => 20], |
| | | 'artform' => ['filename' => 'shapes', 'label' => 'Art Form', 'size' => 20], |
| | | 'city' => ['filename' => 'map-pin', 'label' => 'City', 'size' => 20], |
| | | 'type' => ['filename' => 'users-three', 'label' => 'Artist Type', 'size' => 20], |
| | | 'pstyle' => ['filename' => 'nut', 'label' => 'Body Modifications', 'size' => 20], |
| | | 'past' => ['filename' => 'clock-counter-clockwise', 'label' => 'Past Events', 'size' => 20], |
| | | 'future' => ['filename' => 'clock-clockwise', 'label' => 'Upcoming Events', 'size' => 20], |
| | | 'repeat' => ['filename' => 'repeat', 'label' => 'Recurring Events', 'size' => 20], |
| | | 'project' => ['filename' => 'code', 'label' => 'Project'], |
| | | 'gauge' => ['filename' => 'gauge', 'label'=> 'Dashboard'], |
| | | 'map' => ['filename' => 'map-trifold', 'label' => 'Map'], |
| | | ], |
| | | 'users' => [ |
| | | 'new-user' => ['filename' => 'user-circle-plus', 'label' => 'New Artist'], |
| | | 'user' => ['filename' => 'user', 'label' => 'Artist', 'size' => 20], |
| | | 'bio' => ['filename' => 'user', 'label' => 'Your Bio', 'size' => 20], |
| | | 'login' => ['filename' => 'sign-in', 'label' => 'Login'], |
| | | 'logout' => ['filename' => 'sign-out', 'label' => 'Logout'] |
| | | ], |
| | | 'actions' => [ |
| | | 'save' => ['filename' => 'floppy-disk', 'label' => 'Save'], |
| | | 'restore'=> ['filename' => 'arrow-counter-clockwise', 'label' => 'Restore', 'size' => 32], |
| | | 'edit' => ['filename' => 'pencil-simple', 'label' => 'Edit', 'size' => 24], |
| | | 'delete'=> ['filename' => 'trash', 'label' => 'Delete', 'size' => 32], |
| | | 'search'=> ['filename' => 'magnifying-glass', 'label' => 'Search', 'size' => 32], |
| | | 'add' => ['filename' => 'plus-square', 'label' => 'Add New', 'size' => 32], |
| | | 'minus' => ['filename' => 'minus-square', 'label' => 'Collapse', 'size' => 32], |
| | | 'help' => ['filename' => 'question', 'label' => 'Toggle Quick Help'], |
| | | 'support' => ['filename' => 'question', 'label' => 'Toggle Quick Help'], |
| | | 'grab' => ['filename' => 'dots-six-vertical', 'label'=> 'Grab'], |
| | | 'share' => ['filename' => 'share', 'label' => 'Share', 'size'=> 24], |
| | | 'cart' => ['filename' => 'shopping-cart', 'label' => 'Your Cart', 'size'=> 24], |
| | | 'checkout' => ['filename' => 'receipt', 'label' => 'Checkout', 'size'=> 24], |
| | | 'upload' => ['filename' => 'cloud-arrow-up', 'label' => 'Upload to Server', 'size'=> 24], |
| | | 'download' => ['filename' => 'cloud-arrow-down', 'label' => 'Downloading', 'size'=> 24], |
| | | 'synced' => ['filename' => 'cloud-check', 'label' => 'Synced', 'size'=> 24], |
| | | 'syncing' => ['filename' => 'cloud-sync-thin.svg', 'label' => 'Synced', 'size'=> 24], |
| | | 'cloud' => ['filename' => 'cloud', 'label' => 'Synced', 'size'=> 24], |
| | | // 'pending' => ['filename' => 'cloud', 'label' => 'Synced', 'size'=> 24], |
| | | 'offline' => ['filename' => 'cloud-slash', 'label' => 'Offline', 'size'=> 24], |
| | | 'error' => ['filename' => 'cloud-warning', 'label' => 'Upload Failed', 'size'=> 24], |
| | | |
| | | ], |
| | | 'status' => [ |
| | | 'show' => ['filename' => 'eye', 'label' => 'Show', 'size' => 20], |
| | | 'publish' => ['filename' => 'eye', 'label' => 'Public', 'size' => 20], |
| | | 'hide' => ['filename' => 'eye-closed', 'label' => 'Hide', 'size' => 20], |
| | | 'draft' => ['filename' => 'eye-closed', 'label' => 'Hidden', 'size' => 20], |
| | | 'pinned' => ['filename' => 'push-pin-simple', 'label' => 'Pin', 'size' => 32], |
| | | 'bell' => ['filename' => 'bell', 'label' => 'Notification', 'size' => 32], |
| | | 'bell-ringing' => ['filename' => 'bell-ringing', 'label' => 'Notification', 'size' => 32] |
| | | ], |
| | | 'sorting' => [ |
| | | 'table' => ['filename' => 'table', 'label' => 'Table View', 'size' => 20], |
| | | 'columns' => ['filename' => 'columns', 'label' => 'Show Columns', 'size' => 20], |
| | | 'alphabetical' => ['filename' => 'alphabetical', 'label' => 'Alphabetical', 'size' => 20], |
| | | 'calendar' => ['filename' => 'calendar-heart', 'label' => 'Date', 'size' => 20], |
| | | 'asc' => ['filename' => 'sort-ascending', 'label' => 'Sort Ascending', 'size' => 20], |
| | | 'ASC' => ['filename' => 'sort-ascending', 'label' => 'Sort Ascending', 'size' => 20], |
| | | 'desc' => ['filename' => 'sort-descending', 'label' => 'Sort Descending', 'size' => 20], |
| | | 'DESC' => ['filename' => 'sort-descending', 'label' => 'Sort Descending', 'size' => 20], |
| | | 'filter' => ['filename' => 'faders', 'label' => 'Filter'], |
| | | 'all' => ['filename' => 'infinity', 'label' => 'All', 'size' => 32], |
| | | 'random' => ['filename' => 'shuffle', 'label' => 'Random', 'size' => 20] |
| | | ], |
| | | 'business' => [ |
| | | 'location' => ['filename' => 'map-pin', 'label' => 'Location'], |
| | | 'hours' => ['filename' => 'clock', 'label' => 'Hours'], |
| | | 'star' => ['filename' => 'star', 'label' => 'Empty Star'], |
| | | 'star-fill' => ['filename' => 'star-fill.svg', 'label' => 'Star'], |
| | | 'star-half' => ['filename' => 'star-half-fill.svg', 'label' => 'Half Star'], |
| | | ], |
| | | 'social' => [ |
| | | 'heart' => ['filename' => 'heart', 'label' => 'Favourite', 'size' => 24], |
| | | 'favourite' => ['filename' => 'heart', 'label' => 'Favourite', 'size' => 24], |
| | | 'favourites'=> ['filename' => 'heart', 'label' => 'Your Favourites', 'size' => 24], |
| | | 'heart-fill'=> ['filename' => 'heart-fill.svg', 'label' => 'Favourited', 'size' => 24], |
| | | 'share' => ['filename' => 'share', 'label' => 'Share', 'size' => 32], |
| | | 'email' => ['filename' => 'envelope','label'=> 'Email'], |
| | | 'text' => ['filename' => 'chat','label'=> 'Text'], |
| | | 'phone' => ['filename' => 'phone','label'=> 'Call'], |
| | | ], |
| | | 'external' => [ |
| | | 'integrations' => ['filename' => 'plugs-connected', 'label' => 'Integrations'], |
| | | 'connected' => ['filename' => 'plugs-connected', 'label' => 'Connected'], |
| | | 'disconnected' => ['filename' => 'plugs', 'label' => 'Disconnected'], |
| | | 'instagram' => ['filename' => 'instagram-logo', 'label' => 'Instagram'], |
| | | 'facebook' => ['filename' => 'facebook-logo', 'label' => 'Facebook'], |
| | | 'messenger' => ['filename' => 'messenger-logo', 'label' => 'Facebook Messenger'], |
| | | 'twitter' => ['filename' => 'twitter-logo', 'label' => 'Twitter'], |
| | | 'x' => ['filename' => 'x-logo', 'label' => 'X'], |
| | | 'youtube' => ['filename' => 'youtube-logo', 'label' => 'YouTube'], |
| | | 'mastadon' => ['filename' => 'mastadon-logo', 'label' => 'Mastadon'], |
| | | 'fediverse' => ['filename' => 'fediverse-logo', 'label' => 'Fediverse'], |
| | | 'linktree' => ['filename' => 'linktree-logo', 'label' => 'LinkTree'], |
| | | 'snapchat' => ['filename' => 'snapchat-logo', 'label' => 'Snapchat'], |
| | | 'twitch' => ['filename' => 'twitch-logo', 'label' => 'Twitch'], |
| | | 'threads' => ['filename' => 'threads-logo', 'label' => 'Threads'], |
| | | 'tiktok' => ['filename' => 'tiktok-logo', 'label' => 'TikTok'], |
| | | 'square-up' => ['filename' => 'square-logo', 'label' => 'Square'], |
| | | 'umami' => ['filename' => 'chart-line', 'label' => 'Umami'], |
| | | ] |
| | | ]; |
| | | private string $defaultStyle = '-thin'; |
| | | private int $defaultSize = 16; |
| | | protected string $name; |
| | | protected array $used; |
| | | protected int $size = 20; |
| | | protected CacheManager $cache; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache = new CacheManager('icons', 604800); //1 week in seconds |
| | | // $this->cache->invalidateGroup('icons'); |
| | | $this->style = JVB_SITE['icons']??'regular'; |
| | | |
| | | |
| | | $this->used = get_option(BASE.'used_icons', [ |
| | | $this->style => [ |
| | | 'heart', |
| | | 'hours', |
| | | 'random', |
| | | 'alphabetical', |
| | | 'calendar', |
| | | 'asc', |
| | | 'desc', |
| | | 'all', |
| | | 'paragraph', |
| | | 'h1', |
| | | 'h2', |
| | | 'h3', |
| | | 'bold', |
| | | 'italic', |
| | | 'underline', |
| | | 'strike', |
| | | 'list-bullets', |
| | | 'list-numbers', |
| | | 'image', |
| | | 'align-left', |
| | | 'align-right', |
| | | 'align-center', |
| | | 'link', |
| | | 'note', |
| | | 'password', |
| | | 'light', |
| | | 'left', |
| | | 'dark', |
| | | 'grid', |
| | | 'list', |
| | | 'save', |
| | | 'restore', |
| | | 'edit', |
| | | 'delete', |
| | | 'search', |
| | | 'add', |
| | | 'minus', |
| | | 'help', |
| | | 'support', |
| | | 'grab', |
| | | 'share', |
| | | 'cart', |
| | | 'checkout', |
| | | 'upload', |
| | | 'download', |
| | | 'synced', |
| | | 'syncing', |
| | | 'cloud', |
| | | 'offline', |
| | | 'error', |
| | | 'show', |
| | | 'publish', |
| | | 'hide', |
| | | 'draft', |
| | | 'pinned', |
| | | 'bell', |
| | | 'bell-ringing', |
| | | 'back', |
| | | 'home', |
| | | 'right', |
| | | 'elbow-right-down', |
| | | 'elbow-right-up', |
| | | 'elbow-left-down', |
| | | 'elbow-left-up', |
| | | 'menu', |
| | | 'submenu', |
| | | 'close', |
| | | 'close-square', |
| | | 'dashboard', |
| | | 'dash', |
| | | 'settings', |
| | | 'system', |
| | | 'options', |
| | | 'news', |
| | | 'metrics', |
| | | 'prev', |
| | | 'up', |
| | | 'down', |
| | | 'right', |
| | | 'copy', |
| | | 'next', |
| | | 'refresh', |
| | | 'pending', |
| | | 'clock', |
| | | 'copy', |
| | | 'list-heart', |
| | | 'karma', |
| | | 'response', |
| | | 'reply', |
| | | 'approval', |
| | | 'check', |
| | | 'hamburger', |
| | | 'location', |
| | | 'hours', |
| | | 'star', |
| | | 'star-half', |
| | | 'exclamation-mark' |
| | | ], |
| | | 'fill' => [ |
| | | 'heart', |
| | | 'arrow-fat-down', |
| | | 'arrow-fat-up', |
| | | 'star' |
| | | ] |
| | | ]); |
| | | } |
| | | |
| | | protected function updateUsed():void |
| | | { |
| | | $icons = []; |
| | | foreach ($this->used as $style => $items) { |
| | | $temp = array_unique($items); |
| | | sort($temp); |
| | | $icons[$style] = $temp; |
| | | } |
| | | update_option(BASE . 'used_icons', $icons); |
| | | } |
| | | |
| | | protected function checkMap(string $name): string |
| | | { |
| | | $result = apply_filters('jvbIconMap', (array_key_exists($name, $this->map)) ? $this->map[$name] : $name, $name); |
| | | return $result; |
| | | } |
| | | |
| | | protected function checkName(string $name) { |
| | | $name = $this->checkMap($name); |
| | | $path = (str_contains($name, '.svg')) ? '/assets/icons/' : '/assets/phosphor-icons/regular/'; |
| | | $filename = (str_contains($name, '.svg')) ? $name : $name.'.svg'; |
| | | return file_exists(JVB_DIR . $path . $filename); |
| | | } |
| | | public function getIcon(string $name, array $options = []):?string |
| | | { |
| | | if (!$this->checkName($name)) { |
| | | error_log('[Icons]Icon not found for: '.print_r($name, true)); |
| | | return ''; |
| | | } |
| | | $style = (array_key_exists('style', $options) && in_array($options['style'], $this->styles)) ? $options['style'] : 'regular'; |
| | | |
| | | $update = false; |
| | | if (!array_key_exists($style, $this->used)) { |
| | | $this->used[$style] = []; |
| | | $update = true; |
| | | } |
| | | if (!in_array($name, $this->used[$style])) { |
| | | $this->used[$style][] = $name; |
| | | $update = true; |
| | | } |
| | | if ($update) { |
| | | $this->updateUsed(); |
| | | } |
| | | |
| | | $name = $this->checkMap($name); |
| | | $this->name = str_replace('.svg', '', $name); |
| | | |
| | | |
| | | // Merge options with defaults and icon config |
| | | $options = array_merge([ |
| | | 'title' => $options['label']??$this->getIconLabel($name), |
| | | 'size' => $options['size'] ?? $this->size, |
| | | 'style' => $style, |
| | | 'class' => '', |
| | | 'wrap' => true, |
| | | 'color' => 'currentColor' |
| | | ], $options); |
| | | |
| | | return $this->cache->remember( |
| | | array_merge($options, ['name' => $name]), |
| | | function () use ($name, $options) { |
| | | return $this->buildIcon($name, $options); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | public function getIconsByGroup(string $group):array |
| | | { |
| | | if (!isset(self::ICON_GROUPS[$group])) { |
| | | return []; |
| | | } |
| | | |
| | | $icons = []; |
| | | foreach (self::ICON_GROUPS[$group] as $name => $config) { |
| | | $icons[$name] = $this->getIcon($name); |
| | | } |
| | | |
| | | return array_filter($icons); |
| | | } |
| | | |
| | | public function getAllIcons():array |
| | | { |
| | | $icons = []; |
| | | foreach (self::ICON_GROUPS as $group => $groupIcons) { |
| | | foreach ($groupIcons as $name => $config) { |
| | | $icons[$name] = $this->getIcon($name); |
| | | } |
| | | } |
| | | |
| | | return array_filter($icons); |
| | | } |
| | | |
| | | public function getGroups():array |
| | | { |
| | | return array_keys(self::ICON_GROUPS); |
| | | } |
| | | |
| | | private function findIconConfig(string $name):?array |
| | | { |
| | | foreach (self::ICON_GROUPS as $groupIcons) { |
| | | if (isset($groupIcons[$name])) { |
| | | return $groupIcons[$name]; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private function buildIcon(string $name, array $options):?string |
| | | { |
| | | |
| | | $filepath = $this->buildFilePath($name, $options['style']); |
| | | if (!file_exists($filepath)) { |
| | | error_log("Icon file not found: $filepath"); |
| | | return null; |
| | | } |
| | | |
| | | $svg = file_get_contents($filepath); |
| | | if ($svg === false) { |
| | | return null; |
| | | } |
| | | |
| | | return $this->formatSvg($svg, $options); |
| | | } |
| | | |
| | | private function buildFilePath(string $filename, string $style = ''):string |
| | | { |
| | | $svg = false; |
| | | if (str_contains($filename, '.svg')) { |
| | | $svg = true; |
| | | $nameExtra = ''; |
| | | $path = '/assets/icons/'; |
| | | } else { |
| | | $nameExtra = ($style === 'regular') ? '' : '-'.$style; |
| | | $path = '/assets/phosphor-icons/'; |
| | | } |
| | | |
| | | $filename = (str_contains($filename, '.svg')) ? $filename : $filename . $nameExtra . '.svg'; |
| | | $style = ($style === '') ? '' : $style .'/'; |
| | | $style = ($svg) ? '' : $style; |
| | | return JVB_DIR . $path . $style . $filename; |
| | | } |
| | | |
| | | private function formatSvg(string $svg, array $options): string |
| | | { |
| | | // Clean up SVG |
| | | $svg = preg_replace("/([\n\t]+)/", ' ', $svg); |
| | | $svg = preg_replace('/>\s*</', '><', $svg); |
| | | |
| | | // Add size attributes - FIXED: assign results back to $svg |
| | | if ($options['size'] !== 32) { |
| | | $svg = str_replace('width="32"', 'width="' . (int)$options['size'] . '"', $svg); |
| | | $svg = str_replace('height="32"', 'height="' . (int)$options['size'] . '"', $svg); |
| | | } |
| | | |
| | | // Add color if provided - FIXED: assign results back to $svg |
| | | if ($options['color'] !== 'currentColor') { |
| | | $svg = str_replace('currentColor', $options['color'], $svg); |
| | | } |
| | | |
| | | // Add title if provided |
| | | if (!empty($options['title'])) { |
| | | $svg = str_replace('<svg', '<svg title="' . esc_attr($options['title']) . '"', $svg); |
| | | $svg = str_replace('aria-hidden="true"', '', $svg); |
| | | } |
| | | |
| | | $classes = trim('icon row ' . $this->name . ' ' . $options['class']); |
| | | if (array_key_exists('wrap', $options) && $options['wrap'] === false) { |
| | | return $svg; |
| | | } |
| | | return sprintf( |
| | | '<i class="%s">%s</i>', |
| | | esc_attr($classes), |
| | | $svg |
| | | ); |
| | | } |
| | | |
| | | /**************************** |
| | | * Converts to CSS icon |
| | | ***************************/ |
| | | public function getCSSIcon(string $name, array $options = []): ?string |
| | | { |
| | | if (!$this->checkName($name)) { |
| | | return ''; |
| | | } |
| | | $name = $this->checkMap($name); |
| | | $style = (array_key_exists('style', $options) && in_array($options['style'], $this->styles)) ? $options['style'] : 'regular'; |
| | | |
| | | $this->name = str_replace('.svg', '', $name); |
| | | |
| | | // Merge options with defaults and icon config |
| | | $options = array_merge([ |
| | | 'title' => $options['label']??$this->getIconLabel($name), |
| | | 'size' => $options['size'] ?? $this->size, |
| | | 'style' => $style, |
| | | 'class' => '', |
| | | 'wrap' => true, |
| | | 'color' => 'currentColor' |
| | | ], $options); |
| | | |
| | | $svg = $this->cache->remember( |
| | | array_merge($options, ['name' => $name, 'css' => true]), |
| | | function() use ($name, $options) { |
| | | return $this->buildRawSvg($name, $options); |
| | | } |
| | | ); |
| | | // Convert to base64 data URI |
| | | $svg = ($svg) ? 'data:image/svg+xml;base64,' . base64_encode($svg) : null; |
| | | return $svg; |
| | | } |
| | | |
| | | private function buildRawSvg(string $filename, array $options): ?string |
| | | { |
| | | $filepath = $this->buildFilePath($filename, $options['style']); |
| | | if (!file_exists($filepath)) { |
| | | error_log("Icon file not found: $filepath"); |
| | | return null; |
| | | } |
| | | |
| | | $svg = file_get_contents($filepath); |
| | | if ($svg === false) { |
| | | return null; |
| | | } |
| | | |
| | | return $this->formatRawSvg($svg, $options); |
| | | } |
| | | |
| | | private function formatRawSvg(string $svg, array $options): string |
| | | { |
| | | // Clean up SVG |
| | | $svg = preg_replace("/([\n\t]+)/", ' ', $svg); |
| | | $svg = preg_replace('/>\s*</', '><', $svg); |
| | | |
| | | // Add size attributes - FIXED: assign results back to $svg |
| | | if ($options['size'] > 0) { |
| | | $svg = str_replace('width="32"', 'width="' . (int)$options['size'] . '"', $svg); |
| | | $svg = str_replace('height="32"', 'height="' . (int)$options['size'] . '"', $svg); |
| | | } |
| | | |
| | | // Add color if provided and not currentColor - FIXED: assign results back to $svg |
| | | if ($options['color'] !== 'currentColor') { |
| | | $svg = str_replace('currentColor', $options['color'], $svg); |
| | | } |
| | | |
| | | return $svg; |
| | | } |
| | | |
| | | public function localizeIcons():array |
| | | { |
| | | if (empty($this->used)) { |
| | | return []; |
| | | } |
| | | $used = []; |
| | | foreach ($this->used as $style => $icons) { |
| | | foreach ($icons as $icon) { |
| | | $used[$icon] = $this->getIcon($icon, ['style' => $style]); |
| | | } |
| | | } |
| | | return $used; |
| | | } |
| | | |
| | | public function getIconLabel(string $name): string |
| | | { |
| | | $name = str_replace('.svg', '', $name); |
| | | $label = (array_key_exists($name, $this->labels)) ? $this->labels[$name] : ''; |
| | | |
| | | $result = apply_filters('jvbIconLabel', $label, $name); |
| | | |
| | | return $result; |
| | | } |
| | | } |
| | |
| | | $this->cache = CacheManager::for('blocks', WEEK_IN_SECONDS); |
| | | $this->imgCache = CacheManager::for('images', WEEK_IN_SECONDS); |
| | | |
| | | add_action('render_block', [$this, 'render'], 10, 3); |
| | | |
| | | add_filter('render_block', [$this, 'render'], 999, 3); |
| | | |
| | | |
| | | add_action('init', [$this, 'registerBlockStyles']); |
| | | } |
| | | |
| | |
| | | |
| | | public function render(string $content, array $block, WP_Block $instance) |
| | | { |
| | | $method = 'render_'.$this->sanitizeBlockName($block); |
| | | $blockName = $this->sanitizeBlockName($block); |
| | | $method = 'render_'.$blockName; |
| | | $function = BASE.$method; |
| | | |
| | | if (function_exists($function)) { |
| | | // return $this->cache->remember( |
| | | // $block, |
| | |
| | | // } |
| | | // ); |
| | | return $function($block, $content); |
| | | } |
| | | if (method_exists($this, $method)) { |
| | | } else if (method_exists($this, $method)) { |
| | | return $this->$method($block, $content); |
| | | //TODO: Recache it |
| | | // return $this->cache->remember( |
| | |
| | | 'core/post-title', |
| | | 'core/list-item', |
| | | 'core/site-title', |
| | | 'jvb/forms' |
| | | ]; |
| | | if (!in_array($block['blockName'], $ignore)) { |
| | | jvbDump('No method found for '.print_r($block['blockName'], true)); |
| | | // jvbDump('No method found for '.print_r($block['blockName'], true)); |
| | | } |
| | | } |
| | | if ($block['blockName'] === 'jvb/feed') { |
| | |
| | | $attrs = $block['attrs'] ?? []; |
| | | $innerContent = $this->innerBlocks($block); |
| | | |
| | | $position = 'object-position: center;'; |
| | | if (array_key_exists('focalPoint', $attrs)) { |
| | | $x = (array_key_exists('x', $attrs['focalPoint'])) ? ($attrs['focalPoint']['x'] * 100).'%' : 'center'; |
| | | $y = (array_key_exists('y', $attrs['focalPoint'])) ? ($attrs['focalPoint']['y'] * 100).'%' : 'center'; |
| | |
| | | $backgroundType = $attrs['backgroundType'] ?? 'image'; |
| | | $background = ''; |
| | | |
| | | if ($backgroundType === 'image' && isset($attrs['id'])) { |
| | | $background .= str_replace('<img', '<img style="'.$position.'"', $this->image($attrs['id'])); |
| | | $ID = false; |
| | | if (array_key_exists('useFeaturedImage', $attrs)) { |
| | | global $post; |
| | | $ID = get_post_thumbnail_id($post->ID); |
| | | } else { |
| | | $ID = (int)$attrs['id']; |
| | | } |
| | | |
| | | if ($backgroundType === 'image' && $ID) { |
| | | $background .= str_replace('<img', '<img style="'.$position.'"', $this->image($ID)); |
| | | } elseif ($backgroundType === 'video' && isset($attrs['url'])) { |
| | | $background .= '<video style="'.$position.'"autoplay muted loop playsinline src="' . esc_url($attrs['url']) . '"></video>'; |
| | | } |
| | |
| | | aria-label="Open Menu" |
| | | aria-controls="navigation-' .$ID. '" |
| | | aria-expanded="false">'. |
| | | jvbIcon('menu', ['title'=>'Toggle Menu']). |
| | | jvbIcon('close', ['title'=>'Toggle Menu']). |
| | | jvbIcon('list', ['title'=>'Toggle Menu']). |
| | | jvbIcon('x', ['title'=>'Toggle Menu']). |
| | | '</button>'; |
| | | $class = ($toggle === '') ? |
| | | $this->getClassesAndStyles($block['attrs'], ['mobile']) : |
| | |
| | | $linkOpen = $this->build_navigation_link($block['attrs'], $aria); |
| | | $content = '<li'.$classes.'>'.$linkOpen.$block['attrs']['label']. |
| | | '</a><button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open '.$block['attrs']['label'].' Submenu" aria-expanded="false" aria-controls="'.$id.'-submenu">'. |
| | | jvbIcon('submenu', ['title'=>'Toggle Submenu']). |
| | | jvbIcon('caret-down', ['title'=>'Toggle Submenu']). |
| | | '</button><ul class="submenu" id='.$id.'-submenu">'; |
| | | |
| | | $content .= $this->innerBlocks($block); |
| | |
| | | //core_post_excerpt |
| | | protected function render_core_post_featured_image(array $block):string |
| | | { |
| | | global $post; |
| | | $ID = get_post_thumbnail_id($post->ID); |
| | | return '<figure'.$this->getClassesAndStyles($block['attrs']).'>'. |
| | | $this->imageLink(true). |
| | | $this->image($ID). |
| | | '</figure>'; |
| | | } |
| | | //core_post_navigation_link |
| | |
| | | |
| | | $check = ['header', 'footer']; |
| | | $isHeaderTemplate = ( |
| | | (array_key_exists('slug', $block['attrs']) && str_contains($block['attrs']['slug'], 'header')) || |
| | | (array_key_exists('tagName', $block['attrs']) && str_contains($block['attrs']['tagName'], 'header')) |
| | | (array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'header')) || |
| | | (array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'header')) |
| | | ) ? 'header' : false; |
| | | $isFooterTemplate = ( |
| | | (array_key_exists('slug', $block['attrs']) && str_contains($block['attrs']['slug'], 'footer')) || |
| | | (array_key_exists('tagName', $block['attrs']) && str_contains($block['attrs']['tagName'], 'footer')) |
| | | (array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'footer')) || |
| | | (array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'footer')) |
| | | ) ? 'footer' : false; |
| | | |
| | | |
| | |
| | | } |
| | | $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : ''; |
| | | $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode'; |
| | | $themeSwitch = '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher"> |
| | | $showThemeSwitch = (bool)apply_filters('jvb_show_theme_switch', true); |
| | | $themeSwitch = ($showThemeSwitch) ? '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher"> |
| | | <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'. |
| | | jvbIcon('light', ['title'=> 'Light Mode']). |
| | | jvbIcon('dark', ['title'=>'Dark Mode']). |
| | | '</span></label>'; |
| | | jvbIcon('sun-dim', ['title'=> 'Light Mode']). |
| | | jvbIcon('moon', ['title'=>'Dark Mode']). |
| | | '</span></label>' : ''; |
| | | $breadcrumbs = jvbBuildBreadcrumbs(); |
| | | $afterHeader = apply_filters('jvbBelowHeader', $afterHeader); |
| | | if ($afterHeader !== '') { |
| | |
| | | 'id' => get_current_user_id() |
| | | ] : null, |
| | | 'from' => get_the_ID(), |
| | | 'icons' => jvbFeedBlockIcons(), |
| | | |
| | | ]); |
| | | } |
| | | |
| | |
| | | |
| | | public function innerBlocks(array $block, string $before = '', string $after = ''):string |
| | | { |
| | | $content = ''; |
| | | foreach ($block['innerBlocks'] as $b) { |
| | | $method = 'render_'.$this->sanitizeBlockName($b); |
| | | $content .= $before; |
| | | if (method_exists($this, $method)) { |
| | | $content .= $this->$method($b, ''); |
| | | } else { |
| | | $content .= render_block($b); |
| | | } |
| | | $content .= $after; |
| | | } |
| | | return $content; |
| | | $content = ''; |
| | | foreach ($block['innerBlocks'] as $b) { |
| | | $method = 'render_'.$this->sanitizeBlockName($b); |
| | | $function = BASE.$method; |
| | | |
| | | $content .= $before; |
| | | if (function_exists($function)) { |
| | | $content .= $function($b, ''); |
| | | } else if (method_exists($this, $method)) { |
| | | $content .= $this->$method($b, ''); |
| | | } else { |
| | | $content .= render_block($b); |
| | | } |
| | | $content .= $after; |
| | | } |
| | | return $content; |
| | | } |
| | | |
| | | public function inside(array $block, mixed $tag = false, mixed $o = false):string |
| | |
| | | |
| | | public function imageID(int|string $ID, array $block = []):int|false |
| | | { |
| | | |
| | | if ($ID === '' && !empty($block)) { |
| | | |
| | | if (($block['blockName'] === 'core/post-featured-image' || |
| | | (!array_key_exists('attrs', $block) && !array_key_exists('id', $block['attrs'])))) { |
| | | $ID = get_post_thumbnail_id(); |
| | |
| | | if (!$ID) { |
| | | return ''; |
| | | } |
| | | $imgSlug = get_post($ID)->post_name; |
| | | $img = get_post($ID); |
| | | if (!$img) { |
| | | return ''; |
| | | } |
| | | $imgSlug = $img->post_name; |
| | | |
| | | $img = '<a class="open-gallery" target="_blank" rel="nofollow" data-opens="gallery-'.$postSlug.'" data-focus="'.$postSlug.'-'.$imgSlug.'">'.$image; |
| | | if ($close) { |
| | |
| | | } |
| | | protected function getClass(string $key, string|bool|array|int $value, array $attrs):string|array |
| | | { |
| | | |
| | | switch ($key) { |
| | | //Any additional classes the user adds |
| | | case 'className': |
| | |
| | | default => str_replace('is-style-', '', $value), |
| | | }; |
| | | case 'contentPosition': |
| | | |
| | | $classes = []; |
| | | $pos = explode(' ', $value); |
| | | foreach($pos as $p) { |
| | |
| | | // |
| | | // } |
| | | if (!array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) { |
| | | $classes[] = 'start'; |
| | | $classes[] = ($type === 'row') ? 'start' : 'a-start'; |
| | | } |
| | | if (array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) { |
| | | if (in_array($value['justifyContent'], ['left', 'right','space-between'])) { |
| | | // jvbDump($type); |
| | | switch ($value['justifyContent']) { |
| | | case 'right': |
| | | $classes[] = 'end'; |
| | | $classes[] = ($type === 'row') ? 'end' : 'a-end'; |
| | | break; |
| | | case 'space-between': |
| | | $classes[] = 'btw'; |
| | |
| | | return ''; |
| | | default: |
| | | $ignore = [ |
| | | 'useFeaturedImage', |
| | | 'opacity', |
| | | 'borderColor', |
| | | 'backgroundColor', |
| | |
| | | ]; |
| | | if (!is_admin() &&!in_array($key, $ignore)) { |
| | | // TESTING |
| | | jvbDump($key, 'getClass'); |
| | | jvbDump($attrs); |
| | | // jvbDump($key, 'getClass'); |
| | | // jvbDump($attrs); |
| | | } |
| | | |
| | | return ''; |
| | |
| | | // Any other attributes that need direct styling |
| | | default: |
| | | $ignore = [ |
| | | 'useFeaturedImage', |
| | | 'opacity', |
| | | 'textAlign', |
| | | 'minHeightUnit', |
| | |
| | | ]; |
| | | if (!is_admin() && !in_array($key, $ignore)) { |
| | | //TESTING |
| | | jvbDump($key, 'getStyle'); |
| | | jvbDump($attrs); |
| | | // jvbDump($key, 'getStyle'); |
| | | // jvbDump($attrs); |
| | | } |
| | | // No default inline styles |
| | | break; |
| | |
| | | <div class="filter-actions row"> |
| | | <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY') ?> |
| | | <button type="button" class="clear-filters row"> |
| | | <?= jvbIcon('close', ['title' => 'Clear']) ?> |
| | | <?= jvbIcon('x', ['title' => 'Clear']) ?> |
| | | Clear All Filters |
| | | </button> |
| | | </div> |
| | |
| | | |
| | | <input type="radio" id="order-random" class="btn" name="orderby" value="random"> |
| | | <label for="order-random" title="Random Order" class="row"> |
| | | <?= jvbIcon('random') ?> |
| | | <?= jvbIcon('shuffle') ?> |
| | | <span class="label">Random</span> |
| | | </label> |
| | | </div> |
| | |
| | | <span class="label">ORDER:</span> |
| | | <input type="radio" id="order-desc" class="btn" name="order" value="desc" checked> |
| | | <label for="order-desc" title="Newest First" class="row"> |
| | | <?= jvbIcon('desc') ?> |
| | | <?= jvbIcon('sort-descending') ?> |
| | | </label> |
| | | |
| | | <input type="radio" id="order-asc" class="btn" name="order" value="asc"> |
| | | <label for="order-asc" title="Oldest First" class="row"> |
| | | <?= jvbIcon('asc') ?> |
| | | <?= jvbIcon('sort-ascending') ?> |
| | | </label> |
| | | </div> |
| | | </div> |
| | |
| | | { |
| | | ?> |
| | | <button type="button" class="load-more"> |
| | | <?= jvbIcon('elbow-left-down', ['title' =>'More']) ?> |
| | | <?= jvbIcon('arrow-elbow-left-down', ['title' =>'More']) ?> |
| | | Show Me More |
| | | <?= jvbIcon('elbow-right-down', ['title'=> 'More']) ?> |
| | | <?= jvbIcon('arrow-elbow-right-down', ['title'=> 'More']) ?> |
| | | </button> |
| | | |
| | | <?= jvbLoadingScreen() ?> |
| | |
| | | namespace JVBase\forms; |
| | | |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\JVBIcons; |
| | | use JVBase\managers\CacheManager; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Term; |
| | |
| | | </template> |
| | | <template class="termChildrenToggle"> |
| | | <button type="button" class="toggle-children" aria-expanded="false"> |
| | | <?=jvbIcon('add')?> |
| | | <?=jvbIcon('plus-square')?> |
| | | </button> |
| | | </template> |
| | | <template class="selectedTerm"> |
| | | <div class="selected-item row"> |
| | | <span class="item-name"></span> |
| | | <button type="button" class="remove-item row"><?=jvbIcon('close')?></button> |
| | | <button type="button" class="remove-item row"><?=jvbIcon('x')?></button> |
| | | </div> |
| | | </template> |
| | | <template class="termBreadcrumb"> |
| | |
| | | <?= $hasAutocomplete ?> |
| | | title="Open <?= $this->singular ?> Selector" |
| | | aria-label="Select <?= esc_attr($this->plural) ?>"> |
| | | <?= jvbIcon('add', ['title' => 'Add ' . $this->title]) ?> |
| | | <?= jvbIcon('plus-square', ['title' => 'Add ' . $this->title]) ?> |
| | | </button> |
| | | <input type="text" id="<?= $this->base ?><?= esc_attr($this->config['name']) ?>-autocomplete" autocomplete="off" data-ignore data-autocomplete> |
| | | <ul class="autocomplete-dropdown" hidden> |
| | |
| | | <button type="button" |
| | | class="remove-item row" |
| | | aria-label="Remove <?= esc_attr($term->name) ?>"> |
| | | <?= jvbIcon('close') ?> |
| | | <?= jvbIcon('x') ?> |
| | | </button> |
| | | </div> |
| | | <?php |
| | |
| | | <?php |
| | | namespace JVBase\forms; |
| | | |
| | | use JVBase\JVBIcons; |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\managers\IconsManager; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Term; |
| | |
| | | protected string $id; |
| | | protected string $name; |
| | | protected string $taxonomyName; |
| | | protected JVBIcons $icon; |
| | | protected IconsManager $icon; |
| | | protected string $plural; |
| | | |
| | | protected string $taxonomy; |
| | |
| | | $this->id = sanitize_key($id); |
| | | $this->taxonomy = jvbCheckBase($taxonomy); |
| | | $this->name = str_replace(BASE, '', $taxonomy); |
| | | $this->icon = new JVBIcons(); |
| | | $this->icon = IconsManager::getInstance(); |
| | | $this->cache = CacheManager::for(jvbNoBase($taxonomy), WEEK_IN_SECONDS); |
| | | |
| | | $this->base = $config['base'] ?? ''; |
| | |
| | | require(JVB_DIR . '/inc/helpers/email.php'); |
| | | require(JVB_DIR . '/inc/helpers/forms.php'); |
| | | require(JVB_DIR . '/inc/helpers/formatting.php'); |
| | | require(JVB_DIR . '/inc/helpers/icons.php'); |
| | | //require(JVB_DIR . '/inc/helpers/icons.php'); |
| | | require(JVB_DIR . '/inc/helpers/inlineStyles.php'); |
| | | require(JVB_DIR . '/inc/helpers/legacy.php'); |
| | | require(JVB_DIR . '/inc/helpers/media.php'); |
| | |
| | | $crumbs = []; |
| | | $crumbs[] = [ |
| | | 'name' => 'Home', |
| | | 'icon' => jvbIcon('home'), |
| | | 'icon' => jvbIcon('house'), |
| | | 'url' => get_home_url(), |
| | | ]; |
| | | |
| | |
| | | </button> |
| | | </div> |
| | | </div>', |
| | | jvbIcon('close', ['title'=>'Cancel']) |
| | | jvbIcon('x', ['title'=>'Cancel']) |
| | | ); |
| | | } |
| | | $out .= '</div>'; |
| | |
| | | return []; |
| | | } |
| | | $image = [ |
| | | 'tiny' => wp_get_attachment_image_src($imgID, 'tiny')[0], |
| | | 'small' => wp_get_attachment_image_src($imgID, 'medium')[0], |
| | | 'medium' => wp_get_attachment_image_src($imgID, 'large')[0], |
| | | 'large' => wp_get_attachment_image_src($imgID, 'full')[0], |
| | | 'alt' => get_post_meta($imgID, '_wp_attachment_image_alt', true), |
| | | 'title' => get_the_title($imgID), |
| | | 'caption' => get_the_excerpt($imgID), |
| | | 'tiny' => wp_get_attachment_image_src($imgID, 'tiny')[0], |
| | | 'small' => wp_get_attachment_image_src($imgID, 'medium')[0], |
| | | 'medium' => wp_get_attachment_image_src($imgID, 'large')[0], |
| | | 'large' => wp_get_attachment_image_src($imgID, 'full')[0], |
| | | 'image-alt-text'=> get_post_meta($imgID, '_wp_attachment_image_alt', true), |
| | | 'image-title' => get_the_title($imgID), |
| | | 'image-caption' => get_the_excerpt($imgID), |
| | | ]; |
| | | $cache->set($imgID, $image); |
| | | return $image; |
| | |
| | | $meta->render('form', $field, $config); |
| | | } |
| | | ?> |
| | | <?= (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('save').'Save</button>' : '' ?> |
| | | <?= (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('floppy-disk').'Save</button>' : '' ?> |
| | | </form> |
| | | <?php |
| | | $out = ob_get_clean(); |
| | |
| | | |
| | | function jvbRandomFooterText():string |
| | | { |
| | | return apply_filters('jvbRandomFooterText', '<p>©'.date('Y').' '.get_bloginfo('name').'</p><p>Built with ♡ by <a href="https://jakevan.ca">Jake Van</a>'); |
| | | $privacy = get_privacy_policy_url(); |
| | | $privacy = ($privacy === '') ? '' : ' | <a href="'.$privacy.'">Privacy Policy</a>'; |
| | | return apply_filters('jvbRandomFooterText', '<p class="font-small">©'.date('Y').' <a href="'.get_home_url().'">'.get_bloginfo('name').'</a>'.$privacy.'</p><p class="font-small">Built with ♡ by <a href="https://jakevan.ca">Jake Van</a></p>'); |
| | | // |
| | | // $aOpen = '<a href="https://legacytattooremoval.ca" title="Learn more about Legacy Tattoo Removal">'; |
| | | // $options = array( |
| | |
| | | ?> |
| | | <dialog class="gallery" aria-modal="true" aria-label="Image Gallery"> |
| | | <div class="wrap"> |
| | | <button type="button" class="cancel" aria-label="Close Gallery"> <?= jvbIcon('close') ?></button> |
| | | <button type="button" class="cancel" aria-label="Close Gallery"> <?= jvbIcon('x') ?></button> |
| | | <button class="nav prev row" aria-label="Previous image"> |
| | | <?= jvbIcon('prev') ?> |
| | | <?= jvbIcon('caret-left') ?> |
| | | </button> |
| | | |
| | | <button class="nav next row" aria-label="Next image"> |
| | | <?= jvbIcon('next') ?> |
| | | <?= jvbIcon('caret-right') ?> |
| | | </button> |
| | | |
| | | <div class="content row"> |
| | |
| | | return 'admin'; |
| | | } |
| | | } |
| | | $user = ($ID === 0) ? wp_get_current_user() : get_userdata($ID); |
| | | |
| | | if (user_can($user, 'manage_options')) { |
| | | if ($ID > 0 && user_can($ID, 'manage_options')) { |
| | | return 'admin'; |
| | | } |
| | | $user = ($ID === 0) ? wp_get_current_user() : get_userdata($ID); |
| | | error_log('Current User: '.print_r($user, true)); |
| | | return array_values(array_intersect( |
| | | array_keys(JVB_USER), |
| | | array_keys(array_merge(JVB_USER, ['administrator'])), |
| | | array_map(function ($role) { |
| | | return jvbNoBase($role); |
| | | }, |
| | |
| | | $url = $link['url']; |
| | | switch (true) { |
| | | case strpos($url, 'facebook'): |
| | | $i = jvbIcon('facebook'); |
| | | $i = jvbIcon('facebook-logo'); |
| | | break; |
| | | case strpos($url, 'instagram'): |
| | | $i = jvbIcon('instagram'); |
| | | $i = jvbIcon('instagram-logo'); |
| | | break; |
| | | case strpos($url, 'tiktok'): |
| | | $i = jvbIcon('tiktok'); |
| | | $i = jvbIcon('tiktok-logo'); |
| | | break; |
| | | case strpos($url, 'x.com'): |
| | | $i = jvbIcon('x'); |
| | | break; |
| | | case strpos($url, 'youtube'): |
| | | $i = jvbIcon('youtube'); |
| | | $i = jvbIcon('youtube-logo'); |
| | | break; |
| | | case strpos($url, 'mastadon'): |
| | | $i = jvbIcon('mastadon'); |
| | | $i = jvbIcon('mastadon-logo'); |
| | | break; |
| | | case strpos($url, 'fediverse'): |
| | | case strpos($url, 'bluesky'): |
| | | $i = jvbIcon('fediverse'); |
| | | $i = jvbIcon('fediverse-logo'); |
| | | break; |
| | | case strpos($url, 'linktree'): |
| | | $i = jvbIcon('linktree'); |
| | | $i = jvbIcon('linktree-logo'); |
| | | break; |
| | | case strpos($url, 'snapchat'): |
| | | $i = jvbIcon('snapchat'); |
| | | $i = jvbIcon('snapchat-logo'); |
| | | break; |
| | | case strpos($url, 'twitch'): |
| | | $i = jvbIcon('twitch'); |
| | | $i = jvbIcon('twitch-logo'); |
| | | break; |
| | | case strpos($url, 'threads'): |
| | | $i = jvbIcon('threads'); |
| | | $i = jvbIcon('threads-logo'); |
| | | break; |
| | | case strpos($url, 'twitter'): |
| | | $i = jvbIcon('twitter'); |
| | | $i = jvbIcon('twitter-logo'); |
| | | break; |
| | | default: |
| | | $i = jvbIcon('link'); |
| | |
| | | switch ($p) { |
| | | case 'text': |
| | | $link = 'sms:+1'.$phone.';?body='.rawurlencode('Hey! I found you on edmonton.ink, and I wanted to reach out.'); |
| | | $label = jvbIcon('text').'<span>Text</span>'; |
| | | $label = jvbIcon('chat').'<span>Text</span>'; |
| | | break; |
| | | case 'call': |
| | | $link = 'tel:+1'.$phone; |
| | |
| | | case 'email': |
| | | $link = 'mailto:'.$meta->getValue('email').'?subject='.rawurlencode('Contact from edmonton.ink').'&body='.rawurlencode('Hey, |
| | | I found you on edmonton.ink, and I wanted to reach out!'); |
| | | $label = jvbIcon('email').'<span>Email</span>'; |
| | | $label = jvbIcon('envelope').'<span>Email</span>'; |
| | | break; |
| | | } |
| | | if ($link) { |
| | |
| | | </div> |
| | | <div class="group-actions"> |
| | | <button type="button" data-action="add-to-group" title="Add selected uploads to this group"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | Add Here |
| | | </button> |
| | | <button type="button" data-action="delete-group" title="Delete group"> |
| | | <?= jvbIcon('delete') ?> |
| | | <?= jvbIcon('trash') ?> |
| | | Delete Group |
| | | </button> |
| | | </div> |
| | |
| | | </div> |
| | | |
| | | <button type="button" data-action="remove-from-group" title="Remove from Group"> |
| | | <?=jvbIcon('delete')?> |
| | | <?=jvbIcon('trash')?> |
| | | </button> |
| | | </div> |
| | | </template> |
| | |
| | | </div> |
| | | <div class="selection-controls"> |
| | | <button type="button" data-action="add-to-group"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | New Group |
| | | </button> |
| | | </div> |
| | |
| | | </div> |
| | | <div class="m-actions row nowrap"> |
| | | <button type="button" data-action="restore"> |
| | | <?= jvbIcon('restore') ?> |
| | | <?= jvbIcon('arrow-counter-clockwise') ?> |
| | | Restore Selected |
| | | </button> |
| | | <button type="button" data-action="clear-cache" title="Clear cache and close window"> |
| | | <?= jvbIcon('close') ?> |
| | | <?= jvbIcon('x') ?> |
| | | Clear Cache |
| | | </button> |
| | | </div> |
| | |
| | | add_action('save_post', 'jvbClearListCache', 10, 2); |
| | | function jvbClearListCache(int $post_id, \WP_Post $post):void |
| | | { |
| | | // SAFETY: Skip attachments and other non-content post types |
| | | if (in_array($post->post_type, jvbIgnoredPostTypes())) { |
| | | return; |
| | | } |
| | | if (jvbNoSaveIt($post_id, $post)) { |
| | | return; |
| | | } |
| | |
| | | <div class="refresh row btw"> |
| | | <span class="countdown row" title="Will refresh again...">5</span> |
| | | <button class="refreshNow row" title="Check now"> |
| | | <?= jvbIcon('refresh', ['title'=> 'Check now']) ?> |
| | | <?= jvbIcon('arrows-clockwise', ['title'=> 'Check now']) ?> |
| | | </button> |
| | | </div> |
| | | <div class="popup row"><span></span></div> |
| | |
| | | </div> |
| | | </aside> |
| | | <button class="qtoggle row" title="Show Queue" aria-controls="queue" hidden> |
| | | <?= jvbIcon('save') ?> |
| | | <?= jvbIcon('floppy-disk') ?> |
| | | <span class="screen-reader-text"></span> |
| | | <span class="indicator"></span> |
| | | <span class="count row"></span> |
| | |
| | | ?> |
| | | <li> |
| | | <a href="<?=get_home_url(null, '/dash/')?>" title="Behind the Scenes"> |
| | | <?= jvbIcon('dashboard', ['title' => 'Behind the Scenes'])?> |
| | | <?= jvbIcon('door', ['title' => 'Behind the Scenes'])?> |
| | | <span class="screen-reader-text">Go Behind the Scenes</span> |
| | | </a> |
| | | </li> |
| | |
| | | } |
| | | |
| | | $out = '<li class="has-submenu"> |
| | | <button class="toggle quick-help" type="button" title="Toggle Quick Help Menu" aria-expanded="false" aria-controls="quick-help" aria-label="Toggle Quick Help Menu">'.jvbIcon('help', ['title'=> 'Quick Help']).'</button> |
| | | <button class="toggle quick-help" type="button" title="Toggle Quick Help Menu" aria-expanded="false" aria-controls="quick-help" aria-label="Toggle Quick Help Menu">'.jvbIcon('question', ['title'=> 'Quick Help']).'</button> |
| | | <ul class="submenu"> |
| | | '.$links.' |
| | | </ul> |
| | |
| | | } |
| | | |
| | | if (is_user_logged_in()) { |
| | | $out .= '<li><a href="'.wp_logout_url(get_home_url()).'" title="Logout">'.jvbIcon('logout').'<span class="screen-reader-text">Logout</span></a></li>'; |
| | | $out .= '<li><a href="'.wp_logout_url(get_home_url()).'" title="Logout">'.jvbIcon('sign-out').'<span class="screen-reader-text">Logout</span></a></li>'; |
| | | } else { |
| | | $out .= '<li><a href="'.wp_login_url(null, true).'" title="Login">'.jvbIcon('login').'<span class="screen-reader-text">Login</span></a></li>'; |
| | | $out .= '<li><a href="'.wp_login_url(null, true).'" title="Login">'.jvbIcon('sign-in').'<span class="screen-reader-text">Login</span></a></li>'; |
| | | } |
| | | return $out; |
| | | } |
| | |
| | | aria-label="Clear search" |
| | | onclick="this.previousElementSibling.value = \'\'; this.previousElementSibling.focus();" |
| | | >'.jvbIcon('x', ['title'=> 'Clear Search']).'</button> |
| | | <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">'.jvbIcon('search').'</button> |
| | | <button type="button" title="Search" class="toggle search" aria-label="Toggles search input visually" onclick="this.parentNode.classList.toggle(\'open\');this.previousElementSibling.previousElementSibling.focus();">'.jvbIcon('magnifying-glass').'</button> |
| | | </div>'; |
| | | } |
| | | |
| | |
| | | function jvbModalActions() |
| | | { |
| | | return '<div class="m-actions row"> |
| | | <button type="button" class="cancel">'.jvbIcon('close').'<span class="screen-reader-text">Cancel</span></button> |
| | | <button type="submit" class="save">'.jvbIcon('save').'<span class="screen-reader-text">Save</span></button> |
| | | <button type="button" class="cancel">'.jvbIcon('x').'<span class="screen-reader-text">Cancel</span></button> |
| | | <button type="submit" class="save">'.jvbIcon('floppy-disk').'<span class="screen-reader-text">Save</span></button> |
| | | </div>'; |
| | | } |
| | | |
| | |
| | | <div class="toast-content row btw"> |
| | | <p></p> |
| | | <button type="button" class="close-toast" aria-label="Close"> |
| | | <?= jvbIcon('close') ?> |
| | | <?= jvbIcon('x') ?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | |
| | | <nav id="<?=$id?>" class="on-this-page index"> |
| | | <label>Jump to: |
| | | <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | </button> |
| | | </label> |
| | | <ul> |
| | | <li> |
| | | <a href="#top" title="Back to Top"> |
| | | <?= jvbIcon('up') ?> |
| | | <?= jvbIcon('caret-circle-up') ?> |
| | | </a> |
| | | </li> |
| | | <?php |
| | |
| | | <div class="col"> |
| | | <div class="spinner"></div> |
| | | <div class="status col"> |
| | | <div class="icon">'.apply_filters('jvbLoadingIcon', jvbIcon('tattoo')).'</div> |
| | | <div class="icon">'.apply_filters('jvbLoadingIcon', jvbIcon('drop-simple')).'</div> |
| | | <h3>Loading</h3> |
| | | <p class="typeText">Please wait...</p> |
| | | </div> |
| | |
| | | <span>Start Over</span> |
| | | </button> |
| | | <button type="button" data-action="dismiss-restore"> |
| | | '.jvbIcon('close').' |
| | | '.jvbIcon('x').' |
| | | <span>Dismiss</span> |
| | | </button> |
| | | </div> |
| | |
| | | /** |
| | | * Helper: Get user's Facebook pages |
| | | */ |
| | | private function getUserPages(string $access_token = null): array |
| | | private function getUserPages(?string $access_token = null): array |
| | | { |
| | | $token = $access_token ?: $this->credentials['access_token']; |
| | | |
| | |
| | | |
| | | class GoogleMyBusiness extends Integrations |
| | | { |
| | | private string $access_token; |
| | | private ?string $access_token = null; |
| | | protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours'; |
| | | private ?string $location = null; |
| | | private ?string $refresh_token = null; |
| | | private ?string $client_id = null; |
| | | private ?string $client_secret = null; |
| | | private ?string $account_id = null; |
| | | |
| | | public function __construct(?int $userID = null) |
| | |
| | | if (empty($this->credentials)) { |
| | | $this->loadCredentials(); |
| | | } |
| | | $this->access_token = $this->credentials['access_token'] ?? ''; |
| | | $this->refresh_token = $this->credentials['refresh_token'] ?? ''; |
| | | $this->client_id = $this->credentials['client_id'] ?? ''; |
| | | $this->client_secret = $this->credentials['client_secret'] ?? ''; |
| | | $this->location = $this->credentials['location'] ?? null; |
| | | $this->account_id = $this->credentials['account'] ?? null; |
| | | $this->access_token = (array_key_exists('access_token', $this->credentials)) ? $this->credentials['access_token'] : null; |
| | | $this->refresh_token = (array_key_exists('refresh_token', $this->credentials)) ? $this->credentials['refresh_token'] : null; |
| | | $this->client_id = (array_key_exists('client_id', $this->credentials)) ? $this->credentials['client_id'] : null; |
| | | $this->client_secret = (array_key_exists('client_secret', $this->credentials)) ? $this->credentials['client_secret'] : null; |
| | | $this->location = (array_key_exists('location', $this->credentials)) ? $this->credentials['location'] : null; |
| | | $this->account_id = (array_key_exists('account', $this->credentials)) ? $this->credentials['account'] : null; |
| | | |
| | | if ($this->account_id) { |
| | | $this->apiEndpoints[] = "/v1/{$this->account_id}/locations"; |
| | |
| | | } |
| | | ?> |
| | | <button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false" hidden> |
| | | <?= jvbIcon('cart')?><span class="abs"></span><span class="abs count"></span> |
| | | <?= jvbIcon('shopping-cart')?><span class="abs"></span><span class="abs count"></span> |
| | | </button> |
| | | <aside id="cart"> |
| | | <form id="checkout" data-form-id="checkout" data-save="checkout"> |
| | |
| | | <label for="quantity"></label> |
| | | <div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="" data-id=""> |
| | | <button type="button" class="decrease" aria-label="Decrease quantity"> |
| | | <?= jvbIcon('minus') ?> |
| | | <?= jvbIcon('minus-square') ?> |
| | | </button> |
| | | <input type="number" name="quantity" value="1" min="0" max="50"> |
| | | <button type="button" class="increase" aria-label="Increase quantity"> |
| | |
| | | switch ($action) { |
| | | case 'save_credentials': |
| | | $title = $label; |
| | | $label = jvbIcon('save'); |
| | | $label = jvbIcon('floppy-disk'); |
| | | break; |
| | | case 'clear_credentials': |
| | | $title = $label; |
| | |
| | | */ |
| | | class PostMark extends Integrations |
| | | { |
| | | protected ?string $server_token = null; |
| | | protected string $message_stream = 'outbound'; |
| | | protected string $from_email; |
| | | protected string $from_name; |
| | | protected bool $track_open; |
| | | protected bool $track_links; |
| | | /** |
| | | * Constructor |
| | | */ |
| | |
| | | { |
| | | $this->service_name = 'postmark'; |
| | | $this->title = 'PostMark'; |
| | | $this->description = 'Professional transactional email delivery service'; |
| | | $this->icon = 'envelope-simple'; |
| | | |
| | | $this->cacheName = ($userID)? 'gmb_'.$userID : 'postmark'; |
| | | |
| | | // PostMark API configuration |
| | | $this->apiBase = 'https://api.postmarkapp.com'; |
| | | $this->apiVersion = 'v1'; |
| | |
| | | ]; |
| | | |
| | | // Required credentials |
| | | $this->requiredCredentials = [ |
| | | $this->fields = [ |
| | | 'server_token' => [ |
| | | 'label' => 'Server API Token', |
| | | 'type' => 'password', |
| | | 'type' => 'text', |
| | | 'subtype'=> 'password', |
| | | 'placeholder' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', |
| | | 'description' => 'Your PostMark Server API Token' |
| | | 'hint' => 'Your PostMark Server API Token' |
| | | ], |
| | | 'message_stream' => [ |
| | | 'label' => 'Message Stream ID', |
| | |
| | | parent::__construct($userID); |
| | | |
| | | // Hook into WordPress mail system if integration is healthy |
| | | if ($this->is_healthy && $this->hasValidCredentials()) { |
| | | if ($this->is_healthy && $this->isSetUp()) { |
| | | $this->initializeMailHooks(); |
| | | } |
| | | } |
| | |
| | | return true; |
| | | |
| | | } catch (\Exception $e) { |
| | | $this->handleException($e, 'email'); |
| | | $this->logError('[POSTMARK]', ['method' => 'sendEmail', 'error' => $e->getMessage()]); |
| | | return new WP_Error( |
| | | 'postmark_send_failed', |
| | | $e->getMessage() |
| | |
| | | /** |
| | | * Override makeRequest to add PostMark headers |
| | | */ |
| | | protected function makeRequest( |
| | | string $method, |
| | | string $endpoint, |
| | | array $params = [], |
| | | array $headers = [] |
| | | ): mixed { |
| | | protected function getRequestHeaders(): array { |
| | | $credentials = $this->getCredentials(); |
| | | |
| | | // Add PostMark specific headers |
| | | $headers = array_merge([ |
| | | return [ |
| | | 'Accept' => 'application/json', |
| | | 'Content-Type' => 'application/json', |
| | | 'X-Postmark-Server-Token' => $credentials['server_token'] ?? '' |
| | | ], $headers); |
| | | |
| | | return parent::makeRequest($method, $endpoint, $params, $headers); |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | |
| | | return $stats; |
| | | } |
| | | } catch (\Exception $e) { |
| | | $this->handleException($e, 'stats/outbound'); |
| | | $this->logError('stats/outbound', ['error' => $e->getMessage()]); |
| | | } |
| | | |
| | | return []; |
| | |
| | | 'offset' => $offset |
| | | ]); |
| | | } catch (\Exception $e) { |
| | | $this->handleException($e, 'bounces'); |
| | | $this->logError('[POSTMARK]', ['method' => 'getBounces', 'error' => $e->getMessage()]); |
| | | return []; |
| | | } |
| | | } |
| | |
| | | |
| | | protected function initialize(): void |
| | | { |
| | | // TODO: Implement initialize() method. |
| | | } |
| | | |
| | | protected function getRequestHeaders(): array |
| | | { |
| | | // TODO: Implement getRequestHeaders() method. |
| | | if (empty($this->credentials)) { |
| | | $this->loadCredentials(); |
| | | } |
| | | $this->server_token = (array_key_exists('server_token', $this->credentials)) ? $this->credentials['server_token'] : null; |
| | | $this->from_email = (array_key_exists('from_email', $this->credentials)) ? $this->credentials['from_email'] : get_bloginfo('admin_email'); |
| | | $this->from_name = (array_key_exists('from_name', $this->credentials)) ? $this->credentials['from_name'] : get_bloginfo('name'); |
| | | $this->message_stream = (array_key_exists('message_stream', $this->credentials)) ? $this->credentials['message_stream'] : 'outbound'; |
| | | $this->track_open = (array_key_exists('track_open', $this->credentials)) ? $this->credentials['track_open'] : false; |
| | | $this->track_links = (array_key_exists('track_links', $this->credentials)) ? $this->credentials['track_links'] : false; |
| | | } |
| | | } |
| | |
| | | <label for="quantity"></label> |
| | | <div class="quantity field" data-min="0" data-max="50" data-step="1" data-price="17" data-id=""> |
| | | |
| | | <button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus').'</button> |
| | | <button type="button" class="decrease"aria-label="Decrease Add to Order">'.jvbIcon('minus-square').'</button> |
| | | |
| | | <input type="number" id="quantity" name="quantity" value="0" min="0" max="50" step="1" class="quantity-input"> |
| | | |
| | | <button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('add').'</button> |
| | | <button type="button" class="increase" aria-label="Increase Add to Order">'.jvbIcon('plus-square').'</button> |
| | | </div> |
| | | </td> |
| | | <td class="price"> |
| | |
| | | |
| | | $actions[] = [ |
| | | 'button' => '<button type="button" class="toggle-cart row" title="Your Cart" data-action="toggle-cart" aria-label="Open Cart" aria-controls="checkout" aria-expanded="false"> |
| | | '.jvbIcon('cart').'<span class="abs"></span><span class="abs count"></span> |
| | | '.jvbIcon('shopping-cart').'<span class="abs"></span><span class="abs count"></span> |
| | | </button>', |
| | | 'content' => $form |
| | | ]; |
| | |
| | | <a data-action="<?=$action['slug']?>" class="jvb-action"> |
| | | <?= jvbIcon($action['icon']); ?> |
| | | <span class="jvb-link-title"><?= esc_html($action['label'])?></span> |
| | | <span class="loader"><?=jvbIcon('refresh')?><?=jvbIcon('check')?></span> |
| | | <span class="loader"><?=jvbIcon('arrows-clockwise')?><?=jvbIcon('check')?></span> |
| | | </a> |
| | | <?php |
| | | } |
| | |
| | | protected array $sections; |
| | | protected array $stuck; |
| | | |
| | | |
| | | //For Timeline-specific posts |
| | | protected bool $isTimeline = false; |
| | | protected array $nonTimelineFields = []; |
| | | protected array $timelineSharedFields = []; |
| | | protected array $timelineUniqueFields = []; |
| | | |
| | | protected bool $userCanPublish = false; |
| | | |
| | | public function __construct(string $content) |
| | |
| | | $this->plural = $this->config['plural']; |
| | | $this->content = $content; |
| | | $this->fields = jvbGetFields($this->content, 'post'); |
| | | $this->maybeSetupTimeline(); |
| | | $this->sections = jvbGetSections($this->content, 'post'); |
| | | $this->stuck = [ |
| | | 'post_title', |
| | |
| | | |
| | | } |
| | | |
| | | protected function maybeSetupTimeline():void { |
| | | $this->isTimeline = Features::forContent($this->content)->has('is_timeline'); |
| | | |
| | | if (!$this->isTimeline) { |
| | | return; |
| | | } |
| | | $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) { |
| | | if (!array_key_exists('for_all', $field) || $field['for_all'] === false){ |
| | | return true; |
| | | } |
| | | return false; |
| | | })); |
| | | array_unshift($this->timelineSharedFields, 'post_thumbnail'); |
| | | array_unshift($this->timelineSharedFields, 'post_title'); |
| | | |
| | | $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) { |
| | | if (array_key_exists('for_all', $field) && $field['for_all'] === true) { |
| | | return true; |
| | | } |
| | | return false; |
| | | })); |
| | | |
| | | $all = array_merge($this->timelineUniqueFields, $this->timelineSharedFields); |
| | | $this->nonTimelineFields = array_filter($this->fields, function ($field) use ($all) { |
| | | return !in_array($field, $all); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | } |
| | | |
| | | protected function initTaxonomies():void |
| | | { |
| | | $this->taxonomies = array_filter(JVB_TAXONOMY, function ($config) { |
| | |
| | | ], |
| | | 'future'=> [ |
| | | 'label' => 'Upcoming', |
| | | 'icon' => 'future', |
| | | 'icon' => 'clock-clockwise', |
| | | ], |
| | | 'past' => [ |
| | | 'label' => 'Past', |
| | | 'icon' => 'past', |
| | | 'icon' => 'clock-counter-clockwise', |
| | | ], |
| | | 'repeat'=> [ |
| | | 'label' => 'Recurring', |
| | | 'icon' => 'repeat', |
| | | ], |
| | | 'draft' => [ |
| | | 'icon' => 'hide', |
| | | 'icon' => 'eye-closed', |
| | | 'label' => 'Hidden', |
| | | ], |
| | | 'trash' => [ |
| | | 'label' => 'Scrapped', |
| | | 'icon' => 'delete', |
| | | 'icon' => 'trash', |
| | | ], |
| | | ] : |
| | | [ |
| | | 'all' => [ |
| | | 'icon' => 'all', |
| | | 'icon' => 'infinity', |
| | | 'label' => 'Everything', |
| | | ], |
| | | 'publish'=> [ |
| | | 'icon' => 'publish', |
| | | 'icon' => 'eye', |
| | | 'label' => 'Live', |
| | | ], |
| | | 'draft' => [ |
| | | 'icon' => 'hide', |
| | | 'icon' => 'eye-closed', |
| | | 'label' => 'Hidden', |
| | | ], |
| | | 'trash' => [ |
| | | 'label' => 'Scrapped', |
| | | 'icon' => 'delete', |
| | | 'icon' => 'trash', |
| | | ], |
| | | ]; |
| | | } |
| | |
| | | foreach ($this->taxonomies as $taxonomy=> $config) { |
| | | $this->filters['taxonomy'][$taxonomy] = [ |
| | | 'label' => $config['singular'], |
| | | 'icon' => $taxonomy |
| | | 'icon' => $config['icon']??'folder' |
| | | ]; |
| | | } |
| | | } |
| | |
| | | ob_start(); |
| | | ?> |
| | | <div class="empty-state"> |
| | | <h3><?=jvbIcon($this->content)?>Nothing here<?=jvbIcon($this->content)?></h3> |
| | | <h3><?=jvbIcon($this->config['icon'])?>Nothing here<?=jvbIcon($this->config['icon'])?></h3> |
| | | <p>It doesn't look like you have any <?=$this->config['plural'] ?> yet.</p> |
| | | <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('add')?>" button to add one at a time.</small></p> |
| | | <p><small><i>Add many by uploading images above.</i>, or click the "<?=jvbIcon('plus-square')?>" button to add one at a time.</small></p> |
| | | </div> |
| | | <?php |
| | | return ob_get_clean(); |
| | |
| | | $this->renderDateFilters(); |
| | | ?> |
| | | <button type="button" class="clear-filters row" hidden> |
| | | <?= jvbIcon('close', ['title' => 'Clear']); ?> |
| | | <?= jvbIcon('x', ['title' => 'Clear']); ?> |
| | | Clear All Filters |
| | | </button> |
| | | </div> |
| | |
| | | <?php |
| | | $order = [ |
| | | 'orderby' => [ |
| | | 'date' => 'Order by date created', |
| | | 'calendar' => 'Order by date created', |
| | | 'alphabetical' => 'Order alphabetically' |
| | | ], |
| | | 'order' => [ |
| | | 'asc' => 'In ascending order (Z-A, oldest to newest)', |
| | | 'desc' => 'In descending order (A-Z, newest to oldest)' |
| | | 'sort-ascending' => 'In ascending order (Z-A, oldest to newest)', |
| | | 'sort-descending' => 'In descending order (A-Z, newest to oldest)' |
| | | ] |
| | | ]; |
| | | |
| | |
| | | |
| | | <?php |
| | | $views = [ |
| | | 'grid' => ['icon' => 'grid', 'label' => 'Grid View'], |
| | | 'list' => ['icon' => 'list', 'label' => 'List View'], |
| | | 'grid' => ['icon' => 'squares-four', 'label' => 'Grid View'], |
| | | 'list' => ['icon' => 'rows', 'label' => 'List View'], |
| | | 'table' => ['icon' => 'table', 'label' => 'Table View'] |
| | | ]; |
| | | |
| | |
| | | { |
| | | ob_start(); |
| | | ?> |
| | | <form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>"> |
| | | <form class="edit-form" data-save="content" data-form-id="edit-<?=$this->content?>" data-autosave<?= ($this->isTimeline) ? ' data-timeline' : ''?>> |
| | | <?= jvbFormStatus() ?> |
| | | <input type="hidden" name="form-id" value="<?=uniqid('new-')?>" /> |
| | | <input type="hidden" name="content" value="<?=$this->content?>" /> |
| | |
| | | $tabs = false; |
| | | } |
| | | |
| | | $isTimeline = Features::forContent($this->content)->has('is_timeline'); |
| | | |
| | | |
| | | $fields = $this->fields; |
| | | if (!$isTimeline) { |
| | | if (!$this->isTimeline) { |
| | | $first = ['post_thumbnail', 'post_title', 'price']; |
| | | |
| | | foreach ($first as $f) { |
| | |
| | | } |
| | | } |
| | | |
| | | if ($isTimeline) { |
| | | if ($this->isTimeline) { |
| | | $temp = array_filter($fields, function ($field) { |
| | | if (array_key_exists('for_all', $field) && $field['for_all'] === true) { |
| | | return true; |
| | | } |
| | | return false; |
| | | }); |
| | | return in_array($field, $this->timelineUniqueFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $config = [ |
| | | 'type' => 'gallery', |
| | | 'subtype' => 'timeline', |
| | |
| | | ]; |
| | | $content = ''; |
| | | foreach ($fields as $slug=> $field) { |
| | | if (!array_key_exists('for_all', $field) || $field['for_all'] === false) { |
| | | if (in_array($slug, $this->timelineSharedFields)) { |
| | | $content .= $this->form->render($slug, null, $field, false, true); |
| | | } |
| | | } |
| | | |
| | | |
| | | $content .= $this->meta->render('form', 'timeline', $config, false,true); |
| | | |
| | | $tabs['progression']['content'] = $content; |
| | | $this->fields = array_filter($fields, function ($field) { |
| | | if (array_key_exists('for_all', $field) && $field['for_all'] === true) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }); |
| | | $fields = $this->fields; |
| | | $fields = $this->nonTimelineFields; |
| | | } |
| | | foreach ($fields as $n => $config) { |
| | | if ($tabs) { |
| | |
| | | </div> |
| | | |
| | | <button type="button" class="add-repeater-row btn secondary"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | <span>Add Progress Step</span> |
| | | </button> |
| | | </div> |
| | |
| | | if ($status === 'future') { |
| | | $status = 'publish'; |
| | | $config = [ |
| | | 'icon' => 'publish', |
| | | 'icon' => 'eye', |
| | | 'label' => 'Live', |
| | | ]; |
| | | } else { |
| | |
| | | $this->renderGridView(); |
| | | $this->renderTableView(); |
| | | $this->renderTableRow(); |
| | | if (Features::forContent($this->content)->has('is_timeline')) { |
| | | if ($this->isTimeline) { |
| | | $temp = array_filter($this->fields, function ($field) { |
| | | if (array_key_exists('for_all', $field) && $field['for_all'] === true) { |
| | | return true; |
| | | } |
| | | return false; |
| | | }); |
| | | return in_array($field, $this->timelineUniqueFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | $form = new MetaForm(); |
| | | echo '<template class="uploadTimeline">'; |
| | | $form->renderImagePreview(null,$temp); |
| | | echo '<template class="timelineItem">'; |
| | | $form->renderImagePreview(null,['fields' => $temp]); |
| | | echo '</template>'; |
| | | } |
| | | echo jvbGetEmptyStateTemplate(); |
| | |
| | | ?> |
| | | <div class="item-actions"> |
| | | <button type="button" class="action" data-action="edit" title="Edit <?= $this->singular ?>"> |
| | | <?=jvbIcon('edit')?> |
| | | <?=jvbIcon('pencil-simple')?> |
| | | <span class="screen-reader-text">Edit <?= $this->singular ?></span> |
| | | </button> |
| | | <button type="button" class="action" data-action="trash" title="Scrap <?= $this->singular ?>"> |
| | | <?=jvbIcon('delete')?> |
| | | <?=jvbIcon('trash')?> |
| | | <span class="screen-reader-text">Scrap <?= $this->singular ?></span> |
| | | </button> |
| | | <!-- <button type="button" class="action" data-action="toggle-status">--> |
| | |
| | | if ($status === 'future') { |
| | | $status = 'publish'; |
| | | $config = [ |
| | | 'icon' => 'publish', |
| | | 'icon' => 'eye', |
| | | 'label' => 'Live' |
| | | ]; |
| | | } elseif ($status === 'past') { |
| | |
| | | 'vertical', |
| | | 'TAB NAV:', |
| | | '', |
| | | jvbIcon('down'), |
| | | jvbIcon('right') |
| | | jvbIcon('caret-double-down'), |
| | | jvbIcon('caret-double-right') |
| | | ) ?> |
| | | |
| | | <button type="button" class="add-row" title="Add new row"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | <span>Add Row</span> |
| | | </button> |
| | | </div> |
| | |
| | | $this->renderCreateModal(); |
| | | $content = ob_get_clean(); |
| | | $create = [ |
| | | 'button' => '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('add').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>', |
| | | 'button' => '<button type="button" class="create-item row" title="Create New '.$this->singular.'">'.jvbIcon('plus-square').'<span class="screen-reader-text">Create New '.$this->singular.'</span></button>', |
| | | 'content' => $content, |
| | | ]; |
| | | $actions[] = $create; |
| | |
| | | public function __construct() |
| | | { |
| | | $this->cache = CacheManager::for('dashboard', WEEK_IN_SECONDS); |
| | | $this->cache->invalidate(); |
| | | add_action('init', [$this, 'registerDashboard']); |
| | | if (!$this->isRegistered()) { |
| | | add_action('init', [$this, 'buildDashboard']); |
| | | } |
| | | $this->user = wp_get_current_user(); |
| | | $this->role = jvbUserRole(); |
| | | $this->role = jvbUserRole($this->user->ID); |
| | | $this->userLink = (int)get_user_meta($this->user->ID, BASE.'link', true); |
| | | |
| | | |
| | |
| | | |
| | | // Get current page/section |
| | | $page = $this->getCurrentPage(); |
| | | $config = $this->getConfig($page); |
| | | if(!empty($config)) { |
| | | add_filter('jvbLoadingIcon', function() use ($config) { |
| | | return jvbIcon($config['icon']); |
| | | }); |
| | | } |
| | | $integrationSlugs = array_map(function($name) { |
| | | return sanitize_title(str_replace('_', '-', $name)); |
| | | }, array_keys(JVB()->getAvailableServices(false))); |
| | |
| | | // Pass along to the Integrations template handler which knows to check for subpages |
| | | $page = 'integrations'; |
| | | } |
| | | |
| | | echo $this->cache->remember( |
| | | $page, |
| | | function() use ($page) { |
| | | return $this->renderDashboard($page); |
| | | } |
| | | ); |
| | | echo $this->renderDashboard($page); |
| | | //TODO: Reenable |
| | | // echo $this->cache->remember( |
| | | // $page, |
| | | // function() use ($page) { |
| | | // return $this->renderDashboard($page); |
| | | // } |
| | | // ); |
| | | |
| | | return ''; |
| | | } |
| | |
| | | $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode'; |
| | | echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher"> |
| | | <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'. |
| | | jvbIcon('light', ['title'=> 'Light Mode']). |
| | | jvbIcon('dark', ['title'=>'Dark Mode']). |
| | | jvbIcon('sun-dim', ['title'=> 'Light Mode']). |
| | | jvbIcon('moon', ['title'=>'Dark Mode']). |
| | | '</span></label>'; |
| | | ?> |
| | | <p class="title"> |
| | |
| | | } |
| | | } |
| | | if ($out == '') { |
| | | $out =jvbIcon('home'); |
| | | $out =jvbIcon('house'); |
| | | } |
| | | ?><?= $out ?> |
| | | </a> |
| | |
| | | <?php |
| | | $current_page = $this->getCurrentPage(); |
| | | $pages = $this->getUserAllowedPages()?:[]; |
| | | error_log('PageS: '.print_r($pages, true)); |
| | | |
| | | echo '<ul>'; |
| | | foreach ($pages as $page) { |
| | |
| | | echo '<p>Welcome back!</p>'; |
| | | |
| | | $pages = $this->getUserAllowedPages(); |
| | | |
| | | error_log('Pages: '.print_r($pages, true)); |
| | | |
| | | echo '<h2>What would you like to do today?</h2>'; |
| | | |
| | |
| | | continue; |
| | | } |
| | | $title = $this->getTitle($page); |
| | | $url = sanitize_title($title); |
| | | |
| | | $description = $this->getDescription($page); |
| | | $icon = $page; |
| | | if (!is_numeric($slug)) { |
| | |
| | | if (array_key_exists('icon', $config)) { |
| | | $icon = $config['icon']; |
| | | } |
| | | } else { |
| | | $slug = sanitize_title($page); |
| | | } |
| | | if ($title !== '') { |
| | | echo '<li><p><a href="'.get_home_url(null, '/dash/'.$url.'/').'" |
| | | data-page="'.$url.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>'; |
| | | echo '<li><p><a href="'.get_home_url(null, '/dash/'.$slug.'/').'" |
| | | data-page="'.$slug.'" data-dash>'.jvbIcon($icon).ucwords($title).'</a></p></li>'; |
| | | } |
| | | |
| | | } |
| | |
| | | if ($content !== '') { |
| | | echo $content; |
| | | } else { |
| | | jvbRenderSections($this->user->ID, 'user', jvbUserRole()); |
| | | jvbRenderSections($this->user->ID, 'user', jvbUserRole()); |
| | | } |
| | | |
| | | } |
| | |
| | | <div class="approvals container"> |
| | | <nav class="tabs row start" role="tablist"> |
| | | <button type="button" class="tab active" data-tab="summary" role="tab" aria-selected="true"> |
| | | <h2><?= jvbIcon('all')?>All</h2> |
| | | <h2><?= jvbIcon('infinity')?>All</h2> |
| | | </button> |
| | | <button type="button" class="tab" data-tab="artists" role="tab" aria-selected="false"> |
| | | <h2><?= jvbIcon('artists')?>Artists</h2> |
| | | <h2><?= jvbIcon('users-three')?>Artists</h2> |
| | | </button> |
| | | <button type="button" class="tab" data-tab="terms" role="tab" aria-selected="false"> |
| | | <h2><?= jvbIcon('style')?>Terms</h2> |
| | | <h2><?= jvbIcon('hash')?>Terms</h2> |
| | | </button> |
| | | <button type="button" class="tab" data-tab="yours" role="tab" aria-selected="false"> |
| | | <h2><?= jvbIcon('artist')?>Yours</h2> |
| | | <h2><?= jvbIcon('user')?>Yours</h2> |
| | | </button> |
| | | </nav> |
| | | </div> |
| | |
| | | 'vertical', |
| | | 'TAB NAV:', |
| | | '', |
| | | jvbIcon('down'), |
| | | jvbIcon('right'))?> |
| | | jvbIcon('caret-double-down'), |
| | | jvbIcon('caret-double-right'))?> |
| | | |
| | | </div> |
| | | <div class="items-container"> |
| | |
| | | <template class="<?= $type ?>Row"> |
| | | <tr> |
| | | <td> |
| | | <?= jvbIcon('grab') ?> |
| | | <?= jvbIcon('dots-six-vertical') ?> |
| | | </td> |
| | | <td data-id="actions" class="col"> |
| | | <?= jvbRenderToggleTextField( |
| | | 'public', |
| | | '', |
| | | '', |
| | | jvbIcon('show'), |
| | | jvbIcon('hide')) |
| | | jvbIcon('eye'), |
| | | jvbIcon('eye-closed')) |
| | | ?> |
| | | <button type="button" data-action="edit"> |
| | | <?= jvbIcon('edit') ?> |
| | | <?= jvbIcon('pencil-simple') ?> |
| | | </button> |
| | | </td> |
| | | <?php |
| | |
| | | 'not_found' => "No {$plural} found", |
| | | 'not_found_in_trash' => "No {$plural} found in Trash" |
| | | ], |
| | | 'menu_icon' => jvbCSSIcon('asc'), |
| | | 'menu_icon' => jvbCSSIcon('sort-ascending'), |
| | | 'public' => true, |
| | | 'publicly_queryable' => true, |
| | | 'show_in_menu' => true, |
| | |
| | | class EmailManager |
| | | { |
| | | |
| | | // Brand colors |
| | | private string $action_color = '#FF0080'; |
| | | private string $bg_light = '#F9F9F9'; |
| | | private string $bg_dark = '#1B1B1B'; |
| | | private string $text_color = '#151515'; |
| | | private string $text_light = '#F9F9F9'; |
| | | private array $colours = JVB_EMAIL['colours']; |
| | | private string $title = JVB_EMAIL['content']['title']; |
| | | private string $prefix = JVB_EMAIL['content']['subjectPrefix']; |
| | | private string $signature = JVB_EMAIL['content']['signature']; |
| | | private string $footer; |
| | | |
| | | // Site info |
| | | private string $site_name; |
| | | private string $site_url; |
| | | private string $signature; |
| | | |
| | | /** |
| | | * Constructor - sets up all filters |
| | |
| | | { |
| | | $this->site_name = wp_specialchars_decode(get_option('blogname'), ENT_QUOTES); |
| | | $this->site_url = get_site_url(); |
| | | $this->signature = jvbSignature(); |
| | | $this->footer = (is_array(JVB_EMAIL['content']['footer'])) ? implode('', JVB_EMAIL['content']['footer']) : JVB_EMAIL['content']['footer']; |
| | | |
| | | add_filter('wp_mail_content_type', [$this, 'setHtmlContentType']); |
| | | // User registration emails |
| | |
| | | */ |
| | | private function getEmailTemplate(string $content, string $header = ''):string |
| | | { |
| | | // Get logo URL (replace with your actual logo path) |
| | | $logo_url = esc_url(wp_get_attachment_image_url(get_theme_mod('custom_logo'), 'medium')); |
| | | $logo = get_custom_logo(); |
| | | |
| | | // Default header if none provided |
| | | if (empty($header)) { |
| | | $header = 'edmonton.ink'; |
| | | $header = $this->title; |
| | | } |
| | | |
| | | return '<!DOCTYPE html> |
| | |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | | <title>' . $this->site_name . '</title> |
| | | <title>' . $this->title . ' | ' . $this->site_name .'</title> |
| | | <style> |
| | | body, table, td, p, a, li, blockquote { |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | body { |
| | | background-color: ' . $this->bg_light . '; |
| | | background-color: ' . $this->colours['light'] . '; |
| | | margin: 0; |
| | | padding: 0; |
| | | color: ' . $this->text_color . '; |
| | | color: ' . $this->colours['dark'] . '; |
| | | } |
| | | |
| | | .email-container { |
| | |
| | | } |
| | | |
| | | .header { |
| | | background-color: ' . $this->bg_dark . '; |
| | | background-color: ' . $this->colours['dark'] . '; |
| | | padding: 5px; |
| | | text-align: center; |
| | | color: ' . $this->text_light . '; |
| | | color: ' . $this->colours['light'] . '; |
| | | font-size: 24px; |
| | | font-weight: 900; |
| | | letter-spacing: 1.5px; |
| | |
| | | } |
| | | |
| | | .content { |
| | | background-color: #ffffff; |
| | | background-color: ' . $this->colours['light-50'] . '; |
| | | padding: 30px; |
| | | border-bottom-left-radius: 5px; |
| | | border-bottom-right-radius: 5px; |
| | |
| | | .button { |
| | | display: inline-block; |
| | | padding: 12px 24px; |
| | | background-color: ' . $this->action_color . '; |
| | | color: ' . $this->text_light . ' !important; |
| | | background-color: ' . $this->colours['action-0'] . '; |
| | | color: ' . $this->colours['action-contrast'] . ' !important; |
| | | text-decoration: none; |
| | | border-radius: 3px; |
| | | font-weight: bold; |
| | |
| | | } |
| | | |
| | | .text-link { |
| | | color: ' . $this->action_color . '; |
| | | color: ' . $this->colours['action-0'] . '; |
| | | text-decoration: underline; |
| | | } |
| | | |
| | |
| | | text-align: center; |
| | | margin-top: 20px; |
| | | font-size: 12px; |
| | | color: #666666; |
| | | backgorund-color: ' . $this->colours['light-200'] . '; |
| | | color: ' . $this->colours['dark-200'] . '; |
| | | padding: 10px; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | .divider { |
| | | border-top: 1px solid #dddddd; |
| | | border-top: 1px solid ' . $this->colours['dark-200'] . '; |
| | | margin: 25px 0; |
| | | } |
| | | |
| | |
| | | |
| | | /* Dark mode support for clients that support it */ |
| | | @media (prefers-color-scheme: dark) { |
| | | .email-container { |
| | | background-color: #1B1B1B !important; |
| | | .body { |
| | | background-color: ' . $this->colours['dark'] . '; |
| | | color: ' . $this->colours['light'] . '; |
| | | } |
| | | |
| | | .header { |
| | | background-color: ' . $this->colours['light'] . '; |
| | | color: ' . $this->colours['dark'] . '; |
| | | } |
| | | |
| | | .content { |
| | | background-color: #2A2A2A !important; |
| | | color: #F9F9F9 !important; |
| | | } |
| | | |
| | | h1, h2, h3, p, li, a.text-link { |
| | | color: #F9F9F9 !important; |
| | | background-color: ' . $this->colours['dark-50'] . '; |
| | | } |
| | | |
| | | .footer { |
| | | color: #999999 !important; |
| | | } |
| | | |
| | | .divider { |
| | | border-color: #444444 !important; |
| | | background-color: ' . $this->colours['dark-200'] . '; |
| | | color: ' . $this->colours['light-200'] . '; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | |
| | | <div class="header"> |
| | | <a href="' . $this->site_url . '"> |
| | | <img src="' . $logo_url . '" alt="Edmonton Ink"> |
| | | ' . $logo . ' |
| | | </a> |
| | | <p>' . $header . '</p> |
| | | </div> |
| | |
| | | ' . $content . ' |
| | | </div> |
| | | <div class="footer"> |
| | | <p>© ' . date('Y') . ' edmonton.ink — Your tattoo scene on your screen.</p> |
| | | <p><a href="' . $this->site_url . '" class="text-link">edmonton.ink</a></p> |
| | | ' . $this->footer . ' |
| | | </div> |
| | | </div> |
| | | </body> |
| | |
| | | */ |
| | | public function customizeNewUserEmail($wp_new_user_notification_email, $user, $blogname) |
| | | { |
| | | $user_login = $user->user_login; |
| | | $message = sprintf( |
| | | '<p>Hi %s!</p> |
| | | <p>Thanks for signing up for an account on %s.</p> |
| | | <p><b>Your username:</b> %s</p> |
| | | <p><b>Your password:</b> <i>Your chosen password.</i></p>', |
| | | $user->display_name, |
| | | get_bloginfo('name'), |
| | | $user->user_login, |
| | | ); |
| | | $message = apply_filters('jvbNewUserEmail', $message, $user); |
| | | |
| | | // Only create the password key if the user can change their password |
| | | $key = get_password_reset_key($user); |
| | | if (!is_wp_error($key)) { |
| | | $reset_url = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'login'); |
| | | |
| | | $message = sprintf('<p>Hey %s!</p>', $user->first_name); |
| | | $message .= '<p>Thanks for joining Edmonton\'s tattoo scene. Here\'s your login information:</p>'; |
| | | $message .= sprintf('<p><strong>Username:</strong> %s</p>', $user_login); |
| | | $message .= '<p>To set your password and access your account, click the button below:</p>'; |
| | | $message .= sprintf('<p style="text-align: center;"><a href="%s" class="button">Set Your Password</a></p>', $reset_url); |
| | | $message .= '<p>Or copy and paste this link into your browser:</p>'; |
| | | $message .= sprintf('<p style="user-select:all;">%s</p>', $reset_url); |
| | | $message .= '<p>This link will expire in 24 hours, for security reasons.</p>'; |
| | | if (in_array('jvb_artist', array_values($user->roles))) { |
| | | $message .= '<div class="divider"></div>'; |
| | | $message .= '<h3>NOTE:</h3> |
| | | <p>Once you set your password, you\'ll have access to your custom dashboard where you can:</p> |
| | | <ul> |
| | | <li>Manage your profile information</li> |
| | | <li>Upload tattoos/piercings, and artwork</li> |
| | | </ul> |
| | | <p>Nothing will be published until you\'ve been approved by 3 already approved artists, or the admin.</p> |
| | | <p>Admins check every day or three, but, if you are in a rush, you can contact us directly by replying to this email, or texting us at 825-925-9916.</p>'; |
| | | } elseif (in_array('jvb_partner', array_values($user->roles))) { |
| | | $message .= '<div class="divider"></div>'; |
| | | $message .= '<h3>NOTE:</h3> |
| | | <p>Once you set your password, you\'ll have access to your custom dashboard where you can:</p> |
| | | <ul> |
| | | <li>Manage your profile information</li> |
| | | <li>Create offers for enthusiasts or partners or both</li> |
| | | </ul> |
| | | <p>Nothing will be published until you\'ve been approved by the admin.</p> |
| | | <p>Admins check every day or three, but, if you are in a rush, you can contact us directly by replying to this email, or texting us at 825-925-9916.</p> |
| | | <p><strong>Note:</strong>Even after approval by admin, your ability to publish depends on your karmic standing by artists. Artists each have a vote they can cast (UP or DOWN) - if your karmic score dips too far in the negative, you account is subject to reconsideration or even a ban.</p>'; |
| | | } |
| | | $message .= '<div class="divider"></div>'; |
| | | $message = apply_filters(BASE.'new_user_email_content', $message, $user); |
| | | |
| | | $message .= '<p>If you didn\'t create this account, please ignore this email and the link will expire.</p>'; |
| | | $message .= sprintf('<p>Ink on, %s</p>', $user->first_name); |
| | | $message .= $this->signature; |
| | | $wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, 'Welcome to edmonton.ink'); |
| | | } |
| | | $message .= $this->signature; |
| | | $wp_new_user_notification_email['message'] = $this->getEmailTemplate($message, JVB_EMAIL['subjects']['newUser']); |
| | | |
| | | // Change the subject line |
| | | $wp_new_user_notification_email['subject'] = sprintf('Welcome to %s! Confirm your account', $this->site_name); |
| | | $prefix = JVB_EMAIL['types']['newUser']['showPrefix']??true; |
| | | $prefix = ($prefix) ? $this->prefix : ''; |
| | | $wp_new_user_notification_email['subject'] = $prefix.JVB_EMAIL['types']['newUser']['subject']; |
| | | |
| | | return $wp_new_user_notification_email; |
| | | } |
| | |
| | | public function customizeNewUserEmailAdmin(array $emailData, WP_User $user, string $blogname):array |
| | | { |
| | | $message = '<p>A new user has registered on <strong>' . $this->site_name . '</strong>:</p>'; |
| | | $message .= '<p><strong>Username:</strong> ' . $user->user_login . '<br>'; |
| | | $message .= '<strong>Email:</strong> ' . $user->user_email . '<br>'; |
| | | $message .= '<strong>Role:</strong> ' . str_replace(BASE, '', array_values($user->roles)[0]).'</p>'; |
| | | $message .= '<p><strong>Username:</strong> ' . $user->user_login . '</p>'; |
| | | $message .= '<p><strong>Email:</strong> ' . $user->user_email . '</p>'; |
| | | $message = apply_filters('jvbNewUserAdminEmail', $message, $user); |
| | | $message .= $this->signature; |
| | | $emailData['message'] = $this->getEmailTemplate($message, 'New User Registration'); |
| | | $emailData['subject'] = '[' . $this->site_name . '] New ' . str_replace(BASE, '', array_values($user->roles)[0]).': '.$user->display_name; |
| | | $emailData['subject'] = $this->prefix .'New ' . str_replace(BASE, '', array_values($user->roles)[0]).': '.$user->display_name; |
| | | |
| | | return $emailData; |
| | | } |
| | |
| | | * |
| | | * @return string |
| | | */ |
| | | public function customizePasswordResetEmail(string $message, string $key, string $user_login, WP_User $user_data):string |
| | | public function customizePasswordResetEmail(string $message, string $key, string $user_login, WP_User $user):string |
| | | { |
| | | $reset_url = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'login'); |
| | | |
| | | $content = '<p>Hey bud,</p>'; |
| | | $content = '<p>We received a request to reset the password for an account associated with this email:</p>'; |
| | | $content .= '<p><strong>Username:</strong> ' . $user_login . '</p>'; |
| | | $content .= '<p>If you didn\'t make this request, you can safely ignore this email and nothing will happen to your account.</p>'; |
| | | $content .= '<p>To reset your password, click the button below:</p>'; |
| | | $content .= '<p style="text-align: center;"><a href="' . $reset_url . '" class="button">Reset Password</a></p>'; |
| | | $content .= '<p>Or copy and paste this link into your browser:</p>'; |
| | | $content .= '<p style="user-select:all;">' . $reset_url . '</p>'; |
| | | $content .= '<div class="divider"></div>'; |
| | | $content .= '<p>This password reset link is only valid for 24 hours.</p>'; |
| | | $content .= $this->signature; |
| | | $reset_url = network_site_url("wp-login.php?action=rp&key=$key&login=" . rawurlencode($user_login), 'login'); |
| | | $content = sprintf( |
| | | '<p>Hi %s!</p> |
| | | <p>We received a request to reset the password for an account associated with this email:</p> |
| | | <p><strong>Username:</strong> %s</p> |
| | | <p>If you didn\'t make this request, you can safely ignore this email and nothing will happen to your account.</p> |
| | | <p>To reset your password, click the button below:</p> |
| | | %s |
| | | <p>Or copy and paste this link into your browser:</p> |
| | | %s |
| | | <div class="divider"></div> |
| | | <p>This password reset link is only valid for 24 hours.</p>', |
| | | $user->display_name, |
| | | $user_login, |
| | | jvbMailButton($reset_url,'Reset Password'), |
| | | jvbEmailLink($reset_url) |
| | | ); |
| | | $content = apply_filters('jvbPasswordResetEmail', $content, $user_login, $user, $reset_url); |
| | | $content .= $this->signature; |
| | | return $this->getEmailTemplate($content, 'Password Reset'); |
| | | } |
| | | |
| | |
| | | */ |
| | | public function customizePasswordResetTitle(string $title, string $user_login, WP_User $user_data):string |
| | | { |
| | | return '[' . $this->site_name . '] Password Reset Request'; |
| | | } |
| | | $prefix = JVB_EMAIL['types']['resetPass']['showPrefix']??true; |
| | | $prefix = ($prefix) ? $this->prefix : ''; |
| | | return $prefix.JVB_EMAIL['types']['resetPass']['subject']; |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Email change notification to admin |
| | | * @param array $email_change_email |
| | | * @param array $user |
| | | * @param array $userdata |
| | | * @param array $oldUser |
| | | * @param array $newUser |
| | | * |
| | | * @return array |
| | | */ |
| | | public function customizeEmailChangeEmail(array $email_change_email, array $user, array $userdata):array |
| | | public function customizeEmailChangeEmail(array $email_change_email, array $oldUser, array $newUser):array |
| | | { |
| | | $content = sprintf('<p>Hi %s,</p>', $user['first_name']); |
| | | $content .= '<p>Ideally you already know this: you asked to change your email, and here we are.</p>'; |
| | | $content .= '<p><strong>Old Email:</strong> ' . $email_change_email['to'] . '<br>'; |
| | | $content .= '<strong>New Email:</strong> ' . $user['user_email'] . '</p>'; |
| | | |
| | | $content .= '<p>If this is news, or you did not change your email, please contact us immediately. You can <a href="sms:+18258239916">text us</a> or reply to this email."></a></p>'; |
| | | $content .= '<p style="text-align: center;"><a href="' . wp_login_url() . '" class="button">Log In to Your Account</a></p>'; |
| | | $content = sprintf( |
| | | '<p>Hi %s,</p> |
| | | <p>Ideally you already know this: someone asked to change the email for your account.</p> |
| | | <p><strong>Old Email:</strong> %s</p> |
| | | <p><strong>New Email:</strong> %s</p> |
| | | <div class="divider"></div> |
| | | <p>If this is news to you, or you did not request this - please contact us immediately. You can <a href="sms:+18258239916">text us</a> or reply to this email."></a></p> |
| | | %s', |
| | | $newUser['first_name'], |
| | | $oldUser['user_email'], |
| | | $newUser['user_email'], |
| | | jvbMailButton(wp_login_url(), 'Log In To Your Account') |
| | | ); |
| | | $content = apply_filters('jvbEmailChangeRequestEmail', $content, $oldUser, $newUser); |
| | | $content .= $this->signature; |
| | | $email_change_email['message'] = $this->getEmailTemplate($content, 'Email Address Changed'); |
| | | $email_change_email['subject'] = '[' . $this->site_name . '] Email Address Changed'; |
| | | $prefix = JVB_EMAIL['types']['emailChange']['showPrefix']??true; |
| | | $prefix = ($prefix) ? $this->prefix : ''; |
| | | $email_change_email['subject'] = $prefix.JVB_EMAIL['types']['emailChange']['subject']; |
| | | |
| | | |
| | | return $email_change_email; |
| | | } |
| | |
| | | * |
| | | * @return string |
| | | */ |
| | | public function customizeNewUserEmailContent(string $email_text, array $user):string |
| | | public function customizeNewUserEmailContent(string $email_text, array $new_user_email):string |
| | | { |
| | | $confirm_url = esc_url( add_query_arg('newuseremail', $new_user_email['hash'], self_admin_url('profile.php'))); |
| | | $content = sprintf( |
| | | '<p>Hey,</p> |
| | | <p>There was a request to change the email address associated with your account to this one.</p> |
| | | <p>This is just a friendly email to ensure you would like this change.</p> |
| | | <p>You can confirm this change by clicking the button below:</p> |
| | | %s |
| | | <p>Or copy and paste this link into your browser:</p> |
| | | %s', |
| | | jvbMailButton($confirm_url, 'Confirm this Email'), |
| | | jvbEmailLink($confirm_url) |
| | | ); |
| | | |
| | | // Extract the confirmation link from the default message |
| | | $pattern = '/###CONFIRM_URL### (.*?)(?=\n|\r|$)/'; |
| | | preg_match($pattern, $email_text, $matches); |
| | | $confirm_url = isset($matches[1]) ? $matches[1] : admin_url('profile.php?newuseremail=' . $user['hash']); |
| | | $content = apply_filters('jvbEmailChangedEmail', $content, $confirm_url); |
| | | |
| | | $content = '<p>Hey human,</p>'; |
| | | $content .= '<p>Seems you want to change the email associated with your account.</p>'; |
| | | $content .= '<p>You\'d like to change your email to: <strong>' . $user['newemail'] . '</strong></p>'; |
| | | $content .= '<p>Please confirm this change by clicking the button below:</p>'; |
| | | $content .= '<p style="text-align: center;"><a href="' . $confirm_url . '" class="button">Confirm Email Change</a></p>'; |
| | | $content .= '<p>Or copy and paste this link into your browser:</p>'; |
| | | $content .= '<p style="user-select:all">' . $confirm_url . '</p>'; |
| | | $content .= '<div class="divider"></div>'; |
| | | $content .= '<p>If you did not request this change, you can safely ignore this email and nothing will change.</p>'; |
| | | $content .= $this->signature; |
| | |
| | | /** |
| | | * Password change notification |
| | | * @param array $pass_change_email |
| | | * @param array $user |
| | | * @param array $userdata |
| | | * @param array $oldUser |
| | | * @param array $newUser |
| | | * |
| | | * @return array |
| | | */ |
| | | public function customizePasswordChangeEmail(array $pass_change_email, array $user, array $userdata):array |
| | | public function customizePasswordChangeEmail(array $pass_change_email, array $oldUser, array $newUser):array |
| | | { |
| | | |
| | | $content = '<p>Hey bud,</p>'; |
| | | $content .= '<p>Ideally, you\'re expecting this email. You wanted to change your password, and this is to let you know that it\'s definitely updated.</p>'; |
| | | $content .= '<p>If you did not change your password, please contact us immediately.</p>'; |
| | | $content .= '<p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p>'; |
| | | $content .= '<p style="text-align: center;"><a href="' . wp_login_url() . '" class="button">Log In to Your Account</a></p>'; |
| | | $content = sprintf( |
| | | '<p>Hi %s,</p> |
| | | <p>This is a confirmation email to let you know your password has successfully been changed.</p> |
| | | <p>If you\'re not expecting this email, and did not change your password - please <strong>contact us immediately</strong></p> |
| | | <p>You can <a href="sms:+18259257398">text us</a>, or reply to this email.</p> |
| | | %s', |
| | | $oldUser['first_name'], |
| | | jvbMailButton(wp_login_url(), 'Log In to Your Account') |
| | | ); |
| | | $content = apply_filters('jvbPasswordChangeEmail', $content, $oldUser, $newUser); |
| | | $content .= $this->signature; |
| | | |
| | | $pass_change_email['message'] = $this->getEmailTemplate($content, 'Password Changed'); |
| | | $pass_change_email['subject'] = '[' . $this->site_name . '] Password Changed'; |
| | | $prefix = JVB_EMAIL['types']['passwordChange']['showPrefix']??true; |
| | | $prefix = ($prefix) ? $this->prefix : ''; |
| | | $pass_change_email['subject'] = $prefix.JVB_EMAIL['types']['passwordChange']['subject']; |
| | | |
| | | return $pass_change_email; |
| | | } |
| | |
| | | * |
| | | * @return string |
| | | */ |
| | | public function customizeUserRequestEmail(string $content, array $request_data):string |
| | | public function customizeUserRequestEmail(string $content, array $email_data):string |
| | | { |
| | | |
| | | // Extract the confirmation URL from the content |
| | | $pattern = '/<a href="([^"]+)"[^>]*>/'; |
| | | preg_match($pattern, $content, $matches); |
| | | $confirm_url = isset($matches[1]) ? $matches[1] : ''; |
| | | |
| | | $request_type = $request_data['action_name']; |
| | | $request_name = ''; |
| | | $confirm_url = $email_data['confirm_url']; |
| | | $request_type = $email_data['action_name']; |
| | | |
| | | switch ($request_type) { |
| | | case 'export_personal_data': |
| | |
| | | $request_name = 'Data Request'; |
| | | } |
| | | |
| | | $message = '<p>Hi privacy enthusiast,</p>'; |
| | | $message .= '<p>A request has been made to perform the following action on your account:</p>'; |
| | | $message .= '<p><strong>' . $request_name . '</strong></p>'; |
| | | $message .= '<p>If you made this request, you can confirm it by clicking the button below:</p>'; |
| | | $message .= '<p style="text-align: center;"><a href="' . $confirm_url . '" class="button">Confirm Request</a></p>'; |
| | | $message .= '<p>Or copy and paste this link into your browser:</p>'; |
| | | $message .= '<p style="user-select:all;">' . $confirm_url . '</p>'; |
| | | |
| | | $message = sprintf( |
| | | '<p>Hi,</p> |
| | | <p>You\'re receiving this email because a request has been made to <strong>%s</strong></p> |
| | | <p>If you\'re the one who made this request, you can confirm it by clicking the button below:</p> |
| | | %s |
| | | <p>Or copy and paste this link into your browser:</p> |
| | | %s', |
| | | $request_name, |
| | | jvbMailButton($confirm_url, 'Confirm'), |
| | | jvbEmailLink($confirm_url) |
| | | ); |
| | | $message = apply_filters('jvbPersonalDataExport', $message, $request_type, $confirm_url, $email_data); |
| | | |
| | | $message .= '<div class="divider"></div>'; |
| | | $message .= '<p>If you did not make this request, you can safely ignore this email.</p>'; |
| | | $message .= $this->signature; |
| | |
| | | */ |
| | | public function customizePersonalDataEmail(string $content, int $request_id, array $email_data):string |
| | | { |
| | | $download_url = $email_data['export_file_url']; |
| | | $expiresAt = $email_data['expiration_date']; |
| | | |
| | | // Extract the download link |
| | | $pattern = '/<a href="([^"]+)"[^>]*>/'; |
| | | preg_match($pattern, $content, $matches); |
| | | $download_url = isset($matches[1]) ? $matches[1] : ''; |
| | | |
| | | $message = '<p>Your request for an export of personal data has been completed.</p>'; |
| | | $message .= '<p>You can download your personal data by clicking the button below.</p>'; |
| | | $message .= '<p style="text-align: center;"><a href="' . $download_url . '" class="button">Download Personal Data</a></p>'; |
| | | $message .= '<p>Or copy and paste this link into your browser:</p>'; |
| | | $message .= '<p style="user-select:all;">' . $download_url . '</p>'; |
| | | $message .= '<div class="divider"></div>'; |
| | | $message .= '<p><strong>Important:</strong> For privacy reasons, this link will expire in ' . $email_data['expiration'] . '.</p>'; |
| | | $message = sprintf( |
| | | '<p>Hi,</p> |
| | | <p>You\'re receiving this email because you requested an export of your personal data.</p> |
| | | <p>You can download your personal data by clicking the button below:</p> |
| | | %s |
| | | <p>Or you can copy and paste this link into your browser:</p> |
| | | %s |
| | | <div class="divider"></div> |
| | | <p><strong>Important:</strong> For privacy and security, this link will expire at %s.</p>', |
| | | jvbMailButton($download_url, 'Download Your Data'), |
| | | jvbEmailLink($download_url), |
| | | $expiresAt |
| | | ); |
| | | $message = apply_filters('jvbPersonalDataExported', $message, $download_url, $expiresAt, $email_data); |
| | | $message .= $this->signature; |
| | | |
| | | return $this->getEmailTemplate($message, 'Your Personal Data Export'); |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\utility\Features; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class IconsManager |
| | | { |
| | | protected static ?IconsManager $instance = null; |
| | | protected CacheManager $cache; |
| | | protected string $style = 'regular'; |
| | | protected array $styles = ['regular', 'bold', 'duotone', 'fill', 'light', 'thin']; |
| | | // Custom icons registered via filter |
| | | protected array $customIcons = []; |
| | | protected array $usedIcons = []; |
| | | protected array $map = []; |
| | | |
| | | /** |
| | | * Get singleton instance |
| | | */ |
| | | public static function getInstance(): IconsManager |
| | | { |
| | | if (self::$instance === null) { |
| | | self::$instance = new self(); |
| | | } |
| | | return self::$instance; |
| | | } |
| | | private function __construct() |
| | | { |
| | | $this->cache = CacheManager::for('icons', WEEK_IN_SECONDS); |
| | | $this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles)) |
| | | ? JVB_SITE['icons'] |
| | | : 'regular'; |
| | | |
| | | $this->addMap(); |
| | | |
| | | // Allow custom icon registration |
| | | $this->customIcons = apply_filters('jvbRegisterCustomIcons', [ |
| | | 'syncing' => JVB_DIR .'/assets/icons/cloud-sync-thin.svg', |
| | | 'alphabetical' => JVB_DIR.'/assets/icons/alphabetical.svg' |
| | | ]); |
| | | |
| | | |
| | | $this->usedIcons = get_option(BASE.'used_icons', []); |
| | | $this->includeIcons(); |
| | | // Register hooks only once |
| | | $this->registerHooks(); |
| | | } |
| | | |
| | | /** |
| | | * Include icons via filter (for JS usage, etc.) |
| | | */ |
| | | protected function includeIcons():array |
| | | { |
| | | $icons = get_option(BASE.'includeIcons'); |
| | | // $icons = false; |
| | | if (!$icons) { |
| | | $icons = $this->addIncludeIcons(); |
| | | } |
| | | $include = apply_filters('jvbIncludeIcons', []); |
| | | $add = array_filter($include, function($addIt) use ($icons) { |
| | | return !in_array($addIt, $icons); |
| | | }); |
| | | error_log('Adding icons: '.print_r($add, true)); |
| | | if (!empty($add)) { |
| | | $this->addIncludeIcons($add); |
| | | } |
| | | |
| | | return $icons; |
| | | } |
| | | |
| | | protected function addIncludeIcons(array $add = []) { |
| | | $icons = get_option(BASE.'includeIcons'); |
| | | if (!$icons) { |
| | | $icons = [ |
| | | 'check-circle', |
| | | 'close-circle', |
| | | 'cloud-slash', |
| | | 'exclamation-mark', |
| | | 'cloud-arrow-down', |
| | | 'cloud-arrow-up', |
| | | 'cloud-check', |
| | | 'cloud-slash', |
| | | 'cloud-warning', |
| | | 'syncing', |
| | | 'cloud-x', |
| | | 'arrows-clockwise', |
| | | 'share-fat', |
| | | 'trash', |
| | | 'star', |
| | | ['name' => 'star-half', 'style' => 'fill'], |
| | | ['name' => 'star', 'style' => 'fill'], |
| | | //FORMATTING |
| | | 'copy', |
| | | 'paragraph', |
| | | 'text-h-one', |
| | | 'text-h-two', |
| | | 'text-h-three', |
| | | 'text-h-four', |
| | | 'text-h-five', |
| | | 'text-h-six', |
| | | ['name' =>'text-b', 'style' => 'fill'], |
| | | 'text-italic', |
| | | 'text-underline', |
| | | 'text-strikethrough', |
| | | 'list-dashes', |
| | | 'list-numbers', |
| | | 'text-align-left', |
| | | 'text-align-center', |
| | | 'text-align-right', |
| | | // 'text-align-justify', |
| | | 'link', |
| | | //FILE ICONS |
| | | 'file-pdf', |
| | | 'file-csv', |
| | | 'file-doc', |
| | | 'file-txt', |
| | | 'file-xls', |
| | | ]; |
| | | $check = [JVB_CONTENT, JVB_TAXONOMY, JVB_USER]; |
| | | foreach ($check as $constant) { |
| | | foreach ($constant as $key => $value) { |
| | | if (array_key_exists('icon', $value) && !in_array($value['icon'], $icons)) { |
| | | $icons[] = $value['icon']; |
| | | } |
| | | } |
| | | } |
| | | $icons = apply_filters('jvbIncludeIcons', $icons); |
| | | |
| | | update_option(BASE.'includeIcons', $icons); |
| | | update_option(BASE.'icons_needs_update', true); |
| | | } |
| | | $add = apply_filters('jvbIncludeIcons', $add); |
| | | $add = array_filter($add, function($addIt) use ($icons) { |
| | | return !in_array($addIt, $icons); |
| | | }); |
| | | if (!empty($add)) { |
| | | $icons = array_merge($add, $icons); |
| | | update_option(BASE.'usedIcons', $icons); |
| | | update_option(BASE.'icons_needs_update', true); |
| | | } |
| | | return $icons; |
| | | } |
| | | |
| | | protected function addMap():void |
| | | { |
| | | $map = get_option(BASE.'iconMap'); |
| | | if (!$map) { |
| | | $map = []; |
| | | if (Features::forSite()->has('referrals')){ |
| | | $map['referrals'] = 'hand-heart'; |
| | | } |
| | | if (Features::forSite()->has('dashboard')){ |
| | | $map['dash'] = 'door'; |
| | | } |
| | | if (Features::forSite()->has('magicLink')){ |
| | | $map['magicLink'] = 'magic-wand'; |
| | | } |
| | | if (Features::hasAnyIntegration()) { |
| | | $map['integrations'] = 'plugs-connected'; |
| | | } |
| | | update_option(BASE.'iconMap', $map); |
| | | } |
| | | |
| | | $this->map = apply_filters('jvbMapIcons', $map); |
| | | } |
| | | |
| | | /** |
| | | * Register WordPress hooks |
| | | */ |
| | | protected function registerHooks(): void |
| | | { |
| | | add_action('init', [$this, 'checkCSS']); |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueIconStyles']); |
| | | add_action('admin_enqueue_scripts', [$this, 'enqueueIconStyles']); |
| | | } |
| | | |
| | | public function checkCSS():void |
| | | { |
| | | // update_option(BASE.'icons_needs_update', true); |
| | | if (get_option(BASE.'icons_needs_update', false)) { |
| | | error_log('Regenerating CSS'); |
| | | delete_option(BASE.'icons_needs_update'); |
| | | $this->regenerateCSS(); |
| | | } |
| | | } |
| | | |
| | | protected function regenerateCSS(): void |
| | | { |
| | | error_log('[IconsManager]:regenerateCSS'); |
| | | $css = $this->generateIconCSS(); |
| | | $css_path = JVB_DIR.'/assets/css/icons.css'; |
| | | |
| | | if (file_put_contents($css_path, $css) !== false) { |
| | | CacheManager::updateTimestamp('icons'); |
| | | } else { |
| | | error_log('[IconsManager]Could not write css.'); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Prevent cloning |
| | | */ |
| | | private function __clone() {} |
| | | |
| | | /** |
| | | * Prevent unserialization |
| | | */ |
| | | public function __wakeup() |
| | | { |
| | | throw new \Exception("Cannot unserialize singleton"); |
| | | } |
| | | |
| | | /** |
| | | * Get an icon element |
| | | * |
| | | * @param string $name Icon name (e.g., 'heart', 'calendar') |
| | | * @param array $options Options array: |
| | | * - 'style' => 'regular'|'bold'|'fill'|etc. |
| | | * - 'label' => 'Accessible label' (for standalone icons) |
| | | * - 'decorative' => true (for icons next to text) |
| | | * - 'class' => 'additional classes' |
| | | * - 'size' => 24 (for custom sizing via inline style) |
| | | * @return string HTML icon element |
| | | */ |
| | | public function getIcon(string $name, array $options = []): string |
| | | { |
| | | if (!array_key_exists('style', $options)) { |
| | | $options['style'] = $this->style; |
| | | } |
| | | $name = (array_key_exists($name, $this->map)) ? $this->map[$name] : $name; |
| | | |
| | | // Validate icon exists |
| | | if (!$this->iconExists($name, $options['style'])) { |
| | | error_log('[IconsManager] Icon not found: ' . $name); |
| | | return ''; |
| | | } |
| | | |
| | | $style = $options['style'] ?? $this->style; |
| | | |
| | | // Track icon usage |
| | | $this->trackIconUsage($name, $style); |
| | | |
| | | // Build classes |
| | | $classes = ['icon', 'icon-' . $name]; |
| | | if (!empty($options['class'])) { |
| | | $classes[] = $options['class']; |
| | | } |
| | | |
| | | |
| | | $attrs = ['class="' . esc_attr(implode(' ', $classes)) . '"']; |
| | | $attrs[] = 'aria-hidden="true"'; |
| | | |
| | | |
| | | |
| | | return '<i ' . implode(' ', $attrs) . '></i>'; |
| | | } |
| | | |
| | | /** |
| | | * Track icon usage for CSS generation |
| | | */ |
| | | protected function trackIconUsage(string $name, string $style): void |
| | | { |
| | | if (!array_key_exists($style, $this->usedIcons)) { |
| | | $this->usedIcons[$style] = []; |
| | | } |
| | | |
| | | if (!in_array($name, $this->usedIcons[$style])) { |
| | | $this->usedIcons[$style][] = $name; |
| | | update_option(BASE.'used_icons', $this->usedIcons); |
| | | // Flag for regeneration on next init |
| | | update_option(BASE.'icons_needs_update', true); |
| | | |
| | | // Clear cache |
| | | $this->cache->delete('icon_styles_css'); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Check if icon file exists |
| | | */ |
| | | protected function iconExists(string $name, ?string $style = null): bool |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | | } |
| | | // Check custom icons first |
| | | if (array_key_exists($name, $this->customIcons)) { |
| | | return file_exists($this->customIcons[$name]); |
| | | } |
| | | |
| | | // Check standard icons |
| | | $filepath = $this->buildFilePath($name, $style); |
| | | return file_exists($filepath); |
| | | } |
| | | |
| | | /** |
| | | * Build file path for icon |
| | | */ |
| | | protected function buildFilePath(string $name, ?string $style = null): string |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | | } |
| | | // Custom icons (absolute path provided) |
| | | if (array_key_exists($name, $this->customIcons)) { |
| | | return $this->customIcons[$name]; |
| | | } |
| | | |
| | | // Standard SVG icons in /assets/icons/ |
| | | if (str_ends_with($name, '.svg')) { |
| | | return JVB_DIR . '/assets/icons/' . $name; |
| | | } |
| | | $name = ($style === 'regular') ? $name : $name . '-' . $style; |
| | | |
| | | // Phosphor icons with style variants |
| | | return JVB_DIR . '/assets/phosphor-icons/' . $style . '/' . $name . '.svg'; |
| | | } |
| | | |
| | | /** |
| | | * Get raw SVG content for CSS mask-image |
| | | */ |
| | | protected function getRawSvg(string $name, ?string $style = null): ?string |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | | } |
| | | $filepath = $this->buildFilePath($name, $style); |
| | | |
| | | if (!file_exists($filepath)) { |
| | | return null; |
| | | } |
| | | |
| | | $svg = file_get_contents($filepath); |
| | | if ($svg === false) { |
| | | return null; |
| | | } |
| | | |
| | | // Clean up SVG for CSS usage |
| | | $svg = preg_replace("/([\n\t]+)/", ' ', $svg); |
| | | $svg = preg_replace('/>\s*</', '><', $svg); |
| | | $svg = trim($svg); |
| | | |
| | | return $svg; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Enqueue icon styles via REST endpoint |
| | | */ |
| | | public function enqueueIconStyles(): void |
| | | { |
| | | $timestamp = CacheManager::getTimestamp('icons'); |
| | | |
| | | wp_enqueue_style( |
| | | 'jvb-icons', |
| | | JVB_URL.'assets/css/icons.css', |
| | | [], |
| | | $timestamp |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Generate CSS from icon list |
| | | */ |
| | | protected function generateIconCSS(): string |
| | | { |
| | | $css = ''; |
| | | foreach ($this->usedIcons as $style => $icons) { |
| | | $styleClass = ($style !== $this->style) ? '-'.substr($style, 0,2) : ''; |
| | | foreach ($icons as $icon) { |
| | | |
| | | $svg = $this->getEncodedSVG($icon, $style); |
| | | if ($svg !== '') { |
| | | $css .= ".icon-{$icon}{$styleClass}{"; |
| | | $css .= "--icon:url('data:image/svg+xml;base64,{$svg}');"; |
| | | $css .= "}"; |
| | | } |
| | | } |
| | | } |
| | | return $this->minifyCss($css); |
| | | } |
| | | |
| | | protected function minifyCSS(string $css): string |
| | | { |
| | | // Remove comments |
| | | $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css); |
| | | // Remove whitespace |
| | | $css = preg_replace('/\s+/', ' ', $css); |
| | | // Remove spaces around specific characters |
| | | $css = preg_replace('/\s*([:;{}])\s*/', '$1', $css); |
| | | |
| | | return trim($css); |
| | | } |
| | | public function getCSSIcon(string $icon, ?string $style=null):string |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | | } |
| | | $svg = $this->getEncodedSVG($icon, $style); |
| | | if ($svg !== '') { |
| | | return "data:image/svg+xml;base64,{$svg}"; |
| | | } |
| | | return ''; |
| | | } |
| | | public function getEncodedSVG(string $icon, ?string $style = null):string |
| | | { |
| | | if (!$style) { |
| | | $style = $this->style; |
| | | } |
| | | return $this->cache->remember($style.$icon, |
| | | function () use ($icon, $style) { |
| | | $svg = $this->getRawSvg($icon, $style); |
| | | if ($svg) { |
| | | return base64_encode($svg); |
| | | } |
| | | return ''; |
| | | }); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Clear icon cache (useful for development/debugging) |
| | | */ |
| | | public function clearIconCache(): void |
| | | { |
| | | delete_option(BASE . 'icon_usage_list'); // Clear DB option |
| | | $this->cache->delete('icon_styles_css'); |
| | | CacheManager::updateTimestamp('icons'); |
| | | } |
| | | } |
| | |
| | | protected ?MetaForm $metaForm = null; |
| | | protected EmailManager $emailManager; |
| | | protected AjaxRateLimiter $rateLimiter; |
| | | protected CacheManager $cache; |
| | | |
| | | |
| | | protected array $forms =[]; |
| | |
| | | protected string $title = ''; |
| | | |
| | | // Token handlers registry |
| | | protected array $tokenHandlers = []; |
| | | protected array $messageHandlers = []; |
| | | |
| | | private array $allowed_file_types = [ |
| | |
| | | public function __construct() |
| | | { |
| | | $this->siteFeatures = Features::forSite(); |
| | | $this->metaForm = new MetaForm(); |
| | | $this->emailManager = new EmailManager(); |
| | | $this->rateLimiter = new AjaxRateLimiter(); |
| | | |
| | | // Register default token handlers |
| | | $this->registerDefaultHandlers(); |
| | | $this->cache = CacheManager::for('login'); |
| | | |
| | | // Initialize magic link support if enabled |
| | | if ($this->siteFeatures->has('magicLink')) { |
| | |
| | | |
| | | add_action('wp_enqueue_scripts', [$this, 'enqueueScripts'], 15); |
| | | |
| | | // Handle form submissions via AJAX |
| | | add_action('wp_ajax_nopriv_jvb_login', [$this, 'handleAjaxLogin']); |
| | | add_action('wp_ajax_nopriv_jvb_register', [$this, 'handleAjaxRegister']); |
| | | add_action('wp_ajax_nopriv_jvb_lostpassword', [$this, 'handleAjaxLostPassword']); |
| | | add_action('wp_ajax_nopriv_jvb_resetpass', [$this, 'handleAjaxResetPassword']); |
| | | |
| | | // Login success handling |
| | | add_action('wp_login', [$this, 'handleSuccessfulLogin'], 10, 2); |
| | | |
| | |
| | | */ |
| | | public function redirectToCustomLogin(): void |
| | | { |
| | | // Handle interim login |
| | | if (isset($_GET['interim-login'])) { |
| | | // Don't redirect - let WP handle it |
| | | return; |
| | | } |
| | | // Don't redirect if AJAX or REST |
| | | if ((defined('DOING_AJAX') && DOING_AJAX) || (defined('REST_REQUEST') && REST_REQUEST)) { |
| | | return; |
| | |
| | | $select = []; |
| | | //Basic fields, for any |
| | | $fields = [ |
| | | 'name' => [ |
| | | 'user_name' => [ |
| | | 'type' => 'text', |
| | | 'required' => true, |
| | | 'label' => 'Your Name', |
| | | 'placeholder'=> 'Mister Meseeks' |
| | | ], |
| | | 'email' => [ |
| | | 'user_email' => [ |
| | | 'type' => 'email', |
| | | 'required' => true, |
| | | 'label' => 'Your Email', |
| | | 'placeholder'=> 'look@me.com' |
| | | ] |
| | | ]; |
| | | if (Features::forSite()->has('referrals')) { |
| | | $fields['referral_code'] = [ |
| | | 'type' => 'text', |
| | | 'required'=> false, |
| | | 'label' => 'Referral Code', |
| | | 'hint' => 'Have a referral code? Paste it here!' |
| | | ]; |
| | | } |
| | | if (count(JVB_USER) > 1) { |
| | | foreach (JVB_USER as $slug => $config) { |
| | | if (!array_key_exists('can_register', $config) || !$config['can_register']) { |
| | |
| | | $fields = $this->getRegistrationFormFields(); |
| | | break; |
| | | case 'lostpassword': |
| | | case 'magic': |
| | | $fields = [ |
| | | 'user_email' => [ |
| | | 'type' => 'email', |
| | |
| | | return $self->isLoginPage(); |
| | | } |
| | | |
| | | /************************************************************************** |
| | | TOKEN & MESSAGE HANDLERS |
| | | Extensible by other classes |
| | | **************************************************************************/ |
| | | public function registerTokenHandler(string $token_key, callable $handler, int $priority = 10): void |
| | | { |
| | | if (!isset($this->tokenHandlers[$priority])) { |
| | | $this->tokenHandlers[$priority] = []; |
| | | } |
| | | |
| | | $this->tokenHandlers[$priority][$token_key] = $handler; |
| | | ksort($this->tokenHandlers); |
| | | } |
| | | |
| | | public function registerMessageHandler(string $type, callable $handler, ?callable $condition = null): void |
| | | { |
| | | $this->messageHandlers[$type] = [ |
| | | 'handler' => $handler, |
| | | 'condition' => $condition |
| | | ]; |
| | | } |
| | | |
| | | protected function registerDefaultHandlers(): void |
| | | { |
| | | // Invitation handler |
| | | if ($this->siteFeatures->has('invitations')) { |
| | | $this->registerTokenHandler('invite', function($token, $email, $user_id) { |
| | | if (isset($_POST['invite_token'])) { |
| | | JVB()->routes('invites')->acceptInvitation( |
| | | sanitize_text_field($_POST['invite_token']), |
| | | sanitize_email($_POST['invite_email']), |
| | | $user_id |
| | | ); |
| | | } |
| | | }); |
| | | |
| | | $this->registerMessageHandler('invitation', |
| | | function() { |
| | | $data = JVB()->routes('invites')->verifyInvitation( |
| | | sanitize_text_field($_GET['invite']), |
| | | sanitize_email($_GET['email']) |
| | | ); |
| | | $name = $data->name; |
| | | $inviters = json_decode($data->inviters, true); |
| | | $names = []; |
| | | |
| | | foreach ($inviters as $inviter) { |
| | | $artist = jvbContentFromUser((int)$inviter['user_id']); |
| | | $names[] = ($artist['name'] === '') ? $artist['display_name'] : $artist['name']; |
| | | } |
| | | |
| | | $message = (count($names) > 1) |
| | | ? 'are already here, and have invited you to join in!' |
| | | : ' is already here, and invited you to join in!'; |
| | | |
| | | return '<h2>Join the Scene, '.$name.'</h2> |
| | | <p style="text-align:center;">'.jvbCommaList($names).$message.'</p>'; |
| | | }, |
| | | function() { |
| | | return isset($_GET['invite']) && isset($_GET['email']); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | // List sharing handler (Favourites) |
| | | if ($this->siteFeatures->has('favourites')) { |
| | | $this->registerTokenHandler('list_token', function($token, $email, $user_id) { |
| | | if (!empty($_GET['list_token']) && !empty($_GET['email'])) { |
| | | JVB()->routes('favourites')->acceptListInvitation( |
| | | sanitize_text_field($_GET['list_token']), |
| | | sanitize_email($_GET['email']), |
| | | $user_id |
| | | ); |
| | | } |
| | | }); |
| | | |
| | | $this->registerMessageHandler('favourites', |
| | | function() { |
| | | return '<h2>'.(JVB_LOGIN['login_from_favourite_header'] ?? 'Save your Favourites').'</h2>'; |
| | | }, |
| | | function() { |
| | | return isset($_GET['type']) && $_GET['type'] === 'favourites'; |
| | | } |
| | | ); |
| | | } |
| | | |
| | | // Referral handler - FIXED VERSION |
| | | $this->registerTokenHandler('referral_code', function($code, $email, $user_id) { |
| | | // $code is already sanitized from processTokenHandlers |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | } |
| | | $_SESSION[BASE . 'referral_code'] = $code; |
| | | setcookie( |
| | | BASE . 'referral_code', |
| | | $code, |
| | | time() + (86400 * 30), |
| | | '/' |
| | | ); |
| | | }, 5); |
| | | } |
| | | |
| | | protected function initMagicLinkSupport(): void |
| | | { |
| | | if (!Features::forSite()->has('magicLink')) { |
| | |
| | | $this->magicLink = new MagicLinkManager(); |
| | | } |
| | | |
| | | |
| | | |
| | | /********************************************************************* |
| | | RENDERING |
| | | *********************************************************************/ |
| | |
| | | return $template; |
| | | } |
| | | $this->setup(); |
| | | $page = $this->cache->remember( |
| | | $this->getAction(), |
| | | function() { |
| | | return $this->renderPage(); |
| | | }, |
| | | 5 |
| | | ); |
| | | |
| | | echo $page; |
| | | return ''; |
| | | } |
| | | protected function renderPage() { |
| | | ob_start(); |
| | | jvbInlineStyles('nav'); |
| | | jvbInlineStyles('dash'); |
| | |
| | | $this->renderForms(); |
| | | $this->renderFooter(); |
| | | |
| | | echo ob_get_clean(); |
| | | return ''; |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | protected function setup():void |
| | | protected function getAction():string |
| | | { |
| | | if (array_key_exists('action', $_GET)) { |
| | | switch ($_GET['action']){ |
| | |
| | | } else { |
| | | $action = 'login'; |
| | | } |
| | | return $action; |
| | | } |
| | | |
| | | $this->action = $action; |
| | | protected function setup():void |
| | | { |
| | | $this->action = $this->getAction(); |
| | | if (in_array($this->action, ['logout']) || array_key_exists('loggedout', $_GET)) { |
| | | wp_logout(); |
| | | wp_redirect(esc_attr($_GET['redirect_to'] ?? get_home_url())); |
| | | exit; |
| | | } |
| | | if (in_array($this->action, ['rp', 'resetpass']) && !is_user_logged_in()) { |
| | | wp_redirect(wp_login_url()); |
| | | exit; |
| | | } elseif (is_user_logged_in()) { |
| | | wp_redirect(get_home_url(null, '/dash/')); |
| | | } |
| | | $this->setupLabels(); |
| | | $this->setupFields(); |
| | | $this->setupTitle(); |
| | |
| | | justify-content: center; |
| | | position: relative; |
| | | } |
| | | .login .fstatus.fstatus { |
| | | --wrap: nowrap; |
| | | top:0; |
| | | bottom:unset; |
| | | right: 0; |
| | | } |
| | | .login main::before { |
| | | background-size: 20vw; |
| | | inset: 0; |
| | |
| | | |
| | | protected function renderForms():void |
| | | { |
| | | |
| | | $this->metaForm = new MetaForm(); |
| | | $form = $this->action.'form'; |
| | | |
| | | ?> |
| | | <section class="login-box col btw"> |
| | | <h1><?=$this->labels['title']?></h1> |
| | | <?= $this->labels['description'] ?> |
| | | |
| | | |
| | | <form name="<?=$form?>" method="post" data-action="jvb_<?=$this->action?>"> |
| | | <?= jvbFormStatus() ?> |
| | | <?php wp_nonce_field('jvb_'.$this->action, '_wpnonce'); ?> |
| | | <input type="hidden" name="action" value="jvb_<?=$this->action?>"> |
| | | <input type="hidden" name="redirect_to" value="<?= esc_attr($_GET['redirect_to'] ?? '') ?>"> |
| | | <input type="hidden" name="request_id" value="<?= wp_generate_password(16, false) ?>"> |
| | | |
| | | <?= ($this->action === 'magic') ? '<input type="hidden" name="type" value="login">' : '' ?> |
| | | <?php |
| | | $this->addHiddenTokenFields(); |
| | | do_action('jvb_add_token_inputs', $this->action); |
| | | |
| | | foreach ($this->fields as $name => $config) { |
| | | $this->metaForm->render($name, '', $config); |
| | |
| | | $this->maybeTurnstile(); |
| | | ?> |
| | | <div class="row btw nowrap"> |
| | | <button type="submit" class="button button-primary button-large">Log In</button> |
| | | <button type="submit" class="button button-primary button-large"><?=$this->labels['submit']?></button> |
| | | <?php $this->maybeMagicLink(); ?> |
| | | </div> |
| | | </form> |
| | |
| | | <a href="<?= add_query_arg('action', 'lostpassword', get_the_permalink()) ?>">Forgot Password?</a> |
| | | <?php |
| | | break; |
| | | case 'lostpassword': ?> |
| | | case 'lostpassword': |
| | | case 'magic': ?> |
| | | <a href="<?= get_the_permalink() ?>">Login Instead</a> |
| | | <a href="<?= add_query_arg('action', 'register', get_the_permalink()) ?>">Create Account</a> |
| | | <?php |
| | |
| | | $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode'; |
| | | echo '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher"> |
| | | <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'. |
| | | jvbIcon('light', ['title'=> 'Light Mode']). |
| | | jvbIcon('dark', ['title'=>'Dark Mode']). |
| | | jvbIcon('sun-dim', ['title'=> 'Light Mode']). |
| | | jvbIcon('moon', ['title'=>'Dark Mode']). |
| | | '</span></label>'; |
| | | ?> |
| | | <p class="title"> |
| | |
| | | } |
| | | } |
| | | if ($out == '') { |
| | | $out =jvbIcon('home'); |
| | | $out =jvbIcon('house'); |
| | | } |
| | | ?><?= $out ?> |
| | | </a> |
| | |
| | | <?php |
| | | } |
| | | |
| | | protected function addHiddenTokenFields(): void |
| | | { |
| | | foreach ($this->tokenHandlers as $priority => $handlers) { |
| | | foreach ($handlers as $token_key => $handler) { |
| | | if (isset($_GET[$token_key])) { |
| | | $value = sanitize_text_field($_GET[$token_key]); |
| | | echo '<input type="hidden" name="' . esc_attr($token_key) . '" value="' . esc_attr($value) . '">'; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (isset($_GET['email'])) { |
| | | echo '<input type="hidden" name="token_email" value="' . esc_attr(sanitize_email($_GET['email'])) . '">'; |
| | | } |
| | | } |
| | | |
| | | /************************************************************************* |
| | | AJAX HANDLERS |
| | | *************************************************************************/ |
| | | public function handleAjaxLogin(): void |
| | | { |
| | | check_ajax_referer('jvb_login', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('login')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | // Duplicate submission check |
| | | if (!$this->checkRequestId()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Duplicate request detected', |
| | | 'code' => 'duplicate_request' |
| | | ], 409); |
| | | } |
| | | |
| | | $email = sanitize_email($_POST['user_email'] ?? ''); |
| | | $password = $_POST['user_password'] ?? ''; |
| | | $remember = !empty($_POST['remember_me']); |
| | | |
| | | if (empty($email) || empty($password)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Please fill in all fields', |
| | | 'field' => empty($email) ? 'user_email' : 'user_password', |
| | | 'code' => 'missing_fields' |
| | | ]); |
| | | } |
| | | |
| | | // Verify Turnstile if enabled |
| | | if (!$this->verifyTurnstile()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Security verification failed', |
| | | 'code' => 'turnstile_failed' |
| | | ]); |
| | | } |
| | | |
| | | $user = get_user_by('email', $email); |
| | | if (!$user) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Unknown email address', |
| | | 'field' => 'user_email', |
| | | 'code' => 'invalid_email' |
| | | ]); |
| | | } |
| | | |
| | | $user = wp_authenticate($user->user_login, $password); |
| | | |
| | | if (is_wp_error($user)) { |
| | | wp_send_json_error([ |
| | | 'message' => $user->get_error_message(), |
| | | 'field' => 'user_password', |
| | | 'code' => $user->get_error_code() |
| | | ]); |
| | | } |
| | | |
| | | wp_clear_auth_cookie(); |
| | | wp_set_current_user($user->ID); |
| | | wp_set_auth_cookie($user->ID, $remember); |
| | | |
| | | do_action('wp_login', $user->user_login, $user); |
| | | |
| | | $redirect = $_POST['redirect_to'] ?? home_url('/dash'); |
| | | wp_send_json_success(['redirect' => $redirect]); |
| | | } |
| | | |
| | | public function handleAjaxRegister(): void |
| | | { |
| | | check_ajax_referer('jvb_register', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('register')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | // Duplicate submission check |
| | | if (!$this->checkRequestId()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Duplicate request detected', |
| | | 'code' => 'duplicate_request' |
| | | ], 409); |
| | | } |
| | | |
| | | // Verify Turnstile |
| | | if (!$this->verifyTurnstile()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Security verification failed', |
| | | 'code' => 'turnstile_failed' |
| | | ]); |
| | | } |
| | | |
| | | $name = sanitize_text_field($_POST['name'] ?? ''); |
| | | $email = sanitize_email($_POST['email'] ?? ''); |
| | | $user_type = sanitize_text_field($_POST['user_select'] ?? 'subscriber'); |
| | | |
| | | // Spam prevention - if subscriber is selected and there are other options |
| | | if ($user_type === 'subscriber' && count(JVB_USER) > 0) { |
| | | $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false); |
| | | if (!empty($registerable)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Please select a valid account type', |
| | | 'field' => 'user_select', |
| | | 'code' => 'invalid_user_type' |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | // Validate fields |
| | | if (empty($name)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Name is required', |
| | | 'field' => 'name', |
| | | 'code' => 'missing_name' |
| | | ]); |
| | | } |
| | | |
| | | if (empty($email)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Email is required', |
| | | 'field' => 'email', |
| | | 'code' => 'missing_email' |
| | | ]); |
| | | } |
| | | |
| | | // Check if role can register |
| | | if ($user_type !== 'subscriber') { |
| | | if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Invalid account type', |
| | | 'field' => 'user_select', |
| | | 'code' => 'invalid_user_type' |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | // Check if email exists |
| | | if (email_exists($email)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Email already registered', |
| | | 'field' => 'email', |
| | | 'code' => 'duplicate_email' |
| | | ]); |
| | | } |
| | | |
| | | // Create user |
| | | $user_id = wp_create_user($email, wp_generate_password(), $email); |
| | | |
| | | if (is_wp_error($user_id)) { |
| | | wp_send_json_error([ |
| | | 'message' => $user_id->get_error_message(), |
| | | 'code' => 'user_creation_failed' |
| | | ]); |
| | | } |
| | | |
| | | // Update user data |
| | | wp_update_user([ |
| | | 'ID' => $user_id, |
| | | 'display_name' => $name, |
| | | 'first_name' => $name |
| | | ]); |
| | | |
| | | // Set role |
| | | $user = new WP_User($user_id); |
| | | if ($user_type === 'subscriber') { |
| | | $user->set_role('subscriber'); |
| | | } else { |
| | | $role = JVB_USER[$user_type]['role'] ?? 'subscriber'; |
| | | $user->set_role($role); |
| | | |
| | | // Check if needs approval |
| | | if (Features::forMembership()->has('memberVerified') && |
| | | in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) { |
| | | $user->add_cap('skip_moderation', false); |
| | | update_user_meta($user_id, BASE . 'pending_approval', true); |
| | | } |
| | | } |
| | | |
| | | // Save additional fields |
| | | update_user_meta($user_id, BASE . 'user_type', $user_type); |
| | | |
| | | // Process additional fields from form |
| | | foreach ($_POST as $key => $value) { |
| | | if (in_array($key, ['name', 'email', 'action', '_wpnonce', 'request_id', 'user_select'])) { |
| | | continue; |
| | | } |
| | | update_user_meta($user_id, BASE . $key, sanitize_text_field($value)); |
| | | } |
| | | |
| | | // Handle token handlers |
| | | $this->processTokenHandlers($user_id, $email); |
| | | |
| | | // Send welcome email with password setup link |
| | | $this->sendWelcomeEmail($user_id); |
| | | |
| | | // Trigger registration action for other systems |
| | | do_action('jvbAfterUserRegistration', $user_id, $user_type, $_POST); |
| | | |
| | | wp_send_json_success([ |
| | | 'message' => 'Registration successful! Check your email.', |
| | | 'title' => $this->labels['successTitle'] ?? 'Success!', |
| | | 'description' => $this->labels['successDescription'] ?? 'Check your email for next steps', |
| | | 'user_id' => $user_id // Important for file upload dependencies! |
| | | ]); |
| | | } |
| | | |
| | | public function handleAjaxLostPassword(): void |
| | | { |
| | | check_ajax_referer('jvb_lostpassword', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('lostpassword')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | $email = sanitize_email($_POST['user_email'] ?? ''); |
| | | |
| | | if (empty($email)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Email required', |
| | | 'field' => 'user_email', |
| | | 'code' => 'missing_email' |
| | | ]); |
| | | } |
| | | |
| | | // Verify Turnstile |
| | | if (!$this->verifyTurnstile()) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Security verification failed', |
| | | 'code' => 'turnstile_failed' |
| | | ]); |
| | | } |
| | | |
| | | // Use WordPress's built-in function |
| | | $result = retrieve_password($email); |
| | | |
| | | if (is_wp_error($result)) { |
| | | wp_send_json_error([ |
| | | 'message' => $result->get_error_message(), |
| | | 'code' => $result->get_error_code() |
| | | ]); |
| | | } |
| | | |
| | | wp_send_json_success(['message' => 'Check your email for reset link']); |
| | | } |
| | | |
| | | public function handleAjaxResetPassword(): void |
| | | { |
| | | check_ajax_referer('jvb_resetpass', '_wpnonce'); |
| | | |
| | | // Rate limiting |
| | | if (!$this->checkAjaxRateLimit('resetpass')) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Too many attempts. Please wait a moment.', |
| | | 'code' => 'rate_limit' |
| | | ], 429); |
| | | } |
| | | |
| | | $key = sanitize_text_field($_POST['key'] ?? $_GET['key'] ?? ''); |
| | | $login = sanitize_text_field($_POST['login'] ?? $_GET['login'] ?? ''); |
| | | $pass1 = $_POST['pass1'] ?? ''; |
| | | $pass2 = $_POST['pass2'] ?? ''; |
| | | |
| | | if (empty($key) || empty($login)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Invalid reset link', |
| | | 'code' => 'invalid_key' |
| | | ]); |
| | | } |
| | | |
| | | if (empty($pass1) || empty($pass2)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Please enter a password', |
| | | 'field' => empty($pass1) ? 'pass1' : 'pass2', |
| | | 'code' => 'missing_password' |
| | | ]); |
| | | } |
| | | |
| | | if ($pass1 !== $pass2) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Passwords do not match', |
| | | 'field' => 'pass2', |
| | | 'code' => 'password_mismatch' |
| | | ]); |
| | | } |
| | | |
| | | // Verify reset key |
| | | $user = check_password_reset_key($key, $login); |
| | | |
| | | if (is_wp_error($user)) { |
| | | wp_send_json_error([ |
| | | 'message' => 'Invalid or expired reset link', |
| | | 'code' => 'invalid_key' |
| | | ]); |
| | | } |
| | | |
| | | // Reset password |
| | | reset_password($user, $pass1); |
| | | |
| | | wp_send_json_success([ |
| | | 'message' => 'Password reset successfully', |
| | | 'redirect' => home_url('/login') |
| | | ]); |
| | | } |
| | | |
| | | |
| | | /********************************************************************** |
| | | TOKEN PROCESSING |
| | | **********************************************************************/ |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | /*********************************************************************** |
| | | EMAIL SENDING |
| | | ***********************************************************************/ |
| | | protected function sendWelcomeEmail(int $user_id): void |
| | | { |
| | | $user = get_userdata($user_id); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | // Generate password reset key |
| | | $key = get_password_reset_key($user); |
| | | if (is_wp_error($key)) { |
| | | error_log('Failed to generate password reset key: ' . $key->get_error_message()); |
| | | return; |
| | | } |
| | | |
| | | $reset_url = add_query_arg([ |
| | | 'action' => 'rp', |
| | | 'key' => $key, |
| | | 'login' => rawurlencode($user->user_login) |
| | | ], home_url('/login')); |
| | | |
| | | $subject = $this->labels['email'] ?? 'Welcome to ' . get_bloginfo('name'); |
| | | |
| | | $message = '<h2>Welcome, ' . esc_html($user->display_name) . '!</h2>'; |
| | | $message .= '<p>Your account has been created. Click the button below to set your password and get started:</p>'; |
| | | $message .= jvbMailButton($reset_url, 'Set Your Password'); |
| | | $message .= '<p>This link expires in 24 hours.</p>'; |
| | | |
| | | $this->emailManager->sendEmail($user->user_email, $subject, $message); |
| | | } |
| | | |
| | | /************************************************************************* |
| | | * SECURITY & VALIDATION |
| | | *************************************************************************/ |
| | |
| | | protected function setupLabels(): void |
| | | { |
| | | $default = $this->getDefaultLabels(); |
| | | $this->labels = apply_filters('jvbLoginLabels', $default, $_GET); |
| | | $default = apply_filters('jvbLoginLabels', $default, $_GET); |
| | | |
| | | foreach (['description', 'footer', 'extra'] as $location) { |
| | | $text = (!is_array($this->labels[$location])) ? [$this->labels[$location]] : $this->labels[$location]; |
| | | if (!empty($text)) { |
| | | $this->labels[$location] = '<div class="'.$location.'">'; |
| | | foreach ($text as $d) { |
| | | $this->labels[$location] .= '<p>'.$d.'</p>'; |
| | | if(array_key_exists('type', $_GET) && $_GET['type'] === 'favourites') { |
| | | if (array_key_exists('favourites', JVB_LOGIN)) { |
| | | foreach (JVB_LOGIN['favourites'] as $key => $value) { |
| | | $default[$key] = $value; |
| | | } |
| | | $this->labels[$location] .= '</div>'; |
| | | } |
| | | } |
| | | |
| | | foreach (['description', 'footer', 'extra'] as $location) { |
| | | if ($default[$location] === '') { |
| | | continue; |
| | | } |
| | | if (empty($default[$location])) { |
| | | $default[$location] = ''; |
| | | continue; |
| | | } |
| | | $text = (!is_array($default[$location])) ? [$default[$location]] : $default[$location]; |
| | | |
| | | if (!empty($text)) { |
| | | $default[$location] = '<div class="'.$location.'">'; |
| | | foreach ($text as $d) { |
| | | $default[$location] .= '<p>'.$d.'</p>'; |
| | | } |
| | | $default[$location] .= '</div>'; |
| | | } |
| | | } |
| | | $this->labels = $default; |
| | | } |
| | | |
| | | protected function getDefaultLabels(): array |
| | |
| | | 'footer' => JVB_LOGIN['reset_pass']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['reset_pass']['submit'] ?? 'Reset Password', |
| | | ]; |
| | | case 'logout': |
| | | return [ |
| | | 'title' => JVB_LOGIN['logout']['title'] ?? 'Logged Out!', |
| | | 'description' => JVB_LOGIN['logout']['description'] ?? [], |
| | | 'extra' => JVB_LOGIN['logout']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['logout']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['logout']['submit'] ?? '', |
| | | ]; |
| | | case 'magic': |
| | | return [ |
| | | 'title' => JVB_LOGIN['magic']['title'] ?? 'Log in with Magic Link', |
| | | 'description' => JVB_LOGIN['magic']['description'] ?? ['Enter your email.','You\'ll get an email with a magic link.','Click it, and you\'re logged in!'], |
| | | 'extra' => JVB_LOGIN['magic']['extra'] ?? [], |
| | | 'footer' => JVB_LOGIN['magic']['footer'] ?? '', |
| | | 'submit' => JVB_LOGIN['magic']['submit'] ?? jvbIcon('magic-wand').'Send Magic Link', |
| | | |
| | | ]; |
| | | case 'login': |
| | | default: |
| | | return [ |
| | |
| | | return; |
| | | } |
| | | ?> |
| | | <button type="button" id="magic-link-btn" class="button button-secondary button-large"> |
| | | <?= jvbIcon('email', ['size' => 20]); ?> |
| | | Get Login Link |
| | | </button> |
| | | <script type="text/javascript"> |
| | | document.getElementById('magic-link-btn')?.addEventListener('click', function(e) { |
| | | e.preventDefault(); |
| | | const email = document.querySelector('input[name="user_email"]')?.value; |
| | | if (!email) { |
| | | alert('Please enter your email address first'); |
| | | return; |
| | | } |
| | | |
| | | fetch('<?= rest_url('jvb/v1/magic-link'); ?>', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': '<?= wp_create_nonce('wp_rest') ?>' |
| | | }, |
| | | body: JSON.stringify({ email: email, type: 'login' }) |
| | | }) |
| | | .then(r => r.json()) |
| | | .then(data => { |
| | | alert(data.success ? 'Check your email!' : (data.message || 'Failed to send link')); |
| | | }); |
| | | }); |
| | | </script> |
| | | <a class="button" href="<?= add_query_arg('action', 'magic', wp_login_url()) ?>" title="Email yourself a link to log you in auto-magically!"> |
| | | <?= jvbIcon('magic-wand'); ?> |
| | | Magic Link |
| | | </a> |
| | | <?php |
| | | } |
| | | |
| | |
| | | |
| | | $this->maybeTurnstileScripts(); |
| | | wp_enqueue_script('jvb-form'); |
| | | |
| | | $script = " |
| | | $action = $this->getAction(); |
| | | ob_start(); |
| | | ?> |
| | | <script type="text/javascript"> |
| | | window.checkedEmails = new Set(); |
| | | document.addEventListener('DOMContentLoaded', () => { |
| | | const form = document.querySelector('.login form'); |
| | | if (form && window.jvbForm) { |
| | | let controller = new window.jvbForm(); |
| | | controller.registerForm(form, { |
| | | autosave: false, |
| | | endpoint: false |
| | | }); |
| | | } else if (form && !window.jvbForm) { |
| | | if (!form) return; |
| | | |
| | | if (!window.jvbForm) { |
| | | console.error('jvbForm not loaded'); |
| | | return; |
| | | } |
| | | });"; |
| | | |
| | | window.LoginController = new window.jvbForm(); |
| | | window.LoginController.registerForm(form, { |
| | | autosave: false, |
| | | endpoint: <?= "'{$action}'" ?>, |
| | | formStatus: false, |
| | | cache: false, |
| | | }); |
| | | |
| | | window.LoginController.subscribe((event, data) => { |
| | | if (event === 'form-submit') { |
| | | handleFormSubmission(data); |
| | | } else if (event === 'field-validated' && data.name === 'user_email') { |
| | | if (!window.checkedEmails.has(data.value)){ |
| | | checkEmail(data.value, data); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | |
| | | async function handleFormSubmission(data) { |
| | | const { formId, config, data: formData } = data; |
| | | const form = config.element; |
| | | const submit = form.querySelector('[type=submit]'); |
| | | let oldText = submit.textContent; |
| | | // Show uploading status |
| | | window.LoginController.showFormStatus(formId, 'uploading'); |
| | | |
| | | try { |
| | | submit.disabled = true; |
| | | submit.textContent = 'Loading...'; |
| | | const response = await fetch(`${jvbSettings.api}<?=($action === 'magic') ? $action : 'auth/'.$action?>`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': jvbSettings.nonce |
| | | }, |
| | | body: JSON.stringify(formData) |
| | | }); |
| | | |
| | | const result = await response.json(); |
| | | |
| | | // Handle errors |
| | | if (!response.ok) { |
| | | window.LoginController.showFormStatus(formId, 'error'); |
| | | window.LoginController.handleFormError(form, result); |
| | | return; |
| | | } |
| | | |
| | | // Handle success |
| | | window.LoginController.showFormStatus(formId, 'submitted'); |
| | | |
| | | // Show success message briefly before redirect |
| | | if (result.message) { |
| | | window.LoginController.handleFormSuccess(form, result); |
| | | } |
| | | |
| | | // Handle redirect |
| | | if (result.redirect) { |
| | | setTimeout(() => { |
| | | window.location.href = result.redirect; |
| | | }, 500); // Brief delay to show success message |
| | | } |
| | | |
| | | } catch (error) { |
| | | console.error('Form submission error:', error); |
| | | window.LoginController.showFormStatus(formId, 'error'); |
| | | window.LoginController.handleFormError(form, { |
| | | message: 'Network error. Please check your connection and try again.', |
| | | code: 'network_error' |
| | | }); |
| | | } finally { |
| | | submit.textContent = oldText; |
| | | submit.disabled = false; |
| | | } |
| | | } |
| | | async function checkEmail(email, input) { |
| | | window.checkedEmails.add(email); |
| | | let wrapper = input.closest('.field'); |
| | | let submit = input.closest('form').querySelector('[type=submit]'); |
| | | try { |
| | | submit.disabled = true; |
| | | window.LoginController.showSuccess(wrapper, 'Checking our records...'); |
| | | const response = await fetch(`${jvbSettings.api}auth/email`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'X-WP-Nonce': jvbSettings.nonce |
| | | }, |
| | | body: JSON.stringify({ email: email }) |
| | | }); |
| | | |
| | | const result = await response.json(); |
| | | |
| | | if (!response.ok) { |
| | | return; // On error, allow to proceed (fail open) |
| | | } |
| | | |
| | | if (result.exists) { |
| | | <?php |
| | | switch ($action) { |
| | | case 'register': |
| | | echo 'window.LoginController.showError(wrapper,\'This email is already registered. Log in instead?\');'; |
| | | break; |
| | | case 'login': |
| | | case 'lostpassword': |
| | | case 'magic': |
| | | echo 'window.LoginController.showSuccess(wrapper,\'Email exists in our system.\');'; |
| | | break; |
| | | } |
| | | ?> |
| | | } else { |
| | | <?php |
| | | switch ($action) { |
| | | case 'register': |
| | | echo 'window.LoginController.showSuccess(wrapper,\'Email is available!\');'; |
| | | break; |
| | | case 'login': |
| | | case 'lostpassword': |
| | | case 'magic': |
| | | echo 'window.LoginController.showError(wrapper,\'This email doesn\\\'t seem to exist in our system. Create account instead?\');'; |
| | | break; |
| | | } |
| | | ?> |
| | | } |
| | | |
| | | return true; // Email is available |
| | | } catch (error) { |
| | | console.error('Email check failed:', error); |
| | | return true; // On network error, allow to proceed |
| | | }finally { |
| | | submit.disabled = false; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | |
| | | </script> |
| | | <?php |
| | | $script = ob_get_clean(); |
| | | |
| | | wp_add_inline_script('jvb-form', $script); |
| | | } |
| | |
| | | <script type="text/javascript"> |
| | | document.addEventListener('DOMContentLoaded', function() { |
| | | let loginLabel = document.querySelector('label[for="user_login"'); |
| | | loginLabel.innerHTML = '<?= jvbIcon('email', ['size' => 20]); ?> Your Email'; |
| | | loginLabel.innerHTML = '<?= jvbIcon('envelope', ['size' => 20]); ?> Your Email'; |
| | | |
| | | let passwordLabel = document.querySelector('label[for="user_pass"'); |
| | | passwordLabel.innerHTML = '<?= jvbIcon('password', ['size' => 20]); ?> Your Password'; |
| | |
| | | <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>> |
| | | <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label> |
| | | <input type="radio" id="artist" name="user_type" value="artist" required> |
| | | <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label> |
| | | <label for="artist"><?=jvbIcon('drop-simple', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label> |
| | | <input type="radio" id="partner" name="user_type" value="partner" required> |
| | | <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label> |
| | | <label for="partner"><?=jvbIcon('currency-circle-dollar', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label> |
| | | <p class="enthusiast">Save your favourites. Get notified.</p> |
| | | <p class="artist">Show off your work.</p> |
| | | <p class="partner">Support the community.</p> |
| | |
| | | // Hook into WordPress auth flow |
| | | add_action('template_redirect', [$this, 'handleMagicLinkClick']); |
| | | add_action('wp_login_failed', [$this, 'handleFailedLogin']); |
| | | |
| | | // NOTE: LoginManager now handles the login form UI |
| | | // If magic_link integration is enabled, LoginManager will call addMagicLinkOption() |
| | | add_action('jvb_process_login_tokens', [$this, 'processRegistrationToken'], 10, 3); |
| | | } |
| | | |
| | | /** |
| | |
| | | /** |
| | | * Verify a token |
| | | */ |
| | | protected function verifyToken(string $token, string $email): array|WP_Error |
| | | public function verifyToken(string $token, string $email): array|WP_Error |
| | | { |
| | | $token_data = $this->cache->get($token); |
| | | |
| | |
| | | |
| | | $magic_url = add_query_arg([ |
| | | 'magic_token' => $token, |
| | | 'email' => urlencode($email), |
| | | 'email' => rawurlencode($email), |
| | | 'action' => 'magic_login' |
| | | ], home_url('/')); |
| | | |
| | |
| | | |
| | | $magic_url = add_query_arg([ |
| | | 'magic_token' => $token, |
| | | 'email' => urlencode($email), |
| | | 'email' => rawurlencode($email), |
| | | 'action' => 'magic_signup' |
| | | ], home_url('/')); |
| | | |
| | |
| | | |
| | | $magic_url = add_query_arg([ |
| | | 'magic_token' => $token, |
| | | 'email' => urlencode($email), |
| | | 'email' => rawurlencode($email), |
| | | 'action' => 'magic_referral' |
| | | ], home_url('/')); |
| | | |
| | |
| | | |
| | | $magic_url = add_query_arg([ |
| | | 'magic_token' => $token, |
| | | 'email' => urlencode($email), |
| | | 'email' => rawurlencode($email), |
| | | 'action' => 'magic_reset' |
| | | ], home_url('/')); |
| | | |
| | |
| | | */ |
| | | public function trackContentCreation(int $post_id, WP_POST $post, bool $update):void |
| | | { |
| | | // SAFETY: Skip attachments and other non-content post types |
| | | if (in_array($post->post_type, jvbIgnoredPostTypes())) { |
| | | return; |
| | | } |
| | | // Skip if not a published post |
| | | if ($post->post_status !== 'publish') { |
| | | return; |
| | |
| | | |
| | | use JVBase\managers\CacheManager; |
| | | use Exception; |
| | | use JVBase\utility\Features; |
| | | use WP_Error; |
| | | use WP_REST_Response; |
| | | use WP_REST_Request; |
| | |
| | | } |
| | | |
| | | protected function updateLastModified(int $user_id) { |
| | | JVB()->routes('queue')->updateUserQueueTimestamp($user_id); |
| | | if (Features::forSite()->has('dashboard')) { |
| | | JVB()->routes('queue')->updateUserQueueTimestamp($user_id); |
| | | } |
| | | } |
| | | |
| | | protected function deepMerge(array $existing, array $new): array |
| | |
| | | protected MagicLinkManager $magic_link; |
| | | protected CacheManager $cache; |
| | | protected string $referrals_table; |
| | | protected ?int $referralPage = null; |
| | | protected string $rewards_table; |
| | | |
| | | // Default reward settings |
| | |
| | | 'referee_reward_applies_to' => 'first_order' // 'first_order' or 'all_orders' |
| | | ]; |
| | | |
| | | protected array $settings; |
| | | |
| | | public function __construct() |
| | | { |
| | | global $wpdb; |
| | |
| | | $this->rewards_table = BASE . 'referral_rewards'; |
| | | $this->magic_link = new MagicLinkManager(); |
| | | |
| | | // Hook into user registration to track referrals |
| | | add_action('user_register', [$this, 'processReferral'], 10, 1); |
| | | $this->referralPage = $this->getReferralPageId(); |
| | | $this->settings = $this->getRewardSettings(); |
| | | |
| | | |
| | | add_action('jvbUserRegistered', [$this, 'processRegistrationToken'], 10, 3); |
| | | add_action('jvb_add_token_inputs', [$this, 'addLoginInputs'], 10, 1); |
| | | add_action('jvbUserRegistered', [$this, 'processReferral'], 10, 1); |
| | | |
| | | // Add meta boxes for admin to manage referrals |
| | | add_action('show_user_profile', [$this, 'displayUserReferralInfo']); |
| | |
| | | add_action('personal_options_update', [$this, 'saveUserReferralCode']); |
| | | add_action('edit_user_profile_update', [$this, 'saveUserReferralCode']); |
| | | |
| | | add_filter(BASE.'new_user_email_content', [$this, 'addReferralToWelcomeEmail'], 99, 2); |
| | | add_filter('jvbNewUserEmail', [$this, 'addReferralToWelcomeEmail'], 99, 2); |
| | | |
| | | |
| | | add_filter('jvbAdditionalActions', [$this, 'outputShareWidget']); |
| | |
| | | $this->registerCronJobs(); |
| | | |
| | | // Register admin subpage |
| | | add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1); |
| | | // add_filter('jvbAdminSubpages', [$this, 'addSubpage'], 10, 1); |
| | | |
| | | // Add admin bar label for referral page |
| | | add_action('admin_bar_menu', [$this, 'addReferralPageLabel'], 999); |
| | |
| | | add_action('admin_init', [$this, 'registerSettings']); |
| | | } |
| | | |
| | | public function addLoginInputs(string $action):void |
| | | { |
| | | if (array_key_exists('ref', $_GET)) { |
| | | echo '<input type="hidden" name="referral_code" value="'.$_GET['ref'].'">'; |
| | | } |
| | | } |
| | | |
| | | public function modifyLoginText(array $defaults):array |
| | | { |
| | | if (!array_key_exists('ref', $_GET)){ |
| | | return $defaults; |
| | | } |
| | | |
| | | $code = $_GET['ref']; |
| | | $user = $this->getUserByReferralCode($code); |
| | | |
| | | $desc = ($user) ? strtok($user->display_name, ' ') . ' invited you ' : 'You\'ve been invited '; |
| | | |
| | | $defaults['title'] = 'Register your Account. Get Your Reward.'; |
| | | $defaults['description'] = [ |
| | | $desc.' to see the difference with us.', |
| | | 'Oh, and you\'ll get 20% off your first treatment!', |
| | | 'Finish this account creation (you\'ll still need to make an account on Jane App to book), or let us know when you come in for your appointment.' |
| | | ]; |
| | | $defaults['extra'] = [ |
| | | 'Once you\'re in, you\'ll get your own code you can share.', |
| | | 'Get a $25 credit for <b>each</b> person you send our way who comes in for their first treatment!' |
| | | ]; |
| | | return $defaults; |
| | | } |
| | | |
| | | public function enqueueScripts():void |
| | | { |
| | | $requirements = [ |
| | |
| | | return !empty($users); |
| | | } |
| | | |
| | | public function processRegistrationToken(int $user_id, string $email, array $data): void |
| | | { |
| | | // Check for referral code in data |
| | | $code = $data['referral_code'] ?? ''; |
| | | if (empty($code)) { |
| | | return; |
| | | } |
| | | |
| | | // Store in session/cookie for processReferral to pick up |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | } |
| | | $_SESSION[BASE . 'referral_code'] = sanitize_text_field($code); |
| | | setcookie( |
| | | BASE . 'referral_code', |
| | | sanitize_text_field($code), |
| | | time() + (86400 * 30), |
| | | '/' |
| | | ); |
| | | } |
| | | /** |
| | | * Track a new referral when user registers |
| | | * |
| | | * @param int $user_id |
| | | */ |
| | | public function processReferral(int $user_id): void |
| | | public function processReferral(int $user_id, string $email, array $data): void |
| | | { |
| | | // Check if user was created via referral magic link |
| | | $referral_code = get_user_meta($user_id, BASE . 'pending_referral_code', true); |
| | | // Try to get code from multiple sources |
| | | $referral_code = $data['referral_code'] ?? |
| | | get_user_meta($user_id, BASE . 'pending_referral_code', true); |
| | | |
| | | if (!$referral_code) { |
| | | // Check session/cookie if not in data |
| | | if (empty($referral_code)) { |
| | | if (session_status() === PHP_SESSION_NONE) { |
| | | session_start(); |
| | | } |
| | | $referral_code = $_SESSION[BASE . 'referral_code'] ?? $_COOKIE[BASE . 'referral_code'] ?? ''; |
| | | } |
| | | |
| | | if (empty($referral_code)) { |
| | | return; |
| | | } |
| | | |
| | |
| | | * @param string $code |
| | | * @return int|false |
| | | */ |
| | | protected function createReferral(int $referrer_id, int $referee_id, string $code) |
| | | public function createReferral(int $referrer_id, int $referee_id, string $code) |
| | | { |
| | | $user = get_user_by('ID', $referee_id); |
| | | |
| | |
| | | */ |
| | | public function getUserByReferralCode(string $code): ?WP_User |
| | | { |
| | | $users = get_users([ |
| | | 'meta_key' => BASE . 'referral_code', |
| | | 'meta_value' => $code, |
| | | 'number' => 1 |
| | | ]); |
| | | |
| | | return $users[0] ?? null; |
| | | return $this->cache->remember( |
| | | $code, |
| | | function () use ($code) { |
| | | $users = get_users([ |
| | | 'meta_key' => BASE . 'referral_code', |
| | | 'meta_value' => $code, |
| | | 'number' => 1 |
| | | ]); |
| | | return $users[0] ?? null; |
| | | } |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | return; |
| | | } |
| | | |
| | | $settings = $this->getRewardSettings(); |
| | | |
| | | // Create referrer reward |
| | | $this->wpdb->insert( |
| | | $this->rewards_table, |
| | |
| | | 'referral_id' => $referral_id, |
| | | 'user_id' => $referral->referrer_id, |
| | | 'reward_type' => 'referrer', |
| | | 'amount' => $settings['referrer_reward_amount'], |
| | | 'amount' => $this->settings['referrer_reward_amount'], |
| | | 'status' => 'available', |
| | | 'created_at' => current_time('mysql') |
| | | ], |
| | |
| | | ); |
| | | |
| | | // Create referee reward |
| | | $referee_amount = $settings['referee_reward_type'] === 'percentage' |
| | | ? $settings['referee_reward_amount'] // Store as percentage |
| | | : $settings['referee_reward_amount']; // Store as fixed amount |
| | | $referee_amount = $this->settings['referee_reward_type'] === 'percentage' |
| | | ? $this->settings['referee_reward_amount'] // Store as percentage |
| | | : $this->settings['referee_reward_amount']; // Store as fixed amount |
| | | |
| | | $this->wpdb->insert( |
| | | $this->rewards_table, |
| | |
| | | 'user_id' => $referral->referee_id, |
| | | 'reward_type' => 'referee', |
| | | 'amount' => $referee_amount, |
| | | 'reward_calculation' => $settings['referee_reward_type'], |
| | | 'reward_calculation' => $this->settings['referee_reward_type'], |
| | | 'status' => 'available', |
| | | 'created_at' => current_time('mysql') |
| | | ], |
| | |
| | | return ''; |
| | | } |
| | | |
| | | $settings = $this->getRewardSettings(); |
| | | $reward_amount = $settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $settings['referee_reward_type'] ?? 'percentage'; |
| | | $reward_amount = $this->settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $this->settings['referee_reward_type'] ?? 'percentage'; |
| | | |
| | | $reward_text = $reward_type === 'percentage' |
| | | ? $reward_amount . '% off' |
| | |
| | | return ob_get_clean(); |
| | | } |
| | | |
| | | function getLoggedInReferral(int $user_id):string |
| | | public function getLoggedInReferral(int $user_id):string |
| | | { |
| | | // Logged-in user widget |
| | | $referral_code = get_user_meta($user_id, BASE . 'referral_code', true); |
| | |
| | | } |
| | | } |
| | | |
| | | $share_url = home_url('/?ref=' . $referral_code); |
| | | $share_url = $this->getShareURL($referral_code); |
| | | |
| | | ob_start(); |
| | | ?> |
| | |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="share-btn facebook-share"> |
| | | <?php echo jvbIcon('facebook', ['size' => 20]); ?> |
| | | <?php echo jvbIcon('facebook-logo', ['size' => 20]); ?> |
| | | Facebook |
| | | </a> |
| | | <a href="https://twitter.com/intent/tweet?url=<?php echo urlencode($share_url); ?>&text=<?php echo urlencode('Check this out!'); ?>" |
| | | target="_blank" |
| | | rel="noopener noreferrer" |
| | | class="share-btn twitter-share"> |
| | | <?php echo jvbIcon('twitter', ['size' => 20]); ?> |
| | | <?php echo jvbIcon('twitter-logo', ['size' => 20]); ?> |
| | | Twitter |
| | | </a> |
| | | </div> |
| | |
| | | return $content; |
| | | } |
| | | |
| | | $settings = get_option(BASE . 'referral_settings', []); |
| | | $reward_amount = $settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $settings['referee_reward_type'] ?? 'percentage'; |
| | | $reward_amount = $this->settings['referee_reward_amount'] ?? 20; |
| | | $reward_type = $this->settings['referee_reward_type'] ?? 'percentage'; |
| | | |
| | | $reward_text = $reward_type === 'percentage' |
| | | ? $reward_amount . '% off' |
| | | : '$' . number_format($reward_amount, 2) . ' off'; |
| | | |
| | | $bonus_content = '<div style="background: #e7f5ff; padding: 20px; border-radius: 8px; margin: 20px 0;">'; |
| | | $bonus_content .= '<h3 style="margin-top: 0; color: #2271b1;">🎉 Welcome Bonus!</h3>'; |
| | | $bonus_content .= '<p>Since you were referred by a friend, you\'ve earned <strong>' . $reward_text . '</strong> your first booking!</p>'; |
| | | $bonus_content .= '<p>Your reward will be automatically applied when you book.</p>'; |
| | | $bonus_content .= '</div>'; |
| | | $bonus_content = sprintf( |
| | | ' |
| | | <h3>Thanks for the ♡</h3> |
| | | <p>Since you were referred by a friend, you\'ve earned <strong>%s</strong> your first booking!</p> |
| | | <p>Your reward will be automatically applied when you book.</p>', |
| | | $reward_text, |
| | | ); |
| | | |
| | | // Insert bonus content after the first paragraph |
| | | $parts = explode('</p>', $content, 2); |
| | | if (count($parts) === 2) { |
| | | return $parts[0] . '</p>' . $bonus_content . $parts[1]; |
| | | |
| | | $code = $this->getUserReferralCode($user->ID); |
| | | $yourCode = ''; |
| | | if (!is_wp_error($code)) { |
| | | $share_url = $this->getShareURL($code); |
| | | $yourCode = sprintf( |
| | | '<div class="callout"> |
| | | <h3>Share the ♡ with Friends</h3> |
| | | <p>If you find you love what we can do for you, you can share your own code!</p> |
| | | <p>Your Referral Code: <strong>%s</strong></p> |
| | | <p>Or click the button below:</p> |
| | | %s |
| | | </div>', |
| | | jvbEmailLink($code), |
| | | jvbMailButton($share_url, 'Share Your Code') |
| | | ); |
| | | } |
| | | |
| | | return $content . $bonus_content; |
| | | |
| | | return $content . $bonus_content . $yourCode; |
| | | } |
| | | |
| | | public function getShareURL(string $code):string |
| | | { |
| | | return add_query_arg( |
| | | [ |
| | | 'ref' => $code, |
| | | 'action' => 'register' |
| | | ], |
| | | wp_login_url() |
| | | ); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | // Get reward text for email |
| | | $settings = $this->getRewardSettings(); |
| | | $reward_text = $settings['referee_reward_type'] === 'percentage' |
| | | ? "Get {$settings['referee_reward_amount']}% off your first treatment!" |
| | | : "Get \${$settings['referee_reward_amount']} off your first treatment!"; |
| | | $reward_text = $this->settings['referee_reward_type'] === 'percentage' |
| | | ? "Get {$this->settings['referee_reward_amount']}% off your first treatment!" |
| | | : "Get \${$this->settings['referee_reward_amount']} off your first treatment!"; |
| | | |
| | | // Record the invitation attempt (for tracking) |
| | | $this->recordInvitationAttempt($user_id, $invitee_email, $invitee_name); |
| | |
| | | echo '<div class="notice notice-success is-dismissible"><p>Settings saved successfully.</p></div>'; |
| | | } |
| | | |
| | | $referral_page_id = $this->getReferralPageId(); |
| | | $settings = $this->getRewardSettings(); |
| | | |
| | | echo $this->renderAdminHTML(); |
| | | } |
| | | |
| | |
| | | wp_dropdown_pages([ |
| | | 'name' => BASE . 'referral_page_id', |
| | | 'id' => BASE . 'referral_page_id', |
| | | 'selected' => $referral_page_id, |
| | | 'selected' => $this->referralPage, |
| | | 'show_option_none' => __('— Select —', 'jvbase'), |
| | | 'option_none_value' => '0' |
| | | ]); |
| | |
| | | </th> |
| | | <td> |
| | | <select name="referrer_reward_type" id="referrer_reward_type"> |
| | | <option value="fixed" <?php selected($settings['referrer_reward_type'], 'fixed'); ?>>Fixed Amount</option> |
| | | <option value="percentage" <?php selected($settings['referrer_reward_type'], 'percentage'); ?>>Percentage</option> |
| | | <option value="fixed" <?php selected($this->settings['referrer_reward_type'], 'fixed'); ?>>Fixed Amount</option> |
| | | <option value="percentage" <?php selected($this->settings['referrer_reward_type'], 'percentage'); ?>>Percentage</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | |
| | | <input type="number" |
| | | name="referrer_reward_amount" |
| | | id="referrer_reward_amount" |
| | | value="<?= esc_attr($settings['referrer_reward_amount']) ?>" |
| | | value="<?= esc_attr($this->settings['referrer_reward_amount']) ?>" |
| | | step="0.01" |
| | | min="0"> |
| | | <p class="description">Amount in dollars or percentage</p> |
| | |
| | | </th> |
| | | <td> |
| | | <select name="referrer_reward_applies_to" id="referrer_reward_applies_to"> |
| | | <option value="per_user" <?php selected($settings['referrer_reward_applies_to'], 'per_user'); ?>>Per User Referred</option> |
| | | <option value="flat_total" <?php selected($settings['referrer_reward_applies_to'], 'flat_total'); ?>>Flat Total</option> |
| | | <option value="per_user" <?php selected($this->settings['referrer_reward_applies_to'], 'per_user'); ?>>Per User Referred</option> |
| | | <option value="flat_total" <?php selected($this->settings['referrer_reward_applies_to'], 'flat_total'); ?>>Flat Total</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | |
| | | </th> |
| | | <td> |
| | | <select name="referee_reward_type" id="referee_reward_type"> |
| | | <option value="percentage" <?php selected($settings['referee_reward_type'], 'percentage'); ?>>Percentage</option> |
| | | <option value="fixed" <?php selected($settings['referee_reward_type'], 'fixed'); ?>>Fixed Amount</option> |
| | | <option value="percentage" <?php selected($this->settings['referee_reward_type'], 'percentage'); ?>>Percentage</option> |
| | | <option value="fixed" <?php selected($this->settings['referee_reward_type'], 'fixed'); ?>>Fixed Amount</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | |
| | | <input type="number" |
| | | name="referee_reward_amount" |
| | | id="referee_reward_amount" |
| | | value="<?= esc_attr($settings['referee_reward_amount']) ?>" |
| | | value="<?= esc_attr($this->settings['referee_reward_amount']) ?>" |
| | | step="0.01" |
| | | min="0"> |
| | | <p class="description">Amount in dollars or percentage</p> |
| | |
| | | </th> |
| | | <td> |
| | | <select name="referee_reward_applies_to" id="referee_reward_applies_to"> |
| | | <option value="first_order" <?php selected($settings['referee_reward_applies_to'], 'first_order'); ?>>First Order Only</option> |
| | | <option value="all_orders" <?php selected($settings['referee_reward_applies_to'], 'all_orders'); ?>>All Orders</option> |
| | | <option value="first_order" <?php selected($this->settings['referee_reward_applies_to'], 'first_order'); ?>>First Order Only</option> |
| | | <option value="all_orders" <?php selected($this->settings['referee_reward_applies_to'], 'all_orders'); ?>>All Orders</option> |
| | | </select> |
| | | </td> |
| | | </tr> |
| | |
| | | return; |
| | | } |
| | | |
| | | $referral_page_id = $this->getReferralPageId(); |
| | | |
| | | if (!$referral_page_id) { |
| | | if (!$this->referralPage) { |
| | | return; |
| | | } |
| | | |
| | | global $pagenow, $post; |
| | | |
| | | // Check if we're editing the referral page |
| | | if ('post.php' === $pagenow && $post && $post->ID === $referral_page_id) { |
| | | if ('post.php' === $pagenow && $post && $post->ID === $this->referralPage) { |
| | | $wp_admin_bar->add_node([ |
| | | 'id' => 'referral-page', |
| | | 'parent' => 'top-secondary', |
| | |
| | | return; |
| | | } |
| | | |
| | | $referral_page_id = $this->getReferralPageId(); |
| | | |
| | | if ($post->ID === $referral_page_id) { |
| | | if ($post->ID === $this->referralPage) { |
| | | echo '<div class="notice notice-info">'; |
| | | echo '<p>' . __('This page is designated as the <strong>Referral Page</strong>.', 'jvbase') . '</p>'; |
| | | echo '</div>'; |
| | |
| | | <input type="radio" id="enthusiast" name="user_type" value="enthusiast" required <?= ($this->fromFavourites()) ? 'checked' : '' ?>> |
| | | <label for="enthusiast"><?=jvbIcon('heart', ['title' =>'Enthusiast', 'size'=>40])?><h4>Enthusiast</h4><p>Start here.</p></label> |
| | | <input type="radio" id="artist" name="user_type" value="artist" required> |
| | | <label for="artist"><?=jvbIcon('tattoo', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label> |
| | | <label for="artist"><?=jvbIcon('drop-simple', ['title'=> 'Artist', 'size'=> 40])?><h4>Artist</h4><p>Show your talent.</p></label> |
| | | <input type="radio" id="partner" name="user_type" value="partner" required> |
| | | <label for="partner"><?=jvbIcon('partner', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label> |
| | | <label for="partner"><?=jvbIcon('currency-circle-dollar', ['title'=>'Partner', 'size' => 40])?><h4>Partner</h4><p>Support the community.</p></label> |
| | | <p class="enthusiast">Save your favourites. Get notified.</p> |
| | | <p class="artist">Show off your work.</p> |
| | | <p class="partner">Support the community.</p> |
| | |
| | | use JVBase\JVB; |
| | | use JVBase\managers\CacheManager; |
| | | use WP_Error; |
| | | use WP_Post; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | |
| | | */ |
| | | public function init():void |
| | | { |
| | | add_action('save_post', [$this, 'updatePostRelationships']); |
| | | add_action('before_delete_post', [$this, 'updatePostRelationships']); |
| | | add_action('save_post', [$this, 'updatePostRelationships'], 10, 2); |
| | | add_action('before_delete_post', [$this, 'updatePostRelationships'], 10, 2); |
| | | add_action('delete_term', [$this, 'deleteTermRelationships']); |
| | | |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | |
| | | * |
| | | * @return void |
| | | */ |
| | | public function updatePostRelationships(int $post_id):void |
| | | public function updatePostRelationships(int $post_id, WP_Post $post):void |
| | | { |
| | | $this->cache->invalidate(); |
| | | $post_type = get_post_type($post_id); |
| | | if (in_array($post_type, [BASE.'directory', BASE.'dash'])) { |
| | | $post_type = $post->post_type; |
| | | if (in_array($post_type, jvbIgnoredPostTypes())) { |
| | | return; |
| | | } |
| | | $this->cache->invalidate(); |
| | | // Get all taxonomies for this post type |
| | | $taxonomies = get_object_taxonomies($post_type); |
| | | |
| | |
| | | if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) { |
| | | return; |
| | | } |
| | | // SAFETY: Skip attachments and other non-content post types |
| | | if (in_array($post->post_type, jvbIgnoredPostTypes())) { |
| | | return; |
| | | } |
| | | |
| | | // Skip non-custom post types |
| | | $post_type = get_post_type($post); |
| | |
| | | $this->wpdb->query("TRUNCATE TABLE {$this->table_name}"); |
| | | |
| | | // Get all users with posts |
| | | $users = $this->wpdb->get_col(" |
| | | SELECT DISTINCT post_author |
| | | FROM {$this->wpdb->posts} |
| | | WHERE post_status = 'publish' |
| | | AND post_type LIKE '" . BASE . "%' |
| | | "); |
| | | $users = $this->wpdb->get_col($this->wpdb->prepare( |
| | | "SELECT DISTINCT post_author |
| | | FROM {$this->wpdb->posts} |
| | | WHERE post_status = 'publish' |
| | | AND post_type LIKE %s", |
| | | $this->wpdb->esc_like(BASE) . '%' |
| | | )); |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'rebuild_user_term_index', |
| | |
| | | $this->clearUserCache($user_id); |
| | | |
| | | // Get all the user's published posts |
| | | $posts = $this->wpdb->get_col($this->wpdb->prepare( |
| | | "SELECT ID FROM {$this->wpdb->posts} |
| | | WHERE post_author = %d |
| | | AND post_status = 'publish' |
| | | AND post_type LIKE '%s'", |
| | | $user_id, |
| | | BASE |
| | | )); |
| | | $posts = $this->wpdb->get_col($this->wpdb->prepare( |
| | | "SELECT ID FROM {$this->wpdb->posts} |
| | | WHERE post_author = %d |
| | | AND post_status = 'publish' |
| | | AND post_type LIKE %s", |
| | | $user_id, |
| | | $this->wpdb->esc_like(BASE) . '%' |
| | | )); |
| | | |
| | | $processed_terms = 0; |
| | | |
| | |
| | | <?php |
| | | use JVBase\utility\Features; |
| | | |
| | | |
| | | require(JVB_DIR . '/inc/managers/CacheManager.php'); |
| | | require(JVB_DIR . '/icons.php'); |
| | | require(JVB_DIR . '/inc/managers/IconsManager.php'); |
| | | require(JVB_DIR . '/inc/managers/ErrorHandler.php'); |
| | | require(JVB_DIR . '/inc/managers/OperationQueue.php'); |
| | | require(JVB_DIR . '/inc/managers/EmailManager.php'); |
| | |
| | | |
| | | |
| | | //IF SITE HAS DASHBOARD AND FEED BLOCK |
| | | if (Features::forSite()->hasAny(['dashboard', 'feed_block'])) { |
| | | //if (Features::forSite()->hasAny(['dashboard', 'feed_block'])) { |
| | | require(JVB_DIR . '/inc/forms/TaxonomySelector.php'); |
| | | } |
| | | //} |
| | | |
| | | //IF SITE HAS DASHBOARD |
| | | if (Features::forSite()->has('dashboard')) { |
| | |
| | | class="decrease" |
| | | title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>" |
| | | aria-label="Decrease <?= esc_attr($field['label']) ?>"> |
| | | <?= jvbIcon('minus') ?> |
| | | <?= jvbIcon('minus-square') ?> |
| | | </button> |
| | | |
| | | <input type="number" |
| | |
| | | class="increase" |
| | | title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>" |
| | | aria-label="Increase <?= esc_attr($field['label']) ?>"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | </button> |
| | | </div> |
| | | |
| | |
| | | <div class="repeater-row" data-index="<?= esc_attr($index); ?>"> |
| | | <details <?= (is_string($index)) ? 'open' : ''; ?>> |
| | | <summary class="repeater-row-header row btw"> |
| | | <span class="drag-handle"><?= jvbIcon('grab'); ?></span> |
| | | <span class="drag-handle"><?= jvbIcon('dots-six-vertical'); ?></span> |
| | | <span class="row-number">#<?= esc_html($display_number); ?></span> |
| | | <span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span> |
| | | <button type="button" class="remove-row" title="Remove"> |
| | | <?= jvbIcon('delete', ['title'=>'Remove']); ?> |
| | | <?= jvbIcon('trash', ['title'=>'Remove']); ?> |
| | | </button> |
| | | </summary> |
| | | <div class="repeater-row-content"> |
| | |
| | | |
| | | <div class="selection-actions row btw" hidden> |
| | | <button type="button" data-action="add-to-group"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | Group |
| | | </button> |
| | | <button type="button" data-action="delete-upload"> |
| | | <?= jvbIcon('delete') ?> |
| | | <?= jvbIcon('trash') ?> |
| | | Delete |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | <button type="button" data-action="upload" class="submit-uploads"> |
| | | <?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?> |
| | | <?= jvbIcon('cloud-arrow-up') ?> Upload <?= esc_html($plural ?? 'Content'); ?> |
| | | </button> |
| | | </div> |
| | | <?php endif; ?> |
| | |
| | | </div> |
| | | |
| | | <?php if ($destination === 'post_group') : ?> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('arrow-elbow-right-up')?></p> |
| | | </div> |
| | | <div class="sidebar flex col"> |
| | | <div class="header"> |
| | |
| | | <p>Drag here to create a new <?= $singular ?>!</p> |
| | | </div> |
| | | </div> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('arrow-elbow-right-up')?></p> |
| | | </div> |
| | | </div> |
| | | <?php endif; ?> |
| | |
| | | </div> |
| | | |
| | | <button type="button" data-action="delete-upload" title="Remove from Group"> |
| | | <?=jvbIcon('delete')?> |
| | | <?=jvbIcon('trash')?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | <details>'; |
| | | <summary class="row btw"><?=jvbIcon('edit')?><span>Edit Info</span></summary> |
| | | <details> |
| | | <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary> |
| | | |
| | | <?php |
| | | |
| | |
| | | ] |
| | | ], $fields); |
| | | |
| | | $this->render('upload_data', null, $fields); |
| | | $meta = new MetaManager($id); |
| | | foreach ($fields as $field => $config) { |
| | | $meta->render('form', $field, $config); |
| | | } |
| | | ?> |
| | | </details> |
| | | </div> |
| | |
| | | </div> |
| | | |
| | | <button type="button" data-action="delete-upload" title="Remove from Group"> |
| | | <?=jvbIcon('delete')?> |
| | | <?=jvbIcon('trash')?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | <details>'; |
| | | <summary class="row btw"><?=jvbIcon('edit')?><span>Edit Info</span></summary> |
| | | <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary> |
| | | |
| | | <?php |
| | | $fields = array_key_exists('fields', $config) ? $config['fields'] : []; |
| | |
| | | </div> |
| | | |
| | | <button type="button" data-action="delete-upload" title="Remove from Group"> |
| | | <?=jvbIcon('delete')?> |
| | | <?=jvbIcon('trash')?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | <details>'; |
| | | <summary class="row btw"><?=jvbIcon('edit')?><span>Edit Info</span></summary> |
| | | <summary class="row btw"><?=jvbIcon('pencil-simple')?><span>Edit Info</span></summary> |
| | | |
| | | <?php |
| | | $fields = array_key_exists('fields', $config) ? $config['fields'] : []; |
| | |
| | | class="decrease" |
| | | title="<?= array_key_exists('remove', $field) ? $field['remove'] : 'Decrease amount' ?>" |
| | | aria-label="Decrease <?= esc_attr($field['label']); ?>"> |
| | | <?= jvbIcon('minus')?> |
| | | <?= jvbIcon('minus-square')?> |
| | | </button> |
| | | |
| | | <input type="number" |
| | |
| | | class="increase" |
| | | title="<?= array_key_exists('add', $field) ? $field['add'] : 'Increase amount' ?>" |
| | | aria-label="Increase <?= esc_attr($field['label']); ?>"> |
| | | <?= jvbIcon('add')?> |
| | | <?= jvbIcon('plus-square')?> |
| | | </button> |
| | | </div> |
| | | <?php $this->renderDescription($description, $name); ?> |
| | |
| | | <div class="repeater-row" data-index="<?= esc_attr($index); ?>"> |
| | | <details <?= (is_string($index)) ? 'open' : ''; ?>> |
| | | <summary class="repeater-row-header row btw"> |
| | | <span class="drag-handle"><?= jvbIcon('grab'); ?></span> |
| | | <span class="drag-handle"><?= jvbIcon('dots-six-vertical'); ?></span> |
| | | <span class="row-number">#<?= esc_html($display_number); ?></span> |
| | | <span class="row-title"><?= esc_html($this->getRowTitle($fields, $values, $rowTitle)); ?></span> |
| | | <button type="button" class="remove-row" title="Remove"> |
| | | <?= jvbIcon('delete', ['title'=>'Remove']); ?> |
| | | <?= jvbIcon('trash', ['title'=>'Remove']); ?> |
| | | </button> |
| | | </summary> |
| | | <div class="repeater-row-content"> |
| | |
| | | <button title="Add <?= esc_attr(ucfirst($field['post_type'])) ?>" |
| | | class="add-item-btn" |
| | | type="button"> |
| | | <?= jvbIcon('add', ['title' => "Add " . ucfirst($field['post_type'])]) ?> |
| | | <?= jvbIcon('plus-square', ['title' => "Add " . ucfirst($field['post_type'])]) ?> |
| | | </button> |
| | | </div> |
| | | |
| | |
| | | <?php endif; ?> |
| | | |
| | | <button type="button" class="search-button"> |
| | | <?= jvbIcon('search', ['title' => 'Search']); ?> |
| | | <?= jvbIcon('magnifying-glass', ['title' => 'Search']); ?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | |
| | | <span class="item-title"><?= esc_html($item_title); ?></span> |
| | | <span class="item-type"><?= esc_html(ucfirst($item_object)); ?></span> |
| | | <button type="button" class="remove-item row"> |
| | | <?= jvbIcon('close', ['title' => 'Remove']); ?> |
| | | <?= jvbIcon('x', ['title' => 'Remove']); ?> |
| | | </button> |
| | | </li> |
| | | <?php |
| | |
| | | const removeBtn = document.createElement('button'); |
| | | removeBtn.type = 'button'; |
| | | removeBtn.className = 'remove-item'; |
| | | removeBtn.innerHTML = '<?= jvbIcon('close', ['title' => 'Remove']); ?>'; |
| | | removeBtn.innerHTML = '<?= jvbIcon('x', ['title' => 'Remove']); ?>'; |
| | | removeBtn.addEventListener('click', function() { |
| | | removeItem(id, clone); |
| | | }); |
| | |
| | | |
| | | <div class="selection-actions row btw" hidden> |
| | | <button type="button" data-action="add-to-group"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | Group |
| | | </button> |
| | | <button type="button" data-action="delete-upload"> |
| | | <?= jvbIcon('delete') ?> |
| | | <?= jvbIcon('trash') ?> |
| | | Delete |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | <button type="button" data-action="upload" class="submit-uploads"> |
| | | <?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?> |
| | | <?= jvbIcon('cloud-arrow-up') ?> Upload <?= esc_html($plural ?? 'Content'); ?> |
| | | </button> |
| | | </div> |
| | | <?php endif; ?> |
| | |
| | | </div> |
| | | |
| | | <?php if ($destination === 'post_group') : ?> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('arrow-elbow-right-up')?></p> |
| | | </div> |
| | | <div class="sidebar flex col"> |
| | | <div class="header"> |
| | |
| | | <p>Drag here to create a new <?= $singular ?>!</p> |
| | | </div> |
| | | </div> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('arrow-elbow-right-up')?></p> |
| | | </div> |
| | | </div> |
| | | <?php endif; ?> |
| | |
| | | <!-- Selection actions (hidden by default) --> |
| | | <div class="selection-actions" hidden> |
| | | <button type="button" class="create-from-selection"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | Create New <?= $singular ?> |
| | | </button> |
| | | <button type="button" class="remove-selection"> |
| | | <?= jvbIcon('delete') ?> |
| | | <?= jvbIcon('trash') ?> |
| | | Remove |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | <button type="button" class="submit-uploads"> |
| | | <?= jvbIcon('upload') ?> Upload <?= esc_html($plural ?? 'Content'); ?> |
| | | <?= jvbIcon('cloud-arrow-up') ?> Upload <?= esc_html($plural ?? 'Content'); ?> |
| | | </button> |
| | | </div> |
| | | <?php endif; ?> |
| | |
| | | </div> |
| | | |
| | | <?php if ($groupable) : ?> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?> These will become individual <?= $plural ?> <?= jvbIcon('arrow-elbow-right-up')?></p> |
| | | </div> |
| | | <div class="sidebar"> |
| | | <div class="header"> |
| | |
| | | <p class="hint">Select multiple images and click "Add to <?= $singular?>" or create new <?= $plural ?>.</p> |
| | | </div> |
| | | <button type="button" class="create-group-from-selection"> |
| | | <?= jvbIcon('add') ?> |
| | | <?= jvbIcon('plus-square') ?> |
| | | Create New <?= $singular ?> |
| | | </button> |
| | | <div class="item-grid groups"> |
| | |
| | | <p>Drag here to create a new <?= $singular ?>!</p> |
| | | </div> |
| | | </div> |
| | | <p class="hint"><?= jvbIcon('elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('elbow-right-up')?></p> |
| | | <p class="hint"><?= jvbIcon('arrow-elbow-left-up') ?> Each group will become its own <?= $singular ?> <?= jvbIcon('arrow-elbow-right-up')?></p> |
| | | </div> |
| | | </div> |
| | | <?php endif; ?> |
| | |
| | | <?= !empty($field['required']) ? 'required' : ''; ?> |
| | | data-format="<?= esc_attr($format); ?>" |
| | | > |
| | | <?= jvbIcon('event') ?> |
| | | <?= jvbIcon('calendar') ?> |
| | | </div> |
| | | <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?> |
| | | <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?> |
| | |
| | | <?= !empty($field['max']) ? 'max="' . esc_attr($field['max']) . '"' : ''; ?> |
| | | <?= !empty($field['step']) ? 'step="' . esc_attr($field['step']) . '"' : ''; ?> |
| | | > |
| | | <?= jvbIcon('event') ?> |
| | | <?= jvbIcon('calendar') ?> |
| | | </div> |
| | | <?php if (array_key_exists('hint', $field)) { $this->renderHint($field['hint']); } ?> |
| | | <?php if (array_key_exists('description', $field)) { $this->renderDescription($field['description'], $name); } ?> |
| | |
| | | { |
| | | $id = $name.'-help'; |
| | | $out = '<div class="has-tooltip"> |
| | | <span class="tt-toggle">'.jvbIcon('help').'</span> |
| | | <span class="tt-toggle">'.jvbIcon('question').'</span> |
| | | <div role="tooltip" id="'.$id.'"><p>'.$description.'</p></div> |
| | | </div>'; |
| | | echo $out; |
| | |
| | | } |
| | | |
| | | |
| | | echo (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('save').'Save</button>' : ''; |
| | | echo (jvbCheck('submit', $options)) ? '<button type="submit">'.jvbIcon('floppy-disk').'Save</button>' : ''; |
| | | echo '</form>'; |
| | | |
| | | $out = ob_get_clean(); |
| | |
| | | $out = '<p class="'.$name.'">'.jvbIcon('calendar').'<span></span></p>'; |
| | | break; |
| | | case 'time': |
| | | $out = '<p class="'.$name.'">'.jvbIcon('time').'<time></time></p>'; |
| | | $out = '<p class="'.$name.'">'.jvbIcon('clock').'<time></time></p>'; |
| | | break; |
| | | case 'true_false': |
| | | $out = '<p class="'.$name.'"></p>'; |
| | |
| | | $link = 'mailto:'.$value.'?subject='.rawurlencode('Contact from edmonton.ink').'&body='.rawurlencode('Hey, |
| | | I found you on edmonton.ink, and I wanted to reach out!'); |
| | | |
| | | return '<a href="'.$link.'" title="Send an Email">'.jvbIcon('email').'<span>Email</span></a>'; |
| | | return '<a href="'.$link.'" title="Send an Email">'.jvbIcon('envelope').'<span>Email</span></a>'; |
| | | |
| | | } |
| | | protected function renderUrlField(string $name, string $value, array $field):string |
| | |
| | | |
| | | // Add icon if configured |
| | | $show_icon = $field['show_icon'] ?? true; |
| | | $icon_html = $show_icon ? jvbIcon('time') : ''; |
| | | $icon_html = $show_icon ? jvbIcon('clock') : ''; |
| | | |
| | | return sprintf( |
| | | '<div class="time-field-display">%s<span class="time-value">%s</span></div>', |
| | |
| | | |
| | | // Add icon if configured |
| | | $show_icon = $field['show_icon'] ?? true; |
| | | $icon_html = $show_icon ? jvbIcon('event') : ''; |
| | | $icon_html = $show_icon ? jvbIcon('calendar') : ''; |
| | | |
| | | return sprintf( |
| | | '<div class="datetime-field-display">%s<span class="datetime-value">%s</span></div>', |
| | |
| | | |
| | | // Add icon if configured |
| | | $show_icon = $field['show_icon'] ?? true; |
| | | $icon_html = $show_icon ? jvbIcon('event') : ''; |
| | | $icon_html = $show_icon ? jvbIcon('calendar') : ''; |
| | | |
| | | return sprintf( |
| | | '<div class="date-field-display">%s<span class="date-value">%s</span></div>', |
| | |
| | | <?php |
| | | if (!empty(JVB_CONTENT) || !empty(JVB_TAXONOMY) || !empty(JVB_USER)) { |
| | | |
| | | require(JVB_DIR . '/inc/meta/MetaTypeManager.php'); |
| | | require(JVB_DIR . '/inc/meta/MetaManager.php'); |
| | | require(JVB_DIR . '/inc/meta/MetaRegistry.php'); |
| | |
| | | require(JVB_DIR . '/inc/meta/MetaValidator.php'); |
| | | require(JVB_DIR . '/inc/meta/MetaRenderer.php'); |
| | | require(JVB_DIR . '/inc/meta/MetaForm.php'); |
| | | } |
| | |
| | | } |
| | | |
| | | try { |
| | | if (array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard']) { |
| | | // if (array_key_exists('dashboard', $this->JVB_SITE) && $this->JVB_SITE['dashboard']) { |
| | | $tables = array_merge($tables, $this->queueTables(), $this->errorLogTables()); |
| | | } |
| | | // } |
| | | } catch (Exception $e) { |
| | | error_log("JVB: Error in dashboard section: " . $e->getMessage()); |
| | | } |
| | |
| | | } |
| | | // RE-ENABLE favourites tables |
| | | try { |
| | | if ($this->JVB_SITE['favourites']) { |
| | | if (array_key_exists('favourites', $this->JVB_SITE)) { |
| | | error_log('JVB: Creating favourites tables...'); |
| | | $favouriteTables = $this->favouriteTables(); |
| | | error_log('JVB: Favourites tables created: ' . count($favouriteTables)); |
| | |
| | | return true; |
| | | } |
| | | |
| | | public function checkRateLimit(WP_REST_Request $request):bool|WP_Error |
| | | { |
| | | if (!$this->rate_limiter->checkLimit($request)) { |
| | | error_log('Rate Limit Reached'); |
| | | return new WP_Error( |
| | | 'rate_limit', |
| | | 'Too many attempts. Please wait a moment before trying again.', |
| | | ['status' => 429] |
| | | ); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | protected function checkContent(string $content, bool $bool = false):string|bool |
| | | { |
| | | $result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??''; |
| | |
| | | |
| | | return $this->checkHeaders($request, $types, ['user_id' => $user_id]); |
| | | } |
| | | |
| | | /** |
| | | * Helper to return error response |
| | | */ |
| | | protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response |
| | | { |
| | | $data = [ |
| | | 'message' => $message, |
| | | 'code' => $code |
| | | ]; |
| | | |
| | | if ($field) { |
| | | $data['field'] = $field; |
| | | } |
| | | |
| | | return new WP_REST_Response($data, $status); |
| | | } |
| | | /** |
| | | * Helper to return success response |
| | | */ |
| | | protected function success(array $data, int $status = 200): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response($data, $status); |
| | | } |
| | | |
| | | /************************************************************ |
| | | SESSION FINGERPRINT |
| | | ************************************************************/ |
| | | /** |
| | | * Store session fingerprint for hijacking detection |
| | | */ |
| | | protected function storeSessionFingerprint(int $user_id, WP_REST_Request $request): void |
| | | { |
| | | if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) { |
| | | return; |
| | | } |
| | | |
| | | $fingerprint = $this->generateSessionFingerprint($request); |
| | | update_user_meta($user_id, BASE . 'session_fingerprint', $fingerprint); |
| | | update_user_meta($user_id, BASE . 'session_timestamp', time()); |
| | | } |
| | | /** |
| | | * Generate session fingerprint for hijacking detection |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @return string Hashed fingerprint |
| | | */ |
| | | protected function generateSessionFingerprint(WP_REST_Request $request): string |
| | | { |
| | | return hash('sha256', implode('|', [ |
| | | $request->get_header('User-Agent') ?? '', |
| | | // Use IP class instead of full IP to allow for mobile network changes |
| | | $this->getIPClass( |
| | | $request->get_header('X-Forwarded-For') |
| | | ?: $request->get_header('X-Real-IP') |
| | | ?: $_SERVER['REMOTE_ADDR'] ?? '' |
| | | ) |
| | | ])); |
| | | } |
| | | |
| | | /** |
| | | * Get IP class (first 3 octets) for session validation |
| | | * This allows for minor IP changes (common with mobile networks) |
| | | * |
| | | * @param string $ip IP address |
| | | * @return string First 3 octets |
| | | */ |
| | | protected function getIPClass(string $ip): string |
| | | { |
| | | $parts = explode('.', $ip); |
| | | return implode('.', array_slice($parts, 0, 3)); |
| | | } |
| | | |
| | | /** |
| | | * Validate session fingerprint against stored value |
| | | * |
| | | * @param int $user_id User ID to validate |
| | | * @param WP_REST_Request $request Current request |
| | | * @return bool True if valid, false if potential hijacking |
| | | */ |
| | | protected function validateSessionFingerprint(int $user_id, WP_REST_Request $request): bool |
| | | { |
| | | // Only enforce if enabled in config |
| | | if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) { |
| | | return true; |
| | | } |
| | | |
| | | $stored = get_user_meta($user_id, BASE . 'session_fingerprint', true); |
| | | $current = $this->generateSessionFingerprint($request); |
| | | |
| | | if (empty($stored)) { |
| | | // First request - store fingerprint |
| | | update_user_meta($user_id, BASE . 'session_fingerprint', $current); |
| | | update_user_meta($user_id, BASE . 'session_timestamp', time()); |
| | | return true; |
| | | } |
| | | |
| | | // Compare using timing-safe comparison |
| | | return hash_equals($stored, $current); |
| | | } |
| | | |
| | | /** |
| | | * Clear session fingerprint (call on logout) |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return void |
| | | */ |
| | | protected function clearSessionFingerprint(int $user_id): void |
| | | { |
| | | delete_user_meta($user_id, BASE . 'session_fingerprint'); |
| | | delete_user_meta($user_id, BASE . 'session_timestamp'); |
| | | } |
| | | |
| | | /****************************************************************** |
| | | * CSRF PROTECTION |
| | | ******************************************************************/ |
| | | /** |
| | | * Generate CSRF token for a user |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return string CSRF token |
| | | */ |
| | | protected function generateCSRFToken(int $user_id): string |
| | | { |
| | | $token = wp_generate_password(32, false); |
| | | set_transient(BASE . 'csrf_' . $user_id, $token, HOUR_IN_SECONDS); |
| | | return $token; |
| | | } |
| | | |
| | | /** |
| | | * Validate CSRF token from request header |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @return bool True if valid or not required |
| | | */ |
| | | protected function validateCSRFToken(WP_REST_Request $request): bool |
| | | { |
| | | // Only for authenticated requests |
| | | if (!is_user_logged_in()) { |
| | | return true; |
| | | } |
| | | |
| | | // Only for state-changing operations |
| | | if (in_array($request->get_method(), ['GET', 'HEAD', 'OPTIONS'])) { |
| | | return true; |
| | | } |
| | | |
| | | $user_id = get_current_user_id(); |
| | | $token = $request->get_header('X-CSRF-Token'); |
| | | $stored = get_transient(BASE . 'csrf_' . $user_id); |
| | | |
| | | if (empty($stored) || empty($token)) { |
| | | return false; |
| | | } |
| | | |
| | | return hash_equals($stored, $token); |
| | | } |
| | | |
| | | /** |
| | | * Get current CSRF token for user (for frontend to use) |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return string|null CSRF token or null |
| | | */ |
| | | protected function getCSRFToken(int $user_id): ?string |
| | | { |
| | | $token = get_transient(BASE . 'csrf_' . $user_id); |
| | | |
| | | if (!$token) { |
| | | $token = $this->generateCSRFToken($user_id); |
| | | } |
| | | |
| | | return $token; |
| | | } |
| | | |
| | | /*********************************************************** |
| | | * REQUEST SIGNATURE VERIFICATION |
| | | ***********************************************************/ |
| | | /** |
| | | * Verify request signature for sensitive operations |
| | | * Adds an additional layer of security for critical endpoints |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @param array $sensitive_params Params to include in signature |
| | | * @return bool True if valid signature |
| | | */ |
| | | protected function verifyRequestSignature(WP_REST_Request $request, array $sensitive_params): bool |
| | | { |
| | | $signature = $request->get_header('X-Request-Signature'); |
| | | if (empty($signature)) { |
| | | return false; |
| | | } |
| | | |
| | | // Get user's signing key (rotated on each login) |
| | | $user_id = get_current_user_id(); |
| | | if (!$user_id) { |
| | | return false; |
| | | } |
| | | |
| | | $signing_key = get_user_meta($user_id, BASE . 'signing_key', true); |
| | | if (empty($signing_key)) { |
| | | // Generate new key if missing |
| | | $signing_key = wp_generate_password(64, false); |
| | | update_user_meta($user_id, BASE . 'signing_key', $signing_key); |
| | | } |
| | | |
| | | // Build signature from sensitive params |
| | | ksort($sensitive_params); |
| | | $message = json_encode($sensitive_params); |
| | | $expected = hash_hmac('sha256', $message, $signing_key); |
| | | |
| | | return hash_equals($expected, $signature); |
| | | } |
| | | |
| | | /** |
| | | * Rotate signing key (call on login) |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return string New signing key |
| | | */ |
| | | protected function rotateSigningKey(int $user_id): string |
| | | { |
| | | $new_key = wp_generate_password(64, false); |
| | | update_user_meta($user_id, BASE . 'signing_key', $new_key); |
| | | return $new_key; |
| | | } |
| | | /********************************************************** |
| | | * AUDIT LOGGING |
| | | **********************************************************/ |
| | | /** |
| | | * Log security-relevant events |
| | | * |
| | | * @param string $event Event name |
| | | * @param array $data Additional event data |
| | | * @return void |
| | | */ |
| | | protected function auditLog(string $event, array $data = []): void |
| | | { |
| | | // Add standard context |
| | | $context = array_merge($data, [ |
| | | 'timestamp' => current_time('mysql'), |
| | | 'user_id' => get_current_user_id() ?: 0, |
| | | 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', |
| | | 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', |
| | | ]); |
| | | |
| | | // Use existing error handler |
| | | try { |
| | | JVB()->error()->log( |
| | | 'security_audit', |
| | | $event, |
| | | $context, |
| | | 'info' |
| | | ); |
| | | } catch (\Exception $e) { |
| | | // Fallback to error_log if handler fails |
| | | error_log("Security Audit: {$event} - " . json_encode($context)); |
| | | } |
| | | } |
| | | |
| | | /*************************************************************** |
| | | * SANITIZATION HELPERS |
| | | ***************************************************************/ |
| | | /** |
| | | * Sanitize array of IDs |
| | | * |
| | | * @param array $ids Array of IDs |
| | | * @return array Sanitized array of integers |
| | | */ |
| | | protected function sanitizeIDs(array $ids): array |
| | | { |
| | | return array_map('absint', array_filter($ids, function($id) { |
| | | return is_numeric($id) && $id > 0; |
| | | })); |
| | | } |
| | | |
| | | /*************************************************************** |
| | | * RESPONSE HELPERS |
| | | ***************************************************************/ |
| | | /** |
| | | * Return validation error response |
| | | * |
| | | * @param array $errors Validation errors (field => message) |
| | | * @return WP_REST_Response 422 response with errors |
| | | */ |
| | | protected function validationError(array $errors): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Validation failed', |
| | | 'errors' => $errors |
| | | ], 422); |
| | | } |
| | | |
| | | /** |
| | | * Return unauthorized error response |
| | | * |
| | | * @param string $message Error message |
| | | * @return WP_REST_Response 401 response |
| | | */ |
| | | protected function unauthorized(string $message = 'Unauthorized'): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => $message |
| | | ], 401); |
| | | } |
| | | |
| | | /** |
| | | * Return forbidden error response |
| | | * |
| | | * @param string $message Error message |
| | | * @return WP_REST_Response 403 response |
| | | */ |
| | | protected function forbidden(string $message = 'Forbidden'): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => $message |
| | | ], 403); |
| | | } |
| | | |
| | | /** |
| | | * Return not found error response |
| | | * |
| | | * @param string $message Error message |
| | | * @return WP_REST_Response 404 response |
| | | */ |
| | | protected function notFound(string $message = 'Not found'): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => $message |
| | | ], 404); |
| | | } |
| | | |
| | | /***************************************************************** |
| | | * CONCURRENT CHECKS |
| | | *****************************************************************/ |
| | | /** |
| | | * Prevent concurrent requests for the same operation |
| | | * Useful for preventing double-submissions |
| | | * |
| | | * @param string $operation_key Unique key for this operation |
| | | * @param int $lock_duration Lock duration in seconds |
| | | * @return bool True if lock acquired, false if already locked |
| | | */ |
| | | protected function acquireOperationLock(string $operation_key, int $lock_duration = 5): bool |
| | | { |
| | | $lock_key = 'op_lock_' . md5($operation_key); |
| | | |
| | | // Try to acquire lock |
| | | $locked = get_transient($lock_key); |
| | | if ($locked) { |
| | | return false; // Already locked |
| | | } |
| | | |
| | | // Set lock |
| | | set_transient($lock_key, true, $lock_duration); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Release operation lock |
| | | * |
| | | * @param string $operation_key Unique key for this operation |
| | | * @return void |
| | | */ |
| | | protected function releaseOperationLock(string $operation_key): void |
| | | { |
| | | $lock_key = 'op_lock_' . md5($operation_key); |
| | | delete_transient($lock_key); |
| | | } |
| | | } |
| | | // |
| | | //Simple example: |
| | |
| | | // $response = new WP_REST_Response($data); |
| | | // return $this->addCacheHeaders($response); |
| | | //} |
| | | |
| | | |
| | | |
| | | /** |
| | | * Use operation lock in your methods like this: |
| | | * |
| | | * public function updateContent(WP_REST_Request $request): WP_REST_Response |
| | | * { |
| | | * $user_id = get_current_user_id(); |
| | | * $content_id = $request->get_param('content_id'); |
| | | * |
| | | * // Prevent concurrent updates |
| | | * $lock_key = "update_{$user_id}_{$content_id}"; |
| | | * if (!$this->acquireOperationLock($lock_key)) { |
| | | * return $this->error( |
| | | * 'An update is already in progress. Please wait.', |
| | | * 'concurrent_operation', |
| | | * 409 |
| | | * ); |
| | | * } |
| | | * |
| | | * try { |
| | | * // Do your operation |
| | | * $result = $this->doUpdate($content_id); |
| | | * |
| | | * $this->releaseOperationLock($lock_key); |
| | | * return $this->success($result); |
| | | * |
| | | * } catch (\Exception $e) { |
| | | * $this->releaseOperationLock($lock_key); |
| | | * return $this->error($e->getMessage(), 'operation_failed'); |
| | | * } |
| | | * } |
| | | */ |
| | |
| | | require(JVB_DIR . '/inc/rest/routes/TermRoutes.php'); |
| | | } |
| | | |
| | | require(JVB_DIR . '/inc/rest/routes/QueueRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/ErrorRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/UploadRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/SettingsRoutes.php'); |
| | | if (Features::forSite()->has('dashboard')) { |
| | | require(JVB_DIR . '/inc/rest/routes/QueueRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/ErrorRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/AdminRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/ContentRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/UploadRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/BioRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/SettingsRoutes.php'); |
| | | require(JVB_DIR . '/inc/rest/routes/ShopRoutes.php'); |
| | | } |
| | | if (Features::forMembership()->has('forum')) { |
| | |
| | | require(JVB_DIR .'/inc/rest/routes/OptionsRoutes.php'); |
| | | require(JVB_DIR .'/inc/rest/routes/FormRoutes.php'); |
| | | require(JVB_DIR .'/inc/rest/routes/IntegrationsRoutes.php'); |
| | | require(JVB_DIR .'/inc/rest/routes/LoginRoutes.php'); |
| | |
| | | use JVBase\JVB; |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\utility\Features; |
| | | use WP_User; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | |
| | | protected array $voteTables; |
| | | |
| | | protected int $expiryDays = 7; |
| | | protected bool $hasMemberApproval = false; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'approvals'; |
| | | $this->hasMemberApproval = Features::forMembership()->has('member_verified'); |
| | | parent::__construct(); |
| | | |
| | | $this->initTypes(); |
| | | |
| | | if (jvbSiteHasMemberApproval()) { |
| | | if ($this->hasMemberApproval) { |
| | | add_action('user_register', [$this, 'handleNewUserRegistration'], 10, 2); |
| | | } |
| | | |
| | |
| | | $approvals = jvbApprovalTypes(); |
| | | $this->userTypes = []; |
| | | $this->termTypes = []; |
| | | if (jvbSiteHasMemberApproval()) { |
| | | if ($this->hasMemberApproval) { |
| | | $this->userTypes = array_filter( |
| | | array_keys($approvals), |
| | | function ($item) { |
| | |
| | | protected string $post_type = ''; |
| | | protected string $user_id = ''; |
| | | |
| | | //For Timeline-specific posts |
| | | protected array $timelineSharedFields = []; |
| | | protected array $timelineUniqueFields = []; |
| | | |
| | | //TODO: Ensure we are handling the bulk operations for all processes |
| | | //TODO: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create |
| | | //TODO: Also invalidate feed caches on updates!! |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'user_content_'.get_current_user_id(); |
| | | parent::__construct(); |
| | | |
| | | $this->action = 'dash-'; |
| | | $this->operation_type = 'content_update'; |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | |
| | | ]); |
| | | } |
| | | |
| | | protected function initTimelineFields(string $content):void |
| | | { |
| | | $content = jvbNoBase($content); |
| | | if (!Features::forContent($content)->has('is_timeline')){ |
| | | return; |
| | | } |
| | | $config = Features::getConfig($content); |
| | | $this->fields = $config['fields']; |
| | | |
| | | $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) { |
| | | if (!array_key_exists('for_all', $field) || $field['for_all'] === false){ |
| | | return true; |
| | | } |
| | | return false; |
| | | })); |
| | | array_unshift($this->timelineSharedFields, 'post_thumbnail'); |
| | | array_unshift($this->timelineSharedFields, 'post_title'); |
| | | |
| | | $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) { |
| | | if (array_key_exists('for_all', $field) && $field['for_all'] === true) { |
| | | return true; |
| | | } |
| | | return false; |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * Handle content update/creation |
| | | * @param WP_REST_Request $request |
| | |
| | | } |
| | | $post_type = str_replace('-', '_',jvbCheckBase($params['content'])); |
| | | |
| | | $config = Features::getConfig($params['content']); |
| | | |
| | | |
| | | |
| | | // Build query args |
| | | $args = [ |
| | |
| | | |
| | | } |
| | | |
| | | CacheManager::invalidateGroup($post_data['content']); |
| | | CacheManager::for($post_data['content'])->clear(); |
| | | if (jvbSiteUsesFeedBlock()) { |
| | | CacheManager::invalidateGroup($post_data['feed']); |
| | | CacheManager::for('feed')->clear(); |
| | | } |
| | | } |
| | | |
| | | |
| | | CacheManager::invalidateGroup('user_content'); |
| | | if (jvbSiteHasNotifications()) { |
| | | $this->notifications = JVB()->notification(); |
| | | $this->notifications->addNotification( |
| | |
| | | return ['success' => false, 'message' => 'No permission']; |
| | | } |
| | | |
| | | $rows = $post_data['fields'] ?? []; |
| | | if (empty($rows)) { |
| | | return ['success' => false, 'message' => 'No data']; |
| | | } |
| | | $this->fields = jvbGetFields($post_data['content']); |
| | | $this->initTimelineFields($post_data['content']); |
| | | error_log('Received Data: '.print_r($post_data, true)); |
| | | |
| | | $fields = jvbGetFields($post_data['content']); |
| | | |
| | | // First row = parent post |
| | | $parent_row = array_shift($rows); |
| | | if (($parent_row['id'] ?? null) != $parent_id) { |
| | | return ['success' => false, 'message' => 'Parent ID mismatch']; |
| | | } |
| | | |
| | | $allowedFields = array_filter($parent_row, function($key) use ($fields) { |
| | | return array_key_exists($key, $fields); |
| | | // First, process the main fields that will apply to all posts |
| | | $sharedData = array_filter($post_data, function ($key) { |
| | | return in_array($key, $this->timelineSharedFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $parentMeta = new MetaManager($parent_id, 'post'); |
| | | $parentMeta->setAll($allowedFields); |
| | | //Next, process any individual posts, including any menu order changes |
| | | if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) { |
| | | $sharedTaxonomies = $sharedData; |
| | | unset($sharedTaxonomies['post_title']); |
| | | |
| | | // Get existing children to track deletions |
| | | $existing_children = get_children([ |
| | | 'post_parent' => $parent_id, |
| | | 'post_type' => jvbCheckBase($post_data['content']), |
| | | 'fields' => 'ids' |
| | | ]); |
| | | |
| | | $processed_ids = []; |
| | | |
| | | // Process remaining rows as children |
| | | foreach ($rows as $index => $row_data) { |
| | | $row_id = $row_data['id'] ?? null; |
| | | |
| | | // New child post |
| | | if (!$row_id || str_starts_with($row_id, 'new')) { |
| | | $child_id = wp_insert_post([ |
| | | 'post_type' => jvbCheckBase($post_data['content']), |
| | | 'post_parent' => $parent_id, |
| | | 'post_author' => $this->user_id, |
| | | 'post_status' => $post_data['status'] ?? 'draft', |
| | | 'menu_order' => $index |
| | | ]); |
| | | //Ensure the parent post exists and is still first in the array |
| | | $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id')); |
| | | error_log('Found index: '.print_r($index, true)); |
| | | if ($index === false) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Missing parent id. This should not have happened' |
| | | ]; |
| | | } elseif ($index !== 0) { |
| | | // Move that element to the start of the array |
| | | $item = $post_data['timeline'][$index]; |
| | | unset($post_data['timeline'][$index]); |
| | | array_unshift($post_data['timeline'], $item); |
| | | } |
| | | // Existing child post |
| | | else { |
| | | $child_id = (int) $row_id; |
| | | $errors = []; |
| | | $success = []; |
| | | // Get existing children to track deletions |
| | | $existing_children = get_children([ |
| | | 'post_parent' => $parent_id, |
| | | 'post_type' => jvbCheckBase($post_data['content']), |
| | | 'fields' => 'ids' |
| | | ]); |
| | | //Iterate through the timeline posts |
| | | foreach($post_data['timeline'] as $order => $timeline) { |
| | | $allowedFields = array_filter($timeline, function($key) { |
| | | return in_array($key, $this->timelineUniqueFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | // Verify ownership via parent |
| | | if (!in_array($child_id, $existing_children)) { |
| | | continue; // Skip if not actually a child of this parent |
| | | $allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order; |
| | | $allowedFields = array_merge($allowedFields, $sharedTaxonomies); |
| | | if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) { |
| | | $newChild = wp_insert_post([ |
| | | 'post_author' => $this->user_id, |
| | | 'post_type' => jvbCheckBase($post_data['content']), |
| | | 'post_title' => $allowedFields['post_title'], |
| | | 'post_parent' => $parent_id, |
| | | 'menu_order' => $order |
| | | ]); |
| | | if (!$newChild || is_wp_error($newChild)) { |
| | | $errors[] = [ |
| | | 'message' => 'Could not create child post', |
| | | 'data' => $timeline |
| | | ]; |
| | | continue; |
| | | } |
| | | $timeline['id'] = $newChild; |
| | | } |
| | | |
| | | // Update menu_order (position may have changed) |
| | | wp_update_post([ |
| | | 'ID' => $child_id, |
| | | 'menu_order' => $index |
| | | ]); |
| | | if (in_array((int)$timeline['id'], $existing_children)) { |
| | | unset($existing_children[array_search((int)$timeline['id'], $existing_children)]); |
| | | } |
| | | |
| | | //Determine which fields to update |
| | | $meta = new MetaManager($timeline['id'], 'post'); |
| | | $oldValues = $meta->getAll(array_keys($allowedFields)); |
| | | $updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) { |
| | | return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]); |
| | | }, ARRAY_FILTER_USE_BOTH); |
| | | $meta->setAll($updateValues); |
| | | //Update Menu Order, if applicable |
| | | if ((int) $timeline['id'] !== $parent_id) { |
| | | $post = get_post((int) $timeline['id']); |
| | | if ($post && $post->menu_order !== $order) { |
| | | $updated = wp_update_post([ |
| | | 'ID' => $post->ID, |
| | | 'menu_order' => $order, |
| | | ]); |
| | | if (!$updated || is_wp_error($updated)) { |
| | | $errors[] = [ |
| | | 'message' => 'Could not update timeline order for post', |
| | | 'data' => $timeline |
| | | ]; |
| | | } |
| | | } |
| | | } |
| | | $success[] = $timeline['id']; |
| | | } |
| | | |
| | | // Update child meta |
| | | $allowedChildFields = array_filter($row_data, function($key) use ($fields) { |
| | | return array_key_exists($key, $fields) && $key !== 'id' && $key !== 'draggable'; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $childMeta = new MetaManager($child_id, 'post'); |
| | | $childMeta->setAll($allowedChildFields); |
| | | |
| | | $processed_ids[] = $child_id; |
| | | } |
| | | //Delete any remaining children that no longer exist |
| | | if (!empty($existing_children)) { |
| | | foreach ($existing_children as $ID) { |
| | | wp_delete_post($ID); |
| | | } |
| | | } |
| | | |
| | | // Delete removed children |
| | | $deleted_ids = array_diff($existing_children, $processed_ids); |
| | | foreach ($deleted_ids as $delete_id) { |
| | | wp_delete_post($delete_id, true); |
| | | } |
| | | |
| | | return ['success' => true, 'processed' => $processed_ids]; |
| | | return ['success' => true, 'data' => [ |
| | | 'success' => $success, |
| | | 'errors' => $errors |
| | | ]]; |
| | | } |
| | | |
| | | /** |
| | |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function prepareItem(WP_Post $post, $skip = false):array |
| | | protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array |
| | | { |
| | | if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) { |
| | | $this->initTimelineFields($post->post_type); |
| | | return $this->formatTimeline($post); |
| | | } |
| | | $this->meta = new MetaManager($post->ID, 'post'); |
| | |
| | | 'alt' => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true), |
| | | 'icon' => $this->post_type, |
| | | 'taxonomies'=> [], |
| | | 'fields' => $this->meta->getAll(), |
| | | 'fields' => ($fields) ? $this->meta->getAll() : [], |
| | | 'images' => [], |
| | | ]; |
| | | |
| | |
| | | |
| | | return $data; |
| | | } |
| | | protected function extractImages():array |
| | | protected function extractImages(array $fields = []):array |
| | | { |
| | | //Extract images |
| | | $images = []; |
| | | $get = []; |
| | | foreach ($this->fields as $field => $config) { |
| | | $fields = (empty($fields)) ? $this->fields : $fields; |
| | | foreach ($fields as $field => $config) { |
| | | if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') { |
| | | $get[] = $field; |
| | | } |
| | |
| | | |
| | | protected function formatTimeline(WP_Post $post):array |
| | | { |
| | | $data = $this->prepareItem($post, true); |
| | | $firstRow = $data['fields']; |
| | | $firstRow['id'] = $post->ID; |
| | | $firstRow['draggable'] = false; |
| | | $fields = [$firstRow]; |
| | | $item = $this->prepareItem($post, true, false); |
| | | //Step 1: Get the fields that apply to all posts |
| | | $mainMeta = new MetaManager($post->ID, 'post'); |
| | | $item['fields'] = $mainMeta->getAll($this->timelineSharedFields); |
| | | |
| | | $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order']); |
| | | $allImages = []; |
| | | //Step 2: Get the fields for each individual posts |
| | | $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']); |
| | | array_unshift($children, $post->ID); |
| | | |
| | | $subFields = []; |
| | | $images = []; |
| | | foreach ($children as $child) { |
| | | $this->meta = new MetaManager($child->ID, 'post'); |
| | | $row = $this->meta->getAll(); // Store in variable first |
| | | $row['id'] = $child->ID; // Add ID to the row |
| | | $row['draggable'] = true; // Mark as draggable |
| | | $fields[] = $row; // Then append to fields |
| | | $meta = new MetaManager($child, 'post'); |
| | | $f = $meta->getAll($this->timelineUniqueFields); |
| | | $f = ['id' => $child] + $f; |
| | | $subFields[$f['post_thumbnail']] = $f; |
| | | |
| | | $images = $this->extractImages(); |
| | | if (!empty($images)) { |
| | | $allImages = $allImages + $images; |
| | | } |
| | | $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']); |
| | | } |
| | | $item['fields']['timeline'] = $subFields; |
| | | $item['images'] = $item['images'] + $images; |
| | | |
| | | if (!empty($allImages)) { |
| | | if (!array_key_exists('images', $data)) { |
| | | $data['images'] = []; |
| | | } |
| | | $data['images'] = $data['images'] + $allImages; |
| | | } |
| | | $data['fields']['timeline'] = $fields; |
| | | return $data; |
| | | return $item; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | //Clear cache |
| | | CacheManager::invalidateGroup($data['content']); |
| | | CacheManager::invalidateGroup('feed'); |
| | | CacheManager::invalidateGroup('user_content'); |
| | | CacheManager::for($data['content'])->clear(); |
| | | CacheManager::for('feed')->clear(); |
| | | } |
| | | |
| | | return [ |
| | |
| | | add_action('before_delete_post', [$this, 'cleanupPostFavourites']); |
| | | add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3); |
| | | |
| | | add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3); |
| | | |
| | | // Register cleanup scheduler |
| | | add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']); |
| | |
| | | } |
| | | } |
| | | |
| | | public function maybeAcceptListInvite(int $user_id, string $email, array $data):void |
| | | { |
| | | if (array_key_exists('list_token', $data) && !empty($data['list_token'])) { |
| | | $this->acceptListInvitation($data['list_token'], $email); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the owner ID for a content item |
| | | * |
| | |
| | | // Add hooks for processing accepted invitations |
| | | add_action('user_register', [$this, 'checkInvitation'], 10, 1); |
| | | |
| | | add_action('jvbLoginManagerInit', function($loginManager) { |
| | | $loginManager->registerTokenHandler('invite', function($token, $email, $user_id) { |
| | | JVB()->routes('invites')->acceptInvitation($token, $email, $user_id); |
| | | }); |
| | | |
| | | $loginManager->registerMessageHandler('invitation', |
| | | function() { |
| | | return '<h2>You\'ve been invited!</h2><p>Create your account to accept.</p>'; |
| | | }, |
| | | function() { |
| | | return isset($_GET['invite']); |
| | | } |
| | | ); |
| | | }); |
| | | add_filter('jvbLoginLabels', [$this, 'modifyLoginLabels'], 10, 2); |
| | | |
| | | |
| | | |
| | |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | public function modifyLoginLabels(array $labels, array $get_params): array |
| | | { |
| | | // Only modify if invitation params present |
| | | if (!array_key_exists('invite', $get_params) || !array_key_exists('email', $get_params)) { |
| | | return $labels; |
| | | } |
| | | $email = sanitize_email($get_params['email']); |
| | | $token = sanitize_text_field($get_params['invite']); |
| | | $user = email_exists($email); |
| | | if (!$user) { |
| | | return $labels; |
| | | } |
| | | $role = jvbUserRole($user); |
| | | // Get invitation data |
| | | $data = $this->verifyInvitation( |
| | | $token, |
| | | $email, |
| | | $role, |
| | | ); |
| | | |
| | | if (!$data) { |
| | | return $labels; |
| | | } |
| | | |
| | | // Build custom message |
| | | $inviters = json_decode($data->inviters, true); |
| | | $name = $data->name; |
| | | $names = array_map(function($inviter) { |
| | | $artist = jvbContentFromUser((int)$inviter['user_id']); |
| | | return $artist['name'] ?: $artist['display_name']; |
| | | }, $inviters); |
| | | |
| | | $message = count($names) > 1 |
| | | ? 'are already here, and have invited you to join in!' |
| | | : ' is already here, and invited you to join in!'; |
| | | |
| | | // Modify labels |
| | | $labels['title'] = 'Join the Scene, ' . $data->name; |
| | | $labels['description'] = [jvbCommaList($names) . ' ' . $message]; |
| | | |
| | | return $labels; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\EmailManager; |
| | | use JVBase\managers\LoginManager; |
| | | use JVBase\managers\MagicLinkManager; |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\utility\Features; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class LoginRoutes extends RestRouteManager |
| | | { |
| | | protected EmailManager $emailManager; |
| | | protected MagicLinkManager $magic_link; |
| | | protected LoginManager $loginManager; |
| | | protected ?string $requestId = null; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'auth'; |
| | | $this->cache_ttl = WEEK_IN_SECONDS; |
| | | $this->emailManager = new EmailManager(); |
| | | if (Features::forSite()->has('magicLink')) { |
| | | $this->magic_link = new MagicLinkManager(); |
| | | } |
| | | |
| | | parent::__construct(); |
| | | } |
| | | |
| | | public function registerRoutes(): void |
| | | { |
| | | // Login endpoint |
| | | register_rest_route($this->namespace, '/auth/login', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleLogin'], |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'user_email' => [ |
| | | 'required' => true, |
| | | 'type' => 'string', |
| | | 'sanitize_callback' => 'sanitize_email' |
| | | ], |
| | | 'user_password' => [ |
| | | 'required' => true, |
| | | 'type' => 'string' |
| | | ], |
| | | 'remember_me' => [ |
| | | 'required' => false, |
| | | 'type' => 'boolean', |
| | | 'default' => false |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // Logout endpoint |
| | | register_rest_route($this->namespace, '/auth/logout', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleLogout'], |
| | | 'permission_callback' => 'is_user_logged_in' |
| | | ]); |
| | | |
| | | // Check auth status |
| | | register_rest_route($this->namespace, '/auth/status', [ |
| | | 'methods' => 'GET', |
| | | 'callback' => [$this, 'getAuthStatus'], |
| | | 'permission_callback' => '__return_true' |
| | | ]); |
| | | |
| | | // Request password reset |
| | | register_rest_route($this->namespace, '/auth/lostpassword', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'requestPasswordReset'], |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'user_email' => [ |
| | | 'required' => true, |
| | | 'type' => 'string', |
| | | 'format' => 'email', |
| | | 'sanitize_callback' => 'sanitize_email' |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // Complete password reset (with token) |
| | | register_rest_route($this->namespace, '/auth/reset-password', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'resetPassword'], |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'token' => [ |
| | | 'required' => true, |
| | | 'type' => 'string', |
| | | 'sanitize_callback' => 'sanitize_text_field' |
| | | ], |
| | | 'user_email' => [ |
| | | 'required' => true, |
| | | 'type' => 'string', |
| | | 'format' => 'email', |
| | | 'sanitize_callback' => 'sanitize_email' |
| | | ], |
| | | 'password' => [ |
| | | 'required' => true, |
| | | 'type' => 'string' |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | register_rest_route($this->namespace, '/auth/register', [ |
| | | 'method' => 'POST', |
| | | 'callback' => [$this, 'handleRegister'], |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | ]); |
| | | |
| | | // Refresh session |
| | | register_rest_route($this->namespace, '/auth/refresh', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'refreshSession'], |
| | | 'permission_callback' => 'is_user_logged_in' |
| | | ]); |
| | | |
| | | register_rest_route($this->namespace, '/auth/email', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'checkEmailExists'], |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | ]); |
| | | } |
| | | |
| | | public function handleLogin(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | // Verify Turnstile |
| | | if (!$this->verifyTurnstile($request->get_param('cf-turnstile-response') ?? '')) { |
| | | return $this->error('Security verification failed', 'turnstile_failed', 403); |
| | | } |
| | | |
| | | $username = $request->get_param('user_email'); |
| | | $password = $request->get_param('user_password'); |
| | | $remember = (bool)$request->get_param('remember_me'); |
| | | |
| | | // Check for account lockout |
| | | $lockout = $this->checkAccountLockout($username); |
| | | if (is_wp_error($lockout)) { |
| | | return $this->error( |
| | | $lockout->get_error_message(), |
| | | 'account_locked', |
| | | 429 |
| | | ); |
| | | } |
| | | |
| | | // Attempt login |
| | | $user = wp_signon([ |
| | | 'user_login' => $username, |
| | | 'user_email' => $username, |
| | | 'user_password' => $password, |
| | | 'remember' => $remember |
| | | ], false); |
| | | |
| | | if (is_wp_error($user)) { |
| | | // Track failed attempt |
| | | $this->trackFailedLogin($username); |
| | | |
| | | return $this->error( |
| | | 'Invalid username or password', |
| | | 'login_failed', |
| | | 401 |
| | | ); |
| | | } |
| | | |
| | | // Clear failed attempts on success |
| | | $this->clearFailedAttempts($username); |
| | | |
| | | // Set auth cookie with remember me flag |
| | | wp_set_current_user($user->ID); |
| | | wp_set_auth_cookie($user->ID, $remember); |
| | | |
| | | // Store session fingerprint for hijacking protection |
| | | $this->storeSessionFingerprint($user->ID, $request); |
| | | |
| | | // Trigger WordPress login action |
| | | do_action('wp_login', $user->user_login, $user); |
| | | |
| | | // Audit log |
| | | $this->auditLog('user_login', [ |
| | | 'user_id' => $user->ID, |
| | | 'remember' => $remember |
| | | ]); |
| | | |
| | | return $this->success([ |
| | | 'message' => 'Login successful', |
| | | 'user' => $this->formatUserData($user), |
| | | 'redirect' => $this->getLoginRedirect($user) |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Handle logout request |
| | | */ |
| | | public function handleLogout(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $user_id = get_current_user_id(); |
| | | |
| | | // Clear session fingerprint |
| | | $this->clearSessionFingerprint($user_id); |
| | | |
| | | // Audit log |
| | | $this->auditLog('user_logout', ['user_id' => $user_id]); |
| | | |
| | | // WordPress logout |
| | | wp_logout(); |
| | | |
| | | return $this->success([ |
| | | 'message' => 'Logged out successfully' |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Get current authentication status |
| | | */ |
| | | public function getAuthStatus(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | if (!is_user_logged_in()) { |
| | | return $this->success([ |
| | | 'authenticated' => false |
| | | ]); |
| | | } |
| | | |
| | | $user = wp_get_current_user(); |
| | | |
| | | return $this->success([ |
| | | 'authenticated' => true, |
| | | 'user' => $this->formatUserData($user) |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Request password reset |
| | | */ |
| | | public function requestPasswordReset(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $email = $request->get_param('user_email')??''; |
| | | $email = sanitize_email($email); |
| | | |
| | | $token = $request->get_param('cf-turnstile-response')??''; |
| | | if (!$this->verifyTurnstile($token)){ |
| | | return $this->error('Security verification failed', 'turnstile_failed', 403); |
| | | } |
| | | |
| | | // Use WordPress's built-in function |
| | | $result = retrieve_password($email); |
| | | |
| | | // Log but don't expose |
| | | if (is_wp_error($result)) { |
| | | $this->auditLog('password_reset_failed', [ |
| | | 'email' => $email, |
| | | 'reason' => $result->get_error_code() |
| | | ]); |
| | | } else { |
| | | $this->auditLog('password_reset_sent', [ |
| | | 'email' => $email |
| | | ]); |
| | | } |
| | | |
| | | return $this->success([ |
| | | 'message' => 'If an account exists with this email, you will receive a password reset link.' |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Complete password reset with token |
| | | */ |
| | | public function resetPassword(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $key = sanitize_text_field($request->get_param('key')); |
| | | $login = sanitize_text_field($request->get_param('login')); |
| | | $password = $request->get_param('password'); |
| | | |
| | | if (empty($key) || empty($login)) { |
| | | return $this->error('Invalid reset link', 'invalid_key', 400); |
| | | } |
| | | |
| | | // Use WordPress's native key verification |
| | | $user = check_password_reset_key($key, $login); |
| | | |
| | | if (is_wp_error($user)) { |
| | | return $this->error( |
| | | 'Invalid or expired reset link', |
| | | 'invalid_token', |
| | | 400 |
| | | ); |
| | | } |
| | | |
| | | // Validate password strength |
| | | $validation = $this->validatePassword($password); |
| | | if (is_wp_error($validation)) { |
| | | return $this->error( |
| | | $validation->get_error_message(), |
| | | 'weak_password', |
| | | 400 |
| | | ); |
| | | } |
| | | |
| | | // Reset the password |
| | | reset_password($user, $password); |
| | | |
| | | // Log them in |
| | | wp_set_current_user($user->ID); |
| | | wp_set_auth_cookie($user->ID, true); |
| | | |
| | | // Store session fingerprint |
| | | $this->storeSessionFingerprint($user->ID, $request); |
| | | |
| | | // Audit log |
| | | $this->auditLog('password_reset_complete', [ |
| | | 'user_id' => $user->ID |
| | | ]); |
| | | |
| | | return $this->success([ |
| | | 'message' => 'Password reset successfully', |
| | | 'redirect' => home_url('/dash') |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Refresh session (extends session duration) |
| | | */ |
| | | public function refreshSession(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $user_id = get_current_user_id(); |
| | | |
| | | // Validate session fingerprint |
| | | if (!$this->validateSessionFingerprint($user_id, $request)) { |
| | | wp_logout(); |
| | | return $this->unauthorized('Session validation failed'); |
| | | } |
| | | |
| | | // Refresh auth cookie |
| | | wp_set_auth_cookie($user_id, true); |
| | | |
| | | return $this->success([ |
| | | 'message' => 'Session refreshed' |
| | | ]); |
| | | } |
| | | |
| | | |
| | | public function handleRegister(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $data = $request->get_json_params(); |
| | | |
| | | // Duplicate submission check |
| | | if (!$this->checkRequestId($data['request_id'] ?? '')) { |
| | | return $this->error('Duplicate request detected', 'duplicate_request', 409); |
| | | } |
| | | |
| | | // Verify Turnstile |
| | | if (!$this->verifyTurnstile($data['cf-turnstile-response'] ?? '')) { |
| | | return $this->error('Security verification failed', 'turnstile_failed', 403); |
| | | } |
| | | |
| | | $name = sanitize_text_field($data['name'] ?? ''); |
| | | $email = sanitize_email($data['email'] ?? ''); |
| | | $user_type = sanitize_text_field($data['user_select'] ?? 'subscriber'); |
| | | |
| | | // Validate fields |
| | | if (empty($name)) { |
| | | return $this->error('Name is required', 'missing_name', 400, 'name'); |
| | | } |
| | | |
| | | if (empty($email)) { |
| | | return $this->error('Email is required', 'missing_email', 400, 'email'); |
| | | } |
| | | |
| | | // Spam prevention |
| | | if ($user_type === 'subscriber' && count(JVB_USER) > 0) { |
| | | $registerable = array_filter(JVB_USER, fn($config) => $config['can_register'] ?? false); |
| | | if (!empty($registerable)) { |
| | | return $this->error('Please select a valid account type', 'invalid_user_type', 400, 'user_select'); |
| | | } |
| | | } |
| | | |
| | | // Check if role can register |
| | | if ($user_type !== 'subscriber') { |
| | | if (!isset(JVB_USER[$user_type]) || empty(JVB_USER[$user_type]['can_register'])) { |
| | | return $this->error('Invalid account type', 'invalid_user_type', 400, 'user_select'); |
| | | } |
| | | } |
| | | |
| | | // Check if email exists |
| | | if (email_exists($email)) { |
| | | return $this->error('Email already registered', 'duplicate_email', 400, 'email'); |
| | | } |
| | | |
| | | // Allow WP plugins to add registration errors |
| | | $errors = new WP_Error(); |
| | | $errors = apply_filters('registration_errors', $errors, $email, $email); |
| | | |
| | | if ($errors->has_errors()) { |
| | | return $this->error( |
| | | $errors->get_error_message(), |
| | | $errors->get_error_code(), |
| | | 400 |
| | | ); |
| | | } |
| | | |
| | | // Create user |
| | | $user_id = wp_create_user($email, wp_generate_password(), $email); |
| | | |
| | | if (is_wp_error($user_id)) { |
| | | return $this->error($user_id->get_error_message(), 'user_creation_failed', 500); |
| | | } |
| | | |
| | | // Update user data |
| | | wp_update_user([ |
| | | 'ID' => $user_id, |
| | | 'display_name' => $name, |
| | | 'first_name' => strtok($name,' ') |
| | | ]); |
| | | |
| | | // Set role |
| | | $user = new WP_User($user_id); |
| | | if ($user_type === 'subscriber') { |
| | | $user->set_role('subscriber'); |
| | | } else { |
| | | $role = JVB_USER[$user_type]['role'] ?? 'subscriber'; |
| | | $user->set_role($role); |
| | | |
| | | // Check if needs approval |
| | | if (Features::forMembership()->has('memberVerified') && |
| | | in_array($role, JVB_MEMBERSHIP['memberVerified'] ?? [])) { |
| | | $user->add_cap('skip_moderation', false); |
| | | update_user_meta($user_id, BASE . 'pending_approval', true); |
| | | } |
| | | } |
| | | |
| | | if (Features::forUser($user_type)->has('namedDirectory')) { |
| | | $upload_dir = wp_upload_dir(); |
| | | $user_directory = $user_type.'/'.$user_id; |
| | | $target_dir = $upload_dir['basedir'].'/'.$user_directory; |
| | | |
| | | wp_mkdir_p($target_dir); |
| | | } |
| | | |
| | | // Save additional fields |
| | | update_user_meta($user_id, BASE . 'user_type', $user_type); |
| | | |
| | | // Process additional fields from form |
| | | foreach ($data as $key => $value) { |
| | | if (in_array($key, ['name', 'email', 'action', 'request_id', 'user_select', 'cf-turnstile-response'])) { |
| | | continue; |
| | | } |
| | | update_user_meta($user_id, BASE . $key, sanitize_text_field($value)); |
| | | } |
| | | |
| | | // Handle token handlers |
| | | do_action('jvbUserRegistered', $user_id, $email, $data); |
| | | |
| | | |
| | | return $this->success([ |
| | | 'message' => 'Registration successful! Check your email.', |
| | | 'user_id' => $user_id |
| | | ]); |
| | | } |
| | | /************************************************************** |
| | | HELPERS |
| | | **************************************************************/ |
| | | /** |
| | | * Format user data for response |
| | | */ |
| | | protected function formatUserData(WP_User $user): array |
| | | { |
| | | return [ |
| | | 'id' => $user->ID, |
| | | 'username' => $user->user_login, |
| | | 'email' => $user->user_email, |
| | | 'display_name' => $user->display_name, |
| | | 'roles' => $user->roles, |
| | | 'capabilities' => [ |
| | | 'manage_options' => user_can($user, 'manage_options'), |
| | | 'skip_moderation' => user_can($user, 'skip_moderation'), |
| | | // Add other relevant capabilities |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Get login redirect URL based on user role |
| | | */ |
| | | protected function getLoginRedirect(WP_User $user): string |
| | | { |
| | | if (user_can($user, 'manage_options')) { |
| | | return admin_url(); |
| | | } |
| | | |
| | | // Redirect to dashboard for members |
| | | if (function_exists('isOurPeople') && isOurPeople()) { |
| | | return home_url('/dash'); |
| | | } |
| | | |
| | | return home_url(); |
| | | } |
| | | |
| | | /** |
| | | * Validate password strength |
| | | */ |
| | | protected function validatePassword(string $password): bool|WP_Error |
| | | { |
| | | if (strlen($password) < 8) { |
| | | return new WP_Error( |
| | | 'weak_password', |
| | | 'Password must be at least 8 characters long' |
| | | ); |
| | | } |
| | | |
| | | // Add additional strength requirements as needed |
| | | // - Must contain uppercase |
| | | // - Must contain number |
| | | // - Must contain special character |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Check if account is locked out due to failed attempts |
| | | */ |
| | | protected function checkAccountLockout(string $username): bool|WP_Error |
| | | { |
| | | if (!defined('JVB_MAX_LOGIN_ATTEMPTS')) { |
| | | return true; |
| | | } |
| | | |
| | | $cache_key = 'login_attempts_' . md5($username); |
| | | $attempts = $this->cache->get($cache_key); |
| | | |
| | | if (!$attempts || !isset($attempts['count'])) { |
| | | return true; |
| | | } |
| | | |
| | | if ($attempts['count'] >= JVB_MAX_LOGIN_ATTEMPTS) { |
| | | $lockout_duration = defined('JVB_LOCKOUT_DURATION') |
| | | ? JVB_LOCKOUT_DURATION |
| | | : 15 * MINUTE_IN_SECONDS; |
| | | |
| | | $time_remaining = $lockout_duration - (time() - $attempts['timestamp']); |
| | | |
| | | if ($time_remaining > 0) { |
| | | return new WP_Error( |
| | | 'account_locked', |
| | | sprintf( |
| | | 'Too many failed login attempts. Please try again in %d minutes.', |
| | | ceil($time_remaining / 60) |
| | | ) |
| | | ); |
| | | } |
| | | |
| | | // Lockout expired - clear attempts |
| | | $this->cache->delete($cache_key); |
| | | return true; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Track failed login attempt |
| | | */ |
| | | protected function trackFailedLogin(string $username): void |
| | | { |
| | | $cache_key = 'login_attempts_' . md5($username); |
| | | $attempts = $this->cache->get($cache_key) ?: [ |
| | | 'count' => 0, |
| | | 'timestamp' => time() |
| | | ]; |
| | | |
| | | $attempts['count']++; |
| | | $attempts['timestamp'] = time(); |
| | | |
| | | $lockout_duration = defined('JVB_LOCKOUT_DURATION') |
| | | ? JVB_LOCKOUT_DURATION |
| | | : 15 * MINUTE_IN_SECONDS; |
| | | |
| | | $this->cache->set($cache_key, $attempts, $lockout_duration); |
| | | |
| | | // Audit log |
| | | $this->auditLog('failed_login', [ |
| | | 'username' => $username, |
| | | 'attempts' => $attempts['count'] |
| | | ]); |
| | | } |
| | | |
| | | /** |
| | | * Clear failed login attempts on successful login |
| | | */ |
| | | protected function clearFailedAttempts(string $username): void |
| | | { |
| | | $cache_key = 'login_attempts_' . md5($username); |
| | | $this->cache->delete($cache_key); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | public function checkEmailExists(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $data = $request->get_json_params(); |
| | | $email = sanitize_email($data['email'] ?? ''); |
| | | |
| | | return $this->success([ |
| | | 'exists' => is_email($email) && email_exists($email) |
| | | ]); |
| | | } |
| | | /*********************************************************************** |
| | | * HELPER METHODS |
| | | ***********************************************************************/ |
| | | |
| | | protected function checkRequestId(string $request_id): bool |
| | | { |
| | | if (empty($request_id)) { |
| | | return true; |
| | | } |
| | | |
| | | $cache_key = 'request_' . $request_id; |
| | | if (get_transient($cache_key)) { |
| | | return false; |
| | | } |
| | | |
| | | set_transient($cache_key, true, 60); |
| | | return true; |
| | | } |
| | | |
| | | protected function verifyTurnstile(string $token): bool |
| | | { |
| | | if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) { |
| | | return true; |
| | | } |
| | | |
| | | if (empty($token)) { |
| | | return false; |
| | | } |
| | | |
| | | return JVB()->connect('cloudflare')->verifyTurnstile($token); |
| | | } |
| | | |
| | | /** |
| | | * Helper to return error response |
| | | */ |
| | | protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response |
| | | { |
| | | if ($this->requestId) { |
| | | delete_transient('request_'.$this->requestId); |
| | | $this->requestId = null; |
| | | } |
| | | return parent::error($message, $code, $status, $field); |
| | | } |
| | | |
| | | |
| | | } |
| | |
| | | public function registerRoutes(): void |
| | | { |
| | | // Send magic link |
| | | register_rest_route($this->namespace, '/magic-link', [ |
| | | register_rest_route($this->namespace, '/magic', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'sendMagicLink'], |
| | | 'permission_callback' => '__return_true', // Public endpoint |
| | | 'args' => [ |
| | | 'email' => [ |
| | | 'required' => true, |
| | | 'type' => 'string', |
| | | 'format' => 'email', |
| | | 'validate_callback' => function($param) { |
| | | return is_email($param); |
| | | } |
| | | ], |
| | | 'type' => [ |
| | | 'required' => false, |
| | | 'type' => 'string', |
| | | 'default' => 'login', |
| | | 'enum' => ['login', 'signup', 'referral', 'reset'] |
| | | ], |
| | | 'context' => [ |
| | | 'required' => false, |
| | | 'type' => 'object', |
| | | 'default' => [] |
| | | ] |
| | | ] |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | // 'args' => [ |
| | | // 'email' => [ |
| | | // 'required' => true, |
| | | // 'type' => 'string', |
| | | // 'format' => 'email', |
| | | // 'validate_callback' => function($param) { |
| | | // return is_email($param); |
| | | // } |
| | | // ], |
| | | // 'type' => [ |
| | | // 'required' => false, |
| | | // 'type' => 'string', |
| | | // 'default' => 'login', |
| | | // 'enum' => ['login', 'signup', 'referral', 'reset'] |
| | | // ], |
| | | // 'context' => [ |
| | | // 'required' => false, |
| | | // 'type' => 'object', |
| | | // 'default' => [] |
| | | // ] |
| | | // ] |
| | | ]); |
| | | |
| | | // Resend magic link |
| | | register_rest_route($this->namespace, '/magic-link/resend', [ |
| | | register_rest_route($this->namespace, '/magic/resend', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'resendMagicLink'], |
| | | 'permission_callback' => '__return_true', |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'email' => [ |
| | | 'required' => true, |
| | |
| | | ]); |
| | | |
| | | // Check token validity (useful for frontend) |
| | | register_rest_route($this->namespace, '/magic-link/verify', [ |
| | | register_rest_route($this->namespace, '/magic/verify', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'verifyToken'], |
| | | 'permission_callback' => '__return_true', |
| | |
| | | */ |
| | | public function sendMagicLink(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | $email = sanitize_email($request->get_param('email')); |
| | | $email = sanitize_email($request->get_param('email')??$request->get_param('user_email')??''); |
| | | $type = sanitize_text_field($request->get_param('type')); |
| | | $context = $request->get_param('context') ?? []; |
| | | |
| | |
| | | ], 400); |
| | | } |
| | | |
| | | // Check if email exists |
| | | $exists = email_exists($email); |
| | | if (!$exists) { |
| | | |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'User account not found' |
| | | ], 400); |
| | | } |
| | | |
| | | // Send the magic link |
| | | $result = $this->magic_link->sendMagicLink($email, $type, $context); |
| | | error_log('Result: '.print_r($result, true)); |
| | |
| | | $token = sanitize_text_field($request->get_param('token')); |
| | | $email = sanitize_email($request->get_param('email')); |
| | | |
| | | $cache_key = 'magic_token_' . $token; |
| | | $token_data = get_transient($cache_key); |
| | | // This returns array|WP_Error - check for error first |
| | | $token_data = $this->magic_link->verifyToken($token, $email); |
| | | |
| | | if (!$token_data) { |
| | | if (is_wp_error($token_data)) { |
| | | return new WP_REST_Response([ |
| | | 'valid' => false, |
| | | 'message' => 'Token expired or invalid' |
| | | 'message' => $token_data->get_error_message() |
| | | ], 400); |
| | | } |
| | | |
| | | if ($token_data['email'] !== $email) { |
| | | // Now check the data |
| | | if (!isset($token_data['email']) || $token_data['email'] !== $email) { |
| | | return new WP_REST_Response([ |
| | | 'valid' => false, |
| | | 'message' => 'Invalid token' |
| | | ], 400); |
| | | } |
| | | |
| | | if (time() > $token_data['expires_at']) { |
| | | return new WP_REST_Response([ |
| | | 'valid' => false, |
| | | 'message' => 'Token expired' |
| | | ], 400); |
| | | } |
| | | |
| | | // Check expiration - but your cache-based system doesn't store expires_at |
| | | // If token wasn't expired, it wouldn't have been returned from cache |
| | | // So just return valid: |
| | | return new WP_REST_Response([ |
| | | 'valid' => true, |
| | | 'type' => $token_data['type'], |
| | | 'expires_in' => $token_data['expires_at'] - time() |
| | | 'type' => $token_data['type'] ?? 'unknown' |
| | | ], 200); |
| | | } |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\ReferralManager; |
| | | use JVBase\rest\RestRouteManager; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | |
| | | */ |
| | | class ReferralRoutes extends RestRouteManager |
| | | { |
| | | |
| | | protected ReferralManager $manager; |
| | | public function __construct() |
| | | { |
| | | $this->route = 'referrals'; |
| | |
| | | 'permission_callback' => [$this, 'checkPermission'] |
| | | ]); |
| | | |
| | | register_rest_route($this->namespace, '/referrals/register', [ |
| | | register_rest_route($this->namespace, "/{$this->route}/register", [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'registerWithReferral'], |
| | | 'permission_callback' => '__return_true', |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'name' => [ |
| | | 'required' => true, |
| | |
| | | register_rest_route($this->namespace, '/referrals/check-code', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'checkReferralCode'], |
| | | 'permission_callback' => '__return_true', |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'code' => [ |
| | | 'required' => true, |
| | |
| | | register_rest_route($this->namespace, "/{$this->route}/track", [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'trackReferralClick'], |
| | | 'permission_callback' => '__return_true', |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'code' => [ |
| | | 'required' => true, |
| | |
| | | ] |
| | | ]); |
| | | |
| | | |
| | | |
| | | // Get top referrers (admin only) |
| | | register_rest_route($this->namespace, "/{$this->route}/leaderboard", [ |
| | | 'methods' => 'GET', |
| | |
| | | } |
| | | ] |
| | | ]); |
| | | |
| | | register_rest_route($this->namespace, "/{$this->route}/add-code", [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'addReferralCodeAfterRegistration'], |
| | | 'permission_callback' => [$this, 'checkRateLimit'], |
| | | 'args' => [ |
| | | 'code' => [ |
| | | 'required' => true, |
| | | 'type' => 'string', |
| | | 'sanitize_callback' => function ($code) { |
| | | return strtoupper(sanitize_text_field($code)); |
| | | } |
| | | ] |
| | | ] |
| | | ]); |
| | | } |
| | | |
| | | public function checkPermission(WP_REST_Request $request): bool |
| | |
| | | 'message' => 'Invalid referral code' |
| | | ], 404); |
| | | } |
| | | if (is_user_logged_in() && get_current_user_id() === $referrer->ID) { |
| | | return $this->error('You cannot use your own referral code', 'self_referral', 400); |
| | | } |
| | | |
| | | // Return basic referrer info (no sensitive data) |
| | | return new WP_REST_Response([ |
| | |
| | | 'referrer_name' => $referrer->display_name, |
| | | ], 200); |
| | | } |
| | | |
| | | public function addReferralCodePostRegistration(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | if (!$this->manager) { |
| | | $this->manager = JVB()->referrals(); |
| | | } |
| | | $user_id = get_current_user_id(); |
| | | $code = $request->get_param('code'); |
| | | |
| | | // Check if user already has a referral (can't change) |
| | | $existing = $this->manager->getReferralByReferee($user_id); |
| | | if ($existing) { |
| | | return $this->error('You already have a referral code applied', 'already_referred', 400); |
| | | } |
| | | |
| | | // Validate the code exists |
| | | $referrer = $this->manager->getUserByReferralCode($code); |
| | | if (!$referrer) { |
| | | return $this->error('Invalid referral code', 'invalid_code', 400); |
| | | } |
| | | |
| | | // Create the referral |
| | | $user = wp_get_current_user(); |
| | | $result = $this->manager->createReferral($referrer->ID, $user_id, $code); |
| | | |
| | | if ($result) { |
| | | return $this->success([ |
| | | 'message' => 'Referral code applied successfully!', |
| | | 'referrer_name' => $referrer->display_name |
| | | ]); |
| | | } |
| | | |
| | | return $this->error('Failed to apply referral code', 'creation_failed', 500); |
| | | } |
| | | } |
| | |
| | | '.$textarea.' |
| | | </div> |
| | | <div class="actions row"> |
| | | <button type="button" class="cancel">'.jvbIcon('close', ['title'=>'Cancel']).'</button> |
| | | <button type="button" class="cancel">'.jvbIcon('x', ['title'=>'Cancel']).'</button> |
| | | <button type="submit" class="create">Reply</button> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="upload-status"> |
| | | <div class="upload-progress"></div> |
| | | </div> |
| | | <button type="button" class="remove-preview" title="Remove Image">'.jvbIcon('delete').'</button> |
| | | <button type="button" class="move-image" title="Reorder Image">'.jvbIcon('grab').'</button> |
| | | <button type="button" class="remove-preview" title="Remove Image">'.jvbIcon('trash').'</button> |
| | | <button type="button" class="move-image" title="Reorder Image">'.jvbIcon('dots-six-vertical').'</button> |
| | | </div> |
| | | </template>'; |
| | | } |
| | |
| | | |
| | | <section class="invite-artists"> |
| | | <div class="invited-artist"> |
| | | <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('delete')?></button> |
| | | <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('trash')?></button> |
| | | <div> |
| | | <label for="invited-artist-name-1">First Name</label> |
| | | <input type="text" id="invited-artist-name-1" name="invited-artist-name-1"> |
| | |
| | | </div> |
| | | </section> |
| | | <div class="actions"> |
| | | <button type="button" onclick="addArtist(this)" class="add-artist"><?= jvbIcon('add')?>Add Artist</button> |
| | | <button type="button" onclick="sendInvites(this)" class="send-invites"><?=jvbIcon('email')?>Send Invites</button> |
| | | <button type="button" onclick="addArtist(this)" class="add-artist"><?= jvbIcon('plus-square')?>Add Artist</button> |
| | | <button type="button" onclick="sendInvites(this)" class="send-invites"><?=jvbIcon('envelope')?>Send Invites</button> |
| | | </div> |
| | | |
| | | <template class="artistInvite"> |
| | | <div class="invited-artist"> |
| | | <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('delete')?></button> |
| | | <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('trash')?></button> |
| | | <div> |
| | | <label for="invited-artist-name">First Name</label> |
| | | <input type="text" id="invited-artist-name" name="invited-artist-name"> |
| | |
| | | </td> |
| | | <td class="actions"> |
| | | <button type="button" data-action="revoke" title="Revoke Invitation" onclick="revokeInvite(this)"> |
| | | <?=jvbIcon('delete')?> |
| | | <?=jvbIcon('trash')?> |
| | | <span class="screen-reader-text">Revoke invite</span> |
| | | </button> |
| | | <button type="button" data-action="resend"<?=($invite['status'] === 'exipired') ? '' : ' disabled'?> title="Resend Invitation" onclick="resendInvite(this)"> |
| | | <?=jvbIcon('refresh')?> |
| | | <?=jvbIcon('arrows-clockwise')?> |
| | | <span class="screen-reader-text">Try Again</span> |
| | | </button> |
| | | </td> |
| | |
| | | */ |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\utility\Checker; |
| | | use JVBase\utility\Features; |
| | | |
| | |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * Track REST API errors by wrapping request execution |
| | | */ |
| | | add_filter('rest_pre_dispatch', function($result, $server, $request) { |
| | | // Store request details globally for error logging |
| | | $GLOBALS['jvb_rest_request'] = [ |
| | | 'route' => $request->get_route(), |
| | | 'method' => $request->get_method(), |
| | | 'params' => $request->get_params(), |
| | | 'time' => microtime(true), |
| | | ]; |
| | | |
| | | // Set up error handler for this request |
| | | set_error_handler(function($errno, $errstr, $errfile, $errline) { |
| | | if (!(error_reporting() & $errno)) { |
| | | return false; |
| | | } |
| | | |
| | | $context = array_merge($GLOBALS['jvb_rest_request'] ?? [], [ |
| | | 'error_type' => $errno, |
| | | 'error_message' => $errstr, |
| | | 'file' => $errfile, |
| | | 'line' => $errline, |
| | | 'user_id' => get_current_user_id(), |
| | | ]); |
| | | |
| | | error_log('JVB REST Error: ' . json_encode($context)); |
| | | |
| | | return false; // Let PHP handle it normally too |
| | | }); |
| | | |
| | | return $result; |
| | | }, 10, 3); |
| | | |
| | | add_filter('rest_post_dispatch', function($result, $server, $request) { |
| | | // Restore default error handler |
| | | restore_error_handler(); |
| | | |
| | | // Check for fatal errors that occurred during request |
| | | $error = error_get_last(); |
| | | if ($error && in_array($error['type'], [E_ERROR, E_USER_ERROR, E_PARSE, E_COMPILE_ERROR])) { |
| | | $context = array_merge($GLOBALS['jvb_rest_request'] ?? [], [ |
| | | 'fatal_error' => $error, |
| | | 'user_id' => get_current_user_id(), |
| | | ]); |
| | | |
| | | error_log('JVB REST Fatal: ' . json_encode($context)); |
| | | } |
| | | |
| | | // Clean up |
| | | unset($GLOBALS['jvb_rest_request']); |
| | | |
| | | return $result; |
| | | }, 10, 3); |
| | | |
| | | |
| | | const JVB_LOCAL = 'northeh.test'; |
| | | |
| | | function jvbIgnoredPostTypes():array |
| | | { |
| | | return [BASE.'directory', BASE.'dash', 'attachment', 'revision', 'nav_menu_item']; |
| | | } |
| | | add_filter('show_admin_bar', '__return_false'); |
| | | const JVB_TESTING = true; |
| | | |
| | | const JVB_DIR = WP_PLUGIN_DIR . '/jvb'; |
| | | define('JVB_URL', plugin_dir_url(__FILE__)); |
| | | |
| | | // Session Security |
| | | define('JVB_SESSION_FINGERPRINT', true); |
| | | |
| | | // Login Security |
| | | define('JVB_MAX_LOGIN_ATTEMPTS', 5); |
| | | define('JVB_LOCKOUT_DURATION', 15 * MINUTE_IN_SECONDS); |
| | | |
| | | |
| | | require(JVB_DIR.'/base/_setup.php'); |
| | | //error_log('###############################################'); |
| | | //error_log('Registered Base'); |
| | |
| | | global $jvb_everything; |
| | | $jvb_everything = array_merge(JVB_CONTENT, JVB_TAXONOMY); |
| | | |
| | | |
| | | require(JVB_DIR . '/inc/registry/_setup.php'); |
| | | |
| | | require(JVB_DIR . '/activate.php'); |
| | |
| | | require(JVB_DIR . '/inc/helpers/all.php'); |
| | | require(JVB_DIR . '/inc/meta/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/_setup.php'); |
| | | |
| | | function jvbIcon($name, $options = []) { |
| | | return IconsManager::getInstance()->getIcon($name, $options); |
| | | } |
| | | |
| | | function jvbCSSIcon($name, $options = []) { |
| | | $style = array_key_exists('style', $options) ? $options['style'] : null; |
| | | return IconsManager::getInstance()->getCSSIcon($name, $style); |
| | | } |
| | | require(JVB_DIR . '/inc/integrations/_setup.php'); |
| | | require(JVB_DIR . '/inc/rest/_setup.php'); |
| | | |
| | |
| | | wp_enqueue_script('jvb-navigation'); |
| | | // wp_enqueue_script('jvb-ui'); |
| | | wp_enqueue_script('jvb-media'); |
| | | wp_enqueue_script('jvb-gallery'); |
| | | wp_enqueue_script('jvb-cache'); |
| | | |
| | | |
| | | |
| | | $userID = get_current_user_id(); |
| | | $icons = new \JVBase\JVBIcons(); |
| | | $queue = (is_user_logged_in()) ? |
| | | [ |
| | | 'api' => rest_url('jvb/v1/'), |
| | |
| | | 'dash' => wp_create_nonce('dash-'.$userID), |
| | | 'favourites' => wp_create_nonce('favourites-'.$userID), |
| | | 'notifications' => wp_create_nonce('notifications-'.$userID), |
| | | 'icons' => $icons->localizeIcons(), |
| | | 'labels' => jvbGetLabels(), |
| | | ] : |
| | | [ |
| | |
| | | 'nonce' => wp_create_nonce('wp_rest'), |
| | | 'currentUser' => false, |
| | | 'redirect' => wp_login_url(home_url(add_query_arg(null, null))), // Current URL as redirect |
| | | 'icons' => $icons->localizeIcons(), |
| | | 'labels' => jvbGetLabels(), |
| | | ]; |
| | | |
| | |
| | | //add_action('wp_head', 'jvbDumpIt'); |
| | | function jvbDumpIt() |
| | | { |
| | | jvbDump(get_post_type_archive_link(BASE.'faq')); |
| | | } |
| | | |
| | | add_action('after_setup_theme', 'jvbImageSize'); |
| | | function jvbImageSize():void |
| | | { |
| | | add_theme_support('post-thumbnails'); |
| | | add_image_size( 'tiny', 50, 50, false ); |
| | | } |
| | | |
| | | |
| | | // |
| | | //add_action('rest_api_init', function() { |
| | | // error_log('User ' . get_current_user_id() . ' can edit pages: ' . (current_user_can('edit_pages') ? 'yes' : 'no')); |
| | | // error_log('User roles: ' . print_r(wp_get_current_user()->roles, true)); |
| | | //}); |
| | | // |
| | | //add_filter('rest_pre_dispatch', function($result, $server, $request) { |
| | | // if (strpos($request->get_route(), '/wp/v2/pages') !== false) { |
| | | // error_log('Pages request - User: ' . get_current_user_id()); |
| | | // error_log('Can edit pages: ' . (current_user_can('edit_pages') ? 'yes' : 'no')); |
| | | // error_log('Nonce: ' . $request->get_header('X-WP-Nonce')); |
| | | // } |
| | | // return $result; |
| | | //}, 10, 3); |
| | |
| | | <div class="filter-actions"> |
| | | <?= jvbRenderToggleTextField('match', 'Match', 'Filters', 'ALL', 'ANY', true) ?> |
| | | <button type="button" class="clear-filters row" hidden> |
| | | <?= jvbIcon('close', ['title' => 'Clear']) ?> |
| | | <?= jvbIcon('x', ['title' => 'Clear']) ?> |
| | | Clear All Filters |
| | | </button> |
| | | </div> |
| | |
| | | |
| | | <input type="radio" id="order-random" class="btn" name="orderby" value="random"> |
| | | <label for="order-random" title="Random Order"> |
| | | <?= jvbIcon('random') ?> |
| | | <?= jvbIcon('shuffle') ?> |
| | | <span class="label">Random</span> |
| | | </label> |
| | | </div> |
| | |
| | | <div class="order-direction radio-group-label" data-for-order="date,title"> |
| | | <input type="radio" id="order-desc" class="btn" name="order" value="desc" checked> |
| | | <label for="order-desc" title="Newest First"> |
| | | <?= jvbIcon('desc') ?> |
| | | <?= jvbIcon('sort-descending') ?> |
| | | </label> |
| | | |
| | | <input type="radio" id="order-asc" class="btn" name="order" value="asc"> |
| | | <label for="order-asc" title="Oldest First"> |
| | | <?= jvbIcon('asc') ?> |
| | | <?= jvbIcon('sort-ascending') ?> |
| | | </label> |
| | | </div> |
| | | </div> |
| | |
| | | this.navList = this.nav.querySelector('ul'); |
| | | this.activeClass = 'active'; |
| | | this.currentActive = null; |
| | | this.breakpoint = 768; // Adjust this to match your small screen breakpoint |
| | | |
| | | this.init(); |
| | | this.setupResizeHandler(); |
| | |
| | | } |
| | | |
| | | getRootMargin() { |
| | | if (window.innerWidth < this.breakpoint) { |
| | | // On small screens: 5rem from top and bottom |
| | | // Convert rem to pixels |
| | | const remInPixels = parseFloat(getComputedStyle(document.documentElement).fontSize); |
| | | const margin = Math.round(remInPixels * 5); // 5rem in pixels |
| | | return `-${margin}px 0px -${margin}px 0px`; |
| | | } |
| | | // On larger screens: centered (50% from top and bottom) |
| | | return '-50% 0px -50% 0px'; |
| | | } |
| | |
| | | this.terms.forEach(term => { |
| | | const rect = term.getBoundingClientRect(); |
| | | |
| | | // Check if term is within the trigger zone (4rem from top or bottom on small screens) |
| | | const isInZone = window.innerWidth < this.breakpoint |
| | | ? rect.top >= margin && rect.top <= window.innerHeight - margin |
| | | : rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight; |
| | | const isInZone = rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight; |
| | | |
| | | if (isInZone) { |
| | | // Find closest to the trigger point |
| | | const triggerPoint = window.innerWidth < this.breakpoint |
| | | ? margin // Top edge of zone on small screens |
| | | : window.innerHeight / 2; // Center on large screens |
| | | const triggerPoint = window.innerHeight / 2; |
| | | |
| | | const distance = Math.abs(rect.top - triggerPoint); |
| | | |
| | |
| | | <nav id="artist" class="on-this-page index"> |
| | | <label>Jump to: |
| | | <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false"> |
| | | <?= jvbIcon('add')?> |
| | | <?= jvbIcon('plus-square')?> |
| | | </button> |
| | | </label> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> |
| | | <li><a href="#about">About</a></li> |
| | | <li><a href="#styles">Styles</a></li> |
| | | <li><a href="#contact">Contact</a></li> |
| | |
| | | <nav id="shop" class="on-this-page index"> |
| | | <label>Jump to: |
| | | <button type="button" aria-label="Show Index" title="Show Index" class="toggle" aria-expanded="false"> |
| | | <?= jvbIcon('add')?> |
| | | <?= jvbIcon('plus-square')?> |
| | | </button> |
| | | </label> |
| | | <ul> |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('up')?></a></li> <?php |
| | | <li><a href="#top" title="Back to Top"><?=jvbIcon('caret-circle-up')?></a></li> <?php |
| | | if ($rating !== 'none') { |
| | | ?> |
| | | <li><a href="#rating">Rating</a></li> |
| | |
| | | <div class="controls"> |
| | | <div class="type-filters"> |
| | | <button type="button" class="type-filter active" data-type="all"> |
| | | <?= jvbIcon('all'); ?> |
| | | <?= jvbIcon('infinity'); ?> |
| | | <span>Everything</span> |
| | | <span class="count">(<?= array_sum($counts); ?>)</span> |
| | | </button> |
| | |
| | | |
| | | <div class="view-controls"> |
| | | <button type="button" class="view-toggle" data-view="grid" aria-pressed="true"> |
| | | <?= jvbIcon('grid'); ?> |
| | | <?= jvbIcon('squares-four'); ?> |
| | | </button> |
| | | <button type="button" class="view-toggle" data-view="list" aria-pressed="false"> |
| | | <?= jvbIcon('list'); ?> |
| | | <?= jvbIcon('rows'); ?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | |
| | | </select> |
| | | <button type="button" class="apply-bulk">Apply</button> |
| | | <button type="button" class="cancel-bulk"> |
| | | <?= jvbIcon('close', ['title'=>'Cancel']); ?> |
| | | <?= jvbIcon('x', ['title'=>'Cancel']); ?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="tab-content" data-tab="lists" role="tabpanel" aria-hidden="true"> |
| | | <div class="lists-header"> |
| | | <button type="button" class="create-list-btn" onclick="window.userFavouritesManager.showCreateListModal()"> |
| | | <?= jvbIcon('add', ['title'=>'Create List']); ?> |
| | | <?= jvbIcon('plus-square', ['title'=>'Create List']); ?> |
| | | <span>Create List</span> |
| | | </button> |
| | | </div> |
| | |
| | | <span>Share</span> |
| | | </button> |
| | | <button type="button" class="duplicate-list" title="Duplicate List"> |
| | | <?= jvbIcon('copy')?> |
| | | <?= jvbIcon('copy-simple')?> |
| | | <span>Duplicate</span> |
| | | </button> |
| | | <button type="button" class="delete-list" title="Delete List"> |
| | | <?= jvbIcon('delete')?> |
| | | <?= jvbIcon('trash')?> |
| | | <span>Delete</span> |
| | | </button> |
| | | </div> |
| | |
| | | </select> |
| | | <button type="button" class="apply-bulk">Apply</button> |
| | | <button type="button" class="cancel-bulk"> |
| | | <?= jvbIcon('close', ['title'=>'Cancel']); ?> |
| | | <?= jvbIcon('x', ['title'=>'Cancel']); ?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | |
| | | <div class="input-with-button"> |
| | | <input type="email" id="share-email" name="share_email" placeholder="Enter email address"> |
| | | <button type="button" class="add-email"> |
| | | <?= jvbIcon('add', ['title'=>'Add share access'])?> |
| | | <?= jvbIcon('plus-square', ['title'=>'Add share access'])?> |
| | | </button> |
| | | </div> |
| | | </div> |
| | |
| | | <!-- Tab navigation --> |
| | | <div class="tabs" role="tablist"> |
| | | <button type="button" class="tab active" data-tab="news" role="tab" aria-selected="true"> |
| | | <h2><?= jvbIcon('news')?>News</h2> |
| | | <h2><?= jvbIcon('newspaper')?>News</h2> |
| | | </button> |
| | | <button type="button" class="tab" data-tab="mine" role="tab" aria-selected="false"> |
| | | <h2>Yours</h2> |
| | |
| | | </button> |
| | | |
| | | <button type="button" class="add-item-btn"> |
| | | <?= jvbIcon('add', ['title' =>'Add News']) ?> |
| | | <?= jvbIcon('plus-square', ['title' =>'Add News']) ?> |
| | | <span>Post</span> |
| | | </button> |
| | | </div> |
| | |
| | | <details class="type-filters"> |
| | | <summary class="row btw">Filters: |
| | | <button class="clear-filters row"> |
| | | <?= jvbIcon('close', ['title' => 'Clear Filters'])?> |
| | | <?= jvbIcon('x', ['title' => 'Clear Filters'])?> |
| | | <span>Clear Filters</span> |
| | | </span> |
| | | </button> |
| | |
| | | |
| | | <input type="radio" id="order-karma" class="btn" name="orderby" value="karma"> |
| | | <label for="order-karma" title="Order by Standing" class="row"> |
| | | <?= jvbIcon('karma') ?> |
| | | <?= jvbIcon('scales') ?> |
| | | <span class="label">Standing</span> |
| | | </label> |
| | | |
| | | <input type="radio" id="order-random" class="btn" name="orderby" value="random"> |
| | | <label for="order-random" title="Random Order" class="row"> |
| | | <?= jvbIcon('random') ?> |
| | | <?= jvbIcon('shuffle') ?> |
| | | <span class="label">Random</span> |
| | | </label> |
| | | </div> |
| | |
| | | <div class="order-direction radio-group-label" data-for-order="date,title,karma"> |
| | | <input type="radio" id="order-desc" class="btn" name="order" value="DESC" checked> |
| | | <label for="order-desc" title="Newest First" class="row"> |
| | | <?= jvbIcon('desc') ?> |
| | | <?= jvbIcon('sort-descending') ?> |
| | | </label> |
| | | |
| | | <input type="radio" id="order-asc" class="btn" name="order" value="ASC"> |
| | | <label for="order-asc" title="Oldest First" class="row"> |
| | | <?= jvbIcon('asc') ?> |
| | | <?= jvbIcon('sort-ascending') ?> |
| | | </label> |
| | | </div> |
| | | </div> |
| | |
| | | </div> |
| | | <div class="item-actions"> |
| | | <button type="button" class="action" data-action="edit" title="Edit tattoo"> |
| | | <?= jvbIcon('edit') ?> |
| | | <?= jvbIcon('pencil-simple') ?> |
| | | <span class="screen-reader-text">Edit news item</span> |
| | | </button> |
| | | <button type="button" class="action" data-action="trash" title="Scrap tattoo"> |
| | | <?= jvbIcon('delete') ?> |
| | | <?= jvbIcon('trash') ?> |
| | | <span class="screen-reader-text">Scrap news item</span> |
| | | </button> |
| | | <button type="button" class="action" data-action="toggle-status"> |