=Furthering along the complete refactor. Added some basic setup for is_content taxonomies, including a page for an archive of the terms.
15 files added
1 files renamed
45 files modified
1 files deleted
| | |
| | | :target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:0;bottom:0}.dashboard>header{justify-content:flex-end;position:fixed}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.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{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-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(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-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(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 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(--trans-base),width var(--trans-base),padding var(--trans-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(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.crud form.table td .label,.crud form.table td label:not(.select-item-label):not(.radio-option){display:none}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(--txt-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(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}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(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-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}.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(--radius-outer);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}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}.dashboard #queue{bottom:0} |
| | | :target{outline:0!important;padding:0!important}.dashboard .qtoggle{left:0;bottom:0}.dashboard>header{justify-content:flex-end;position:fixed}.dashboard>header img{width:var(--btn)}.dashboard h1:first-of-type{margin-top:4rem!important}nav.dashboard-nav,nav.dashboard-nav ul{--dir:row}nav.dashboard-nav ul{touch-action:pan-x;overflow:auto hidden}main>footer{padding:0}main>*{max-width:min(768px,90vw)!important;margin:0 auto!important}main h1{margin:0!important;font-size:var(--txt-large)}.item-grid .item{position:relative}img{width:100%;height:auto;aspect-ratio:1;object-fit:cover}.replace.replace{grid-column:full;padding:0 var(--btn_);max-width:none!important;margin:0!important}.replace .dashboard-page{max-width:var(--wide)}.group-display .item-grid{grid-template-columns:repeat(2,1fr)}.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{bottom:0;right:0}.item-actions button{min-height:0;width:var(--chipchip);height:var(--chipchip);background-color:rgba(var(--base-rgb),var(--op-45))}.item-actions button:hover{background-color:var(--base)}.list-view h3,.list-view p{margin:0!important}.list-view h3{font-size:var(--txt-medium)}@media (min-width:768px){.grid-view{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media (max-width:768px){.bulk-controls.bulk-controls.nowrap{--wrap:wrap}}.bulk-controls{margin:1rem 0}.bulk-controls .selected-count{font-weight:400;font-size:var(--txt-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(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}.selected label:has(:checked){border-color:var(--action-0);padding:0;opacity:1;filter:none;transition:filter var(--trans-base),opacity var(--trans-base),border var(--trans-base),padding var(--trans-base)}form.table img,form.table label.select-item{width:6rem;height:6rem}form.table .item-grid.preview{margin:0}td p{width:max-content}.timeline-point.is-dragging{opacity:.4;position:relative}.timeline-point.drop-above{position:relative}.timeline-point.drop-above::before{content:'';position:absolute;top:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}.timeline-point.drop-below{position:relative}.timeline-point.drop-below::after{content:'';position:absolute;bottom:-4px;left:0;right:0;height:8px;background:var(--action-0);border-radius:4px;z-index:10;animation:pulse .6s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:.6;transform:scaleY(1)}50%{opacity:1;transform:scaleY(1.2)}}.timeline-point.drop-above{margin-top:8px;transition:margin-top .2s ease}.timeline-point.drop-below{margin-bottom:8px;transition:margin-bottom .2s ease}.drag-handle{cursor:grab;padding:.5rem;background:0 0;border:none;opacity:.6;transition:opacity .2s ease}.drag-handle:hover{opacity:1}.drag-handle:active,.is-dragging .drag-handle{cursor:grabbing}.drag-preview .drag-handle{pointer-events:none}.all-filters{margin:0;padding:1rem 0;border-top:1px solid var(--base-200);border-bottom:1px solid var(--base-200);--gap:0}.all-filters .row{--justify:flex-start}.all-filters[open]{--gap:.5rem}.all-filters summary{width:100%;display:flex;justify-content:space-between}.all-filters summary [data-action=clear-filters]{--w:1em!important;width:max-content;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]{margin-left:auto;--w:1em!important;flex-wrap:nowrap;justify-content:flex-start;transition:var(--trans-size);display:flex;font-size:var(--txt-x-small)}.all-filters [data-action=refresh]:focus,.all-filters [data-action=refresh]:hover{width:max-content}.all-filters [data-action=refresh] span{display:none;white-space:nowrap}.all-filters [data-action=refresh]:focus span,.all-filters [data-action=refresh]:hover span{display:block}.all-filters .btn+label{box-shadow:var(--shdw-none);color:var(--base-200)}.all-filters .radio-options input:not(.ch):checked+label{box-shadow:rgba(var(--base-rgb),var(--op-6)) var(--shdw-inset);color:var(--contrast-200);border-color:var(--contrast-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(--txt-small);font-weight:900;width:15vw;display:inline-flex;align-items:center;padding-right:2rem}@media (max-width:767px){.all-filters>.row{padding:.5rem 0}.all-filters span.label{padding-top:.5rem;width:100%;border-top:1px solid var(--base-200)}}.controls .icon{--w:1.4rem}.all-filters .btn+label,.all-filters button{height:var(--chip_);padding:.125rem!important;min-width:0;min-height:var(--chip_);width:var(--chip_)}.all-filters>.row{padding:.25rem 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(--trans-base),width var(--trans-base),padding var(--trans-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(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}.crud form.table td .label,.crud form.table td label:not(.select-item-label):not(.radio-option){display:none}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:98vh;width:98vw;max-width:none;max-height:none;inset:0;margin:auto}dialog>.wrap{min-height:100%}dialog .item.upload.upload{display:flex;gap:1rem}dialog .item.upload .preview{width:40%}dialog .item.upload .group{width:60%}.upload details{width:100%}.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(--txt-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(--txt-large)}.dashboard.dash .replace>ul{display:flex;list-style:none;align-items:flex-start;justify-content:flex-start;flex-wrap:wrap;gap:.5rem}nav.tabs.tabs{bottom:0;left:0;right:var(--btn)}.dashboard.settings nav.tabs.tabs{--height:3.5rem;--x:var(--btn_);position:fixed;bottom:var(--btn);left:var(--x);right:var(--x);z-index:99;width:calc(100% - var(--x) - var(--x));background-color:var(--base)}.jvb-seo-admin nav.tabs.tabs{position:sticky;padding-bottom:0;bottom:unset;left:0;right:0;top:var(--btn)}.jvb-seo-admin nav.tabs button{border:none;margin:0 .125rem;background-color:var(--base-200);box-shadow:var(--shdw-none)}.jvb-seo-admin nav.tabs button.active{background-color:var(--base);color:var(--action-0)}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(--radius-outer);padding:1rem;position:relative;transition:all var(--trans-base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.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(--txt-medium);margin:0}.integration .meta{margin-bottom:1rem;text-align:right;color:var(--contrast-200);font-size:var(--txt-small)}.integration .setup{font-size:var(--txt-small);font-weight:700;text-transform:uppercase}.integration .setup .indicator{font-size:var(--txt-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}.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(--radius-outer);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}.referral-dashboard{max-width:var(--wide)}.card{background-color:var(--base-100);padding:30px;border-radius:var(--radius-outer);text-align:center;margin-bottom:2rem}.dashboard-page.referral{text-align:center}.referral-dashboard .empty-state{padding:3rem 7vw}.referral-dashboard .empty-state h3{margin-top:0}.referral-dashboard .empty-state h3 .icon:first-of-type{margin-right:1rem}.referral-dashboard .empty-state h3 .icon:last-of-type{margin-left:1rem}.item-grid.stats .card{border:1px solid var(--base);display:flex;justify-content:flex-end;align-items:center;flex-direction:column}.item-grid.stats .card.highlight{box-shadow:var(--contrast-rgb) var(--shadow);background-color:var(--action-200);color:var(--action-contrast);grid-column:1/-1;margin:0 4rem 30px;aspect-ratio:unset}.card h4{font-size:var(--medium);color:var(--contrast-200);font-weight:var(--fw-b-bold);margin:0 0 .5rem}.card span{color:var(--action-0);font-weight:var(--fw-b-bold);font-size:var(--txt-xx-large)}.card.highlight span{color:var(--action-contrast)}nav.sidebar{--wrap:nowrap;position:fixed;top:var(--btn);bottom:0;left:0;z-index:var(--z-4);height:calc(100% - var(--btn));background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto}nav.sidebar .icon{--w:var(--chip_);width:var(--btn);transition:var(--trans-size),margin var(--trans-base)}nav.sidebar.open{width:fit-content;max-width:100%}nav.sidebar.open .icon{--w:var(--chip);margin:.75rem;width:var(--w)}nav.sidebar ul{height:max-content;width:100%;--gap:0}nav.sidebar .title{display:block}nav.sidebar .toggle{width:var(--btn);height:var(--chipchip);box-shadow:none;background-color:transparent;min-height:0}nav.sidebar .toggle:focus,nav.sidebar .toggle:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}nav.sidebar .title{white-space:nowrap}nav.sidebar li{--justify:center;flex-wrap:nowrap;overflow:hidden;align-items:flex-start}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .a{color:var(--contrast-200)}nav.sidebar .a,nav.sidebar a{height:var(--chipchip);display:flex;justify-content:center;align-items:center;transition:none;padding-left:0}nav.sidebar.open .a,nav.sidebar.open a{width:100%;justify-content:flex-start}nav.sidebar .has-submenu ul{max-height:0;height:0;overflow:hidden;transition:var(--trans-size)}nav.sidebar .has-submenu.open>ul{height:100%;max-height:fit-content}header .title,header .title a{height:var(--btn);margin:0;display:block}header .title{margin-left:var(--btn)}header .title a{width:var(--btn)}.dashboard #queue{bottom:0} |
| | |
| | | this.nonces = {}; |
| | | |
| | | this.subscribers = new Set(); |
| | | this.storageKey = 'jvb_auth_state'; |
| | | this.cacheMetaKey = 'jvb_auth_meta'; |
| | | this.storageKey = `${jvbBase.base}auth_state`; |
| | | this.cacheMetaKey = `${jvbBase.base}auth_meta`; |
| | | this.cacheExpiry = 5 * 60 * 1000; // 5 minutes |
| | | |
| | | this.init(); |
| | |
| | | |
| | | if (!this.dbConfig.has(name)) { |
| | | this.dbConfig.set(name, { |
| | | dbName: `jvb_${name}`, |
| | | dbName: `${jvbBase.base}${name}`, |
| | | version: version, |
| | | stores: {}, |
| | | _initialized: false |
| | |
| | | constructor(base, config = {}) { |
| | | this.base = base; |
| | | this.config = { |
| | | namespace: 'jvb_cache', |
| | | namespace: `${jvbBase.base}cache`, |
| | | TTL: 3600000, |
| | | maxSize: 100, |
| | | ...config |
| | |
| | | window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey="jvb_auth_state",this.cacheMetaKey="jvb_auth_meta",this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return this.ready();this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,e={}){const i=async(s=0)=>{const a={"Content-Type":"application/json",...e.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...e,credentials:"same-origin",headers:a});if((403===h.status||401===h.status)&&0===s){const t=await h.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),i(1)}return h};return i()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const e=await t.json(),i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{}}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}}))}ready(){return this.initialized?Promise.resolve():new Promise((t=>{const e=this.subscribe((i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),t())}))}))}}; |
| | | window.auth=new class{constructor(){this.initialized=!1,this.isAuthenticating=!1,this.authenticated=!1,this.user=!1,this.nonces={},this.subscribers=new Set,this.storageKey=`${jvbBase.base}auth_state`,this.cacheMetaKey=`${jvbBase.base}auth_meta`,this.cacheExpiry=3e5,this.init()}async init(){if(this.isAuthenticating)return this.ready();this.isAuthenticating=!0;try{const t=this.getCachedAuth();if(t)return this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!0});await this.fetchAuth()}catch(t){console.error("Failed to initialize auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}async refreshNonce(t="wp_rest"){try{return await this.fetchAuth(),this.getNonce(t)}catch(t){return console.error("Failed to refresh nonce:",t),null}}async fetch(t,e={}){const i=async(s=0)=>{const a={"Content-Type":"application/json",...e.headers,"X-WP-Nonce":this.getNonce()},h=await fetch(t,{...e,credentials:"same-origin",headers:a});if((403===h.status||401===h.status)&&0===s){const t=await h.clone().json();if("rest_cookie_invalid_nonce"===t.code||t.message?.includes("Cookie check"))return console.log("Nonce invalid, refreshing auth..."),await this.refresh(),i(1)}return h};return i()}async fetchAuth(){const t=await fetch(`${jvbSettings.api}auth/status`,{method:"GET",credentials:"same-origin",headers:{"Content-Type":"application/json"}});if(!t.ok)throw new Error("Auth check failed");const e=await t.json(),i=sessionStorage.getItem(this.cacheMetaKey);if(i){const t=JSON.parse(i);t.session_id&&t.session_id!==e.session_id&&(this.clearCachedAuth(),this.notify("session-changed",{}))}this.cacheAuth(e),this.setAuthData(e),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-loaded",{fromCache:!1})}setAuthData(t){this.authenticated=t.authenticated||!1,this.user=t.user||!1,this.nonces=t.nonces||{}}clearAuthData(){this.authenticated=!1,this.user=null,this.nonces={},sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}getCachedAuth(){try{const t=sessionStorage.getItem(this.storageKey),e=sessionStorage.getItem(this.cacheMetaKey);if(!t||!e)return null;const i=JSON.parse(e),s=JSON.parse(t);return Date.now()-i.timestamp>this.cacheExpiry?(this.clearCachedAuth(),null):s}catch(t){return console.error("Error reading cached auth:",t),null}}cacheAuth(t){try{sessionStorage.setItem(this.storageKey,JSON.stringify(t)),sessionStorage.setItem(this.cacheMetaKey,JSON.stringify({session_id:t.session_id||null,timestamp:Date.now()}))}catch(t){console.error("Error caching auth:",t)}}clearCachedAuth(){sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey)}async refresh(){this.isAuthenticating=!0,this.initialized=!1;try{await this.fetchAuth(),this.notify("auth-refreshed",{})}catch(t){console.error("Failed to refresh auth:",t),this.clearAuthData(),this.initialized=!0,this.isAuthenticating=!1,this.notify("auth-error",{error:t})}}getNonce(t="wp_rest"){return this.nonces[t]||""}getUser(){return this.user}isAuthenticated(){return this.authenticated}async handleLogin(t=null){if(sessionStorage.removeItem(this.storageKey),sessionStorage.removeItem(this.cacheMetaKey),t)return this.cacheAuth(t),this.setAuthData(t),this.initialized=!0,this.isAuthenticating=!1,void this.notify("auth-loaded",{fromCache:!1,fromLogin:!0});await this.refresh()}handleLogout(){this.clearAuthData(),this.notify("logged-out",{})}subscribe(t){return this.subscribers.add(t),this.initialized&&t("auth-loaded",{fromCache:!1,immediate:!0}),()=>this.subscribers.delete(t)}notify(t,e){this.subscribers.forEach((i=>{try{i(t,e)}catch(t){console.error("Subscriber error:",t)}}))}ready(){return this.initialized?Promise.resolve():new Promise((t=>{const e=this.subscribe((i=>{"auth-loaded"!==i&&"auth-error"!==i||(e(),t())}))}))}}; |
| | |
| | | window.jvbCache=class{constructor(e,t={}){this.base=e,this.config={namespace:"jvb_cache",TTL:36e5,maxSize:100,...t},this._cache=new Map,this.subscribers=new Set}clearMemoryCache(){const e=this._cache.size;return this._cache.clear(),console.log(`Cleared ${e} items from memory cache`),e}get(e){if(this._cache.has(e))return this._cache.get(e);let t,a=`${this.base}_${e}`;try{if(t=localStorage.getItem(a),!t)return null;t=JSON.parse(t)}catch(e){return console.warn("Error getting from localStorage:",e),null}return t&&this._cache.set(e,t),t}set(e,t){this._cache.set(e,t);let a=`${this.base}_${e}`;try{localStorage.setItem(a,JSON.stringify(t))}catch(t){if(t instanceof DOMException&&22===t.code){this.clearOldestLocalStorageItems();try{localStorage.setItem(e,JSON.stringify(item))}catch(e){console.warn("Still failed to set localStorage item after cleanup:",e)}}else console.warn("Error setting localStorage item:",t)}this.notify("cache-saved",{key:e,value:t})}remove(e){let t=`${this.base}_${e}`;try{localStorage.removeItem(t)}catch(e){console.warn("Error removing localStorage item:",e)}}clearOldestLocalStorageItems(){try{const e=[];for(let t=0;t<localStorage.length;t++){const a=localStorage.key(t);if(a.startsWith(this.config.namespace))try{const t=JSON.parse(localStorage.getItem(a));e.push({key:a,timestamp:t.timestamp||0})}catch(t){e.push({key:a,timestamp:0})}}e.sort(((e,t)=>e.timestamp-t.timestamp));const t=Math.max(1,Math.ceil(.2*e.length));for(let a=0;a<t;a++)e[a]&&localStorage.removeItem(e[a].key)}catch(e){console.warn("Error cleaning up localStorage:",e)}}async loadFromCache(){for(let e=0;e<localStorage.length;e++){const t=localStorage.key(e);if(t.startsWith(`${this.base}_`)){let e=t.replace(`${this.base}_`,"");try{const a=JSON.parse(localStorage.getItem(t));this._cache.set(e,a)}catch(e){console.warn(`Failed to parse cached value for ${t}:`,e)}}}}async clear(){this._cache.clear();try{for(let e=localStorage.length-1;e>=0;e--){const t=localStorage.key(e);t&&t.startsWith(this.config.namespace)&&localStorage.removeItem(t)}}catch(e){console.warn("Error clearing localStorage cache:",e)}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((a=>a(e,t)))}}; |
| | | window.jvbCache=class{constructor(e,t={}){this.base=e,this.config={namespace:`${jvbBase.base}cache`,TTL:36e5,maxSize:100,...t},this._cache=new Map,this.subscribers=new Set}clearMemoryCache(){const e=this._cache.size;return this._cache.clear(),console.log(`Cleared ${e} items from memory cache`),e}get(e){if(this._cache.has(e))return this._cache.get(e);let t,a=`${this.base}_${e}`;try{if(t=localStorage.getItem(a),!t)return null;t=JSON.parse(t)}catch(e){return console.warn("Error getting from localStorage:",e),null}return t&&this._cache.set(e,t),t}set(e,t){this._cache.set(e,t);let a=`${this.base}_${e}`;try{localStorage.setItem(a,JSON.stringify(t))}catch(t){if(t instanceof DOMException&&22===t.code){this.clearOldestLocalStorageItems();try{localStorage.setItem(e,JSON.stringify(item))}catch(e){console.warn("Still failed to set localStorage item after cleanup:",e)}}else console.warn("Error setting localStorage item:",t)}this.notify("cache-saved",{key:e,value:t})}remove(e){let t=`${this.base}_${e}`;try{localStorage.removeItem(t)}catch(e){console.warn("Error removing localStorage item:",e)}}clearOldestLocalStorageItems(){try{const e=[];for(let t=0;t<localStorage.length;t++){const a=localStorage.key(t);if(a.startsWith(this.config.namespace))try{const t=JSON.parse(localStorage.getItem(a));e.push({key:a,timestamp:t.timestamp||0})}catch(t){e.push({key:a,timestamp:0})}}e.sort(((e,t)=>e.timestamp-t.timestamp));const t=Math.max(1,Math.ceil(.2*e.length));for(let a=0;a<t;a++)e[a]&&localStorage.removeItem(e[a].key)}catch(e){console.warn("Error cleaning up localStorage:",e)}}async loadFromCache(){for(let e=0;e<localStorage.length;e++){const t=localStorage.key(e);if(t.startsWith(`${this.base}_`)){let e=t.replace(`${this.base}_`,"");try{const a=JSON.parse(localStorage.getItem(t));this._cache.set(e,a)}catch(e){console.warn(`Failed to parse cached value for ${t}:`,e)}}}}async clear(){this._cache.clear();try{for(let e=localStorage.length-1;e>=0;e--){const t=localStorage.key(e);t&&t.startsWith(this.config.namespace)&&localStorage.removeItem(t)}}catch(e){console.warn("Error clearing localStorage cache:",e)}}subscribe(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}notify(e,t){this.subscribers.forEach((a=>a(e,t)))}}; |
| | |
| | | (()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`jvb_${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),r}t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t}),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=(t.items||[]).filter((e=>e&&"object"==typeof e)),o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const l={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})(); |
| | | (()=>{class e{constructor(){if(e.instance)return e.instance;e.instance=this,this.dbConfig=new Map,this.databases=new Map,this.stores=new Map,this.subscribers=new Map,this.pendingInits=new Map,this.fetchQueue=[],this._initialized=!1,this.body=document.body,this.loading=document.querySelector("dialog.loading"),this.init()}async init(){this._initialized||(this._initialized=!0,"indexedDB"in window||console.warn("IndexedDB not supported"))}register(e,t=[],s=1.25){if(Array.isArray(t)||(t=[t]),0===t.length)return;this.dbConfig.has(e)||this.dbConfig.set(e,{dbName:`${jvbBase.base}${e}`,version:s,stores:{},_initialized:!1});let r=this.dbConfig.get(e);t.forEach((t=>{if(!t.storeName)throw new Error(`Store config for "${e}" missing storeName`);if(!t.keyPath)throw new Error(`Store "${t.storeName}" requires keyPath`);const s=`${e}_${t.storeName}`,i={config:{dbName:r.dbName,storeName:"items",keyPath:"id",indexes:[],endpoint:null,apiBase:jvbSettings.api,filters:{},ignore:[],required:null,TTL:36e5,useHttpCaching:!0,showLoading:!1,delayFetch:!0,validateData:!0,...t},dbKey:e,storeKey:s,data:new Map,cache:new Map,filters:{...t.filters||{}},isFetching:!1,currentRequest:null,lastResponse:null,_initialized:!1};i.ignoreFilters=new Set(["search","page","per_page","orderby","order","context","source",...i.config.ignore]),i.config.headers={"X-WP-Nonce":window.auth.getNonce(),...i.config.headers},r.stores[t.storeName]=s,this.stores.set(s,i),this.subscribers.has(s)||this.subscribers.set(s,new Set)})),this.initDB(e).catch((t=>{console.error(`Failed to initialize store "${e}":`,t)}));const i={};for(const[e,t]of Object.entries(r.stores))i[e]=this.getStoreAPI(t);return i}getStoreAPI(e){const t={fetch:()=>this.fetch(e),save:t=>this.save(e,t),saveMany:t=>this.saveMany(e,t),delete:t=>this.delete(e,t),deleteMany:t=>this.deleteMany(e,t),get:t=>this.get(e,t),getMany:t=>this.getMany(e,t),getAll:()=>this.getAll(e),getAllByIndex:(t,s)=>this.getAllByIndex(e,t,s),filterByIndex:t=>this.filterByIndex(e,t),getFiltered:()=>this.getFiltered(e),clear:()=>this.clear(e),setFilter:(t,s)=>this.setFilter(e,t,s),setFilters:t=>this.setFilters(e,t),removeFilter:t=>this.removeFilter(e,t),clearFilters:()=>this.clearFilters(e),clearCache:()=>this.clearCache(e),subscribe:t=>this.subscribe(e,t),ensureInitialized:()=>this.ensureStoreInitialized(e),get filters(){return{...t.getStore().filters}},get lastResponse(){return t.getStore().lastResponse},get data(){return t.getStore().data},getStore:()=>this.stores.get(e)};return t}formDataToObject(e){const t={_isFormData:!0,entries:{}};for(const[s,r]of e.entries())r instanceof File||r instanceof Blob||(t.entries[s]?(Array.isArray(t.entries[s])||(t.entries[s]=[t.entries[s]]),t.entries[s].push(r)):t.entries[s]=r);return t}async objectToFormData(e){if(!e._isFormData)return e;const t=new FormData;for(const[s,r]of Object.entries(e.entries))Array.isArray(r)?r.forEach((e=>t.append(s,e))):t.append(s,r);if(window.jvbUploads&&e.entries.upload_ids){const s=JSON.parse(e.entries.upload_ids);for(const e of s){const s=await window.jvbUploads.getBlobData(e);s&&t.append("files[]",s)}}return t}async initDB(e){const t=this.dbConfig.get(e);if(!t||t._initialized)return;if(this.pendingInits.has(e))return this.pendingInits.get(e);const s=this._performDBInit(e);this.pendingInits.set(e,s);try{await s,t._initialized=!0}finally{this.pendingInits.delete(e)}}async _performDBInit(e){const t=this.dbConfig.get(e),{dbName:s,version:r}=t,i=Object.values(t.stores);try{if(!this.databases.has(s)){const e=await this.openDatabase(s,r,(e=>{i.forEach((t=>{let s=this.stores.get(t);s&&this.setupStores(e,s.config)}))}));this.databases.set(s,e)}i.forEach((e=>{let t=this.stores.get(e);t&&(t.db=this.databases.get(s),t._initialized=!0,this.loadStoreDataInBackground(e),this.notify(e,"db-init"))}))}catch(t){throw console.error(`Failed to initialize database for store "${e}":`,t),t}}openDatabase(e,t,s){return new Promise(((r,i)=>{const a=indexedDB.open(e,t);a.onupgradeneeded=e=>{s&&s(e.target.result,e.oldVersion,e.newVersion)},a.onsuccess=e=>r(e.target.result),a.onerror=e=>i(e.target.error),a.onblocked=()=>{console.warn(`Database ${e} blocked. Close other tabs.`)}}))}setupStores(e,t){if(!e.objectStoreNames.contains(t.storeName)){const s=e.createObjectStore(t.storeName,{keyPath:t.keyPath});t.indexes.forEach((e=>{s.createIndex(e.name,e.keyPath||e.name,{unique:e.unique||!1})}))}if(t.endpoint&&!e.objectStoreNames.contains("cache")){e.createObjectStore("cache",{keyPath:"key"}).createIndex("timestamp","timestamp",{unique:!1})}}async loadFromObjectStore(e,t,s){const r=this.stores.get(e);return r?.db&&r.db.objectStoreNames.contains(t)?new Promise((e=>{const i=r.db.transaction([t],"readonly").objectStore(t).getAll();i.onsuccess=t=>{const r=t.target.result||[];r.forEach(s),e(r)},i.onerror=()=>e([])})):[]}loadStoreDataInBackground(e){const t=this.stores.get(e);t?.db&&Promise.all([this.loadFromObjectStore(e,t.config.storeName,(e=>{const s=this.getItemKey(e,t.config.keyPath);t.data.set(s,e)})),this.loadFromObjectStore(e,"cache",(e=>{this.isCacheValid(e,t.config.TTL)&&t.cache.set(e.key,e)}))]).then((()=>{this.notify(e,"data-ready"),t.config.endpoint&&t.config.delayFetch?(this.fetchQueue.push(e),1===this.fetchQueue.length&&this.processFetchQueue()):t.config.endpoint&&!t.config.delayFetch&&("requestIdleCallback"in window?requestIdleCallback((()=>this.fetch(e)),{timeout:2e3}):setTimeout((()=>this.fetch(e)),100))})).catch((t=>{console.error(`Background load error for store "${e}":`,t)}))}async processFetchQueue(){if(0===this.fetchQueue.length)return;const e=this.fetchQueue.shift();if(!this.stores.get(e))return this.processFetchQueue();try{await this.fetch(e)}catch(t){console.error(`Queue fetch error for "${e}":`,t)}this.fetchQueue.length>0&&("requestIdleCallback"in window?requestIdleCallback((()=>this.processFetchQueue()),{timeout:2e3}):setTimeout((()=>this.processFetchQueue()),50))}async ensureStoreInitialized(e){const t=this.stores.get(e);if(!t)throw new Error(`Store "${e}" not registered`);t._initialized||await this.initDB(t.dbKey)}async withTransaction(e,t,s,r){const i=this.stores.get(e);return i?.db?("string"==typeof t&&(t=[t]),new Promise(((e,a)=>{const o=i.db.transaction(t,s),n=t.map((e=>o.objectStore(e))),c=1===n.length?n[0]:n;let h;o.oncomplete=()=>e(h),o.onerror=()=>{const e=o.error||new Error("Transaction failed with unknown error");a(e)};try{h=r(c,o)}catch(e){a(e||new Error("Callback failed with unknown error"))}}))):null}async fetch(e){await this.ensureStoreInitialized(e);const t=this.stores.get(e);if(!t.isFetching){if(t.config.required){if((Array.isArray(t.config.required)?t.config.required:[t.config.required]).some((e=>!t.filters[e]||""===t.filters[e])))return}t.isFetching=!0;try{const s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r&&this.isCacheValid(r,t.config.TTL)){let t=r.items.map((t=>this.get(e,t)));return this.notify(e,"data-loaded",{cached:!0,items:t??[]}),r}t.config.showLoading&&this.setLoading(!0);const i=this.buildFetchUrl(e),a={...t.config.headers};t.config.useHttpCaching&&r&&(r.etag&&(a["If-None-Match"]=r.etag),r.lastModified&&(a["If-Modified-Since"]=r.lastModified));const o=new AbortController;t.currentRequest=o;const n=await fetch(i,{method:"GET",headers:a,signal:o.signal});if(304===n.status)return r?(this.notify(e,"data-loaded",{cached:!0,notModified:!0,items:r.items||[]}),r):(this.notify(e,"data-loaded",{cached:!1,notModified:!0,items:[]}),t.lastResponse={has_more:!1,total:0,pages:1,queue_stats:{}},{items:[]});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);const c=await n.json();return await this.processFetchedData(e,c,s,n),this.notify(e,"data-loaded",{cached:!1,items:c.items||[]}),c}catch(t){if(!("AbortError"===t?.name))throw console.error(`Fetch error for store "${e}":`,t),this.notify(e,"fetch-error",{error:t}),t}finally{t.isFetching=!1,t.currentRequest=null,t.config.showLoading&&this.setLoading(!1)}}}buildFetchUrl(e){const t=this.stores.get(e),s=new URLSearchParams;Object.entries(t.filters).forEach((([e,t])=>{null!=t&&""!==t&&("object"==typeof t?s.set(e,JSON.stringify(t)):s.set(e,t))}));const r=t.config.apiBase+t.config.endpoint;return s.toString()?`${r}?${s}`:r}async processFetchedData(e,t,s,r){const i=this.stores.get(e),a=(t.items||[]).filter((e=>e&&"object"==typeof e)),o=[];i.db&&a.length>0&&await this.withTransaction(e,i.config.storeName,"readwrite",(t=>{a.forEach((s=>{try{const r=this._saveItem(e,s);o.push(r),t.put(r.processed)}catch(e){console.error("Error processing item:",e)}}))}));const n={key:s,items:a.map((e=>this.getItemKey(e,i.config.keyPath))),timestamp:Date.now(),endpoint:i.config.endpoint,filters:{...i.filters},etag:r.headers.get("ETag"),lastModified:r.headers.get("Last-Modified"),has_more:t.has_more||!1};i.cache.set(s,n),i.db?.objectStoreNames.contains("cache")&&await this.withTransaction(e,"cache","readwrite",(e=>{e.put(n)})),i.lastResponse={...t,has_more:t.has_more||!1,total:t.total||a.length,pages:t.pages||1,queue_stats:t.queue_stats||{}};for(let[t,s]of Object.entries(i.filters))"string"==typeof s&&s.includes(",")&&this.createSplitCacheEntries(e,a,t,i.filters,r);o.forEach((t=>{t.statusChanged&&this.notify(e,"item-saved",{item:t.item,key:t.key,previousItem:t.previousItem})}))}createSplitCacheEntries(e,t,s,r,i){const a=this.stores.get(e);r[s].split(",").map((e=>e.trim())).forEach((t=>{let o={};o[s]=t;const n={...r,[s]:t},c=this.generateCacheKey(n);if(a.cache.has(c))return;let h=this.filterByIndex(e,o).map((e=>this.getItemKey(e,a.config.keyPath)));const l={key:c,items:h,timestamp:Date.now(),endpoint:a.config.endpoint,filters:n,etag:i.headers.get("Etag"),lastModified:i.headers.get("Last-Modified"),has_more:20===h.length};a.cache.set(c,l),a.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.put(l)}))}))}_saveItem(e,t){const s=this.stores.get(e),r=this.processForStorage(t,s.config.validateData);if(!r.valid)throw new Error(`Non-serializable data: ${r.error}`);const i=r.data,a=this.getItemKey(i,s.config.keyPath),o=s.data.get(a);return s.data.set(a,t),{item:t,previousItem:o,key:a,processed:i,statusChanged:o&&o.status!==t.status}}async save(e,t){const s=this.stores.get(e),r=this._saveItem(e,t);return await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.put(r.processed)})),this.notify(e,"item-saved",{item:r.item,key:r.key,previousItem:r.previousItem}),r.key}async saveMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Map?Array.from(t.values()):Array.isArray(t)?t:Object.values(t);if(0===r.length)return[];const i=[];return r.forEach((t=>{const s=this._saveItem(e,t);i.push(s)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{i.forEach((t=>{e.put(t.processed)}))})),this.notify(e,"items-saved",{count:i.length,keys:i.map((e=>e.key))}),i.map((e=>e.key))}processForStorage(e,t=!0,s="root"){if(null===e)return{valid:!0,data:null};if(void 0===e)return t?{valid:!1,error:`Undefined value at ${s}`}:{valid:!0,data:void 0};const r=typeof e;if(["string","number","boolean"].includes(r))return{valid:!0,data:e};if("function"===r)return t?{valid:!1,error:`Function at ${s}`}:{valid:!0,data:void 0};if(e instanceof HTMLElement||void 0!==e.nodeType)return t?{valid:!1,error:`DOM element at ${s}`}:{valid:!0,data:void 0};if(e instanceof FormData)return{valid:!0,data:this.formDataToObject(e)};if(e instanceof Date||e instanceof ArrayBuffer||ArrayBuffer.isView(e)||e instanceof Blob)return{valid:!0,data:e};if(e instanceof Set)return this.processForStorage(Array.from(e),t,s);if(e instanceof Map&&(e=Object.fromEntries(e)),Array.isArray(e)){const r=[];for(let i=0;i<e.length;i++){const a=this.processForStorage(e[i],t,`${s}[${i}]`);if(!a.valid)return a;void 0!==a.data&&r.push(a.data)}return{valid:!0,data:r}}if("object"===r){const r={};for(const[i,a]of Object.entries(e)){const e=this.processForStorage(a,t,`${s}.${i}`);if(!e.valid)return e;void 0===e.data&&null!==a||(r[i]=e.data)}return{valid:!0,data:r}}return t?{valid:!1,error:`Unknown type at ${s}`}:{valid:!0,data:void 0}}async delete(e,t){const s=this.stores.get(e);s.data.delete(t),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{e.delete(t)})),this.notify(e,"item-deleted",{id:t})}async deleteMany(e,t){const s=this.stores.get(e);if(!s)return[];const r=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===r.length?[]:(r.forEach((e=>{s.data.delete(e)})),await this.withTransaction(e,s.config.storeName,"readwrite",(e=>{r.forEach((t=>{e.delete(t)}))})),this.notify(e,"items-deleted",{count:r.length,ids:r}),r)}get(e,t){return this.stores.get(e).data.get(t)}getMany(e,t,s=!0){const r=this.stores.get(e);if(!r)return[];const i=t instanceof Set?Array.from(t):Array.isArray(t)?t:Object.keys(t);return 0===i.length?[]:s?i.reduce(((e,t)=>{const s=r.data.get(t);return s&&e.push(s),e}),[]):i.map((e=>r.data.get(e)??null))}getAll(e){const t=this.stores.get(e);return Array.from(t.data.values())}filterByIndex(e,t){const s=this.stores.get(e);return s?Array.from(s.data.values()).filter((e=>!(!e||"object"!=typeof e)&&Object.entries(t).every((([t,s])=>(Array.isArray(s)?s:[s]).includes(e[t]))))):[]}async getAllByIndex(e,t,s){const r=this.stores.get(e),i=Array.isArray(s)?s:[s];if(r.db&&r.db.objectStoreNames.contains(r.config.storeName))try{const e=r.db.transaction([r.config.storeName],"readonly").objectStore(r.config.storeName);if(e.indexNames.contains(t)){const s=e.index(t);return(await Promise.all(i.map((e=>new Promise(((t,r)=>{const i=s.getAll(e);i.onsuccess=()=>t(i.result||[]),i.onerror=()=>r(i.error)})))))).flat()}}catch(e){console.warn(`Index query failed for "${t}", falling back to filter:`,e)}return Array.from(r.data.values()).filter((e=>i.includes(e[t])))}getFiltered(e){const t=this.stores.get(e),s=this.generateCacheKey(t.filters),r=t.cache.get(s);if(r?.items){const e=r.items.reduce(((e,s)=>{const r=t.data.get(s);return r&&e.push(r),e}),[]);return this.applyOrdering(e,t)}const i=Array.from(t.data.values()),a=t.filters.search?.toLowerCase().trim()||"",o=[];t.filters.taxonomy&&"object"==typeof t.filters.taxonomy&&Object.entries(t.filters.taxonomy).forEach((([e,t])=>{const s=Array.isArray(t)?t:[t];o.push((t=>{if(!t.taxonomies||!t.taxonomies[e])return!1;const r=Object.keys(t.taxonomies[e]).map((e=>parseInt(e)));return s.some((e=>r.includes(parseInt(e))))}))}));for(const[e,s]of Object.entries(t.filters))if("taxonomy"!==e&&!t.ignoreFilters.has(e)&&null!=s&&""!==s&&"all"!==s)if("string"==typeof s&&s.includes(",")){const t=s.split(",").map((e=>e.trim()));o.push((s=>t.includes(String(s[e]))))}else o.push((t=>String(t[e])===String(s)));const n=i.filter((e=>{for(const t of o)if(!t(e))return!1;return!(a&&!this.searchObject(e,a))}));return this.applyOrdering(n,t)}applyOrdering(e,t){if(Array.isArray(e)||(e=Array.from(e)),0===e.length)return e;const s=t.filters.orderby||"date",r=(t.filters.order||"desc").toLowerCase();return["random","rand"].includes(s)||["random","rand"].includes(r)?this.shuffle(e):(e.sort(((e,t)=>{let i,a;switch(s){case"alphabetical":case"title":i=(e.title||e.name||"").toLowerCase(),a=(t.title||t.name||"").toLowerCase();break;case"modified":i=new Date(e.modified||e.date||0),a=new Date(t.modified||t.date||0);break;default:i=new Date(e.date||e.modified||0),a=new Date(t.date||t.modified||0)}return i<a?"asc"===r?-1:1:i>a?"asc"===r?1:-1:0})),e)}shuffle(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const s=Math.floor(Math.random()*(e+1));[t[e],t[s]]=[t[s],t[e]]}return t}searchObject(e,t){if(!e||"object"!=typeof e)return"string"==typeof e&&e.toLowerCase().includes(t);for(const s of Object.values(e))if(null!=s)if("object"!=typeof s){if("string"==typeof s&&s.toLowerCase().includes(t))return!0}else if(this.searchObject(s,t))return!0;return!1}async clear(e){const t=this.stores.get(e);t.data.clear(),t.cache.clear(),await this.withTransaction(e,t.config.storeName,"readwrite",(e=>{e.clear()})),this.notify(e,"data-cleared")}async updateFilters(e,t,s=!1){const r=this.stores.get(e),i={...r.filters};s&&(r.filters={...r.config.filters}),Object.entries(t).forEach((([e,t])=>{null==t||""===t?delete r.filters[e]:r.filters[e]=t})),this.notify(e,"filters-changed",{oldFilters:i,filters:r.filters,updates:t});const a=await this.shouldFetchWithFilters(e,t,i);if(r.config.endpoint&&a)await this.fetch(e);else{const t=this.getFiltered(e);this.notify(e,"data-loaded",{cached:!0,items:t})}}async shouldFetchWithFilters(e,t,s){const r=this.stores.get(e);if(!r.config.endpoint||!r.lastResponse)return!0;if(!1===r.lastResponse.has_more){if(Object.entries(t).every((([e,t])=>(r.ignoreFilters.has(e),!0))))return!1}if("page"in t){const e=t.page,i=s.page||1;if(e>i&&!r.lastResponse.has_more)return r.filters.page=i,!1}if("search"in t){const e=t.search?.trim()||"",i=s.search?.trim()||"";if(!e&&i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}if(e&&e!==i){const e={...r.filters};if(delete e.search,e.page=1,this.hasCompleteData(r,e))return!1}}return!0}hasCompleteData(e,t){const s=this.generateCacheKey(t),r=e.cache.get(s);return!!r&&(!1===r.has_more||!1===e.lastResponse?.has_more)}setFilter(e,t,s){return this.updateFilters(e,{[t]:s})}async setFilters(e,t){const s=this.stores.get(e);if(Object.keys(t).some((e=>s.filters[e]!==t[e]))||Object.keys(s.filters).some((e=>!(e in t)&&t!==s.config.filters)))return this.updateFilters(e,t)}removeFilter(e,t){return this.updateFilters(e,{[t]:null})}clearFilters(e){return this.updateFilters(e,{},!0)}clearCache(e){const t=this.stores.get(e);t.cache.clear(),t.db?.objectStoreNames.contains("cache")&&this.withTransaction(e,"cache","readwrite",(e=>{e.clear()})),this.notify(e,"cache-cleared")}generateCacheKey(e){const t=Object.keys(e).sort().reduce(((t,s)=>(t[s]=e[s],t)),{});return JSON.stringify(t)}isCacheValid(e,t){if(!e||!e.timestamp)return!1;return Date.now()-e.timestamp<t}subscribe(e,t){this.subscribers.has(e)||this.subscribers.set(e,new Set);const s=this.subscribers.get(e);return s.add(t),()=>s.delete(t)}notify(e,t,s={}){const r=this.subscribers.get(e);r&&r.forEach((r=>{try{r(t,s)}catch(t){console.error(`Subscriber error for store "${e}":`,t)}}))}getItemKey(e,t){if("function"==typeof t)return t(e);const s=t.split(".");let r=e;for(const e of s)r=r?.[e];return r}setLoading(e){this.body.classList.toggle("loading",e),e?this.loading?.showModal():this.loading?.close()}destroy(){this.stores.forEach((e=>{e.currentRequest&&e.currentRequest.abort()})),this.databases.forEach((e=>e.close())),this.stores.clear(),this.subscribers.clear(),this.databases.clear(),this.pendingInits.clear()}}document.addEventListener("DOMContentLoaded",(async function(){window.auth.subscribe((t=>{"auth-loaded"===t&&(window.jvbStore=new e)}))}))})(); |
| | |
| | | return $clean_classes; |
| | | } |
| | | add_filter('body_class', 'jvbBodyClasses'); |
| | | |
| | | |
| | | add_action('admin_init', function () { |
| | | // Redirect any user trying to access comments page |
| | | global $pagenow; |
| | | |
| | | if ($pagenow === 'edit-comments.php') { |
| | | wp_redirect(admin_url()); |
| | | exit; |
| | | } |
| | | |
| | | // Remove comments metabox from dashboard |
| | | remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal'); |
| | | |
| | | // Disable support for comments and trackbacks in post types |
| | | foreach (get_post_types() as $post_type) { |
| | | if (post_type_supports($post_type, 'comments')) { |
| | | remove_post_type_support($post_type, 'comments'); |
| | | remove_post_type_support($post_type, 'trackbacks'); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // Close comments on the front-end |
| | | add_filter('comments_open', '__return_false', 20, 2); |
| | | add_filter('pings_open', '__return_false', 20, 2); |
| | | |
| | | // Hide existing comments |
| | | add_filter('comments_array', '__return_empty_array', 10, 2); |
| | | |
| | | // Remove comments page in menu |
| | | add_action('admin_menu', function () { |
| | | remove_menu_page('edit-comments.php'); |
| | | }); |
| | | |
| | | // Remove comments links from admin bar |
| | | add_action('init', function () { |
| | | if (is_admin_bar_showing()) { |
| | | remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60); |
| | | } |
| | | }); |
| | | // Remove comments links from admin bar |
| | | add_action('add_admin_bar_menus', function () { |
| | | remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60); |
| | | }); |
| | |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\MusicRecording' => ' - - Music Recording', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\Photograph' => ' - - Photograph', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\Review' => ' - - Review', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\VisualArtwork\VisualArtwork' => ' - - Visual Artwork', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\Webpage' => ' - - WebPage', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\AboutPage' => ' - - - About Page', |
| | | 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CheckoutPage' => ' - - - Checkout Page', |
| | |
| | | { |
| | | $this->cache = Cache::for('blocks', WEEK_IN_SECONDS); |
| | | $this->cache->connect('post')->connect('taxonomy'); |
| | | add_filter('render_block', [$this, 'render'], 999, 3); |
| | | add_filter('render_block', [$this, 'render'], 990, 3); |
| | | |
| | | add_action('init', [$this, 'registerBlockStyles']); |
| | | } |
| | |
| | | global $post; |
| | | |
| | | $block['innerBlocks'] = parse_blocks($post->post_content); |
| | | return $this->innerBlocks($block); |
| | | $result = $this->innerBlocks($block); |
| | | } else { |
| | | return $this->inside($block, $tag, $content); |
| | | $result = $this->inside($block, $tag, $content); |
| | | } |
| | | |
| | | return apply_filters('jvb_post_content_output', $result, $block); |
| | | } |
| | | //core_post_date |
| | | public function render_core_post_date(array $block):string |
| | |
| | | if (!$taxonomies) { |
| | | // Combine both content and taxonomy filtering |
| | | $taxonomies = []; |
| | | foreach (Registrar::getFeatured('is_content', 'term') as $registrar) { |
| | | foreach (Registrar::getFeatured('is_content', 'term') as $type) { |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar->hasIntegration($this->service_name)) { |
| | | $taxonomies[] = $registrar->getSlug(); |
| | | } |
| | |
| | | $this->skeleton->addDateFilter(); |
| | | $this->skeleton->addCustomDateRange($this->addDateRanges()); |
| | | if (!empty($this->taxonomies)) { |
| | | $this->skeleton->addTaxonomyFilter(array_keys($this->taxonomies), 'user'); |
| | | $this->skeleton->addTaxonomyFilter($this->taxonomies, 'user'); |
| | | } |
| | | |
| | | // Capabilities |
| | |
| | | |
| | | jvb_register_do_once('buildDirectories', [$this, 'activate']); |
| | | add_action('init', [$this, 'registerDirectories']); |
| | | add_action('render_block', [$this, 'renderBlock'], 99999, 3); |
| | | add_action('render_block', [$this, 'renderBlock'], 998, 3); |
| | | } |
| | | |
| | | public function registerDirectories():void |
| | |
| | | return $content; |
| | | } |
| | | |
| | | error_log('Still working on directory manager...'); |
| | | // For archive page |
| | | if (is_post_type_archive(BASE.'directory') && $block['blockName'] === 'core/group') { |
| | | return ($block['attrs']['tagName']??'' === 'main') ? '<main>'.$this->renderArchive().'</main>' : $content; |
| | |
| | | <p><small>(No data is stored. Your friends will get an email from our email.)</small></p> |
| | | <?php |
| | | $invite = [ |
| | | 'type' => 'tag_list', |
| | | 'type' => 'taglist', |
| | | 'label' => 'Invite Your Friends', |
| | | 'hint' => 'Add friends to send them a referral link', |
| | | 'add_label' => 'Add Invite', |
| | | 'tag_format' => '{name} ({email})', // or 'first_field', 'all_fields', 'email', etc. |
| | | 'tag_format' => '{{name}} ({{email}})', // or 'first_field', 'all_fields', 'email', etc. |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | |
| | | // CONTENT TYPES CONFIGURATION |
| | | // ================================================== |
| | | |
| | | add_filter('the_content', function ($content) { |
| | | |
| | | // ARTIST |
| | | $content['artist'] = array_merge($content['artist'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => '{{name}} | {{primary_style}} Tattoo Artist in {{city}}', |
| | | 'description_template' => '{{name}} is a {{primary_style}} tattoo artist {{location_text}}. {{bio}} Browse portfolio and book appointments.', |
| | | 'variables' => [ |
| | | 'name' => 'post_title', |
| | | 'primary_style' => ['taxonomy' => BASE . 'style', 'primary' => true], |
| | | 'styles' => ['taxonomy' => BASE . 'style'], |
| | | 'city' => ['taxonomy' => BASE . 'city', 'primary' => true], |
| | | 'primary_shop' => ['taxonomy' => BASE . 'shop', 'primary' => true], |
| | | 'bio' => ['meta' => 'bio', 'truncate' => 100], |
| | | 'location_text' => ['callback' => function ($post_id, $context) { |
| | | $city_terms = get_the_terms($post_id, BASE . 'city'); |
| | | $shop_terms = get_the_terms($post_id, BASE . 'shop'); |
| | | |
| | | if ($shop_terms && !is_wp_error($shop_terms)) { |
| | | $shop = $shop_terms[0]; |
| | | return "working at {$shop->name}"; |
| | | } elseif ($city_terms && !is_wp_error($city_terms)) { |
| | | $city = $city_terms[0]; |
| | | return "in {$city->name}"; |
| | | } |
| | | return "in Edmonton"; |
| | | }], |
| | | ], |
| | | 'archive_title' => 'Edmonton\'s Best Tattoo Artists | Browse by Style & Shop', |
| | | 'archive_description' => 'Explore Edmonton\'s top tattoo artists. Filter by style, shop, or location. View portfolios and book your next tattoo today.' |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'Person', |
| | | 'additional_types' => ['Artist'], |
| | | 'properties' => [ |
| | | 'jobTitle' => ['callback' => function ($id, $context, $meta) { |
| | | $job = $meta['job_title'] ?? null; |
| | | if ($job) return $job; |
| | | |
| | | // Try to build from specialties |
| | | $styles = get_the_terms($id, BASE . 'style'); |
| | | if ($styles && !is_wp_error($styles) && count($styles) > 0) { |
| | | $primary = $styles[0]->name; |
| | | return "{$primary} Tattoo Artist"; |
| | | } |
| | | return "Tattoo Artist"; |
| | | }], |
| | | 'telephone' => 'phone', |
| | | 'email' => 'email', |
| | | 'url' => ['callback' => function ($id) { |
| | | return get_permalink($id); |
| | | }], |
| | | 'image' => ['callback' => function ($id) { |
| | | return get_the_post_thumbnail_url($id, 'full'); |
| | | }], |
| | | 'knowsAbout' => ['taxonomy' => BASE . 'style'], |
| | | 'worksFor' => ['callback' => function ($id, $context, $meta) { |
| | | $shops = get_the_terms($id, BASE . 'shop'); |
| | | if (!$shops || is_wp_error($shops)) { |
| | | return null; |
| | | } |
| | | |
| | | return array_map(function ($shop) { |
| | | return [ |
| | | '@type' => 'LocalBusiness', |
| | | '@id' => get_term_link($shop) . '#business', |
| | | 'name' => $shop->name, |
| | | 'url' => get_term_link($shop) |
| | | ]; |
| | | }, $shops); |
| | | }], |
| | | 'memberOf' => [ |
| | | '@id' => 'https://edmonton.ink/#organization' |
| | | ], |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // PARTNER (Shops/Organizations) |
| | | $content['partner'] = array_merge($content['partner'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => '{{name}} | Community Partner', |
| | | 'description_template' => '{{name}} - {{excerpt}}', |
| | | 'variables' => [ |
| | | 'name' => 'post_title', |
| | | 'excerpt' => ['callback' => function ($id) { |
| | | return get_the_excerpt($id); |
| | | }], |
| | | ], |
| | | 'archive_title' => 'Our Community Partners | Supporting Edmonton\'s Tattoo Scene', |
| | | 'archive_description' => 'Meet the businesses and organizations supporting Edmonton\'s tattoo community. Our partners help make edmonton.ink possible.' |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'Organization', |
| | | 'properties' => [ |
| | | 'name' => ['callback' => function ($id) { |
| | | return get_the_title($id); |
| | | }], |
| | | 'description' => ['callback' => function ($id) { |
| | | return get_the_excerpt($id); |
| | | }], |
| | | 'url' => ['callback' => function ($id, $context, $meta) { |
| | | $website = $meta['website'] ?? null; |
| | | return $website ?: get_permalink($id); |
| | | }], |
| | | 'logo' => ['callback' => function ($id, $context, $meta) { |
| | | $image_id = $meta['image'] ?? null; |
| | | if ($image_id) { |
| | | return wp_get_attachment_image_url($image_id, 'full'); |
| | | } |
| | | return get_the_post_thumbnail_url($id, 'full'); |
| | | }], |
| | | 'telephone' => 'phone', |
| | | 'email' => 'email', |
| | | 'address' => 'address', |
| | | 'sameAs' => ['callback' => function ($id, $context, $meta) { |
| | | $links = []; |
| | | if (!empty($meta['instagram'])) $links[] = $meta['instagram']; |
| | | if (!empty($meta['facebook'])) $links[] = $meta['facebook']; |
| | | if (!empty($meta['twitter'])) $links[] = $meta['twitter']; |
| | | return !empty($links) ? $links : null; |
| | | }], |
| | | 'memberOf' => [ |
| | | '@id' => 'https://edmonton.ink/#organization' |
| | | ], |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // TATTOO |
| | | $content['tattoo'] = array_merge($content['tattoo'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => '{{style}} Tattoo by {{artist}} | Edmonton', |
| | | 'description_template' => 'Beautiful {{style}} tattoo by {{artist}}{{location}}. View more work from Edmonton\'s talented artists.', |
| | | 'variables' => [ |
| | | 'style' => ['taxonomy' => BASE . 'style', 'primary' => true], |
| | | 'artist' => ['callback' => function ($id) { |
| | | $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | if ($artist_id) { |
| | | return get_the_title($artist_id); |
| | | } |
| | | return 'Edmonton Artist'; |
| | | }], |
| | | 'location' => ['callback' => function ($id) { |
| | | $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | if (!$artist_id) return ''; |
| | | |
| | | $shops = get_the_terms($artist_id, BASE . 'shop'); |
| | | if ($shops && !is_wp_error($shops)) { |
| | | return ' at ' . $shops[0]->name; |
| | | } |
| | | return ''; |
| | | }], |
| | | ], |
| | | 'archive_title' => 'Edmonton Tattoos | Browse by Style, Artist & Shop', |
| | | 'archive_description' => 'Browse tattoos from Edmonton\'s best artists. Filter by style, theme, or artist to find inspiration for your next piece.' |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'CreativeWork', |
| | | 'additional_types' => ['VisualArtwork'], |
| | | 'properties' => [ |
| | | 'creator' => ['callback' => function ($id) { |
| | | $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | if ($artist_id) { |
| | | return [ |
| | | '@type' => 'Person', |
| | | '@id' => get_permalink($artist_id) . '#person', |
| | | 'name' => get_the_title($artist_id), |
| | | 'url' => get_permalink($artist_id) |
| | | ]; |
| | | } |
| | | return null; |
| | | }], |
| | | 'image' => ['callback' => function ($id) { |
| | | return get_the_post_thumbnail_url($id, 'full'); |
| | | }], |
| | | 'about' => ['taxonomy' => BASE . 'theme'], |
| | | 'artform' => 'Tattoo', |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // PIERCING |
| | | $content['piercing'] = array_merge($content['piercing'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => '{{type}} Piercing by {{artist}} | Edmonton', |
| | | 'description_template' => 'Professional {{type}} piercing by {{artist}}. View more piercing work from Edmonton\'s skilled professionals.', |
| | | 'variables' => [ |
| | | 'type' => ['taxonomy' => BASE . 'pstyle', 'primary' => true], |
| | | 'artist' => ['callback' => function ($id) { |
| | | $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | return $artist_id ? get_the_title($artist_id) : 'Edmonton Professional'; |
| | | }], |
| | | ] |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'CreativeWork', |
| | | 'properties' => [ |
| | | 'creator' => ['callback' => function ($id) { |
| | | $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | if ($artist_id) { |
| | | return [ |
| | | '@type' => 'Person', |
| | | '@id' => get_permalink($artist_id) . '#person', |
| | | 'name' => get_the_title($artist_id), |
| | | 'url' => get_permalink($artist_id) |
| | | ]; |
| | | } |
| | | return null; |
| | | }], |
| | | 'image' => ['callback' => function ($id) { |
| | | return get_the_post_thumbnail_url($id, 'full'); |
| | | }], |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // EVENT |
| | | $content['event'] = array_merge($content['event'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_builder' => function ($post_id, $meta) { |
| | | $title = get_the_title($post_id); |
| | | $date = $meta->getValue('event_date'); |
| | | if ($date) { |
| | | $formatted_date = date('F j, Y', strtotime($date)); |
| | | return "{$title} | {$formatted_date}"; |
| | | } |
| | | return "{$title} | Edmonton Tattoo Event"; |
| | | }, |
| | | 'description_builder' => function ($post_id, $meta) { |
| | | $excerpt = get_the_excerpt($post_id); |
| | | $venue = $meta->getValue('venue'); |
| | | $date = $meta->getValue('event_date'); |
| | | |
| | | $desc = $excerpt; |
| | | if ($venue) $desc .= " Location: {$venue}."; |
| | | if ($date) { |
| | | $formatted = date('F j, Y', strtotime($date)); |
| | | $desc .= " Date: {$formatted}."; |
| | | } |
| | | return $desc; |
| | | } |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'custom_builder' => function ($post_id) { |
| | | $meta = Meta::forPost($post_id); |
| | | |
| | | $schema = [ |
| | | '@type' => 'Event', |
| | | 'name' => get_the_title($post_id), |
| | | 'url' => get_permalink($post_id), |
| | | ]; |
| | | |
| | | $date = $meta->get('event_date'); |
| | | if ($date) { |
| | | $schema['startDate'] = date('c', strtotime($date)); |
| | | } |
| | | |
| | | $venue = $meta->get('venue'); |
| | | $venue_address = $meta->get('venue_address'); |
| | | if ($venue) { |
| | | $schema['location'] = [ |
| | | '@type' => 'Place', |
| | | 'name' => $venue, |
| | | ]; |
| | | if ($venue_address) { |
| | | $schema['location']['address'] = $venue_address; |
| | | } |
| | | } |
| | | |
| | | $image_id = get_post_thumbnail_id($post_id); |
| | | if ($image_id) { |
| | | $schema['image'] = wp_get_attachment_image_url($image_id, 'full'); |
| | | } |
| | | |
| | | return $schema; |
| | | } |
| | | ] |
| | | ]); |
| | | |
| | | return $content; |
| | | }); |
| | | //add_filter('the_content', function ($content) { |
| | | // |
| | | // // ARTIST |
| | | // $content['artist'] = array_merge($content['artist'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => '{{name}} | {{primary_style}} Tattoo Artist in {{city}}', |
| | | // 'description_template' => '{{name}} is a {{primary_style}} tattoo artist {{location_text}}. {{bio}} Browse portfolio and book appointments.', |
| | | // 'variables' => [ |
| | | // 'name' => 'post_title', |
| | | // 'primary_style' => ['taxonomy' => BASE . 'style', 'primary' => true], |
| | | // 'styles' => ['taxonomy' => BASE . 'style'], |
| | | // 'city' => ['taxonomy' => BASE . 'city', 'primary' => true], |
| | | // 'primary_shop' => ['taxonomy' => BASE . 'shop', 'primary' => true], |
| | | // 'bio' => ['meta' => 'bio', 'truncate' => 100], |
| | | // 'location_text' => ['callback' => function ($post_id, $context) { |
| | | // $city_terms = get_the_terms($post_id, BASE . 'city'); |
| | | // $shop_terms = get_the_terms($post_id, BASE . 'shop'); |
| | | // |
| | | // if ($shop_terms && !is_wp_error($shop_terms)) { |
| | | // $shop = $shop_terms[0]; |
| | | // return "working at {$shop->name}"; |
| | | // } elseif ($city_terms && !is_wp_error($city_terms)) { |
| | | // $city = $city_terms[0]; |
| | | // return "in {$city->name}"; |
| | | // } |
| | | // return "in Edmonton"; |
| | | // }], |
| | | // ], |
| | | // 'archive_title' => 'Edmonton\'s Best Tattoo Artists | Browse by Style & Shop', |
| | | // 'archive_description' => 'Explore Edmonton\'s top tattoo artists. Filter by style, shop, or location. View portfolios and book your next tattoo today.' |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'Person', |
| | | // 'additional_types' => ['Artist'], |
| | | // 'properties' => [ |
| | | // 'jobTitle' => ['callback' => function ($id, $context, $meta) { |
| | | // $job = $meta['job_title'] ?? null; |
| | | // if ($job) return $job; |
| | | // |
| | | // // Try to build from specialties |
| | | // $styles = get_the_terms($id, BASE . 'style'); |
| | | // if ($styles && !is_wp_error($styles) && count($styles) > 0) { |
| | | // $primary = $styles[0]->name; |
| | | // return "{$primary} Tattoo Artist"; |
| | | // } |
| | | // return "Tattoo Artist"; |
| | | // }], |
| | | // 'telephone' => 'phone', |
| | | // 'email' => 'email', |
| | | // 'url' => ['callback' => function ($id) { |
| | | // return get_permalink($id); |
| | | // }], |
| | | // 'image' => ['callback' => function ($id) { |
| | | // return get_the_post_thumbnail_url($id, 'full'); |
| | | // }], |
| | | // 'knowsAbout' => ['taxonomy' => BASE . 'style'], |
| | | // 'worksFor' => ['callback' => function ($id, $context, $meta) { |
| | | // $shops = get_the_terms($id, BASE . 'shop'); |
| | | // if (!$shops || is_wp_error($shops)) { |
| | | // return null; |
| | | // } |
| | | // |
| | | // return array_map(function ($shop) { |
| | | // return [ |
| | | // '@type' => 'LocalBusiness', |
| | | // '@id' => get_term_link($shop) . '#business', |
| | | // 'name' => $shop->name, |
| | | // 'url' => get_term_link($shop) |
| | | // ]; |
| | | // }, $shops); |
| | | // }], |
| | | // 'memberOf' => [ |
| | | // '@id' => 'https://edmonton.ink/#organization' |
| | | // ], |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // // PARTNER (Shops/Organizations) |
| | | // $content['partner'] = array_merge($content['partner'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => '{{name}} | Community Partner', |
| | | // 'description_template' => '{{name}} - {{excerpt}}', |
| | | // 'variables' => [ |
| | | // 'name' => 'post_title', |
| | | // 'excerpt' => ['callback' => function ($id) { |
| | | // return get_the_excerpt($id); |
| | | // }], |
| | | // ], |
| | | // 'archive_title' => 'Our Community Partners | Supporting Edmonton\'s Tattoo Scene', |
| | | // 'archive_description' => 'Meet the businesses and organizations supporting Edmonton\'s tattoo community. Our partners help make edmonton.ink possible.' |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'Organization', |
| | | // 'properties' => [ |
| | | // 'name' => ['callback' => function ($id) { |
| | | // return get_the_title($id); |
| | | // }], |
| | | // 'description' => ['callback' => function ($id) { |
| | | // return get_the_excerpt($id); |
| | | // }], |
| | | // 'url' => ['callback' => function ($id, $context, $meta) { |
| | | // $website = $meta['website'] ?? null; |
| | | // return $website ?: get_permalink($id); |
| | | // }], |
| | | // 'logo' => ['callback' => function ($id, $context, $meta) { |
| | | // $image_id = $meta['image'] ?? null; |
| | | // if ($image_id) { |
| | | // return wp_get_attachment_image_url($image_id, 'full'); |
| | | // } |
| | | // return get_the_post_thumbnail_url($id, 'full'); |
| | | // }], |
| | | // 'telephone' => 'phone', |
| | | // 'email' => 'email', |
| | | // 'address' => 'address', |
| | | // 'sameAs' => ['callback' => function ($id, $context, $meta) { |
| | | // $links = []; |
| | | // if (!empty($meta['instagram'])) $links[] = $meta['instagram']; |
| | | // if (!empty($meta['facebook'])) $links[] = $meta['facebook']; |
| | | // if (!empty($meta['twitter'])) $links[] = $meta['twitter']; |
| | | // return !empty($links) ? $links : null; |
| | | // }], |
| | | // 'memberOf' => [ |
| | | // '@id' => 'https://edmonton.ink/#organization' |
| | | // ], |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // // TATTOO |
| | | // $content['tattoo'] = array_merge($content['tattoo'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => '{{style}} Tattoo by {{artist}} | Edmonton', |
| | | // 'description_template' => 'Beautiful {{style}} tattoo by {{artist}}{{location}}. View more work from Edmonton\'s talented artists.', |
| | | // 'variables' => [ |
| | | // 'style' => ['taxonomy' => BASE . 'style', 'primary' => true], |
| | | // 'artist' => ['callback' => function ($id) { |
| | | // $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | // if ($artist_id) { |
| | | // return get_the_title($artist_id); |
| | | // } |
| | | // return 'Edmonton Artist'; |
| | | // }], |
| | | // 'location' => ['callback' => function ($id) { |
| | | // $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | // if (!$artist_id) return ''; |
| | | // |
| | | // $shops = get_the_terms($artist_id, BASE . 'shop'); |
| | | // if ($shops && !is_wp_error($shops)) { |
| | | // return ' at ' . $shops[0]->name; |
| | | // } |
| | | // return ''; |
| | | // }], |
| | | // ], |
| | | // 'archive_title' => 'Edmonton Tattoos | Browse by Style, Artist & Shop', |
| | | // 'archive_description' => 'Browse tattoos from Edmonton\'s best artists. Filter by style, theme, or artist to find inspiration for your next piece.' |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'CreativeWork', |
| | | // 'additional_types' => ['VisualArtwork'], |
| | | // 'properties' => [ |
| | | // 'creator' => ['callback' => function ($id) { |
| | | // $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | // if ($artist_id) { |
| | | // return [ |
| | | // '@type' => 'Person', |
| | | // '@id' => get_permalink($artist_id) . '#person', |
| | | // 'name' => get_the_title($artist_id), |
| | | // 'url' => get_permalink($artist_id) |
| | | // ]; |
| | | // } |
| | | // return null; |
| | | // }], |
| | | // 'image' => ['callback' => function ($id) { |
| | | // return get_the_post_thumbnail_url($id, 'full'); |
| | | // }], |
| | | // 'about' => ['taxonomy' => BASE . 'theme'], |
| | | // 'artform' => 'Tattoo', |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // // PIERCING |
| | | // $content['piercing'] = array_merge($content['piercing'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => '{{type}} Piercing by {{artist}} | Edmonton', |
| | | // 'description_template' => 'Professional {{type}} piercing by {{artist}}. View more piercing work from Edmonton\'s skilled professionals.', |
| | | // 'variables' => [ |
| | | // 'type' => ['taxonomy' => BASE . 'pstyle', 'primary' => true], |
| | | // 'artist' => ['callback' => function ($id) { |
| | | // $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | // return $artist_id ? get_the_title($artist_id) : 'Edmonton Professional'; |
| | | // }], |
| | | // ] |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'CreativeWork', |
| | | // 'properties' => [ |
| | | // 'creator' => ['callback' => function ($id) { |
| | | // $artist_id = get_post_meta($id, BASE . 'link', true); |
| | | // if ($artist_id) { |
| | | // return [ |
| | | // '@type' => 'Person', |
| | | // '@id' => get_permalink($artist_id) . '#person', |
| | | // 'name' => get_the_title($artist_id), |
| | | // 'url' => get_permalink($artist_id) |
| | | // ]; |
| | | // } |
| | | // return null; |
| | | // }], |
| | | // 'image' => ['callback' => function ($id) { |
| | | // return get_the_post_thumbnail_url($id, 'full'); |
| | | // }], |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // // EVENT |
| | | // $content['event'] = array_merge($content['event'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_builder' => function ($post_id, $meta) { |
| | | // $title = get_the_title($post_id); |
| | | // $date = $meta->getValue('event_date'); |
| | | // if ($date) { |
| | | // $formatted_date = date('F j, Y', strtotime($date)); |
| | | // return "{$title} | {$formatted_date}"; |
| | | // } |
| | | // return "{$title} | Edmonton Tattoo Event"; |
| | | // }, |
| | | // 'description_builder' => function ($post_id, $meta) { |
| | | // $excerpt = get_the_excerpt($post_id); |
| | | // $venue = $meta->getValue('venue'); |
| | | // $date = $meta->getValue('event_date'); |
| | | // |
| | | // $desc = $excerpt; |
| | | // if ($venue) $desc .= " Location: {$venue}."; |
| | | // if ($date) { |
| | | // $formatted = date('F j, Y', strtotime($date)); |
| | | // $desc .= " Date: {$formatted}."; |
| | | // } |
| | | // return $desc; |
| | | // } |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'custom_builder' => function ($post_id) { |
| | | // $meta = Meta::forPost($post_id); |
| | | // |
| | | // $schema = [ |
| | | // '@type' => 'Event', |
| | | // 'name' => get_the_title($post_id), |
| | | // 'url' => get_permalink($post_id), |
| | | // ]; |
| | | // |
| | | // $date = $meta->get('event_date'); |
| | | // if ($date) { |
| | | // $schema['startDate'] = date('c', strtotime($date)); |
| | | // } |
| | | // |
| | | // $venue = $meta->get('venue'); |
| | | // $venue_address = $meta->get('venue_address'); |
| | | // if ($venue) { |
| | | // $schema['location'] = [ |
| | | // '@type' => 'Place', |
| | | // 'name' => $venue, |
| | | // ]; |
| | | // if ($venue_address) { |
| | | // $schema['location']['address'] = $venue_address; |
| | | // } |
| | | // } |
| | | // |
| | | // $image_id = get_post_thumbnail_id($post_id); |
| | | // if ($image_id) { |
| | | // $schema['image'] = wp_get_attachment_image_url($image_id, 'full'); |
| | | // } |
| | | // |
| | | // return $schema; |
| | | // } |
| | | // ] |
| | | // ]); |
| | | // |
| | | // return $content; |
| | | //}); |
| | | |
| | | // ================================================== |
| | | // TAXONOMY CONFIGURATION |
| | | // ================================================== |
| | | |
| | | add_filter('jvb_taxonomy', function ($taxonomies) { |
| | | |
| | | // SHOP |
| | | $taxonomies['shop'] = array_merge($taxonomies['shop'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => '{{name}} | Tattoo Shop in {{city}}', |
| | | 'description_template' => '{{name}}{{tagline_text}}{{established_text}} in {{city}}. Featuring {{artist_count}} talented artists. Book your appointment today.', |
| | | 'variables' => [ |
| | | 'name' => 'term_name', |
| | | 'city' => ['callback' => function ($term_id, $context) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $city_id = $meta->get('city'); |
| | | if ($city_id && term_exists((int)$city_id, BASE . 'city')) { |
| | | $city_term = get_term($city_id, BASE . 'city'); |
| | | if ($city_term && !is_wp_error($city_term)) { |
| | | return $city_term->name; |
| | | } |
| | | } |
| | | return 'Edmonton'; |
| | | }], |
| | | 'tagline_text' => ['callback' => function ($term_id) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $tagline = $meta->get('tagline'); |
| | | return $tagline ? " - {$tagline}" : ''; |
| | | }], |
| | | 'established_text' => ['callback' => function ($term_id) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $established = $meta->get('established'); |
| | | return $established ? " Established in {$established}" : ''; |
| | | }], |
| | | 'artist_count' => ['callback' => function ($term_id) { |
| | | $artists = get_posts([ |
| | | 'post_type' => BASE . 'artist', |
| | | 'tax_query' => [[ |
| | | 'taxonomy' => BASE . 'shop', |
| | | 'terms' => $term_id |
| | | ]], |
| | | 'posts_per_page' => -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | return count($artists); |
| | | }], |
| | | ] |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'LocalBusiness', |
| | | 'additional_types' => ['TattooParlor'], |
| | | 'properties' => [ |
| | | 'address' => 'address', |
| | | 'telephone' => 'phone', |
| | | 'email' => 'email', |
| | | 'openingHours' => 'hours', |
| | | 'priceRange' => 'price_range', |
| | | 'image' => 'logo', |
| | | 'url' => ['callback' => function ($term_id) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $website = $meta->get('website'); |
| | | return $website ?: get_term_link($term_id); |
| | | }], |
| | | 'sameAs' => ['callback' => function ($term_id) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $links = []; |
| | | if ($ig = $meta->get('instagram')) $links[] = $ig; |
| | | if ($fb = $meta->get('facebook')) $links[] = $fb; |
| | | return !empty($links) ? $links : null; |
| | | }], |
| | | 'memberOf' => [ |
| | | '@id' => 'https://edmonton.ink/#organization' |
| | | ], |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // STYLE |
| | | $taxonomies['style'] = array_merge($taxonomies['style'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => 'Edmonton {{name}} Tattoo Artists | Specialists in {{name}}', |
| | | 'description_template' => '{{name}}{{alt_names}} is a distinctive tattoo style. {{characteristics}} Find Edmonton artists specializing in {{name}} tattoos.', |
| | | 'variables' => [ |
| | | 'name' => 'term_name', |
| | | 'alt_names' => ['callback' => function ($term_id) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $alts = $meta->get('alternate_name'); |
| | | if (!empty($alts) && is_array($alts)) { |
| | | $names = array_filter(array_column($alts, 'name')); |
| | | if (!empty($names)) { |
| | | return ' (also known as ' . implode(', ', array_slice($names, 0, 2)) . ')'; |
| | | } |
| | | } |
| | | return ''; |
| | | }], |
| | | 'characteristics' => ['meta' => 'characteristics', 'truncate' => 100], |
| | | ] |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'CreativeWork', |
| | | 'properties' => [ |
| | | 'name' => ['callback' => function ($term_id) { |
| | | return get_term($term_id)->name . ' Tattoo Style'; |
| | | }], |
| | | 'description' => 'characteristics', |
| | | 'about' => ['meta' => 'description'], |
| | | 'alternateName' => ['callback' => function ($term_id) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $alts = $meta->get('alternate_name'); |
| | | if (!empty($alts) && is_array($alts)) { |
| | | return array_filter(array_column($alts, 'name')); |
| | | } |
| | | return null; |
| | | }], |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // THEME |
| | | $taxonomies['theme'] = array_merge($taxonomies['theme'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => 'Edmonton {{name}} Tattoos | Find {{name}} Tattoo Designs', |
| | | 'description_template' => 'Explore {{name}} tattoos, a popular motif in Edmonton\'s tattoo scene. {{similar}}Find artists specializing in {{name}} designs.', |
| | | 'variables' => [ |
| | | 'name' => 'term_name', |
| | | 'similar' => ['callback' => function ($term_id) { |
| | | $meta = Meta::forTerm($term_id); |
| | | $similar = $meta->get('similar'); |
| | | if (!empty($similar)) { |
| | | $similar_names = []; |
| | | foreach ((array)$similar as $similar_id) { |
| | | $term = get_term($similar_id, BASE . 'theme'); |
| | | if ($term && !is_wp_error($term)) { |
| | | $similar_names[] = html_entity_decode($term->name); |
| | | } |
| | | } |
| | | if (!empty($similar_names)) { |
| | | return 'Similar themes include ' . implode(', ', array_slice($similar_names, 0, 2)) . '. '; |
| | | } |
| | | } |
| | | return ''; |
| | | }], |
| | | ] |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'CreativeWork', |
| | | 'properties' => [ |
| | | 'name' => ['callback' => function ($term_id) { |
| | | return get_term($term_id)->name . ' Tattoo Theme'; |
| | | }], |
| | | 'description' => ['meta' => 'description'], |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | // CITY |
| | | $taxonomies['city'] = array_merge($taxonomies['city'] ?? [], [ |
| | | 'seo' => [ |
| | | 'title_template' => '{{name}} Tattoo Artists & Shops | edmonton.ink', |
| | | 'description_template' => 'Discover {{name}}\'s vibrant tattoo scene featuring {{shop_count}} local shops and {{artist_count}} talented artists. Find top local talent and book your next tattoo today.', |
| | | 'variables' => [ |
| | | 'name' => 'term_name', |
| | | 'shop_count' => ['callback' => function ($term_id) { |
| | | $shops = get_terms([ |
| | | 'taxonomy' => BASE . 'shop', |
| | | 'meta_key' => BASE . 'city', |
| | | 'meta_value' => $term_id, |
| | | 'fields' => 'count' |
| | | ]); |
| | | return is_wp_error($shops) ? 0 : $shops; |
| | | }], |
| | | 'artist_count' => ['callback' => function ($term_id) { |
| | | $artists = get_posts([ |
| | | 'post_type' => BASE . 'artist', |
| | | 'tax_query' => [[ |
| | | 'taxonomy' => BASE . 'city', |
| | | 'terms' => $term_id |
| | | ]], |
| | | 'posts_per_page' => -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | return count($artists); |
| | | }], |
| | | ] |
| | | ], |
| | | |
| | | 'schema' => [ |
| | | 'type' => 'Place', |
| | | 'properties' => [ |
| | | 'address' => ['callback' => function ($term_id) { |
| | | $term = get_term($term_id); |
| | | return [ |
| | | '@type' => 'PostalAddress', |
| | | 'addressLocality' => html_entity_decode($term->name), |
| | | 'addressRegion' => 'Alberta', |
| | | 'addressCountry' => 'CA' |
| | | ]; |
| | | }], |
| | | ] |
| | | ] |
| | | ]); |
| | | |
| | | return $taxonomies; |
| | | }); |
| | | //add_filter('jvb_taxonomy', function ($taxonomies) { |
| | | // |
| | | // // SHOP |
| | | // $taxonomies['shop'] = array_merge($taxonomies['shop'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => '{{name}} | Tattoo Shop in {{city}}', |
| | | // 'description_template' => '{{name}}{{tagline_text}}{{established_text}} in {{city}}. Featuring {{artist_count}} talented artists. Book your appointment today.', |
| | | // 'variables' => [ |
| | | // 'name' => 'term_name', |
| | | // 'city' => ['callback' => function ($term_id, $context) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $city_id = $meta->get('city'); |
| | | // if ($city_id && term_exists((int)$city_id, BASE . 'city')) { |
| | | // $city_term = get_term($city_id, BASE . 'city'); |
| | | // if ($city_term && !is_wp_error($city_term)) { |
| | | // return $city_term->name; |
| | | // } |
| | | // } |
| | | // return 'Edmonton'; |
| | | // }], |
| | | // 'tagline_text' => ['callback' => function ($term_id) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $tagline = $meta->get('tagline'); |
| | | // return $tagline ? " - {$tagline}" : ''; |
| | | // }], |
| | | // 'established_text' => ['callback' => function ($term_id) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $established = $meta->get('established'); |
| | | // return $established ? " Established in {$established}" : ''; |
| | | // }], |
| | | // 'artist_count' => ['callback' => function ($term_id) { |
| | | // $artists = get_posts([ |
| | | // 'post_type' => BASE . 'artist', |
| | | // 'tax_query' => [[ |
| | | // 'taxonomy' => BASE . 'shop', |
| | | // 'terms' => $term_id |
| | | // ]], |
| | | // 'posts_per_page' => -1, |
| | | // 'fields' => 'ids' |
| | | // ]); |
| | | // return count($artists); |
| | | // }], |
| | | // ] |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'LocalBusiness', |
| | | // 'additional_types' => ['TattooParlor'], |
| | | // 'properties' => [ |
| | | // 'address' => 'address', |
| | | // 'telephone' => 'phone', |
| | | // 'email' => 'email', |
| | | // 'openingHours' => 'hours', |
| | | // 'priceRange' => 'price_range', |
| | | // 'image' => 'logo', |
| | | // 'url' => ['callback' => function ($term_id) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $website = $meta->get('website'); |
| | | // return $website ?: get_term_link($term_id); |
| | | // }], |
| | | // 'sameAs' => ['callback' => function ($term_id) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $links = []; |
| | | // if ($ig = $meta->get('instagram')) $links[] = $ig; |
| | | // if ($fb = $meta->get('facebook')) $links[] = $fb; |
| | | // return !empty($links) ? $links : null; |
| | | // }], |
| | | // 'memberOf' => [ |
| | | // '@id' => 'https://edmonton.ink/#organization' |
| | | // ], |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // // STYLE |
| | | // $taxonomies['style'] = array_merge($taxonomies['style'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => 'Edmonton {{name}} Tattoo Artists | Specialists in {{name}}', |
| | | // 'description_template' => '{{name}}{{alt_names}} is a distinctive tattoo style. {{characteristics}} Find Edmonton artists specializing in {{name}} tattoos.', |
| | | // 'variables' => [ |
| | | // 'name' => 'term_name', |
| | | // 'alt_names' => ['callback' => function ($term_id) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $alts = $meta->get('alternate_name'); |
| | | // if (!empty($alts) && is_array($alts)) { |
| | | // $names = array_filter(array_column($alts, 'name')); |
| | | // if (!empty($names)) { |
| | | // return ' (also known as ' . implode(', ', array_slice($names, 0, 2)) . ')'; |
| | | // } |
| | | // } |
| | | // return ''; |
| | | // }], |
| | | // 'characteristics' => ['meta' => 'characteristics', 'truncate' => 100], |
| | | // ] |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'CreativeWork', |
| | | // 'properties' => [ |
| | | // 'name' => ['callback' => function ($term_id) { |
| | | // return get_term($term_id)->name . ' Tattoo Style'; |
| | | // }], |
| | | // 'description' => 'characteristics', |
| | | // 'about' => ['meta' => 'description'], |
| | | // 'alternateName' => ['callback' => function ($term_id) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $alts = $meta->get('alternate_name'); |
| | | // if (!empty($alts) && is_array($alts)) { |
| | | // return array_filter(array_column($alts, 'name')); |
| | | // } |
| | | // return null; |
| | | // }], |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // // THEME |
| | | // $taxonomies['theme'] = array_merge($taxonomies['theme'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => 'Edmonton {{name}} Tattoos | Find {{name}} Tattoo Designs', |
| | | // 'description_template' => 'Explore {{name}} tattoos, a popular motif in Edmonton\'s tattoo scene. {{similar}}Find artists specializing in {{name}} designs.', |
| | | // 'variables' => [ |
| | | // 'name' => 'term_name', |
| | | // 'similar' => ['callback' => function ($term_id) { |
| | | // $meta = Meta::forTerm($term_id); |
| | | // $similar = $meta->get('similar'); |
| | | // if (!empty($similar)) { |
| | | // $similar_names = []; |
| | | // foreach ((array)$similar as $similar_id) { |
| | | // $term = get_term($similar_id, BASE . 'theme'); |
| | | // if ($term && !is_wp_error($term)) { |
| | | // $similar_names[] = html_entity_decode($term->name); |
| | | // } |
| | | // } |
| | | // if (!empty($similar_names)) { |
| | | // return 'Similar themes include ' . implode(', ', array_slice($similar_names, 0, 2)) . '. '; |
| | | // } |
| | | // } |
| | | // return ''; |
| | | // }], |
| | | // ] |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'CreativeWork', |
| | | // 'properties' => [ |
| | | // 'name' => ['callback' => function ($term_id) { |
| | | // return get_term($term_id)->name . ' Tattoo Theme'; |
| | | // }], |
| | | // 'description' => ['meta' => 'description'], |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // // CITY |
| | | // $taxonomies['city'] = array_merge($taxonomies['city'] ?? [], [ |
| | | // 'seo' => [ |
| | | // 'title_template' => '{{name}} Tattoo Artists & Shops | edmonton.ink', |
| | | // 'description_template' => 'Discover {{name}}\'s vibrant tattoo scene featuring {{shop_count}} local shops and {{artist_count}} talented artists. Find top local talent and book your next tattoo today.', |
| | | // 'variables' => [ |
| | | // 'name' => 'term_name', |
| | | // 'shop_count' => ['callback' => function ($term_id) { |
| | | // $shops = get_terms([ |
| | | // 'taxonomy' => BASE . 'shop', |
| | | // 'meta_key' => BASE . 'city', |
| | | // 'meta_value' => $term_id, |
| | | // 'fields' => 'count' |
| | | // ]); |
| | | // return is_wp_error($shops) ? 0 : $shops; |
| | | // }], |
| | | // 'artist_count' => ['callback' => function ($term_id) { |
| | | // $artists = get_posts([ |
| | | // 'post_type' => BASE . 'artist', |
| | | // 'tax_query' => [[ |
| | | // 'taxonomy' => BASE . 'city', |
| | | // 'terms' => $term_id |
| | | // ]], |
| | | // 'posts_per_page' => -1, |
| | | // 'fields' => 'ids' |
| | | // ]); |
| | | // return count($artists); |
| | | // }], |
| | | // ] |
| | | // ], |
| | | // |
| | | // 'schema' => [ |
| | | // 'type' => 'Place', |
| | | // 'properties' => [ |
| | | // 'address' => ['callback' => function ($term_id) { |
| | | // $term = get_term($term_id); |
| | | // return [ |
| | | // '@type' => 'PostalAddress', |
| | | // 'addressLocality' => html_entity_decode($term->name), |
| | | // 'addressRegion' => 'Alberta', |
| | | // 'addressCountry' => 'CA' |
| | | // ]; |
| | | // }], |
| | | // ] |
| | | // ] |
| | | // ]); |
| | | // |
| | | // return $taxonomies; |
| | | //}); |
| | |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar && !empty($registrar->getConfig('seo')['schema']??[])) { |
| | | $seo = $registrar->getSEO(); |
| | | error_log('SEO: '.print_r($seo->schema(), true)); |
| | | $schema[] = $seo->schema()->outputArchiveSchema(); |
| | | } |
| | | } |
| | | $isContent = array_values(array_filter(array_map(function($item) { |
| | | return intval(get_option(BASE.$item.'_archive', false)); |
| | | },Registrar::getFeatured('is_content', 'term')))); |
| | | |
| | | if (is_page($isContent)){ |
| | | $type = get_post_meta(get_the_id(), BASE.'for_type', true); |
| | | error_log('Type: '.print_r($type, true)); |
| | | $registrar = Registrar::getInstance($type); |
| | | if ($registrar) { |
| | | $schema[] = $registrar->getSEO()->schema()->outputContentTaxArchiveSchema(); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | $breadcrumbs = $this->buildBreadcrumbs(); |
| | | if ($breadcrumbs) { |
| | |
| | | if (!empty($schema)) { |
| | | $website = get_option(BASE.'WebsiteSchema'); |
| | | if (!empty($website)) { |
| | | Cache::for('websiteSchema')->flush(); |
| | | if (JVB_TESTING) { |
| | | Cache::for('websiteSchema')->flush(); |
| | | } |
| | | $website = Cache::for('websiteSchema')->remember( |
| | | 'schema', |
| | | function () { |
| | |
| | | echo '<script type="application/ld+json">' . "\n"; |
| | | echo wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); |
| | | echo "\n" . '</script>' . "\n"; |
| | | echo "\n" . '<!-- / SEO Schema by JakeVan -->'."\n"; |
| | | } |
| | | |
| | | public function buildBreadcrumbs():array |
| | |
| | | } |
| | | |
| | | public function buildBasicWebsiteSchema():array |
| | | { Cache::for('websiteSchema')->flush(); |
| | | { |
| | | if (JVB_TESTING){ |
| | | Cache::for('websiteSchema')->flush(); |
| | | } |
| | | |
| | | return Cache::for('websiteSchema')->remember( |
| | | 'reference', |
| | | function () { |
| | |
| | | |
| | | public function getCreator(bool $reference = false):LocalBusiness |
| | | { |
| | | Cache::for('JakeVanCreator')->flush(); |
| | | if (JVB_TESTING){ |
| | | Cache::for('JakeVanCreator')->flush(); |
| | | } |
| | | |
| | | if ($reference) { |
| | | return Cache::for('JakeVanCreator')->remember( |
| | | 'reference', |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Thing\CreativeWork\VisualArtwork; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\artEditionTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\artformTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\artistTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\artMediumTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\artworkSurfaceTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\coloristTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\depthTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\heightTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\inkerTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\lettererTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\pencilerTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\weightTrait; |
| | | use JVBase\managers\SEO\render\Traits\_Properties\widthTrait; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class VisualArtwork extends CreativeWork |
| | | { |
| | | use artEditionTrait, artMediumTrait, artFormTrait, artMediumTrait, artistTrait, |
| | | artworkSurfaceTrait, coloristTrait, depthTrait, heightTrait, inkerTrait, lettererTrait, |
| | | pencilerTrait, weightTrait, widthTrait; |
| | | } |
| New file |
| | |
| | | <?php |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php'); |
| | |
| | | <?php |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/CreativeWork.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/DefinedTermSet.php'); |
| | | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/CategoryCodeSet.php'); |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Thing\Place\AdministrativeArea; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress; |
| | | use JVBase\managers\SEO\render\Thing\Thing; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class City extends AdministrativeArea { |
| | | |
| | | } |
| | |
| | | <?php |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/AdministrativeArea.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Thing/Place/AdministrativeArea/Country.php'); |
| | |
| | | |
| | | protected array $ignore = [ |
| | | 'mappedMethods', |
| | | 'ignore' |
| | | 'ignore', |
| | | 'id' |
| | | ]; |
| | | public function outputSchema():array |
| | | { |
| | | global $wp; |
| | | $current = home_url( add_query_arg( $_GET, $wp->request ) ); |
| | | $id = (isset($this->id)) ? $this->id : $current.'/#'.strtolower($this->getTypeName()); |
| | | $id = (isset($this->id)) ? $this->id : $current.'#'.strtolower($this->getTypeName()); |
| | | $elements = array_map( |
| | | function ($value) { |
| | | |
| | |
| | | } |
| | | $this->id = $id; |
| | | } |
| | | |
| | | public function delete(string $property):void |
| | | { |
| | | if (property_exists($this, $property)) { |
| | | unset($this->$property); |
| | | } |
| | | } |
| | | } |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/amountOfThisGoodTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/applicableCountryTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/areaServedTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artistTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artFormTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/associatedMediaTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/attendeeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/audienceTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/codeValueTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/colleagueTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/colorTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/companyRegistrationTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/composerTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/contactOptionTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/imageTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/includesObjectTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/inCodeSetTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/inDefinedTermSetTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/ineligibleLocationTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/ineligibleRegionTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/legalAddressTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/legalNameTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/legalRepresentativeTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/licenseTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/lifeEventTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/lineTrait.php'); |
| | |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/patternTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/paymentAcceptedTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/paymentMethodTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/performerInTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/performerTrait.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/render/Traits/_Properties/performTimeTrait.php'); |
| | |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Thing; |
| | | use JVBase\registrar\config\seo\Resolver; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | { |
| | | return $this->about??null; |
| | | } |
| | | public function setAbout(Thing $about):void |
| | | public function setAbout(Thing|array $about):void |
| | | { |
| | | if (is_array($about)) { |
| | | if (!array_key_exists('type', $about)) { |
| | | error_log('[aboutTrait] Needs to have a type key'.print_r($about, true)); |
| | | return; |
| | | } |
| | | if (!class_exists($about['type'])) { |
| | | error_log('[aboutTrait] Class not found for: '.print_r($about['type'], true)); |
| | | return; |
| | | } |
| | | $class = new $about['type'](); |
| | | $className = $about['type']; |
| | | unset($about['type']); |
| | | foreach ($about as $key => $value) { |
| | | $method = 'set'.ucfirst($key); |
| | | if (method_exists($class, $method)) { |
| | | if (is_string($value) && str_contains($value, '{{')) { |
| | | $value = Resolver::resolveForSchema($key, $value, $class); |
| | | } |
| | | error_log('Setting '.$key.' to '.print_r($value, true)); |
| | | $class->$method($value); |
| | | } else { |
| | | error_log('[aboutTrait] Invalid method: '.$method.', for class: '.$className); |
| | | } |
| | | } |
| | | $about = $class; |
| | | } |
| | | $this->about = $about; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait artEditionTrait { |
| | | /** |
| | | * @var int|string The number of copies when multiple copies of a piece of artwork are produced - e.g. for a limited edition of 20 prints, 'artEdition' refers to the total number of copies (in this example "20"). |
| | | */ |
| | | protected int|string $artEdition; |
| | | |
| | | public function getArtEdition():int|string|null |
| | | { |
| | | return $this->artEdition??null; |
| | | } |
| | | public function setArtEdition(int|string $artEdition):void |
| | | { |
| | | $this->artEdition = $artEdition; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait artMediumTrait { |
| | | /** |
| | | * @var string The material used. (E.g. Oil, Watercolour, Acrylic, Linoprint, Marble, Cyanotype, Digital, Lithograph, DryPoint, Intaglio, Pastel, Woodcut, Pencil, Mixed Media, etc.) |
| | | */ |
| | | protected string $artMedium; |
| | | |
| | | public function getArtMedium():?string |
| | | { |
| | | return $this->artMedium??null; |
| | | } |
| | | public function setArtMedium(string $artMedium):void |
| | | { |
| | | $this->artMedium = $artMedium; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait artformTrait { |
| | | /** |
| | | * @var string e.g. Painting, Drawing, Sculpture, Print, Photograph, Assemblage, Collage, etc. |
| | | */ |
| | | protected string $artform; |
| | | |
| | | public function getArtform():?string |
| | | { |
| | | return $this->artform??null; |
| | | } |
| | | public function setArtform(string $artform):void |
| | | { |
| | | $this->artform = $artform; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Person\Person; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait artistTrait { |
| | | /** |
| | | * @var Person The primary artist for a work in a medium other than pencils or digital line art--for example, if the primary artwork is done in watercolors or digital paints. |
| | | */ |
| | | protected Person $artist; |
| | | |
| | | public function getArtist():?Person |
| | | { |
| | | return $this->artist??null; |
| | | } |
| | | public function setArtist(Person $artist):void |
| | | { |
| | | $this->artist = $artist; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait artworkSurfaceTrait { |
| | | /** |
| | | * @var string The supporting materials for the artwork, e.g. Canvas, Paper, Wood, Board, etc. Supersedes surface. |
| | | */ |
| | | protected string $artworkSurface; |
| | | |
| | | public function getArtworkSurface():?string |
| | | { |
| | | return $this->artworkSurface??null; |
| | | } |
| | | public function setArtworkSurface(string $artworkSurface):void |
| | | { |
| | | $this->artworkSurface = $artworkSurface; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Person\Person; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait coloristTrait { |
| | | /** |
| | | * @var Person The individual who adds color to inked drawings. |
| | | */ |
| | | protected Person $colorist; |
| | | |
| | | public function getColorist():?Person |
| | | { |
| | | return $this->colorist??null; |
| | | } |
| | | public function setColorist(Person $colorist):void |
| | | { |
| | | $this->colorist = $colorist; |
| | | } |
| | | } |
| | |
| | | use JVBase\managers\SEO\render\Thing\Organization\Organization; |
| | | use JVBase\managers\SEO\render\Thing\Person\Person; |
| | | use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper; |
| | | use JVBase\registrar\config\seo\Resolver; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | { |
| | | return $this->creator??null; |
| | | } |
| | | public function setCreator(Organization|Person|array $creator):void |
| | | public function setCreator(string|Organization|Person|array $creator):void |
| | | { |
| | | if (is_string($creator)) { |
| | | if (empty($creator)) { |
| | | return; |
| | | } |
| | | error_log('Creator value: '.print_r($creator, true)); |
| | | //TODO: Set creator from string |
| | | return; |
| | | } |
| | | if (is_array($creator)) { |
| | | $creator = $this->mixedArray('creator', $creator, [ |
| | | 'JVBase\managers\SEO\render\Thing\Organization\Organization', |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Person\Person; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait inkerTrait { |
| | | /** |
| | | * @var Person The individual who traces over the pencil drawings in ink after pencils are complete. |
| | | */ |
| | | protected Person $inker; |
| | | |
| | | public function getInker():?Person |
| | | { |
| | | return $this->inker??null; |
| | | } |
| | | public function setInker(Person $inker):void |
| | | { |
| | | $this->inker = $inker; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Person\Person; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait lettererTrait { |
| | | /** |
| | | * @var Person The individual who adds lettering, including speech balloons and sound effects, to artwork. |
| | | */ |
| | | protected Person $letterer; |
| | | |
| | | public function getLetterer():?Person |
| | | { |
| | | return $this->letterer??null; |
| | | } |
| | | public function setLetterer(Person $letterer):void |
| | | { |
| | | $this->letterer = $letterer; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\managers\SEO\render\Traits\_Properties; |
| | | |
| | | use JVBase\managers\SEO\render\Thing\Person\Person; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | trait pencilerTrait { |
| | | /** |
| | | * @var Person The individual who draws the primary narrative artwork. |
| | | */ |
| | | protected Person $penciler; |
| | | |
| | | public function getPenciler():?Person |
| | | { |
| | | return $this->penciler??null; |
| | | } |
| | | public function setPenciler(Person $penciler):void |
| | | { |
| | | $this->penciler = $penciler; |
| | | } |
| | | } |
| | |
| | | { |
| | | return $this->sourceOrganization??null; |
| | | } |
| | | public function setSourceOrganization(Organization $sourceOrganization):void |
| | | public function setSourceOrganization(string|Organization $sourceOrganization):void |
| | | { |
| | | if (is_string($sourceOrganization)) { |
| | | if (empty($sourceOrganization)) { |
| | | return; |
| | | } |
| | | //TODO: create organization from string |
| | | return; |
| | | } |
| | | $this->sourceOrganization = $sourceOrganization; |
| | | } |
| | | } |
| | |
| | | $version, |
| | | $strategy |
| | | ); |
| | | wp_localize_script('jvb-auth', 'jvbBase', ['base' => BASE]); |
| | | wp_register_script( |
| | | 'jvb-interactions', |
| | | JVB_URL.'assets/js/min/interactions.min.js', |
| | |
| | | $strategy |
| | | ); |
| | | |
| | | |
| | | //SEO Admin |
| | | wp_register_script( |
| | | 'jvb-schema', |
| | |
| | | 'type' => $config['subtype'], |
| | | ], $config); |
| | | |
| | | $registrar = Registrar::getInstance($config['subtype']); |
| | | $registrar = Registrar::getInstance($config[$config['subtype']]); |
| | | $icon = jvbDefaultIcon(); |
| | | if ($registrar){ |
| | | $icon = $registrar->getIcon()??jvbDefaultIcon(); |
| | |
| | | { |
| | | $fields = $config['fields'] ?? []; |
| | | $rows = is_array($value) ? $value : []; |
| | | if(array_key_exists('row_label', $config)) { |
| | | $config['data']['label'] = esc_attr($config['row_label']); |
| | | if(array_key_exists('add_label', $config)) { |
| | | $config['data']['label'] = esc_attr($config['add_label']); |
| | | } |
| | | |
| | | $input = sprintf( |
| | |
| | | 'user_email', |
| | | ], |
| | | 'term' => [ |
| | | 'term_name', |
| | | 'name', |
| | | 'description' |
| | | ] |
| | | ]; |
| | |
| | | $values = is_array($value) ? $value : array(); |
| | | |
| | | $conditional = $this->handleConditionalField($field); |
| | | $row_label = isset($field['row_label']) ? $field['row_label'] : ''; |
| | | $row_label = isset($field['add_label']) ? $field['add_label'] : ''; |
| | | $rowTitle = (array_key_exists('new_row', $field)) ? $field['new_row'] : 'New Item'; |
| | | if (array_key_exists('group', $field)) { |
| | | $name = $field['group'].'::'.$name; |
| | |
| | | 'sanitize' => 'sanitizeUser', |
| | | 'default' => '', |
| | | ], |
| | | 'post' => [ |
| | | 'type' => 'string', |
| | | 'sanitize' => 'sanitizePost', |
| | | 'default' => '', |
| | | ], |
| | | 'repeater' => [ |
| | | 'type' => 'object', |
| | | 'sanitize' => 'sanitizeRepeater', |
| | | 'default' => [], |
| | | ], |
| | | 'tag_list' => [ |
| | | 'taglist' => [ |
| | | 'type' => 'object', |
| | | 'sanitize' => 'sanitizeTagList', |
| | | 'default' => [] |
| | |
| | | 'type' => 'string', |
| | | 'sanitize' => 'sanitize_text_field', |
| | | 'default' => '', |
| | | ] |
| | | ], |
| | | 'selector' => [ |
| | | 'type' => 'string', |
| | | 'sanitize' => 'sanitizeSelector', |
| | | 'default' => '', |
| | | ] |
| | | ]; |
| | | public static function getType(string $field_name):array |
| | | { |
| | |
| | | */ |
| | | public function rowLabelField(): ?string |
| | | { |
| | | return $this->config['row_label'] ?? null; |
| | | return $this->config['add_label'] ?? null; |
| | | } |
| | | } |
| | |
| | | MetaTypeManager::getSanitizeCallback($field_config['type']); |
| | | } |
| | | |
| | | protected static function sanitizeTaxonomy(array|string $values, array $field_config):string |
| | | protected static function sanitizeTaxonomy(string $values, array $field_config):string |
| | | { |
| | | if (!is_array($values)) { |
| | | $values = explode(',', $values); |
| | | } |
| | | $values = array_map('absint', explode(',', $values)); |
| | | |
| | | // Ensure taxonomy starts with BASE |
| | | $taxonomy = (str_starts_with($field_config['taxonomy'], BASE)) |
| | |
| | | return implode(',', $values); |
| | | } |
| | | |
| | | protected static function sanitizeUser(array|string $values, array $field_config):string |
| | | protected static function sanitizeUser(string $values, array $field_config):string |
| | | { |
| | | if (!is_array($values)) { |
| | | $values = explode(',', $values); |
| | | } |
| | | $values = array_map('absint', explode(',', $values)); |
| | | |
| | | $values = array_filter($values, fn($value) => (bool)get_userdata((int)$value)); |
| | | |
| | | return implode(',', $values); |
| | | } |
| | | |
| | | protected static function sanitizePost(string $values, array $config):string |
| | | { |
| | | $values = array_map('absint', explode(',', $values)); |
| | | return implode(',', array_filter($values, fn($value) => (bool)get_post((int)$value))); |
| | | } |
| | | |
| | | protected static function sanitizeTagList(array $values, array $field_config): array |
| | | { |
| | | if (empty(array_filter($values, fn($value) => !empty($value)))) { |
| | |
| | | return $sanitized; |
| | | } |
| | | |
| | | protected static function sanitizeSelector(string $value, array $config):string |
| | | { |
| | | if (array_key_exists('type', $config)) { |
| | | return match ($config['type']) { |
| | | 'user' => self::sanitizeUser($value, $config), |
| | | 'taxonomy'=> self::sanitizeTaxonomy($value, $config), |
| | | 'post' => self::sanitizePost($value, $config), |
| | | }; |
| | | } |
| | | return implode(',',array_map('absint', explode(',',$value))); |
| | | } |
| | | |
| | | protected static function sanitizeUpload(array|string $value):string |
| | | { |
| | | if (empty($value)) { |
| | |
| | | protected function getTermField(Item $item, string $name): mixed |
| | | { |
| | | return match ($name) { |
| | | 'term_name' => get_term_field('name', $item->id), |
| | | 'name' => get_term_field('name', $item->id), |
| | | 'description' => get_term_field('description', $item->id), |
| | | default => '' |
| | | }; |
| | |
| | | use JVBase\registrar\fields\Field; |
| | | use JVBase\registrar\fields\GroupedField; |
| | | use JVBase\registrar\fields\OptionsField; |
| | | use JVBase\registrar\fields\TaxonomyField; |
| | | use JVBase\registrar\fields\Upload; |
| | | use JVBase\registrar\fields\RepeaterField; |
| | | use JVBase\registrar\fields\TagListField; |
| | | use JVBase\registrar\fields\SelectorField; |
| | | use JVBase\registrar\fields\UploadField; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | protected Registrar $registrar; |
| | | |
| | | public function __construct(?string $type = null, ?Registrar $registrar = null) { |
| | | $this->registrar = $registrar; |
| | | switch ($type) { |
| | | case 'post': |
| | | $this->addPostFields(); |
| | |
| | | $this->addUserFields(); |
| | | break; |
| | | } |
| | | $this->registrar = $registrar; |
| | | } |
| | | |
| | | public function addField(string $name, array $config):self { |
| | | $this->fields[$name] = match ($config['type']) { |
| | | 'upload', 'image', 'gallery' => new Upload($name, $config), |
| | | 'upload', 'image', 'gallery' => new UploadField($name, $config), |
| | | 'checkbox', 'radio', 'select', 'set' => new OptionsField($name, $config), |
| | | 'repeater', 'group', 'tagList' => new GroupedField($name, $config), |
| | | 'selector', 'taxonomy', 'user', 'post' => new TaxonomyField($name, $config), |
| | | 'group' => new GroupedField($name, $config), |
| | | 'repeater' => new RepeaterField($name, $config), |
| | | 'tagList' => new TagListField($name, $config), |
| | | 'selector', 'taxonomy', 'user', 'post' => new SelectorField($name, $config), |
| | | default => new Field($name, $config), |
| | | }; |
| | | |
| | |
| | | 'label' => 'Description', |
| | | ] |
| | | ]; |
| | | if ($this->registrar->registrar->hierarchical){ |
| | | if ($this->registrar->args()['hierarchical']??false && $this->registrar->args()['hierarchical'] === true){ |
| | | $fields['parent'] = [ |
| | | 'type' => 'taxonomy', |
| | | 'taxonomy_type' => 'reference', |
| | | 'isReference' => true, |
| | | 'autocomplete' => true, |
| | | 'label' => 'Term Parent' |
| | | ]; |
| | |
| | | { |
| | | return $this->fields; |
| | | } |
| | | |
| | | public function addCommon(string $name):self |
| | | { |
| | | match ($name) { |
| | | 'wiki' => $this->addWikiField(), |
| | | 'links' => $this->addLinksField(), |
| | | 'contact' => $this->addContactField(), |
| | | 'reviews', 'review' => $this->addReviewField(), |
| | | 'alternate_name' => $this->addAlternateName(), |
| | | 'keywords' => $this->addKeywords(), |
| | | default => error_log('[Field]addCommon: No configuration found for '.$name.'.') |
| | | }; |
| | | return $this; |
| | | } |
| | | |
| | | protected function addWikiField():void |
| | | { |
| | | $this->addField( |
| | | 'wiki', |
| | | [ |
| | | 'type' => 'url', |
| | | 'label' => 'Wikipedia Page', |
| | | 'description' => 'For the schema', |
| | | 'quickEdit' => true, |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addLinksField():void |
| | | { |
| | | $this->addField( |
| | | 'links', |
| | | [ |
| | | 'type' => 'repeater', |
| | | 'quickEdit' => true, |
| | | 'add_label' => 'title', |
| | | 'label' => 'Online Links', |
| | | 'description' => 'These are listed publicly on the website', |
| | | 'fields' => [ |
| | | 'url' => [ |
| | | 'type' => 'url', |
| | | 'label' => 'URL', |
| | | ], |
| | | 'title' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Label', |
| | | ], |
| | | 'tracker' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Tracker', |
| | | 'description' => 'If you are set up to track link referrals, add what comes after the ? here.', |
| | | ], |
| | | ], |
| | | 'section' => 'contact' |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addContactField():void |
| | | { |
| | | $this->addField( |
| | | 'admin_contact', |
| | | [ |
| | | 'type' => 'set', |
| | | 'label' => 'Admin Contact', |
| | | 'quickEdit' => true, |
| | | 'options' => [ |
| | | 'text' => 'Text', |
| | | 'call' => 'Call', |
| | | 'email' => 'Email', |
| | | 'insta' => 'Instagram', |
| | | ], |
| | | 'section' => 'contact' |
| | | ] |
| | | ); |
| | | $this->addField( |
| | | 'public_contact', |
| | | [ |
| | | 'type' => 'set', |
| | | 'label' => 'Public Contact', |
| | | 'quickEdit' => true, |
| | | 'options' => [ |
| | | 'text' => 'Text', |
| | | 'call' => 'Call', |
| | | 'email' => 'Email', |
| | | 'insta' => 'Instagram', |
| | | ], |
| | | 'section' => 'contact' |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addReviewField():void |
| | | { |
| | | $this->addField( |
| | | 'reviews', |
| | | [ |
| | | 'type' => 'repeater', |
| | | 'add_label' => 'name', |
| | | 'label' => 'Reviews', |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Reviewer Name', |
| | | ], |
| | | 'review' => [ |
| | | 'type' => 'textarea', |
| | | 'quill' => false, |
| | | 'label' => 'Review', |
| | | ], |
| | | 'rating' => [ |
| | | 'type' => 'select', |
| | | 'label' => 'Rating', |
| | | 'options' => [ |
| | | 'none' => 'Not Given', |
| | | '0.5' => '0.5', |
| | | '1' => '1', |
| | | '1.5' => '1.5', |
| | | '2' => '2', |
| | | '2.5' => '2.5', |
| | | '3' => '3', |
| | | '3.5' => '3.5', |
| | | '4' => '4', |
| | | '4.5' => '4.5', |
| | | '5' => '5', |
| | | ], |
| | | 'default' => 'none' |
| | | ], |
| | | 'date' => [ |
| | | 'type' => 'date', |
| | | 'label' => 'Date of Review', |
| | | ], |
| | | 'url' => [ |
| | | 'type' => 'url', |
| | | 'label' => 'Link to Review (optional)', |
| | | ], |
| | | ], |
| | | 'section' => 'seo' |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addAlternateName():void |
| | | { |
| | | $this->addField( |
| | | 'alternate_name', |
| | | [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Alternate Name', |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Name', |
| | | ] |
| | | ], |
| | | 'section' => 'seo' |
| | | ] |
| | | ); |
| | | } |
| | | protected function addKeywords():void |
| | | { |
| | | $this->addField( |
| | | 'keywords', |
| | | [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Keywords', |
| | | 'fields' => [ |
| | | 'keyword' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Keyword', |
| | | ], |
| | | ], |
| | | 'default' => $labels ?? [ 'Edmonton tattoos', 'Edmonton tattoo artist', 'Edmonton tattooist' ], |
| | | 'section' => 'seo', |
| | | 'quickEdit' => true, |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\registrar; |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\CRUD; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\config\Breadcrumbs; |
| | | use JVBase\registrar\config\Dashboard; |
| | | use JVBase\registrar\config\Directory; |
| | |
| | | use JVBase\registrar\helpers\MakeTrackChanges; |
| | | use JVBase\registrar\helpers\MakeVerification; |
| | | use JVBase\utility\Features; |
| | | use WP_Query; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | |
| | | protected ?string $upload_title = null; |
| | | |
| | | protected int|false $page = false; |
| | | |
| | | protected static array $allFlags = [ |
| | | //Shared Flags |
| | | 'favouritable', 'karma', 'show_feed', 'show_directory', 'approve_new', 'has_responses', 'invitable', |
| | |
| | | return $this->fields; |
| | | } |
| | | |
| | | public function args():array |
| | | { |
| | | return $this->args; |
| | | } |
| | | |
| | | public function setFields():void |
| | | { |
| | | $this->fields = new Fields($this->type, $this); |
| | |
| | | }); |
| | | foreach ($flags as $flag) { |
| | | $this->$flag = true; |
| | | switch ($flag) { |
| | | case 'is_content': |
| | | add_action('init', [$this, 'setupContent'], 20); |
| | | break; |
| | | } |
| | | } |
| | | return $this; |
| | | } |
| | |
| | | |
| | | return $content; |
| | | } |
| | | |
| | | public function setupContent():void |
| | | { |
| | | if (!$this->is_content) return; |
| | | //We need a pseudo-archive page for this content taxonomy. We create a post with the plural name |
| | | $this->page = get_option(BASE.$this->slug.'_archive', false); |
| | | if (!$this->page || !(bool)get_post((int)$this->page)) { |
| | | $exists = new WP_Query([ |
| | | 'post_type' => 'page', |
| | | 'title'=> $this->plural, |
| | | 'posts_per_page' => 1, |
| | | 'post_status' => 'publish', |
| | | 'fields' => 'ids', |
| | | ]); |
| | | if ($exists->have_posts()) { |
| | | $page = $exists->posts[0]; |
| | | } else { |
| | | $page = wp_insert_post([ |
| | | 'post_type' => 'page', |
| | | 'post_title' => $this->plural, |
| | | 'post_content' => '', |
| | | 'post_status' => 'publish', |
| | | ]); |
| | | } |
| | | |
| | | if ($page && !is_wp_error($page)) { |
| | | update_post_meta($page, BASE.'for_type', $this->slug); |
| | | update_option(BASE.$this->slug.'_archive', $page); |
| | | $this->page = $page; |
| | | } |
| | | wp_reset_postdata(); |
| | | } |
| | | |
| | | add_filter('jvb_post_content_output', [$this, 'renderContent'], 20, 2); |
| | | } |
| | | public function renderContent(string $content, array $block):string |
| | | { |
| | | if (!is_page($this->page)) { |
| | | return $content; |
| | | } |
| | | if ($block['blockName'] !== 'core/post-content') { |
| | | return $content; |
| | | } |
| | | if (JVB_TESTING) { |
| | | Cache::for($this->slug)->flush(); |
| | | } |
| | | |
| | | $out = Cache::for($this->slug)->remember( |
| | | get_the_ID(), |
| | | function() { |
| | | |
| | | $items = get_terms([ |
| | | 'taxonomy' => jvbCheckBase($this->slug), |
| | | // 'hide_empty' => true, |
| | | 'fields' => 'ids', |
| | | ]); |
| | | $out = []; |
| | | if ($items && !is_wp_error($items)) { |
| | | foreach ($items as $item) { |
| | | $meta = Meta::forTerm($item); |
| | | $slug = sanitize_title($meta->get('name')); |
| | | $item = sprintf( |
| | | '<li id="%s"><h3><a href="%s">%s</a></h3><p>%s</p><ul>', |
| | | $slug, |
| | | get_term_link($item, jvbCheckBase($this->slug))??'', |
| | | $meta->get('name'), |
| | | $meta->get('description') |
| | | ); |
| | | $postTypes = array_map(function($type) { return jvbCheckBase($type);}, $this->registrar->for); |
| | | $posts = new WP_Query([ |
| | | 'post_type' => $postTypes, |
| | | 'post_status' => 'publish', |
| | | 'posts_per_page' => 3, |
| | | 'fields' => 'ids', |
| | | ]); |
| | | if ($posts->have_posts()) { |
| | | while($posts->have_posts()) { |
| | | $posts->the_post(); |
| | | $ID = get_the_id(); |
| | | $postMeta = Meta::forPost($ID); |
| | | $img = $postMeta->get('post_thumbnail'); |
| | | $img = !empty($img) ? jvbFormatImage((int)$img, 'tiny', 'medium') : ''; |
| | | $item .= sprintf( |
| | | '<li id="%s"><h4><a href="%s">%s</a></h4>%s</li>', |
| | | $slug.'-'.sanitize_title(get_the_title($ID)), |
| | | get_the_permalink($ID), |
| | | $postMeta->get('post_title'), |
| | | $img |
| | | ); |
| | | } |
| | | } |
| | | wp_reset_postdata(); |
| | | $item .= '</ul></li>'; |
| | | $out[] = $item; |
| | | } |
| | | } |
| | | return empty($out) ? '' : '<ul class="content-term-list">'.implode('',$out).'</ul>'; |
| | | } |
| | | ); |
| | | error_log('Built the '.$this->slug.' page content.'); |
| | | return $content . $out; |
| | | } |
| | | } |
| | |
| | | |
| | | public function register():void |
| | | { |
| | | $args = [ |
| | | 'labels' => $this->labels, |
| | | 'public' => $this->public, |
| | | 'hierarchical' => $this->hierarchical, |
| | | ]; |
| | | if (isset($this->description)) { |
| | | $args['description'] = $this->description; |
| | | } |
| | | if (isset($this->publicly_queryable)) { |
| | | $args['publicly_queryable'] = $this->publicly_queryable; |
| | | } |
| | | if (isset($this->show_ui)) { |
| | | $args['show_ui'] = $this->show_ui; |
| | | } |
| | | if (isset($this->show_in_menu)) { |
| | | $args['show_in_menu'] = $this->show_in_menu; |
| | | } |
| | | if (isset($this->show_in_nav_menus)) { |
| | | $args['show_in_nav_menus'] = $this->show_in_nav_menus; |
| | | } |
| | | if (isset($this->show_in_rest)) { |
| | | $args['show_in_rest'] = $this->show_in_rest; |
| | | } |
| | | if (isset($this->rest_base)) { |
| | | $args['rest_base'] = $this->rest_base; |
| | | } |
| | | if (isset($this->rest_namespace)) { |
| | | $args['rest_namespace'] = $this->rest_namespace; |
| | | } |
| | | if (isset($this->rest_controller_class)) { |
| | | $args['rest_controller_class'] = $this->rest_controller_class; |
| | | } |
| | | if (isset($this->show_tag_cloud)) { |
| | | $args['show_tag_cloud'] = $this->show_tag_cloud; |
| | | } |
| | | if (isset($this->show_quick_edit)) { |
| | | $args['show_quick_edit'] = $this->show_quick_edit; |
| | | } |
| | | if (isset($this->show_admin_column)) { |
| | | $args['show_admin_column'] = $this->show_admin_column; |
| | | } |
| | | if (isset($this->meta_box_cb) && is_callable($this->meta_box_cb)) { |
| | | $args['meta_box_cb'] = $this->meta_box_cb; |
| | | } |
| | | if (isset($this->meta_box_sanitize_cb) && is_callable($this->meta_box_sanitize_cb)) { |
| | | $args['meta_box_sanitize_cb'] = $this->meta_box_sanitize_cb; |
| | | } |
| | | if (isset($this->capabilities)) { |
| | | $allowed = ['manage_terms', 'edit_terms', 'delete_terms', 'assign_terms']; |
| | | $caps = array_filter($this->capabilities, function ($cap) use ($allowed) { |
| | | return in_array($cap, $allowed); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | $args['capabilities'] = $caps; |
| | | } |
| | | if (isset($this->query_var)) { |
| | | $args['query_var'] = $this->query_var; |
| | | } |
| | | if (isset($this->update_count_callback) && is_callable($this->update_count_callback)) { |
| | | $args['update_count_callback'] = $this->update_count_callback; |
| | | } |
| | | if (isset($this->default_term)) { |
| | | $args['default_term'] = $this->default_term; |
| | | } |
| | | if (isset($this->sort)) { |
| | | $args['sort'] = $this->sort; |
| | | } |
| | | if (isset($this->args)) { |
| | | $args['args'] = $this->args; |
| | | } |
| | | |
| | | $args = array_filter(get_object_vars($this)); |
| | | // $args = [ |
| | | // 'labels' => $this->labels, |
| | | // 'public' => $this->public, |
| | | // 'hierarchical' => $this->hierarchical, |
| | | // ]; |
| | | // if (isset($this->description)) { |
| | | // $args['description'] = $this->description; |
| | | // } |
| | | // if (isset($this->publicly_queryable)) { |
| | | // $args['publicly_queryable'] = $this->publicly_queryable; |
| | | // } |
| | | // if (isset($this->show_ui)) { |
| | | // $args['show_ui'] = $this->show_ui; |
| | | // } |
| | | // if (isset($this->show_in_menu)) { |
| | | // $args['show_in_menu'] = $this->show_in_menu; |
| | | // } |
| | | // if (isset($this->show_in_nav_menus)) { |
| | | // $args['show_in_nav_menus'] = $this->show_in_nav_menus; |
| | | // } |
| | | // if (isset($this->show_in_rest)) { |
| | | // $args['show_in_rest'] = $this->show_in_rest; |
| | | // } |
| | | // if (isset($this->rest_base)) { |
| | | // $args['rest_base'] = $this->rest_base; |
| | | // } |
| | | // if (isset($this->rest_namespace)) { |
| | | // $args['rest_namespace'] = $this->rest_namespace; |
| | | // } |
| | | // if (isset($this->rest_controller_class)) { |
| | | // $args['rest_controller_class'] = $this->rest_controller_class; |
| | | // } |
| | | // if (isset($this->show_tag_cloud)) { |
| | | // $args['show_tag_cloud'] = $this->show_tag_cloud; |
| | | // } |
| | | // if (isset($this->show_quick_edit)) { |
| | | // $args['show_quick_edit'] = $this->show_quick_edit; |
| | | // } |
| | | // if (isset($this->show_admin_column)) { |
| | | // $args['show_admin_column'] = $this->show_admin_column; |
| | | // } |
| | | // if (isset($this->meta_box_cb) && is_callable($this->meta_box_cb)) { |
| | | // $args['meta_box_cb'] = $this->meta_box_cb; |
| | | // } |
| | | // if (isset($this->meta_box_sanitize_cb) && is_callable($this->meta_box_sanitize_cb)) { |
| | | // $args['meta_box_sanitize_cb'] = $this->meta_box_sanitize_cb; |
| | | // } |
| | | // if (isset($this->capabilities)) { |
| | | // $allowed = ['manage_terms', 'edit_terms', 'delete_terms', 'assign_terms']; |
| | | // $caps = array_filter($this->capabilities, function ($cap) use ($allowed) { |
| | | // return in_array($cap, $allowed); |
| | | // }, ARRAY_FILTER_USE_KEY); |
| | | // $args['capabilities'] = $caps; |
| | | // } |
| | | // if (isset($this->query_var)) { |
| | | // $args['query_var'] = $this->query_var; |
| | | // } |
| | | // if (isset($this->update_count_callback) && is_callable($this->update_count_callback)) { |
| | | // $args['update_count_callback'] = $this->update_count_callback; |
| | | // } |
| | | // if (isset($this->default_term)) { |
| | | // $args['default_term'] = $this->default_term; |
| | | // } |
| | | // if (isset($this->sort)) { |
| | | // $args['sort'] = $this->sort; |
| | | // } |
| | | // if (isset($this->args)) { |
| | | // $args['args'] = $this->args; |
| | | // } |
| | | unset($args['for']); |
| | | |
| | | $for = array_map(function($item) { return jvbCheckBase($item);}, $this->for); |
| | | register_taxonomy(jvbCheckBase($this->taxonomy), $for, $args); |
| | |
| | | |
| | | require(JVB_DIR . '/inc/registrar/fields/Field.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/GroupedField.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/RepeaterField.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/TagListField.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/OptionsField.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/TaxonomyField.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/Upload.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/SelectorField.php'); |
| | | require(JVB_DIR . '/inc/registrar/fields/UploadField.php'); |
| | | |
| | | require(JVB_DIR . '/inc/registrar/helpers/MakeCalendarType.php'); |
| | | require(JVB_DIR . '/inc/registrar/helpers/MakeTrackChanges.php'); |
| | |
| | | |
| | | protected function initSchema():void |
| | | { |
| | | if (!array_key_exists('type', $this->config['schema'])){ |
| | | if (!array_key_exists('type', $this->config['schema'])) { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | if ($registrar) { |
| | | switch ($registrar->getType()) { |
| | | case 'term': |
| | | $this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | break; |
| | | case 'post': |
| | | $this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork'; |
| | | break; |
| | | case 'user': |
| | | $this->config['schema']['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ProfilePage'; |
| | | break; |
| | | } |
| | | |
| | | } |
| | | } |
| | | if (!array_key_exists('type', $this->config['schema'])) { |
| | | error_log('Missing schema type'); |
| | | return; |
| | | } |
| | |
| | | function jvbTSFDoIt(string $slug, ?array $args):bool |
| | | { |
| | | $tsf = tsf(); |
| | | |
| | | return (null === $args) |
| | | ? $tsf->query()->is_singular() |
| | | && BASE.$slug === $tsf->query()->get_current_post_type() |
| | | && jvbCheckBase($slug) === $tsf->query()->get_current_post_type() |
| | | : 'single' === The_SEO_Framework\get_query_type_from_args( $args ) |
| | | && BASE.$slug === $tsf->query()->get_post_type_real_id( $args['id'] ); |
| | | && jvbCheckBase($slug) === $tsf->query()->get_post_type_real_id( $args['id'] ); |
| | | } |
| | | |
| | | function jvbTSFGetID(?array $args):int |
| | |
| | | use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject; |
| | | use JVBase\managers\SEO\render\Thing\Intangible\Quantity\Distance; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class Resolver { |
| | | protected static Meta $meta; |
| | | protected static ?Meta $meta; |
| | | |
| | | public static function resolve(string $template, Meta $meta): string |
| | | public static function resolve(string $template, ?Meta $meta): string |
| | | { |
| | | self::$meta = $meta; |
| | | return preg_replace_callback( |
| | |
| | | $template |
| | | ); |
| | | } |
| | | protected static function resolveVariable(string $variable, Meta $meta): string |
| | | protected static function resolveVariable(string $variable, ?Meta $meta = null): mixed |
| | | { |
| | | $variable = trim($variable); |
| | | |
| | | switch ($variable) { |
| | | case 'CREATOR': |
| | | return JVB()->seo()->getCreator(); |
| | | break; |
| | | } |
| | | if (str_contains($variable, '.')) { |
| | | return self::resolveRelation($variable, $meta); |
| | | } |
| | | if ($variable === 'post_permalink') { |
| | | if ($meta && $variable === 'post_permalink') { |
| | | return get_the_permalink($meta->id()); |
| | | } |
| | | |
| | | if (!$meta) { |
| | | return ''; |
| | | } |
| | | $config = $meta->config($variable); |
| | | if (!$config) { |
| | | error_log('[SEO]Meta Resolver. Could not find meta configuration for variable: '.$variable); |
| | |
| | | }; |
| | | } |
| | | |
| | | protected static function resolveRelation(string $path, Meta $meta): string |
| | | protected static function resolveRelation(string $path, ?Meta $meta = null): string |
| | | { |
| | | $parts = explode('.', $path); |
| | | $relation = array_shift($parts); |
| | |
| | | |
| | | //We need to: |
| | | // 1) Get the id of the item we're fetching (meta value of the $relation) |
| | | if ($relation === 'registrar') { |
| | | if (count($parts) === 2) { |
| | | return self::resolveRegistrar($parts[0], $parts[1]); |
| | | } else { |
| | | error_log('[Resolver]::resolveRelation: Registrar relation requires registrar.[slug].[property]'); |
| | | return ''; |
| | | } |
| | | |
| | | } |
| | | $ID = $meta->get($relation); |
| | | if (!$ID || $ID === '') { |
| | | return ''; |
| | |
| | | return self::resolve($field, $newMeta); |
| | | } |
| | | |
| | | protected static function resolveImage(string $variable, Meta $meta, bool $returnID = false): string |
| | | protected static function resolveImage(string $variable, ?Meta $meta, bool $returnID = false): string |
| | | { |
| | | $imgID = $meta->get($variable); |
| | | if (!$imgID || $imgID === '') { |
| | |
| | | } |
| | | return $image[0]; |
| | | } |
| | | protected static function resolveObject(string $variable, Meta $meta): string |
| | | protected static function resolveObject(string $variable, ?Meta $meta): string |
| | | { |
| | | //Hmmm... this should already be handled by dot notation. |
| | | return ''; |
| | |
| | | * We need to map the values to what schema.org expects. |
| | | * Most are defined in the JVBase\managers\schema\render namespace. |
| | | */ |
| | | public static function resolveForSchema(string $property, string $value, mixed $schema, Meta $meta):mixed |
| | | public static function resolveForSchema(string $property, string $value, mixed $schema, ?Meta $meta = null):mixed |
| | | { |
| | | $check = 'resolve'.ucfirst($property).'Property'; |
| | | if (method_exists(self::class, $check)) { |
| | |
| | | return self::resolve($value, $meta); |
| | | } |
| | | |
| | | public static function checkPropertyType(string $property, mixed $value, mixed $schema, Meta $meta):mixed |
| | | public static function checkPropertyType(string $property, mixed $value, mixed $schema, ?Meta $meta):mixed |
| | | { |
| | | return match($property) { |
| | | 'logo', 'image', 'photo', 'primaryImageOfPage', 'thumbnail', 'associatedMedia' => self::resolveImageProperty($property, $value, $schema, $meta), |
| | | 'creator' => self::resolveCreator($property, $value, $schema, $meta), |
| | | default => false |
| | | }; |
| | | |
| | | } |
| | | |
| | | public static function resolveImageProperty(string $property, mixed $value, mixed $schema, Meta $meta):?ImageObject |
| | | public static function resolveImageProperty(string $property, mixed $value, mixed $schema, ?Meta $meta):?ImageObject |
| | | { |
| | | if (!$meta) { |
| | | return null; |
| | | } |
| | | $value = str_replace('{{', '', str_replace('}}', '', $value)); |
| | | $imgID = $meta->get($value); |
| | | error_log('Got image id: '.print_r($imgID, true)); |
| | |
| | | if (!$img) { |
| | | return null; |
| | | } |
| | | Cache::for('imageSchemaObject')->flush(); |
| | | if (JVB_TESTING){ |
| | | Cache::for('imageSchemaObject')->flush(); |
| | | } |
| | | |
| | | return Cache::for('imageSchemaObject')->connect('post')->remember( |
| | | $imgID, |
| | | function () use ($imgID, $img) { |
| | |
| | | ); |
| | | |
| | | } |
| | | |
| | | public static function resolveCreator(string $type, mixed $value, mixed $schema, ?Meta $meta):mixed |
| | | { |
| | | if (is_numeric($value)) { |
| | | //TODO generate from id |
| | | |
| | | } else if (str_contains($value, 'CREATOR')) { |
| | | return JVB()->seo()->getCreator(); |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | public static function resolveRegistrar(string $type, string $property):string |
| | | { |
| | | $registrar = Registrar::getInstance($type); |
| | | if (!$registrar) { |
| | | return ''; |
| | | } |
| | | $method = 'get'.ucfirst($property); |
| | | if (!method_exists($registrar, $method)) { |
| | | error_log('[Resolver]::resolveRegistrar: Invalid property getter: '.$property); |
| | | return ''; |
| | | } |
| | | return $registrar->$method(); |
| | | } |
| | | } |
| | |
| | | 'description' => '{{post_excerpt}}', |
| | | ]; |
| | | |
| | | protected array $defaultArchive = []; |
| | | protected array $defaultArchive = [ |
| | | 'type' => 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage', |
| | | 'title' => '{{registrar.plural}}' |
| | | ]; |
| | | |
| | | public function __construct(string $slug, string $type) |
| | | { |
| | |
| | | $this->cache->connect($registrar->getType()); |
| | | $this->referenceCache->connect($registrar->getType()); |
| | | $this->archiveCache->connect($registrar->getType()); |
| | | |
| | | switch ($registrar->getType()) { |
| | | case 'term': |
| | | $this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | break; |
| | | case 'user': |
| | | $this->defaultSchema['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\ProfilePage'; |
| | | break; |
| | | } |
| | | $this->defaultArchive['description'] = '{{registrar.'.$slug.'.description}}'; |
| | | } |
| | | $this->initFilters(); |
| | | $this->registerHooks(); |
| | |
| | | { |
| | | add_action('wp_head', [$this, 'outputSchema'], 1); |
| | | add_filter('the_seo_framework_schema_graph_data', [$this, 'filterTSFSchema'], 10, 2); |
| | | add_filter('the_seo_framework_title_from_custom_field', [$this, 'filterTSFOGTitle'], 10, 2); |
| | | |
| | | $this->maybeExcludeSingles(); |
| | | } |
| | | protected function maybeExcludeSingles(): void |
| | |
| | | } |
| | | public function filterTSFSchema(array $graph, ?array $args):array |
| | | { |
| | | if (jvbTSFDoIt($this->slug, $args)){ |
| | | $based = jvbCheckBase($this->slug); |
| | | if (is_front_page() || is_singular($based) || is_post_type_archive($based) || is_tax($based)) { |
| | | return []; |
| | | } |
| | | |
| | | // if (jvbTSFDoIt($this->slug, $args)){ |
| | | // return []; |
| | | // } |
| | | return $graph; |
| | | } |
| | | |
| | | public function outputSchema():void |
| | | { |
| | | $registrar = Registrar::getInstance($this->slug); |
| | | if (is_singular()) { |
| | | $this->outputSingularSchema(); |
| | | } elseif (is_post_type_archive(jvbCheckBase($this->slug) || is_tax(jvbCheckBase($this->slug)))) { |
| | | $this->outputArchiveSchema(); |
| | | } if ($registrar && $registrar->hasFeature('is_content') && is_single(get_option(BASE.$this->slug.'_archive'))) { |
| | | $this->outputContentTaxArchiveSchema(); |
| | | } |
| | | } |
| | | public function outputSingularSchema():array |
| | | { |
| | | $ID = get_the_ID(); |
| | | $this->cache->flush(); |
| | | if (JVB_TESTING){ |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | return $this->cache->remember( |
| | | $ID, |
| | | function () use ($ID) { |
| | | $meta = Meta::forPost($ID); |
| | | $config = $this->getConfig(); |
| | | |
| | | $class = new $config['type'](); |
| | | unset($config['type']); |
| | | foreach ($config as $property => $value){ |
| | | $value = Resolver::resolveForSchema($property, $value, $config, $meta); |
| | | $method = 'set'.ucfirst($property); |
| | | $class->$method($value); |
| | | } |
| | | $class = $this->classFromConfig($config, $meta); |
| | | |
| | | $class->setAuthor(JVB()->seo()->getCreator(true)); |
| | | $schema = $class->outputSchema(); |
| | | error_log('Generated archive schema: '.print_r($schema, true)); |
| | | return $schema; |
| | | return $class->outputSchema(); |
| | | } |
| | | ); |
| | | |
| | | } |
| | | |
| | | public function outputContentTaxArchiveSchema():array |
| | | { |
| | | $ID = get_the_ID(); |
| | | if (JVB_TESTING) { |
| | | $this->cache->flush(); |
| | | } |
| | | return $this->cache->remember( |
| | | $ID, |
| | | function() use ($ID) { |
| | | $action = BASE.ucfirst($this->slug).'Schema'; |
| | | $config = get_option($action, apply_filters($action, $this->defaultSchema)); |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | } |
| | | if (!class_exists($config['type'])) { |
| | | error_log('No class found for archive schema output: '.$config['type']); |
| | | return []; |
| | | } |
| | | $class = $this->classFromConfig($config); |
| | | |
| | | $class->setIsPartOf(get_home_url().'/#website'); |
| | | $itemList = new render\Thing\Intangible\ItemList\ItemList(); |
| | | $items = get_terms([ |
| | | 'taxonomy' => jvbCheckBase($this->slug), |
| | | // 'hide_empty' => true, |
| | | 'fields' => 'ids', |
| | | ]); |
| | | |
| | | $pos = 1; |
| | | $itemListItems = []; |
| | | foreach ($items as $ID) { |
| | | $item = $this->outputReferenceSchema($ID, 'term',false); |
| | | $listItem = new render\Thing\Intangible\ListItem(); |
| | | $listItem->setPosition($pos); |
| | | $listItem->setItem($item); |
| | | $itemListItems[] = $listItem; |
| | | $pos++; |
| | | } |
| | | wp_reset_postdata(); |
| | | $itemList->setItemListElement($itemListItems); |
| | | $class->setMainEntity($itemList); |
| | | |
| | | return $class->outputSchema(); |
| | | } |
| | | ); |
| | | } |
| | | |
| | | public function outputArchiveSchema():array |
| | | { |
| | | $this->archiveCache->flush(); |
| | | if (JVB_TESTING){ |
| | | $this->archiveCache->flush(); |
| | | } |
| | | |
| | | return $this->archiveCache->remember( |
| | | $this->slug, |
| | | function() { |
| | | $action = BASE.ucfirst($this->slug).'Archive'; |
| | | $config = get_option($action, apply_filters($action, $this->defaultArchive)); |
| | | |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = 'JVBase\managers\SEO\render\Thing\CreativeWork\WebPage\CollectionPage'; |
| | | } |
| | | if (!class_exists($config['type'])) { |
| | | error_log('No class found for archive schema output: '.$config['type']); |
| | | return []; |
| | | } |
| | | $obj = get_queried_object(); |
| | | $meta = (property_exists($obj, 'taxonomy')) ? Meta::forTerm($obj->term_id) : null; |
| | | |
| | | $class = new $config['type']; |
| | | unset($config['type']); |
| | | foreach ($config as $property=>$value) { |
| | | $method = 'set'.ucfirst($property); |
| | | $class->$method($value); |
| | | } |
| | | $class = $this->classFromConfig($config, $meta); |
| | | |
| | | $class->setIsPartOf(get_home_url().'/#website'); |
| | | $itemList = new render\Thing\Intangible\ItemList\ItemList(); |
| | | $items = new WP_Query([ |
| | |
| | | $pos = 1; |
| | | $itemListItems = []; |
| | | foreach ($items->posts as $ID) { |
| | | $item = $this->outputReferenceSchema($ID, false); |
| | | $item = $this->outputReferenceSchema($ID, 'post',false); |
| | | $listItem = new render\Thing\Intangible\ListItem(); |
| | | $listItem->setPosition($pos); |
| | | $listItem->setItem($item); |
| | |
| | | ); |
| | | } |
| | | |
| | | public function outputReferenceSchema(int $ID, bool $outputSchema = true):mixed |
| | | public function outputReferenceSchema(int $ID, string $type, bool $outputSchema = true):mixed |
| | | { |
| | | $this->referenceCache->flush(); |
| | | if (JVB_TESTING){ |
| | | $this->referenceCache->flush(); |
| | | } |
| | | |
| | | $cached = $this->referenceCache->remember( |
| | | $ID, |
| | | function () use ($ID) { |
| | | $meta = Meta::forPost($ID); |
| | | $config = $this->getConfig(); |
| | | $class = new $config['type'](); |
| | | $class->setId(get_the_permalink($ID).'/#'.$class->getTypeName()); |
| | | foreach ($this->referenceProperties as $property => $value){ |
| | | $value = Resolver::resolveForSchema($property, $value, $this->schema, $meta); |
| | | $method = 'set'.ucfirst($property); |
| | | $class->$method($value); |
| | | function () use ($ID, $type) { |
| | | switch ($type) { |
| | | case 'post': |
| | | $meta = Meta::forPost($ID); |
| | | break; |
| | | case 'term': |
| | | $meta = Meta::forTerm($ID); |
| | | break; |
| | | case 'user': |
| | | $meta = Meta::forUser($ID); |
| | | break; |
| | | default: |
| | | error_log('Invalid type used for reference: '.print_r($type, true)); |
| | | $meta = null; |
| | | } |
| | | $config = $this->getConfig('archive'); |
| | | $class = $this->classFromConfig($config, $meta); |
| | | $class->delete('about'); |
| | | |
| | | switch ($type) { |
| | | case 'post': |
| | | $class->setId(get_the_permalink($ID).'#'.$class->getTypeName()); |
| | | break; |
| | | case 'term': |
| | | $class->setId(get_term_link($ID).'#'.$class->getTypeName()); |
| | | break; |
| | | } |
| | | |
| | | $schema = $class->outputSchema(); |
| | | error_log('Generated archive schema: '.print_r($schema, true)); |
| | | |
| | | return $class; |
| | | } |
| | | ); |
| | |
| | | } |
| | | public function defineReference(string $property, string $value):void |
| | | { |
| | | $class = $this->getConfig('schema')['type']; |
| | | $config = $this->getConfig(); |
| | | if (!array_key_exists('type', $config)) { |
| | | $config['type'] = $this->defaultSchema['type']; |
| | | update_option(BASE.ucfirst($this->slug).'Schema', $config); |
| | | } |
| | | $class = $this->getConfig()['type']; |
| | | if (!class_exists($class)) { |
| | | error_log('[SEO]Schema::defineReference Class not found: '.$class); |
| | | return; |
| | |
| | | $this->defineReference($property, $value); |
| | | } |
| | | } |
| | | |
| | | public function filterTSFOGTitle(string $title, ?array $args):string{ |
| | | $based = jvbCheckBase($this->slug); |
| | | |
| | | if (is_singular($based)){ |
| | | $config = $this->getConfig('meta'); |
| | | $meta = Meta::forPost(get_the_ID()); |
| | | $title = Resolver::resolve($config['name'], $meta); |
| | | } elseif (is_post_type_archive($based) || is_tax($based)) { |
| | | $config = $this->getConfig('archive'); |
| | | $title = $config['name']; |
| | | } |
| | | return $title; |
| | | } |
| | | |
| | | protected function classFromConfig(array $config, ?Meta $meta = null):mixed |
| | | { |
| | | if (!array_key_exists('type', $config)) { |
| | | error_log('[Schema]::classFromConfig No class defined in config: '.print_r($config, true)); |
| | | return false; |
| | | } |
| | | $className = $config['type']; |
| | | unset($config['type']); |
| | | $class = new $className(); |
| | | |
| | | foreach ($config as $property=>$value) { |
| | | if (is_array ($value)) { |
| | | $value = $this->classFromConfig($value, $meta); |
| | | } |
| | | $method = 'set'.ucfirst($property); |
| | | if (!method_exists($class, $method)) { |
| | | error_log('[Schema]::classFromConfig - method: '.$method.' does not exist in class: '.$className); |
| | | continue; |
| | | } |
| | | if (is_string($value) && str_contains($value, '{{')) { |
| | | $value = Resolver::resolveForSchema($property, $value, $config, $meta); |
| | | } |
| | | if (!empty($value)) { |
| | | $class->$method($value); |
| | | } |
| | | } |
| | | return $class; |
| | | } |
| | | } |
| | |
| | | return; |
| | | } |
| | | } |
| | | $current = get_class($this); |
| | | $class = match($config['type']) { |
| | | 'group' => ($current !== GroupedField::class) ? new GroupedField($name, $config) : $this, |
| | | 'select', 'radio', 'checkbox' => ($current !== OptionsField::class) ? new OptionsField($name, $config) : $this, |
| | | 'repeater' => ($current !== RepeaterField::class) ? new RepeaterField($name, $config) : $this, |
| | | 'taglist' => ($current !== TagListField::class) ? new TagListField($name, $config) : $this, |
| | | 'taxonomy', 'post', 'user', 'selector' => ($current !== SelectorField::class) ? new SelectorField($name, $config) : $this, |
| | | 'upload' => ($current !== UploadField::class) ? new UploadField($name, $config) : $this, |
| | | default => $this |
| | | }; |
| | | |
| | | foreach ($config as $key => $value) { |
| | | if (property_exists($this, $key)) { |
| | | if (property_exists($class, $key)) { |
| | | $method = 'set' . ucfirst($key); |
| | | $this->$method($value); |
| | | $class->$method($value); |
| | | } else { |
| | | error_log('Instance: '.print_r($this, true)); |
| | | error_log('Instance: '.print_r($class, true)); |
| | | error_log('[JVBase\registrar\Field] Invalid key for '.$name.': '.$key); |
| | | } |
| | | } |
| | |
| | | { |
| | | foreach ($fields as $name => $config) { |
| | | $this->fields[$name] = match ($config['type']) { |
| | | 'upload', 'image', 'gallery' => new Upload($name, $config), |
| | | 'upload', 'image', 'gallery' => new UploadField($name, $config), |
| | | 'checkbox', 'radio', 'select', 'set' => new OptionsField($name, $config), |
| | | 'repeater', 'group', 'tagList' => new GroupedField($name, $config), |
| | | 'selector', 'taxonomy', 'user', 'post' => new TaxonomyField($name, $config), |
| | | 'repeater' => new RepeaterField($name, $config), |
| | | 'tagList' => new TagListField($name, $config), |
| | | 'group' => new GroupedField($name, $config), |
| | | 'selector', 'taxonomy', 'user', 'post' => new SelectorField($name, $config), |
| | | default => new Field($name, $config), |
| | | }; |
| | | } |
| | | } |
| | | public function getFields():array |
| | | { |
| | | return $this->fields; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\registrar\fields; |
| | | |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class RepeaterField extends GroupedField { |
| | | protected array $fields = []; |
| | | protected string $add_label = 'Add Row'; |
| | | protected string $row_label = 'New Item'; |
| | | |
| | | public function setAdd_label(string $label):void |
| | | { |
| | | $this->add_label = $label; |
| | | } |
| | | public function getAdd_label():string |
| | | { |
| | | return $this->add_label; |
| | | } |
| | | protected function checkFieldVariables(string $variables):bool |
| | | { |
| | | if (!str_contains($variables, '{{')) { |
| | | return false; |
| | | } |
| | | $fields = array_filter(array_map(function($field) { return str_replace('}}','',$field);}, explode('{{',$variables))); |
| | | foreach ($fields as $field) { |
| | | if (!array_key_exists($field, $this->fields)) { |
| | | error_log('['.(new \ReflectionClass($this))->getShortName().']Invalid field attempted: '.$field); |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | public function setRow_label(string $label):void |
| | | { |
| | | if (str_contains($label, '{{')) { |
| | | if (!$this->checkFieldVariables($label)) { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | $this->row_label = $label; |
| | | } |
| | | public function getRow_label():string |
| | | { |
| | | return $this->row_label; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\registrar\fields; |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class SelectorField extends Field { |
| | | protected string $taxonomy; |
| | | protected string $post_type; |
| | | protected string $user_role; |
| | | protected int $max = 0; |
| | | protected bool $search = true; |
| | | protected bool $createNew = false; |
| | | protected bool $autocomplete = true; |
| | | /** |
| | | * @var bool isReference If true, does not set default taxonomy functionality, but is a reference to the selected items. (example: if it's a taxonomy subtype, does not set_object_terms with the selection) |
| | | */ |
| | | protected bool $isReference = false; |
| | | protected bool $update = true; |
| | | protected string $subtype; |
| | | protected array $allowedSubtype = ['taxonomy', 'user', 'post']; |
| | | |
| | | public function setTaxonomy(string $taxonomy):void |
| | | { |
| | | $allowed = array_merge(['tag', 'category'], Registrar::getRegistered('term')); |
| | | if (!in_array(jvbNoBase($taxonomy), $allowed)) { |
| | | error_log('[SelectorField]Attempted taxonomy not allowed: '.$taxonomy); |
| | | return; |
| | | } |
| | | $this->taxonomy = $taxonomy; |
| | | } |
| | | public function getTaxonomy():string { |
| | | return $this->taxonomy; |
| | | } |
| | | |
| | | public function setPost_type(string $post_type):void |
| | | { |
| | | $allowed = array_merge(['post', 'page'], Registrar::getRegistered('post')); |
| | | if (!in_array(jvbNoBase($post_type), $allowed)) { |
| | | error_log('[SelectorField]Attempted post type not allowed: '.$post_type); |
| | | return; |
| | | } |
| | | $this->post_type = $post_type; |
| | | } |
| | | public function getPost_type():string { |
| | | return $this->post_type; |
| | | } |
| | | |
| | | public function setUser_role(string $user_role):void |
| | | { |
| | | $allowed = array_merge(['administrator'], Registrar::getRegistered('user')); |
| | | if (!in_array(jvbNoBase($user_role), $allowed)) { |
| | | error_log('[SelectorField]Attempted user not allowed: '.$user_role); |
| | | return; |
| | | } |
| | | $this->user_role = $user_role; |
| | | } |
| | | public function getUser_role():string { |
| | | return $this->user_role; |
| | | } |
| | | |
| | | public function setMax(int $max):void |
| | | { |
| | | $this->max = $max; |
| | | } |
| | | public function getMax():int |
| | | { |
| | | return $this->max; |
| | | } |
| | | public function setSearch(bool $search):void { |
| | | $this->search = $search; |
| | | } |
| | | public function getSearch():bool |
| | | { |
| | | return $this->search; |
| | | } |
| | | public function setCreateNew(bool $createNew):void |
| | | { |
| | | $this->createNew = $createNew; |
| | | } |
| | | public function getCreateNew():bool |
| | | { |
| | | return $this->createNew; |
| | | } |
| | | public function setAutocomplete(bool $autocomplete):void |
| | | { |
| | | $this->autocomplete = $autocomplete; |
| | | } |
| | | public function getAutocomplete():bool |
| | | { |
| | | return $this->autocomplete; |
| | | } |
| | | public function setUpdate(bool $update):void |
| | | { |
| | | $this->update = $update; |
| | | } |
| | | public function getUpdate():bool |
| | | { |
| | | return $this->update; |
| | | } |
| | | public function setSubtype(string $subtype):void |
| | | { |
| | | if (!in_array($subtype, $this->allowedSubtype)) { |
| | | error_log('[SelectorField]Attempted subtype not allowed: '.$subtype); |
| | | return; |
| | | } |
| | | $this->subtype = $subtype; |
| | | } |
| | | public function getSubtype():string |
| | | { |
| | | return $this->subtype; |
| | | } |
| | | |
| | | public function setIsReference(bool $isReference):void |
| | | { |
| | | $this->isReference = $isReference; |
| | | } |
| | | public function getIsReference():bool |
| | | { |
| | | return $this->isReference; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\registrar\fields; |
| | | |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class TagListField extends RepeaterField { |
| | | protected string $tag_format = 'first_field'; |
| | | protected string $add_label = 'Add'; |
| | | |
| | | public function setTag_format(string $tag_format):void |
| | | { |
| | | if (str_contains($tag_format, '{{')) { |
| | | if (!$this->checkFieldVariables($tag_format)) { |
| | | return; |
| | | } |
| | | $this->tag_format = $tag_format; |
| | | return; |
| | | } |
| | | |
| | | if (!in_array($tag_format, ['first_field', 'all_fields'])) { |
| | | error_log('[TagListField]Could not validate tag format: '.$tag_format); |
| | | return; |
| | | } |
| | | $this->tag_format = $tag_format; |
| | | } |
| | | public function getTag_format():string |
| | | { |
| | | return $this->tag_format; |
| | | } |
| | | } |
| File was renamed from inc/registrar/fields/Upload.php |
| | |
| | | exit; |
| | | } |
| | | |
| | | class Upload extends Field { |
| | | class UploadField extends Field { |
| | | protected bool $multiple = false; |
| | | protected string $subtype; |
| | | |
| | |
| | | // Run query |
| | | $query = new WP_Query($args); |
| | | |
| | | $registrar = Registrar::getInstance(str_replace('-', '_', $this->post_type)); |
| | | $this->fields = $registrar->getFields()??[]; |
| | | |
| | | $this->fields = Registrar::getFieldsFor(str_replace('-', '_', $this->post_type)); |
| | | $this->taxonomies = $this->getTaxonomies($this->post_type); |
| | | $posts = array_map([$this, 'prepareItem'], $query->posts); |
| | | |
| | |
| | | $get = []; |
| | | $fields = (empty($fields)) ? $this->fields : $fields; |
| | | $get = $this->getUploadFields($fields); |
| | | |
| | | error_log('Upload fields: '.print_r($get, true)); |
| | | |
| | | if (!empty($get)) { |
| | | $actualGet = array_map(function($fieldName) { |
| | |
| | | */ |
| | | public function addTaxonomyFilter(array $taxonomies, ?string $limit = null): self { |
| | | foreach($taxonomies as $taxonomy) { |
| | | error_log('Fetchinig taxonomy: '.print_r($taxonomy, true)); |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | $this->taxonomies[$taxonomy] = [ |
| | | 'type' => 'taxonomy', |
| | | 'taxonomy'=> $taxonomy, |
| | | 'limit' => $limit, |
| | | 'label' => $registrar->getPlural(), |
| | | 'icon' => $registrar->getIcon() |
| | | ]; |
| | | |
| | | if ($registrar) { |
| | | $this->taxonomies[$taxonomy] = [ |
| | | 'type' => 'taxonomy', |
| | | 'taxonomy'=> $taxonomy, |
| | | 'limit' => $limit, |
| | | 'label' => $registrar->getPlural(), |
| | | 'icon' => $registrar->getIcon() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | return $this; |
| | |
| | | <option value="<?=$control?>"<?=$disabled?>><?=$label?></option> |
| | | <?php |
| | | } |
| | | foreach ($this->taxonomies as $taxonomy) { |
| | | |
| | | foreach ($this->taxonomies as $taxonomy =>$config) { |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | if (!$registrar) continue; |
| | | ?> |
| | |
| | | $section = (array_key_exists('section', $config)) ? $config['section'] : 'basic'; |
| | | $tabs[$section]['content'] .= Form::render($n, '', $config); |
| | | } else { |
| | | jvbDump($config, $n); |
| | | echo Form::render($n, '', $config); |
| | | } |
| | | } |
| | |
| | | 'section' => 'newsletter', |
| | | ], |
| | | 'owner_of' => [ |
| | | 'type' => 'taxonomy', |
| | | 'type' => 'selector', |
| | | 'subtype'=> 'taxonomy', |
| | | 'label' => __('Owner of', 'jvb'), |
| | | 'taxonomy_type' => 'reference', |
| | | 'isReference' => true, |
| | | 'taxonomy' => BASE. 'shop', |
| | | 'quickEdit' => true, |
| | | 'default' => '', |
| | | ], |
| | | 'manager_of' => [ |
| | | 'type' => 'taxonomy', |
| | | 'type' => 'selector', |
| | | 'subtype' => 'taxonomy', |
| | | 'label' => __('Manager of', 'jvb'), |
| | | 'taxonomy_type' => 'reference', |
| | | 'isReference' => true, |
| | | 'taxonomy' => BASE. 'shop', |
| | | 'hidden' => true, |
| | | 'default' => '', |
| | |
| | | { |
| | | return [BASE.'directory', BASE.'dash', 'attachment', 'revision', 'nav_menu_item']; |
| | | } |
| | | add_filter('show_admin_bar', '__return_false'); |
| | | |
| | | |
| | | define('JVB_TESTING', str_contains(get_home_url(),'.test')); |
| | | |
| | | if (JVB_TESTING) { |
| | | add_filter('show_admin_bar', '__return_false'); |
| | | } |
| | | //if (JVB_TESTING) { |
| | | // error_log('In testing mode...'); |
| | | //} else { |