From c19264ac916707096fe294d996a1b7fb85206b34 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 08 Mar 2026 19:55:31 +0000
Subject: [PATCH] =Furthering along the complete refactor. Added some basic setup for is_content taxonomies, including a page for an archive of the terms.
---
inc/managers/SEO/render/Traits/_Properties/artformTrait.php | 21
inc/registrar/config/seo/Helpers.php | 5
inc/managers/SEO/render/SchemaOutput.php | 31
assets/css/dash.min.css | 2
jvb.php | 6
inc/registrar/Terms.php | 141 +-
inc/meta/Form.php | 6
inc/rest/routes/ContentRoutes.php | 5
inc/managers/CRUDManager.php | 2
inc/registrar/config/seo/Resolver.php | 75 +
inc/blocks/CustomBlocks.php | 8
inc/registrar/_setup.php | 6
assets/js/concise/SimpleCache.js | 2
inc/meta/MetaFormOld.php | 2
inc/registrar/config/seo/Schema.php | 207 +++
assets/js/min/cache.min.js | 2
inc/managers/SEO/render/Traits/_Properties/aboutTrait.php | 29
inc/managers/SEO/render/Traits/_Properties/_setup.php | 9
inc/ui/CRUDSkeleton.php | 22
inc/integrations/Integrations.php | 3
inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php | 21
inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php | 2
inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php | 28
inc/registrar/fields/UploadField.php | 2
inc/managers/SEO/render/Traits/_Properties/inkerTrait.php | 23
inc/registrar/fields/RepeaterField.php | 50 +
inc/admin/SEOAdmin.php | 1
inc/managers/SEO/render/Traits/_Properties/artistTrait.php | 23
inc/managers/SEO/render/Traits/_Properties/creatorTrait.php | 11
inc/managers/DirectoryManager.php | 3
inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php | 9
cleanup.php | 45 +
inc/registrar/Registrar.php | 117 ++
inc/meta/Storage.php | 2
inc/registrar/fields/SelectorField.php | 125 ++
inc/registrar/fields/TagListField.php | 33
inc/managers/SEO/render/Thing/CreativeWork/_setup.php | 1
inc/meta/MetaTypeManager.php | 14
inc/managers/ReferralManager.php | 4
inc/managers/SEO/_edmonotonink.php | 980 +++++++++++-----------
inc/registrar/fields/GroupedField.php | 12
inc/meta/Sanitizer.php | 30
assets/js/concise/AuthManager.js | 4
assets/js/min/auth.min.js | 2
inc/registrar/Fields.php | 198 ++++
inc/meta/Item.php | 2
inc/registrar/fields/Field.php | 17
inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php | 21
inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php | 23
inc/managers/ScriptLoader.php | 2
inc/managers/SEO/render/Traits/_Properties/coloristTrait.php | 23
assets/js/concise/DataStore.js | 2
inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php | 1
inc/managers/SEO/render/Traits/ThingSchema.php | 12
inc/registrar/config/SEO.php | 19
/dev/null | 19
assets/js/min/dataStore.min.js | 2
inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php | 13
inc/managers/SEO/render/Traits/_Properties/lettererTrait.php | 23
inc/users/UserSettings.php | 10
inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php | 21
inc/meta/Repeater.php | 2
62 files changed, 1,831 insertions(+), 705 deletions(-)
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 583ee8c..d557a97 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-: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}
\ No newline at end of file
+: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}
\ No newline at end of file
diff --git a/assets/js/concise/AuthManager.js b/assets/js/concise/AuthManager.js
index 25db258..3d8c381 100644
--- a/assets/js/concise/AuthManager.js
+++ b/assets/js/concise/AuthManager.js
@@ -17,8 +17,8 @@
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();
diff --git a/assets/js/concise/DataStore.js b/assets/js/concise/DataStore.js
index 29dc9c7..7948f97 100644
--- a/assets/js/concise/DataStore.js
+++ b/assets/js/concise/DataStore.js
@@ -51,7 +51,7 @@
if (!this.dbConfig.has(name)) {
this.dbConfig.set(name, {
- dbName: `jvb_${name}`,
+ dbName: `${jvbBase.base}${name}`,
version: version,
stores: {},
_initialized: false
diff --git a/assets/js/concise/SimpleCache.js b/assets/js/concise/SimpleCache.js
index 30f074c..e33fd14 100644
--- a/assets/js/concise/SimpleCache.js
+++ b/assets/js/concise/SimpleCache.js
@@ -18,7 +18,7 @@
constructor(base, config = {}) {
this.base = base;
this.config = {
- namespace: 'jvb_cache',
+ namespace: `${jvbBase.base}cache`,
TTL: 3600000,
maxSize: 100,
...config
diff --git a/assets/js/min/auth.min.js b/assets/js/min/auth.min.js
index 7a8b15d..bc6fa26 100644
--- a/assets/js/min/auth.min.js
+++ b/assets/js/min/auth.min.js
@@ -1 +1 @@
-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())}))}))}};
\ No newline at end of file
+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())}))}))}};
\ No newline at end of file
diff --git a/assets/js/min/cache.min.js b/assets/js/min/cache.min.js
index 1df21e6..b75ead6 100644
--- a/assets/js/min/cache.min.js
+++ b/assets/js/min/cache.min.js
@@ -1 +1 @@
-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)))}};
\ No newline at end of file
+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)))}};
\ No newline at end of file
diff --git a/assets/js/min/dataStore.min.js b/assets/js/min/dataStore.min.js
index 35242db..0ee0509 100644
--- a/assets/js/min/dataStore.min.js
+++ b/assets/js/min/dataStore.min.js
@@ -1 +1 @@
-(()=>{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)}))}))})();
\ No newline at end of file
+(()=>{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)}))}))})();
\ No newline at end of file
diff --git a/cleanup.php b/cleanup.php
index e646909..8aef5fa 100644
--- a/cleanup.php
+++ b/cleanup.php
@@ -264,3 +264,48 @@
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);
+});
diff --git a/inc/admin/SEOAdmin.php b/inc/admin/SEOAdmin.php
index 055f812..6c01e01 100644
--- a/inc/admin/SEOAdmin.php
+++ b/inc/admin/SEOAdmin.php
@@ -25,6 +25,7 @@
'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',
diff --git a/inc/blocks/CustomBlocks.php b/inc/blocks/CustomBlocks.php
index 93e16ac..ffb46b8 100644
--- a/inc/blocks/CustomBlocks.php
+++ b/inc/blocks/CustomBlocks.php
@@ -18,7 +18,7 @@
{
$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']);
}
@@ -616,10 +616,12 @@
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
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 54f4cf4..3a6643a 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -319,7 +319,8 @@
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();
}
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 3cbf9a7..7ad84df 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -87,7 +87,7 @@
$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
diff --git a/inc/managers/DirectoryManager.php b/inc/managers/DirectoryManager.php
index 46f4ef8..f336d60 100644
--- a/inc/managers/DirectoryManager.php
+++ b/inc/managers/DirectoryManager.php
@@ -37,7 +37,7 @@
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
@@ -684,6 +684,7 @@
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;
diff --git a/inc/managers/ReferralManager.php b/inc/managers/ReferralManager.php
index 6182a7f..48245fa 100644
--- a/inc/managers/ReferralManager.php
+++ b/inc/managers/ReferralManager.php
@@ -2491,11 +2491,11 @@
<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',
diff --git a/inc/managers/SEO/_edmonotonink.php b/inc/managers/SEO/_edmonotonink.php
index 248f76f..6689413 100644
--- a/inc/managers/SEO/_edmonotonink.php
+++ b/inc/managers/SEO/_edmonotonink.php
@@ -42,498 +42,498 @@
// 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;
+//});
diff --git a/inc/managers/SEO/render/SchemaOutput.php b/inc/managers/SEO/render/SchemaOutput.php
index d187f7d..6109e90 100644
--- a/inc/managers/SEO/render/SchemaOutput.php
+++ b/inc/managers/SEO/render/SchemaOutput.php
@@ -49,9 +49,24 @@
$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) {
@@ -61,7 +76,9 @@
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 () {
@@ -104,6 +121,7 @@
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
@@ -112,7 +130,11 @@
}
public function buildBasicWebsiteSchema():array
- { Cache::for('websiteSchema')->flush();
+ {
+ if (JVB_TESTING){
+ Cache::for('websiteSchema')->flush();
+ }
+
return Cache::for('websiteSchema')->remember(
'reference',
function () {
@@ -137,7 +159,10 @@
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',
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php
new file mode 100644
index 0000000..d77b11f
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php
@@ -0,0 +1,28 @@
+<?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;
+}
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php
new file mode 100644
index 0000000..50ef64a
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/_setup.php
@@ -0,0 +1,2 @@
+<?php
+require(JVB_DIR . '/inc/managers/SEO/render/Thing/CreativeWork/VisualArtwork/VisualArtwork.php');
diff --git a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
index 5d7b483..b7922b9 100644
--- a/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
+++ b/inc/managers/SEO/render/Thing/CreativeWork/_setup.php
@@ -1,5 +1,6 @@
<?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');
diff --git a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php
new file mode 100644
index 0000000..30d517e
--- /dev/null
+++ b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php
@@ -0,0 +1,13 @@
+<?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 {
+
+}
diff --git a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
index 1faa572..6965d2e 100644
--- a/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
+++ b/inc/managers/SEO/render/Thing/Place/AdministrativeArea/_setup.php
@@ -1,3 +1,4 @@
<?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');
diff --git a/inc/managers/SEO/render/Traits/ThingSchema.php b/inc/managers/SEO/render/Traits/ThingSchema.php
index e77d3e1..cfebbf6 100644
--- a/inc/managers/SEO/render/Traits/ThingSchema.php
+++ b/inc/managers/SEO/render/Traits/ThingSchema.php
@@ -31,13 +31,14 @@
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) {
@@ -118,4 +119,11 @@
}
$this->id = $id;
}
+
+ public function delete(string $property):void
+ {
+ if (property_exists($this, $property)) {
+ unset($this->$property);
+ }
+ }
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/_setup.php b/inc/managers/SEO/render/Traits/_Properties/_setup.php
index 7877c1d..5359535 100644
--- a/inc/managers/SEO/render/Traits/_Properties/_setup.php
+++ b/inc/managers/SEO/render/Traits/_Properties/_setup.php
@@ -28,6 +28,11 @@
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');
@@ -78,6 +83,7 @@
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');
@@ -195,6 +201,7 @@
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');
@@ -234,6 +241,7 @@
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');
@@ -294,6 +302,7 @@
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');
diff --git a/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php b/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php
index 633666a..4ef2753 100644
--- a/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/aboutTrait.php
@@ -2,6 +2,7 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\Thing;
+use JVBase\registrar\config\seo\Resolver;
if (!defined('ABSPATH')) {
exit;
@@ -17,8 +18,34 @@
{
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;
}
}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php b/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php
new file mode 100644
index 0000000..c240cc8
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artEditionTrait.php
@@ -0,0 +1,21 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php b/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php
new file mode 100644
index 0000000..a53ca16
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artMediumTrait.php
@@ -0,0 +1,21 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artformTrait.php b/inc/managers/SEO/render/Traits/_Properties/artformTrait.php
new file mode 100644
index 0000000..24b05b5
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artformTrait.php
@@ -0,0 +1,21 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artistTrait.php b/inc/managers/SEO/render/Traits/_Properties/artistTrait.php
new file mode 100644
index 0000000..b98e907
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artistTrait.php
@@ -0,0 +1,23 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php b/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php
new file mode 100644
index 0000000..da7413f
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/artworkSurfaceTrait.php
@@ -0,0 +1,21 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php b/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php
new file mode 100644
index 0000000..bea9627
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/coloristTrait.php
@@ -0,0 +1,23 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php b/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php
index 738e5b7..30b69cc 100644
--- a/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/creatorTrait.php
@@ -4,6 +4,7 @@
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;
@@ -19,8 +20,16 @@
{
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',
diff --git a/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php b/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php
new file mode 100644
index 0000000..4dfa2b0
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/inkerTrait.php
@@ -0,0 +1,23 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php b/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php
new file mode 100644
index 0000000..e963913
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/lettererTrait.php
@@ -0,0 +1,23 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php b/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php
new file mode 100644
index 0000000..ed9a76d
--- /dev/null
+++ b/inc/managers/SEO/render/Traits/_Properties/pencilerTrait.php
@@ -0,0 +1,23 @@
+<?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;
+ }
+}
diff --git a/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php b/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php
index 28cf23f..4cb72f9 100644
--- a/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php
+++ b/inc/managers/SEO/render/Traits/_Properties/sourceOrganizationTrait.php
@@ -16,8 +16,15 @@
{
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;
}
}
diff --git a/inc/managers/ScriptLoader.php b/inc/managers/ScriptLoader.php
index 6418d9c..838644e 100644
--- a/inc/managers/ScriptLoader.php
+++ b/inc/managers/ScriptLoader.php
@@ -44,6 +44,7 @@
$version,
$strategy
);
+ wp_localize_script('jvb-auth', 'jvbBase', ['base' => BASE]);
wp_register_script(
'jvb-interactions',
JVB_URL.'assets/js/min/interactions.min.js',
@@ -212,6 +213,7 @@
$strategy
);
+
//SEO Admin
wp_register_script(
'jvb-schema',
diff --git a/inc/meta/Form.php b/inc/meta/Form.php
index 57bdd85..0af4d26 100644
--- a/inc/meta/Form.php
+++ b/inc/meta/Form.php
@@ -1179,7 +1179,7 @@
'type' => $config['subtype'],
], $config);
- $registrar = Registrar::getInstance($config['subtype']);
+ $registrar = Registrar::getInstance($config[$config['subtype']]);
$icon = jvbDefaultIcon();
if ($registrar){
$icon = $registrar->getIcon()??jvbDefaultIcon();
@@ -1597,8 +1597,8 @@
{
$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(
diff --git a/inc/meta/Item.php b/inc/meta/Item.php
index 65e9ecf..ff1fded 100644
--- a/inc/meta/Item.php
+++ b/inc/meta/Item.php
@@ -47,7 +47,7 @@
'user_email',
],
'term' => [
- 'term_name',
+ 'name',
'description'
]
];
diff --git a/inc/meta/MetaFormOld.php b/inc/meta/MetaFormOld.php
index 871e771..ddfd94f 100644
--- a/inc/meta/MetaFormOld.php
+++ b/inc/meta/MetaFormOld.php
@@ -642,7 +642,7 @@
$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;
diff --git a/inc/meta/MetaTypeManager.php b/inc/meta/MetaTypeManager.php
index 69a2fd9..e0da68f 100644
--- a/inc/meta/MetaTypeManager.php
+++ b/inc/meta/MetaTypeManager.php
@@ -85,12 +85,17 @@
'sanitize' => 'sanitizeUser',
'default' => '',
],
+ 'post' => [
+ 'type' => 'string',
+ 'sanitize' => 'sanitizePost',
+ 'default' => '',
+ ],
'repeater' => [
'type' => 'object',
'sanitize' => 'sanitizeRepeater',
'default' => [],
],
- 'tag_list' => [
+ 'taglist' => [
'type' => 'object',
'sanitize' => 'sanitizeTagList',
'default' => []
@@ -133,7 +138,12 @@
'type' => 'string',
'sanitize' => 'sanitize_text_field',
'default' => '',
- ]
+ ],
+ 'selector' => [
+ 'type' => 'string',
+ 'sanitize' => 'sanitizeSelector',
+ 'default' => '',
+ ]
];
public static function getType(string $field_name):array
{
diff --git a/inc/meta/Repeater.php b/inc/meta/Repeater.php
index 583ae44..ae1622e 100644
--- a/inc/meta/Repeater.php
+++ b/inc/meta/Repeater.php
@@ -392,6 +392,6 @@
*/
public function rowLabelField(): ?string
{
- return $this->config['row_label'] ?? null;
+ return $this->config['add_label'] ?? null;
}
}
diff --git a/inc/meta/Sanitizer.php b/inc/meta/Sanitizer.php
index e6cad1c..869065d 100644
--- a/inc/meta/Sanitizer.php
+++ b/inc/meta/Sanitizer.php
@@ -31,11 +31,9 @@
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))
@@ -47,17 +45,21 @@
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)))) {
@@ -171,6 +173,18 @@
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)) {
diff --git a/inc/meta/Storage.php b/inc/meta/Storage.php
index 74538ff..fd0f927 100644
--- a/inc/meta/Storage.php
+++ b/inc/meta/Storage.php
@@ -440,7 +440,7 @@
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 => ''
};
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 47f9f55..7386cd7 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -4,8 +4,10 @@
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;
@@ -16,6 +18,7 @@
protected Registrar $registrar;
public function __construct(?string $type = null, ?Registrar $registrar = null) {
+ $this->registrar = $registrar;
switch ($type) {
case 'post':
$this->addPostFields();
@@ -27,15 +30,16 @@
$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),
};
@@ -117,10 +121,10 @@
'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'
];
@@ -170,4 +174,182 @@
{
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,
+ ]
+ );
+ }
}
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index ad174ca..33a8687 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -1,8 +1,10 @@
<?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;
@@ -15,6 +17,7 @@
use JVBase\registrar\helpers\MakeTrackChanges;
use JVBase\registrar\helpers\MakeVerification;
use JVBase\utility\Features;
+use WP_Query;
if (!defined('ABSPATH')) {
exit;
@@ -36,6 +39,8 @@
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',
@@ -281,6 +286,11 @@
return $this->fields;
}
+ public function args():array
+ {
+ return $this->args;
+ }
+
public function setFields():void
{
$this->fields = new Fields($this->type, $this);
@@ -401,6 +411,11 @@
});
foreach ($flags as $flag) {
$this->$flag = true;
+ switch ($flag) {
+ case 'is_content':
+ add_action('init', [$this, 'setupContent'], 20);
+ break;
+ }
}
return $this;
}
@@ -639,4 +654,106 @@
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;
+ }
}
diff --git a/inc/registrar/Terms.php b/inc/registrar/Terms.php
index 20890dd..e1b875e 100644
--- a/inc/registrar/Terms.php
+++ b/inc/registrar/Terms.php
@@ -179,75 +179,78 @@
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);
diff --git a/inc/registrar/_setup.php b/inc/registrar/_setup.php
index a6eddc2..897eb11 100644
--- a/inc/registrar/_setup.php
+++ b/inc/registrar/_setup.php
@@ -9,9 +9,11 @@
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');
diff --git a/inc/registrar/config/SEO.php b/inc/registrar/config/SEO.php
index 7617b7b..4797cf9 100644
--- a/inc/registrar/config/SEO.php
+++ b/inc/registrar/config/SEO.php
@@ -33,7 +33,24 @@
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;
}
diff --git a/inc/registrar/config/seo/Helpers.php b/inc/registrar/config/seo/Helpers.php
index ce4ae53..c5f0f5f 100644
--- a/inc/registrar/config/seo/Helpers.php
+++ b/inc/registrar/config/seo/Helpers.php
@@ -9,11 +9,12 @@
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
diff --git a/inc/registrar/config/seo/Resolver.php b/inc/registrar/config/seo/Resolver.php
index e754477..3f18043 100644
--- a/inc/registrar/config/seo/Resolver.php
+++ b/inc/registrar/config/seo/Resolver.php
@@ -5,15 +5,16 @@
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(
@@ -22,17 +23,23 @@
$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);
@@ -45,7 +52,7 @@
};
}
- 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);
@@ -53,6 +60,15 @@
//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 '';
@@ -79,7 +95,7 @@
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 === '') {
@@ -94,7 +110,7 @@
}
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 '';
@@ -105,7 +121,7 @@
* 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)) {
@@ -122,17 +138,20 @@
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));
@@ -143,7 +162,10 @@
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) {
@@ -176,4 +198,29 @@
);
}
+
+ 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();
+ }
}
diff --git a/inc/registrar/config/seo/Schema.php b/inc/registrar/config/seo/Schema.php
index 91f03cb..b48b8de 100644
--- a/inc/registrar/config/seo/Schema.php
+++ b/inc/registrar/config/seo/Schema.php
@@ -34,7 +34,10 @@
'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)
{
@@ -52,6 +55,16 @@
$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();
@@ -75,6 +88,8 @@
{
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
@@ -119,67 +134,120 @@
}
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([
@@ -191,7 +259,7 @@
$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);
@@ -209,24 +277,43 @@
);
}
- 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;
}
);
@@ -264,7 +351,12 @@
}
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;
@@ -294,4 +386,47 @@
$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;
+ }
}
diff --git a/inc/registrar/fields/Field.php b/inc/registrar/fields/Field.php
index 6bbe401..e34c80e 100644
--- a/inc/registrar/fields/Field.php
+++ b/inc/registrar/fields/Field.php
@@ -37,12 +37,23 @@
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);
}
}
diff --git a/inc/registrar/fields/GroupedField.php b/inc/registrar/fields/GroupedField.php
index 99a8211..b82c322 100644
--- a/inc/registrar/fields/GroupedField.php
+++ b/inc/registrar/fields/GroupedField.php
@@ -13,12 +13,18 @@
{
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;
+ }
}
diff --git a/inc/registrar/fields/RepeaterField.php b/inc/registrar/fields/RepeaterField.php
new file mode 100644
index 0000000..601634a
--- /dev/null
+++ b/inc/registrar/fields/RepeaterField.php
@@ -0,0 +1,50 @@
+<?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;
+ }
+}
diff --git a/inc/registrar/fields/SelectorField.php b/inc/registrar/fields/SelectorField.php
new file mode 100644
index 0000000..d41145c
--- /dev/null
+++ b/inc/registrar/fields/SelectorField.php
@@ -0,0 +1,125 @@
+<?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;
+ }
+}
diff --git a/inc/registrar/fields/TagListField.php b/inc/registrar/fields/TagListField.php
new file mode 100644
index 0000000..5aef076
--- /dev/null
+++ b/inc/registrar/fields/TagListField.php
@@ -0,0 +1,33 @@
+<?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;
+ }
+}
diff --git a/inc/registrar/fields/TaxonomyField.php b/inc/registrar/fields/TaxonomyField.php
deleted file mode 100644
index da92231..0000000
--- a/inc/registrar/fields/TaxonomyField.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-namespace JVBase\registrar\fields;
-
-if (!defined('ABSPATH')) {
- exit;
-}
-
-class TaxonomyField extends Field {
- protected bool $autocomplete;
- protected string $taxonomy;
- protected string $taxonomy_type;
-
- public function setTaxonomy(string $taxonomy) {
- $this->taxonomy = $taxonomy;
- }
- public function setTaxonomy_type(string $taxonomy_type) {
- $this->taxonomy_type = $taxonomy_type;
- }
-}
diff --git a/inc/registrar/fields/Upload.php b/inc/registrar/fields/UploadField.php
similarity index 95%
rename from inc/registrar/fields/Upload.php
rename to inc/registrar/fields/UploadField.php
index 8f04148..dfc2f1b 100644
--- a/inc/registrar/fields/Upload.php
+++ b/inc/registrar/fields/UploadField.php
@@ -6,7 +6,7 @@
exit;
}
-class Upload extends Field {
+class UploadField extends Field {
protected bool $multiple = false;
protected string $subtype;
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 7f12b55..ea70a8b 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -280,8 +280,9 @@
// 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);
@@ -480,7 +481,7 @@
$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) {
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index c8eeda1..5bb76e4 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -216,14 +216,18 @@
*/
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;
@@ -977,7 +981,8 @@
<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;
?>
@@ -1624,7 +1629,6 @@
$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);
}
}
diff --git a/inc/users/UserSettings.php b/inc/users/UserSettings.php
index 1ee08e9..87a3659 100644
--- a/inc/users/UserSettings.php
+++ b/inc/users/UserSettings.php
@@ -126,17 +126,19 @@
'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' => '',
diff --git a/jvb.php b/jvb.php
index 21bc092..6bbe5ef 100644
--- a/jvb.php
+++ b/jvb.php
@@ -82,9 +82,13 @@
{
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 {
--
Gitblit v1.10.0