=Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5
70 files modified
40 files deleted
1 files added
1 files renamed
| | |
| | | |
| | | use JVBase\blocks\CustomBlocks; |
| | | use JVBase\base\Site; |
| | | use JVBase\managers\ApprovalManager; |
| | | use JVBase\managers\EmailManager; |
| | | use JVBase\managers\ErrorHandler; |
| | | use JVBase\managers\FavouritesManager; |
| | | use JVBase\managers\InvitationsManager; |
| | | use JVBase\managers\LoginManager; |
| | | use JVBase\managers\MagicLinkManager; |
| | |
| | | $this->managers['notifications'] = new NotificationManager(); |
| | | $this->routes['notifications'] = new NotificationsRoutes(); |
| | | } |
| | | if (!empty(Registrar::getFeatured('approve_new'))) { |
| | | $this->managers['approvals'] = new ApprovalManager(); |
| | | } |
| | | if (Site::has('feed_block') || Site::has('dashboard')) { |
| | | $this->routes['term'] = new TermRoutes(); |
| | | } |
| | |
| | | } |
| | | |
| | | if (Site::has('favourites')) { |
| | | $this->managers['favourites'] = new FavouritesManager(); |
| | | $this->routes['favourites'] = new FavouritesRoutes(); |
| | | } |
| | | |
| | |
| | | |
| | | public function userCanConnect(string $service, int $userID): bool |
| | | { |
| | | $allowed = JVB_USER[jvbUserRole($userID)]['integrations'] ?? []; |
| | | return user_can($userID, 'manage_options') || in_array($service, $allowed); |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | return user_can($userID, 'manage_options') || $registrar->hasIntegration($service); |
| | | } |
| | | |
| | | public function getAvailableServices(bool $keys = true): array |
| | |
| | | { |
| | | return $this->managers['terms']; |
| | | } |
| | | |
| | | public function approvals ():ApprovalManager|false |
| | | { |
| | | return $this->managers['approvals']??false; |
| | | } |
| | | } |
| | |
| | | error_log('Starting schedules...'); |
| | | jvbSchedules(); |
| | | error_log('Schedules done!'); |
| | | error_log('checking Admin capabilities...'); |
| | | |
| | | jvb_register_do_once('admin_caps', 'jvbAddAdminCaps'); |
| | | RoleManager::activate(); |
| | | |
| | | error_log('Admin caps done!'); |
| | | error_log('Removing unneeded roles...'); |
| | | remove_role('contributor'); |
| | | remove_role('author'); |
| | | remove_role('editor'); |
| | | error_log('Roles removed!'); |
| | | error_log('New Roles done!'); |
| | | jvbRegisterAdminPages(); |
| | | // if (array_key_exists('integrations', JVB_SITE) && array_key_exists('umami', JVB_SITE['integrations']) && JVB_SITE['integrations']['umami']=== true) { |
| | | // error_log('Adding Umami tables'); |
| | |
| | | .group-fields{position:relative}.hours-copy-btn:hover{background-color:var(--action-50);transform:scale(1.05)}.hours-copy-btn:active{transform:scale(.95)}.hours-copy-btn .icon{--w:0.875rem}.copy-hours-content h3{margin:0 0 1rem 0;color:var(--contrast);font-size:var(--txt-large)}.copy-hours-source{background-color:var(--base-100);padding:1rem;border-radius:var(--radius);margin-bottom:1.5rem;border:1px solid var(--base-200)}.copy-hours-source h4{margin:0 0 .5rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--txt-small);font-weight:600}.source-info{--gap:.25rem}.source-day{font-weight:600;color:var(--contrast);text-transform:capitalize}.source-hours{--gap:1rem;font-weight:500;color:var(--contrast)}.source-hours.closed{color:var(--contrast-200);font-style:italic}.copy-hours-targets{margin-bottom:2rem}.copy-hours-targets h4{margin:0 0 1rem 0;color:var(--contrast-100);text-transform:uppercase;font-size:var(--txt-small);font-weight:600}.day-checkboxes{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem}.feedback{position:fixed;top:2rem;right:2rem;background-color:var(--action-50);color:var(--action-contrast);padding:1rem 1.5rem;border-radius:var(--radius);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:10000;opacity:0;transform:translateX(100px);transition:all var(--trans-base);display:flex;align-items:center;gap:.5rem}.feedback.show{opacity:1;transform:translateX(0)}.feedback .icon{--w:1.25rem} |
| | | :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} |
| | |
| | | nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color)}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav a:hover,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;left:0;max-height:0;transform:scaleY(0);transform-origin:top;width:max(100%,max-content);background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content;--wrap:wrap!important;--justify:flex-start!important}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}.on-this-page a,.on-this-page li{width:100%;height:100%}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}body:has(.additional-actionsbutton) nav.on-this-page{width:calc(100vw - var(--btn_) - 1rem)}.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset;height:var(--btn)}.on-this-page.open button{order:0}.on-this-page ul{width:100%;gap:0}.on-this-page li{justify-content:center}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}.on-this-page a{padding:0}.on-this-page #back-to-top span{display:none}.on-this-page.open #back-to-top span{display:block}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:100%}.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}.index li.active,.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem} |
| | | nav,nav ol,nav ul{--padding:0 1rem;--wrap:nowrap;display:flex;flex-direction:var(--dir,row);justify-content:var(--justify,flex-start);align-items:var(--align,center);gap:var(--gap,0);flex-wrap:var(--wrap,nowrap);height:var(--btn,3rem);max-width:100%;font-family:var(--heading);padding:0;margin:0}nav li{display:flex;align-items:center;height:max(var(--btn),max-content);width:100%;max-inline-size:none;padding:0}nav a,nav button{display:flex;text-decoration:none;align-items:center;justify-content:center;height:var(--btn);width:100%;white-space:nowrap;text-transform:uppercase;transition:var(--trans-color);border-radius:0;background-color:transparent}nav a{height:var(--btn);padding:var(--padding)}nav button{justify-content:center;aspect-ratio:1;padding:0;border:2px solid var(--base);color:var(--contrast);border-radius:0}nav .current a,nav a.current,nav a:focus,nav a:focus:visited,nav button:focus{background-color:var(--action-0);color:var(--action-contrast)}.toggle .icon-caret-down{transform:rotate(0);transition:transform var(--trans-base)}.has-submenu.open>button .icon-caret-down{transform:rotate(900deg)}.has-submenu{position:relative}ul.submenu{--dir:column;height:max-content;position:absolute;top:100%;right:0;max-height:0;transform:scaleY(0);transform-origin:top;width:100%;min-width:max-content;background-color:rgba(var(--base-rgb),var(--op-3));border:2px solid rgba(var(--base-rgb),var(--op-3));transition:all var(--trans-t) var(--trans-fn);box-shadow:var(--shdw-none);overflow:hidden}.submenu li{background-color:rgba(var(--base-rgb),var(--op-6));border:1px solid var(--base-50)}.submenu a{height:var(--chipchip)}.open>ul.submenu{transform:scaleY(1);max-height:1000%;box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw)}.screen-reader-text{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}nav a:focus:not(:focus-visible){outline:0}nav a:focus-visible{outline:2px solid var(--action-0);outline-offset:2px}nav.always{--dir:column;--wrap:nowrap;position:fixed;bottom:0;right:0;width:var(--btn);z-index:var(--z-10)}nav.always.open{--justify:flex-end;width:100vw;height:100vh;padding-bottom:var(--btn_);background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px)}nav.always>ul{--dir:column;--align:center;--justify:flex-start;--gap:0;height:100%;position:relative;right:-300vw;width:100vw;max-height:100%;padding:1rem 0 0;overflow:hidden auto;transition:right var(--trans-base)}nav.always.open>ul{right:0}nav.always li{flex-wrap:wrap;background-color:rgba(var(--base-rgb),var(--op-6))}nav.always a{padding:1rem;max-width:calc(100% - var(--btn));text-align:center}nav.always .has-submenu{display:flex}nav.always .has-submenu>a{flex:1}nav.always .has-submenu>button{flex:0 0 var(--btn)}nav.always .submenu{position:relative;padding-right:4rem;height:max-content;top:0;width:100%;border:2px solid var(--action-0);background-color:rgba(var(--contrast-rgb),var(--op-1))}nav.always .submenu li{background-color:rgba(var(--base-rgb),var(--op-3))}nav.always>button{position:fixed;bottom:0;right:0;width:var(--btn);height:var(--btn);background-color:var(--base);color:var(--contrast);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);transition:width var(--trans-base)}nav.always>button:hover{background-color:var(--action-0);color:var(--action-contrast)}nav.always.open>button{width:100%;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:1000000}nav.always.open>button .icon-list,nav.always>button .icon-x{display:none}nav.always.open>button .icon-x,nav.always>button .icon-list{display:block;width:32px;height:32px}@media (min-width:768px){nav.always>ul{padding-top:var(--btn)}}nav#breadcrumbs{height:max-content;--wrap:wrap;--gap:0;width:max-content;max-width:var(--full);position:absolute;background-color:rgba(var(--base-rgb),var(--op-4));font-size:var(--txt-x-small);padding:.125em;z-index:var(--z-7)}#breadcrumbs ol{height:max-content;--wrap:wrap!important;--justify:flex-start!important}#breadcrumbs li{width:max-content}#breadcrumbs a{height:var(--chip)}#breadcrumbs li::after{content:'/';color:var(--contrast-200);padding:0 .25rem}#breadcrumbs li:last-of-type::after{display:none}#breadcrumbs :is(a,span){padding:0 .125rem;color:var(--contrast);text-transform:none}#breadcrumbs a:focus{background-color:transparent;color:var(--action-0)}nav.fixed.bottom{position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}nav.fixed.bottom ul{--justify:space-between;width:100%;background-color:var(--base);padding:0 .25rem}nav.fixed.bottom li{flex:1;justify-content:center}nav.fixed.bottom a{gap:1rem;--w:var(--chip_);color:var(--contrast);font-size:var(--txt-x-small)}@media (min-width:768px){nav.fixed.bottom a{font-size:var(--txt-medium)}}nav.on-this-page{--justify:space-between;position:fixed;bottom:0;left:0;width:calc(100% - var(--btn));max-width:none;padding:0 .5rem;background-color:rgba(var(--base-rgb),var(--op-4));color:var(--base-200);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-6)}.on-this-page a,.on-this-page li{width:100%;height:100%}body:has(nav.fixed) nav.on-this-page{bottom:var(--btn)}body:has(.additional-actionsbutton) nav.on-this-page{width:calc(100vw - var(--btn_) - 1rem)}.on-this-page button{order:3;padding:0 1rem;width:max-content;aspect-ratio:unset;height:var(--btn)}.on-this-page.open button{order:0}.on-this-page ul{width:100%;gap:0}.on-this-page li{justify-content:center}.on-this-page .active a{background-color:rgba(var(--base-rgb),var(--op-6));color:var(--action-contrast)}.on-this-page a{padding:0}.on-this-page #back-to-top span{display:none}.on-this-page.open #back-to-top span{display:block}nav.letters li{height:var(--chip);max-width:calc(7.69% - 2px)}nav.letters ul{--wrap:wrap}nav.letters,nav.letters ul{height:var(--chipchip)}@media (min-width:768px){nav.letters,nav.letters ul{height:var(--chip)}nav.letters ul{--wrap:nowrap}nav.letters li{max-width:none}nav.letters a{padding:.25rem .66rem}}nav.index{--justify:space-between;--padding:0;background-color:rgba(var(--base-rgb),var(--op-6))}.index ul{width:100%}.index li{flex-shrink:0;transform:scaleX(0);max-width:0;overflow:hidden}.index li.active,.index li.adj{transform:scaleX(1);width:calc(100% - var(--btn_));flex-shrink:1;max-width:none}.index li:first-of-type{flex-shrink:1;transform:scaleX(1);order:9999;width:var(--btn);height:var(--btn);max-width:none}@media (max-width:767px){.index li.adj{transform:scaleX(0);max-width:0}}.index a{border-bottom:4px solid transparent}.index .active a{border-color:var(--action-0);color:var(--contrast)}.index.open{--dir:column-reverse;height:var(--maxHeight);width:100%;align-items:flex-end;background-color:rgba(var(--base-rgb),var(--op-6));backdrop-filter:blur(5px);z-index:var(--z-10)}.index.open ul{--dir:column;--justify:flex-end;height:100%;width:100%}.index.open li{width:100%;height:var(--btn);max-width:100%!important;transform:scaleX(1);overflow:visible}.index.open a{justify-content:flex-end;padding:0 2rem 0 0;background-color:transparent}nav.condensed{height:max-content;--wrap:wrap;--gap:0 .25rem}nav.condensed ul{min-height:var(--chip_);height:max-content;--justify:center;--wrap:wrap}.condensed li{width:max-content;min-height:var(--chip)}.condensed li+li::before{content:'·';padding:0 .25em}.condensed a{height:max-content;min-height:var(--chip);font-size:var(--txt-x-small);padding:0 .25rem;text-transform:none;border-bottom:2px solid transparent}.condensed a:focus{border-color:var(--action-0)}ul.socials{--dir:row;height:max-content;--gap:.5rem;--justify:stretch;--wrap:nowrap;overflow:auto hidden;touch-action:pan-x}.always ul.socials,.always ul.socials a,.always ul.socials li{width:100%}ul.socials a{padding:.5rem;max-width:none}ul.socials .icon{margin:0}nav.tabs{position:fixed;bottom:var(--btn);left:var(--btnbtn);right:var(--btnbtn);padding-bottom:2px;z-index:var(--z-6);touch-action:pan-x pan-y;--wrap:nowrap;overflow:auto hidden}nav.tabs button{aspect-ratio:unset}nav.tabs button.active{cursor:default}nav.tabs button.active:hover{background-color:var(--base-100);color:var(--contrast)}nav.tabs button h2{--wrap:nowrap;margin:0;font-size:var(--txt-x-small)}.tab-content nav.tabs button{height:var(--chip_);padding:.25rem .75rem;min-height:0}.tab-content.active{padding:1rem 0}.tab-content h2{margin:0 0 .5rem}.tab-content nav.tabs{height:max-content;background-color:var(--base);--gap:0}.tab-content .tab-content nav.tabs{background-color:var(--base-100)}.tab-content .tab-content .tab-content nav.tabs{background-color:var(--base-200)}.tab-content nav.tabs button.active h2{color:var(--action-0)}nav.menu a{padding:.5rem .66rem}nav.share{height:max-content;margin:1rem 0}nav.share ul{overflow:visible}nav.share h4{display:inline-block;width:max-content;margin:.25rem .5rem .25rem 0;font-size:var(--txt-x-small)}:where(body>header,.wp-site-blocks>header){--dir:row;--justify:space-between;position:sticky;top:0;left:0;right:0;height:var(--btn);width:100vw;display:flex;align-items:center;padding:0 .5rem;background-color:var(--base);box-shadow:rgba(var(--base-rgb),var(--op-45)) var(--shdw);z-index:var(--z-9)}.wp-site-blocks>header img{width:var(--btn)}nav.term-navigation:has([hidden]){display:none}.dashboard-nav{--justify:flex-start;width:100%}nav.filters{--dir:row;--justify:flex-start;overflow:auto hidden}nav.filters .filter{width:auto;padding:.25rem .75rem} |
| | |
| | | window.decodeHelper.innerHTML = text; |
| | | return window.decodeHelper.value; |
| | | } |
| | | |
| | | |
| | | window.focusNextElement = function() { |
| | | //add all elements we want to include in our selection |
| | | var focussableElements = |
| | | 'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'; |
| | | if (document.activeElement && document.activeElement.form) { |
| | | var focussable = Array.prototype.filter.call( |
| | | document.activeElement.form.querySelectorAll(focussableElements), |
| | | function (element) { |
| | | //check for visibility while always include the current activeElement |
| | | return ( |
| | | element.offsetWidth > 0 || |
| | | element.offsetHeight > 0 || |
| | | element === document.activeElement |
| | | ); |
| | | } |
| | | ); |
| | | var index = focussable.indexOf(document.activeElement); |
| | | if (index > -1) { |
| | | var nextElement = focussable[index + 1] || focussable[0]; |
| | | nextElement.focus(); |
| | | } |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | this.openNav = null; |
| | | this.openSubmenu = null; |
| | | this.releaseFocusTrap = null; |
| | | this.clicked = new Set(); |
| | | this.initListeners(); |
| | | } |
| | | |
| | |
| | | this.counter++; |
| | | } |
| | | if (nav.querySelector('.submenu')) { |
| | | nav.addEventListener('mouseenter', this.hoverOnListener); |
| | | nav.addEventListener('mouseleave', this.hoverOffListener); |
| | | nav.addEventListener('mouseenter', this.handleHoverOn.bind(this)); |
| | | nav.addEventListener('mouseleave', this.handleHoverOff.bind(this)); |
| | | } |
| | | |
| | | let [ |
| | |
| | | submenus: submenus, |
| | | submenuToggles: submenuToggles |
| | | } |
| | | this.navs.set(navID, elements); |
| | | this.counter++; |
| | | submenus.forEach(menu => { |
| | | menu.id = 'submenu-'+this.counter; |
| | | menu.addEventListener('mouseenter', this.handleHoverOn.bind(this)); |
| | | menu.addEventListener('mouseleave', this.handleHoverOff.bind(this)); |
| | | this.counter++; |
| | | }); |
| | | this.navs.set(navID, elements); |
| | | }); |
| | | } |
| | | |
| | | initListeners() { |
| | | this.clickListener = this.handleClick.bind(this); |
| | | this.escapeListener = this.handleEscape.bind(this); |
| | | this.keysListener = this.handleKeys.bind(this); |
| | | this.hoverOnListener = this.handleHoverOn.bind(this); |
| | | this.hoverOffListener = this.handleHoverOff.bind(this); |
| | | |
| | |
| | | if (this.navs.size === 0) { |
| | | return; |
| | | } |
| | | if (this.openNav && e.target.closest(`#${this.openNav}`) === null) { |
| | | console.log('Closing nav', this.openNav); |
| | | this.toggleNav(false, this.openNav); |
| | | } |
| | | |
| | | // if (!e.target.closest(this.openNav)) { |
| | | // console.log('Not closest nav ids'); |
| | | // console.log(this.navIDs()); |
| | | // return; |
| | | // } |
| | | |
| | | let toggle = e.target.closest('.toggle.main'); |
| | | if (toggle) { |
| | | let nav = toggle.closest('nav'); |
| | | this.toggleNav(!nav.classList.contains('open'), nav.id); |
| | | let isOpening = !this.clicked.has(nav); |
| | | let shouldToggle = nav.classList.contains('open') !== isOpening; |
| | | if (shouldToggle) { |
| | | this.toggleNav(isOpening, nav.id); |
| | | } |
| | | |
| | | if (isOpening) { |
| | | this.clicked.add(nav); |
| | | } else { |
| | | this.clicked.delete(nav); |
| | | } |
| | | return; |
| | | } |
| | | |
| | | let submenuToggle = e.target.closest('[data-action="toggle-submenu"], .has-submenu .a') |
| | | if (submenuToggle) { |
| | | let li = submenuToggle.closest('li'); |
| | | this.toggleSubmenu(!li.classList.contains('open'), li); |
| | | } |
| | | let isOpening = !this.clicked.has(li); |
| | | let shouldToggle = li.classList.contains('open') !== isOpening; |
| | | |
| | | if (isOpening) { |
| | | this.clicked.add(li); |
| | | } else { |
| | | this.clicked.delete(li); |
| | | } |
| | | if (shouldToggle) { |
| | | this.toggleSubmenu(isOpening, li); |
| | | } |
| | | return; |
| | | } |
| | | if (!this.openNav) { |
| | | return; |
| | | } |
| | | let close = true; |
| | | for (let [navID, elements] of this.navs) { |
| | | if (e.target.closest('#'+navID)) { |
| | | close = false; |
| | | break; |
| | | } |
| | | } |
| | | if (close) { |
| | | this.toggleNav(false, this.openNav); |
| | | } |
| | | } |
| | | |
| | | handleHoverOn(e) { |
| | | let nav = e.target.closest('nav'); |
| | | if (nav) { |
| | | this.toggleNav(true, nav.id); |
| | | let target = e.currentTarget; |
| | | if (this.clicked.has(target)) { |
| | | return; |
| | | } |
| | | let submenu = e.target.closest('.has-submenu'); |
| | | if (submenu) { |
| | | this.toggleSubmenu(true, submenu); |
| | | if (target.classList.contains('has-submenu')) { |
| | | this.toggleSubmenu(true, target); |
| | | } else if (target.tagName === 'NAV') { |
| | | if (!target.classList.contains('mobile')) { |
| | | this.toggleNav(true, target.id); |
| | | } |
| | | } |
| | | } |
| | | |
| | | handleHoverOff(e) { |
| | | let nav = e.target.closest('nav'); |
| | | if (nav) { |
| | | this.toggleNav(false, nav.id); |
| | | let target = e.currentTarget; |
| | | if (this.clicked.has(target)) { |
| | | return; |
| | | } |
| | | // let submenu = e.target.closest('.has-submenu'); |
| | | // if (submenu) { |
| | | // this.toggleSubmenu(false, submenu); |
| | | // } |
| | | if (target.classList.contains('has-submenu')) { |
| | | this.toggleSubmenu(false, target); |
| | | } else if (target.tagName === 'NAV') { |
| | | if (target.classList.contains('mobile')) { |
| | | return; |
| | | } |
| | | let nav = this.navs.get(target.id); |
| | | let shouldToggle = true; |
| | | for (let submenu of nav.submenus) { |
| | | if (this.clicked.has(submenu)) { |
| | | shouldToggle = false; |
| | | break; |
| | | } |
| | | } |
| | | if (shouldToggle) { |
| | | this.toggleNav(false, target.id); |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | | handleEscape(e) { |
| | | if (this.openNav && e.key === 'Escape') { |
| | | this.toggleNav(false, this.openNav); |
| | | handleKeys(e) { |
| | | if (!this.openNav) return; |
| | | |
| | | switch (e.key) { |
| | | case 'Escape': |
| | | this.closeAll(); |
| | | break; |
| | | |
| | | case 'ArrowDown': |
| | | this.focusNextItem(); |
| | | e.preventDefault(); |
| | | break; |
| | | |
| | | case 'ArrowUp': |
| | | this.focusPrevItem(); |
| | | e.preventDefault(); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | closeAll() { |
| | | let nav = this.navs.get(this.openNav); |
| | | |
| | | if (nav && this.clicked.has(nav.nav)) { |
| | | this.clicked.delete(nav.nav); |
| | | } |
| | | this.toggleNav(false, this.openNav); |
| | | } |
| | | |
| | | focusNextItem() { |
| | | const items = this.getFocusableItems(); |
| | | const i = items.indexOf(document.activeElement); |
| | | const next = items[i + 1] || items[0]; |
| | | next.focus(); |
| | | } |
| | | |
| | | focusPrevItem() { |
| | | const items = this.getFocusableItems(); |
| | | const i = items.indexOf(document.activeElement); |
| | | const prev = items[i - 1] || items[items.length - 1]; |
| | | prev.focus(); |
| | | } |
| | | |
| | | getFocusableItems() { |
| | | return Array.from(document.querySelectorAll( |
| | | 'nav.open a, nav.open button' |
| | | )).filter(el => !el.disabled && !el.closest('[hidden]') && !el.closest('[inert]')); |
| | | } |
| | | |
| | | toggleNav(on, id) { |
| | | let nav = this.navs.get(id); |
| | | if (!nav) { |
| | |
| | | } |
| | | if (on) { |
| | | this.openNav = id; |
| | | document.addEventListener('keydown', this.escapeListener); |
| | | document.addEventListener('keydown', this.keysListener); |
| | | if (nav.nav.classList.contains('mobile')) { |
| | | this.releaseFocusTrap = window.jvbA11y.trapFocus(nav.nav); |
| | | } |
| | | |
| | | } else { |
| | | if (this.releaseFocusTrap) { |
| | | this.releaseFocusTrap(); |
| | | this.releaseFocusTrap = null; |
| | | } |
| | | if (this.openNav === id) { |
| | | this.openNav = null; |
| | | } |
| | | document.removeEventListener('keydown', this.escapeListener); |
| | | document.removeEventListener('keydown', this.keysListener); |
| | | if (!nav.nav.classList.contains('sidebar')) { |
| | | Array.from(nav.submenus).forEach(submenu => { |
| | | if(submenu.classList.contains('open')) { |
| | |
| | | } |
| | | }); |
| | | } |
| | | Array.from(nav.submenus).forEach(submenu => { |
| | | if (this.clicked.has(submenu)) { |
| | | this.clicked.delete(submenu); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | nav.nav.ariaExpanded = on; |
| | | nav.nav.classList.toggle('open', on); |
| | | nav.ariaHidden = !on; |
| | | |
| | | if (nav.nav.classList.contains('mobile')) { |
| | | const content = nav.nav.querySelector(':scope > ul'); |
| | | if (content) content.inert = !on; |
| | | } |
| | | |
| | | nav.nav.setAttribute('aria-expanded', on); |
| | | if (on) { |
| | | nav.nav.querySelector('a:not(.skip-to-content)')?.focus(); |
| | | } |
| | | |
| | | } |
| | | |
| | | toggleSubmenu(on, submenu) { |
| | | if (on && this.openSubmenu && this.openSubmenu !== submenu) { |
| | | this.toggleSubmenu(false, this.openSubmenu); |
| | | } |
| | | |
| | | if (on) { |
| | | this.openSubmenu = submenu; |
| | | } else if (this.openSubmenu === submenu) { |
| | | this.openSubmenu = null; |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | let [ |
| | | toggle, |
| | | firstLink |
| | |
| | | submenu.querySelector('a') |
| | | ]; |
| | | |
| | | if (!on) { |
| | | toggle.focus(); |
| | | } |
| | | const content = submenu.querySelector(':scope > ul'); |
| | | if (content) content.inert = !on; |
| | | let label = toggle.getAttribute('aria-label'); |
| | | window.jvbA11y.announce(on ? `${label} expanded` : `${label} collapsed`); |
| | | |
| | | submenu.classList.toggle('open', on); |
| | | submenu.ariaHidden = !on; |
| | | toggle.ariaExpanded = on; |
| | | toggle.setAttribute('aria-expanded', on); |
| | | if (on && firstLink) { |
| | | firstLink.focus(); |
| | | } |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | document.addEventListener('DOMContentLoaded', function() { |
| | |
| | | (()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.hoverOnListener),e.addEventListener("mouseleave",this.hoverOffListener));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.navs.set(t,a),this.counter++}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.escapeListener=this.handleEscape.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;this.openNav&&null===e.target.closest(`#${this.openNav}`)&&(console.log("Closing nav",this.openNav),this.toggleNav(!1,this.openNav));let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav");this.toggleNav(!e.classList.contains("open"),e.id)}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li");this.toggleSubmenu(!e.classList.contains("open"),e)}}handleHoverOn(e){let t=e.target.closest("nav");t&&this.toggleNav(!0,t.id);let s=e.target.closest(".has-submenu");s&&this.toggleSubmenu(!0,s)}handleHoverOff(e){let t=e.target.closest("nav");t&&this.toggleNav(!1,t.id)}handleEscape(e){this.openNav&&"Escape"===e.key&&this.toggleNav(!1,this.openNav)}toggleNav(e,t){let s=this.navs.get(t);s&&(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.escapeListener)):(this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.escapeListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)}))),s.nav.ariaExpanded=e,s.nav.classList.toggle("open",e),s.ariaHidden=!e,e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus())}toggleSubmenu(e,t){let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];t.classList.toggle("open",e),t.ariaHidden=!e,s.ariaExpanded=e,e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})(); |
| | | (()=>{class e{constructor(){this.counter=0,this.initElements(),0!==this.navs.size&&(this.openNav=null,this.openSubmenu=null,this.releaseFocusTrap=null,this.clicked=new Set,this.initListeners())}initElements(){this.navs=new Map,document.querySelectorAll("nav:has(.submenu), nav:has(.toggle)").forEach((e=>{let t=e.id;""===t&&(t=`nav-${this.counter}`,e.id=t,this.counter++),e.querySelector(".submenu")&&(e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)));let[s,n,i]=[e.querySelectorAll("nav .toggle"),e.querySelectorAll(".has-submenu"),e.querySelectorAll(".toggle:not(.main)")],a={nav:e,toggles:s,submenus:n,submenuToggles:i};this.counter++,n.forEach((e=>{e.id="submenu-"+this.counter,e.addEventListener("mouseenter",this.handleHoverOn.bind(this)),e.addEventListener("mouseleave",this.handleHoverOff.bind(this)),this.counter++})),this.navs.set(t,a)}))}initListeners(){this.clickListener=this.handleClick.bind(this),this.keysListener=this.handleKeys.bind(this),this.hoverOnListener=this.handleHoverOn.bind(this),this.hoverOffListener=this.handleHoverOff.bind(this),document.addEventListener("click",this.clickListener)}handleClick(e){if(0===this.navs.size)return;let t=e.target.closest(".toggle.main");if(t){let e=t.closest("nav"),s=!this.clicked.has(e);return e.classList.contains("open")!==s&&this.toggleNav(s,e.id),void(s?this.clicked.add(e):this.clicked.delete(e))}let s=e.target.closest('[data-action="toggle-submenu"], .has-submenu .a');if(s){let e=s.closest("li"),t=!this.clicked.has(e),n=e.classList.contains("open")!==t;return t?this.clicked.add(e):this.clicked.delete(e),void(n&&this.toggleSubmenu(t,e))}if(!this.openNav)return;let n=!0;for(let[t,s]of this.navs)if(e.target.closest("#"+t)){n=!1;break}n&&this.toggleNav(!1,this.openNav)}handleHoverOn(e){let t=e.currentTarget;this.clicked.has(t)||(t.classList.contains("has-submenu")?this.toggleSubmenu(!0,t):"NAV"===t.tagName&&(t.classList.contains("mobile")||this.toggleNav(!0,t.id)))}handleHoverOff(e){let t=e.currentTarget;if(!this.clicked.has(t))if(t.classList.contains("has-submenu"))this.toggleSubmenu(!1,t);else if("NAV"===t.tagName){if(t.classList.contains("mobile"))return;let e=this.navs.get(t.id),s=!0;for(let t of e.submenus)if(this.clicked.has(t)){s=!1;break}s&&this.toggleNav(!1,t.id)}}handleKeys(e){if(this.openNav)switch(e.key){case"Escape":this.closeAll();break;case"ArrowDown":this.focusNextItem(),e.preventDefault();break;case"ArrowUp":this.focusPrevItem(),e.preventDefault()}}closeAll(){let e=this.navs.get(this.openNav);e&&this.clicked.has(e.nav)&&this.clicked.delete(e.nav),this.toggleNav(!1,this.openNav)}focusNextItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t+1]||e[0]).focus()}focusPrevItem(){const e=this.getFocusableItems(),t=e.indexOf(document.activeElement);(e[t-1]||e[e.length-1]).focus()}getFocusableItems(){return Array.from(document.querySelectorAll("nav.open a, nav.open button")).filter((e=>!e.disabled&&!e.closest("[hidden]")&&!e.closest("[inert]")))}toggleNav(e,t){let s=this.navs.get(t);if(s){if(e&&t!==this.openNav&&this.toggleNav(!1,this.openNav),e?(this.openNav=t,document.addEventListener("keydown",this.keysListener),s.nav.classList.contains("mobile")&&(this.releaseFocusTrap=window.jvbA11y.trapFocus(s.nav))):(this.releaseFocusTrap&&(this.releaseFocusTrap(),this.releaseFocusTrap=null),this.openNav===t&&(this.openNav=null),document.removeEventListener("keydown",this.keysListener),s.nav.classList.contains("sidebar")||Array.from(s.submenus).forEach((e=>{e.classList.contains("open")&&this.toggleSubmenu(!1,e)})),Array.from(s.submenus).forEach((e=>{this.clicked.has(e)&&this.clicked.delete(e)}))),s.nav.classList.toggle("open",e),s.nav.classList.contains("mobile")){const t=s.nav.querySelector(":scope > ul");t&&(t.inert=!e)}s.nav.setAttribute("aria-expanded",e),e&&s.nav.querySelector("a:not(.skip-to-content)")?.focus()}}toggleSubmenu(e,t){e&&this.openSubmenu&&this.openSubmenu!==t&&this.toggleSubmenu(!1,this.openSubmenu),e?this.openSubmenu=t:this.openSubmenu===t&&(this.openSubmenu=null);let[s,n]=[t.querySelector(".toggle"),t.querySelector("a")];e||s.focus();const i=t.querySelector(":scope > ul");i&&(i.inert=!e);let a=s.getAttribute("aria-label");window.jvbA11y.announce(e?`${a} expanded`:`${a} collapsed`),t.classList.toggle("open",e),s.setAttribute("aria-expanded",e),e&&n&&n.focus()}}document.addEventListener("DOMContentLoaded",(function(){window.jvbNav=new e}))})(); |
| | |
| | | (()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,(()=>{e.remove()}),500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===t)return n.toLocaleDateString();const e={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return t.replace(/[YyFMmnjDlHishgAa]/g,(t=>e[t]))}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach((e=>{window.templates.has(e)||window.templates.set(e,n)}))}}))},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach((e=>{e.classList.forEach((t=>{this.templates.has(t)||this.templates.set(t,e)}))}))}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",(()=>{window.jvbTemplates.registerAll()})),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout((()=>n=!1),t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach((e=>{const n=t(e);n&&i.append(n)})),n(i),await new Promise((e=>requestAnimationFrame(e)))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise((i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval((()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())}),n)}))},window.eraseText=function(e,t=10){return new Promise((n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval((()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())}),t)}))},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise((e=>setTimeout(e,o))),s)&&(await window.eraseText(e,i),s);)await new Promise((e=>setTimeout(e,r)))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,(function(e){return e[1].toUpperCase()}))},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i},window.sleep=async function(e=50){return new Promise((t=>setTimeout(t,e)))};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout((()=>{t(),this.timeouts.delete(e)}),n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(l))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||e.scrollTop||0)}),20)})),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value}})(); |
| | | (()=>{window.fade=function(e,t=!0){t?e.style.animation="fadeIn var(--transition-base)":(e.style.animation="fadeOut var(--transition-base)",window.debouncer.schedule(`remove-${e.dataset.id??e.id??e.className.replace(" ","-")}`,(()=>{e.remove()}),500))},window.formatTimeAgo=function(e,t="default"){const n=e instanceof Date?e:new Date(e),i=n-new Date,o=i<0,r=Math.floor(Math.abs(i)/1e3),a=Math.floor(r/60),s=Math.floor(a/60),l=Math.floor(s/24);if(0===a)return"Just now";let c="";if(r<10)c="a moment";else if(r<60)c="less than a minute";else if(a<5)c="a few minutes";else if(s<24)c=0===s?`${a} ${1===a?"minute":"minutes"}`:`about ${s} ${1===s?"hour":"hours"}`;else{if(!(l<7)){if("default"===t)return n.toLocaleDateString();const e={Y:n.getFullYear(),y:String(n.getFullYear()).slice(-2),F:n.toLocaleDateString("en-CA",{month:"long"}),M:n.toLocaleDateString("en-CA",{month:"short"}),m:String(n.getMonth()+1).padStart(2,"0"),n:n.getMonth()+1,d:String(n.getDate()).padStart(2,"0"),j:n.getDate(),D:n.toLocaleDateString("en-CA",{weekday:"short"}),l:n.toLocaleDateString("en-CA",{weekday:"long"}),H:String(n.getHours()).padStart(2,"0"),i:String(n.getMinutes()).padStart(2,"0"),s:String(n.getSeconds()).padStart(2,"0"),h:String(n.getHours()%12||12).padStart(2,"0"),g:n.getHours()%12||12,A:n.getHours()>=12?"PM":"AM",a:n.getHours()>=12?"pm":"am"};return t.replace(/[YyFMmnjDlHishgAa]/g,(t=>e[t]))}if(1===l)return o?"yesterday":"tomorrow";c=`about ${l} days`,c=`${l} ${1===l?"day":"days"}`}return o?`${c} ago`:`in ${c}`},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.templates=new Map,document.addEventListener("DOMContentLoaded",(()=>{window.loadTemplates()})),window.loadTemplates=function(){document.querySelectorAll("template").forEach((e=>{const t=Array.from(e.classList);if(t.length>0){const n=e.content.cloneNode(!0).firstElementChild;t.forEach((e=>{window.templates.has(e)||window.templates.set(e,n)}))}}))},window.getTemplate=function(e){return 0===window.templates.size&&window.loadTemplates(),!!window.templates.has(e)&&window.templates.get(e).cloneNode(!0)};window.jvbTemplates=new class{constructor(){this.templates=new Map,this.definitions=new Map}registerAll(e=document){e.querySelectorAll("template").forEach((e=>{e.classList.forEach((t=>{this.templates.has(t)||this.templates.set(t,e)}))}))}define(e,t={},n=null){this.definitions.set(e,{refs:t.refs||null,manyRefs:t.manyRefs||null,setup:t.setup||null,context:n})}create(e,t={}){const n=this.templates.get(e);if(!n)return console.warn(`[TemplateRegistry] Template "${e}" not found`),null;const i=n.content.cloneNode(!0).firstElementChild;if(!i)return null;const o=this.definitions.get(e),r=o?.refs?this.#e(i,o.refs):{},a=o?.manyRefs?this.#e(i,o.manyRefs,!1):{};return o?.setup?.({el:i,refs:r,manyRefs:a,data:t}),i}#e(e,t,n=!0){const i={};for(const[o,r]of Object.entries(t)){let t,a=!1;"string"==typeof r?t=r:(t=r.selector,a=!!r.required);const s=n?e.querySelector(t):e.querySelectorAll(t);a&&(n&&!s&&console.warn(`[TemplateRegistry] Required ref "${o}" not found: ${t}`),n||0!==s.length||console.warn(`[TemplateRegistry] Required manyRef "${o}" not found: ${t}`)),i[o]=n?s:Array.from(s)}return i}},document.addEventListener("DOMContentLoaded",(()=>{window.jvbTemplates.registerAll()})),window.icon=null,window.getIcon=function(e,t=""){if(void 0===e)return"";window.icon||(window.icon=document.createElement("i"),window.icon.className="icon",window.icon.ariaHidden=!0);let n=window.icon.cloneNode(!0);return t=""!==t&&["regular","bold","duotone","fill","light","thin"].includes("style")?`-${t.slice(0,2)}`:"",n.classList.add(`icon-${e}${t}`),n},window.formatNumber=function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")},window.formatPrice=function(e,t="CAD"){return new Intl.NumberFormat("en-CA",{style:"currency",currency:t}).format(e)},window.escapeHtml=function(e){return e?("string"==typeof e||e instanceof String||(e=String(e)),e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")):""},window.removeChildren=function(e){if(0!==e.children.length)for(;e.firstChild;)e.removeChild(e.firstChild)},window.formatDateRange=function(e,t){const n=new Date(e),i=new Date(t);return n.toDateString()===i.toDateString()?n.toLocaleDateString("en-CA",{year:"numeric",month:"short",day:"numeric"}):n.getMonth()===i.getMonth()&&n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.getDate()}, ${i.getFullYear()}`:n.getFullYear()===i.getFullYear()?`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric"})}, ${i.getFullYear()}`:`${n.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})} - ${i.toLocaleDateString("en-CA",{month:"short",day:"numeric",year:"numeric"})}`},window.throttle=function(e,t=300){let n;return function(...i){n||(e.apply(this,i),n=!0,setTimeout((()=>n=!1),t))}},window.chunkIt=async function(e,t,n,i=10){const o=[];for(let t=0;t<e.length;t+=i)o.push(e.slice(t,t+i));for(const e of o){const i=document.createDocumentFragment();e.forEach((e=>{const n=t(e);n&&i.append(n)})),n(i),await new Promise((e=>requestAnimationFrame(e)))}},window.prefixInput=function(e,t,n=null,i=!1,o=!1){if(!e)return void console.warn("prefixInput called with null/undefined input");const r=e.id,a=i?t:`${t}${e.name}`;let s=null;s=n?n.querySelector(`label[for="${r}"]`):e.labels&&e.labels.length>0?e.labels[0]:"LABEL"===e.previousElementSibling?.tagName?e.previousElementSibling:"LABEL"===e.nextElementSibling?.tagName?e.nextElementSibling:e.closest("[data-field]")?.querySelector(`label[for="${r}"]`),s&&(s.htmlFor=a),e.id=a,o&&(e.name=a)},window.uppercaseFirst=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},window.sanitizeHtml=function(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML},window.generateID=function(e="jvb"){return`${e}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`},window.showProgress=function(e,t,n,i="",o=""){const r=t<n;e.progress&&r&&window.fade(e.progress,!0);const a=n>0?t/n*100:0;e.fill&&(e.fill.style.width=`${a}%`),e.details&&(e.details.textContent=i),e.count&&(e.count.textContent=`${t}/${n}`),e.icon&&(e.icon.className=""===o?"icon":"icon icon-"+o),e.progress&&t===n&&window.fade(e.progress,!1)},window.formatDate=function(e){if(!e)return"";const t=new Date(e),n=new Date,i=Math.floor((n-t)/864e5);return i<1?"Today":i<2?"Yesterday":i<7?`${i} days ago`:t.toLocaleDateString()},window.getPluralContent=function(e){return"artwork"===e?"artwork":e+"s"},window.showToast=function(e,t="success",n={}){window.jvbNotifications.showToast(e,t,n)},window.dateFormatter=new Intl.DateTimeFormat("en-CA",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:"short"}),window.formatDate=function(e){return e instanceof Date&&!isNaN(e)||(e=new Date(e)),window.dateFormatter.format(e)},window.typeText=function(e,t,n=50){return new Promise((i=>{e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval);let o=0;e.textContent="",e._typeInterval=setInterval((()=>{o<t.length?(e.textContent+=t.charAt(o),o++):(clearInterval(e._typeInterval),delete e._typeInterval,i())}),n)}))},window.eraseText=function(e,t=10){return new Promise((n=>{e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval);let i=e.textContent,o=i.length;e._eraseInterval=setInterval((()=>{o>0?(o--,e.textContent=i.substring(0,o)):(clearInterval(e._eraseInterval),delete e._eraseInterval,n())}),t)}))},window.typeLoop=function(e,t,n=50,i=10,o=1e3,r=250){const a=e.id||e.dataset.typeKey||`type-${Date.now()}`;e.dataset.typeKey||(e.dataset.typeKey=a),e._stopTyping&&e._stopTyping();let s=!0;const l=function(){s=!1,e._typeInterval&&(clearInterval(e._typeInterval),delete e._typeInterval),e._eraseInterval&&(clearInterval(e._eraseInterval),delete e._eraseInterval)};return e._stopTyping=l,async function(){for(;s&&(await window.typeText(e,t,n),s)&&(await new Promise((e=>setTimeout(e,o))),s)&&(await window.eraseText(e,i),s);)await new Promise((e=>setTimeout(e,r)))}(),l},window.toCamelCase=function(e){return e.replace(/-([a-z])/g,(function(e){return e[1].toUpperCase()}))},window.targetCheck=function(e,t){return Array.isArray(t)&&(t=t.join(",")),"string"==typeof t&&(e.target.closest(t)??!1)},window.getDifferences={VALUE_CREATED:"created",VALUE_UPDATED:"updated",VALUE_DELETED:"deleted",VALUE_UNCHANGED:"unchanged",map:function(e,t){if(this.isFunction(e)||this.isFunction(t))throw"Invalid argument. Function given, object expected.";if(this.isFile(e)||this.isFile(t)){const n=this.compareFiles(e,t);return n===this.VALUE_UNCHANGED?null:{type:n,data:void 0===e?t:e}}if(this.isValue(e)||this.isValue(t)){const n=this.compareValues(e,t);if(n===this.VALUE_UNCHANGED)return null;let i;switch(n){case this.VALUE_CREATED:i=t;break;case this.VALUE_DELETED:i=this.getEmptyValue(e);break;case this.VALUE_UPDATED:default:i=t}return{type:n,data:i}}let n={},i=!1;for(let o in e)if(!this.isFunction(e[o])){let r;t&&void 0!==t[o]&&(r=t[o]);const a=this.map(e[o],r);null!==a&&(a.hasOwnProperty("type")&&a.hasOwnProperty("data")?n[o]=a.data:n[o]=a,i=!0)}if(t)for(let o in t)if(!this.isFunction(t[o])&&(void 0===e||void 0===e[o])){const e=this.map(void 0,t[o]);null!==e&&(e.hasOwnProperty("type")&&e.hasOwnProperty("data")?n[o]=e.data:n[o]=e,i=!0)}return i?n:null},getEmptyValue:function(e){return this.isArray(e)?[]:this.isObject(e)?{}:"number"==typeof e?0:"boolean"!=typeof e&&""},compareValues:function(e,t){return e===t||this.isDate(e)&&this.isDate(t)&&e.getTime()===t.getTime()?this.VALUE_UNCHANGED:void 0===e?this.VALUE_CREATED:void 0===t?this.VALUE_DELETED:this.VALUE_UPDATED},isFunction:function(e){return"[object Function]"===Object.prototype.toString.call(e)},isArray:function(e){return"[object Array]"===Object.prototype.toString.call(e)},isDate:function(e){return"[object Date]"===Object.prototype.toString.call(e)},isObject:function(e){return"[object Object]"===Object.prototype.toString.call(e)},isFile:function(e){return e instanceof File},isValue:function(e){return!this.isObject(e)&&!this.isArray(e)},compareFiles:function(e,t){return!this.isFile(e)&&this.isFile(t)?this.VALUE_CREATED:this.isFile(e)&&!this.isFile(t)?this.VALUE_DELETED:this.isFile(e)&&this.isFile(t)?e.name===t.name&&e.size===t.size&&e.type===t.type&&e.lastModified===t.lastModified?this.VALUE_UNCHANGED:this.VALUE_UPDATED:this.VALUE_UNCHANGED},merge:function(e,t){if(null==e)return t;if(null==t)return e;if(this.isFunction(e)||this.isFunction(t))return t;if(this.isFile(e)||this.isFile(t))return t;if(this.isValue(e)||this.isValue(t)||this.isArray(e)||this.isArray(t))return t;if(this.isObject(e)&&this.isObject(t)){let n={};for(let t in e)this.isFunction(e[t])||(n[t]=e[t]);for(let i in t)this.isFunction(t[i])||(void 0!==e[i]?n[i]=this.merge(e[i],t[i]):n[i]=t[i]);return n}return t}},window.deepMerge=function(e,t){return window.getDifferences.merge(e,t)},window.isInt=function(e){return!isNaN(parseInt(e))&&isFinite(e)},window.isNumeric=function(e){return!isNaN(parseFloat(e))&&isFinite(e)},window.uiFromSelectors=function(e,t=null,n=!1){let i={};for(let[o,r]of Object.entries(e))i[o]="object"==typeof r?window.uiFromSelectors(r,t):t?n?t.querySelectorAll(r):t.querySelector(r):n?document.querySelectorAll(r):document.querySelector(r);return i},window.sleep=async function(e=50){return new Promise((t=>setTimeout(t,e)))};window.debouncer=new class{constructor(){this.timeouts=new Map,window.addEventListener("beforeunload",(()=>this.cleanup()))}schedule(e,t,n=1e3){this.cancel(e),this.timeouts.set(e,setTimeout((()=>{t(),this.timeouts.delete(e)}),n))}cancel(e){this.timeouts.has(e)&&(clearTimeout(this.timeouts.get(e)),this.timeouts.delete(e))}cleanup(){for(let e of this.timeouts.values())clearTimeout(e);this.timeouts.clear()}};document.body;const e=document.documentElement,t=document.querySelector(".scroll-progress .bar");let n=window.scrollY||e.scrollTop||0,i=-1,o=!1,r=0;function a(){r=Math.max(0,e.scrollHeight-window.innerHeight)}function s(e){if(!t)return;const n=r>0?e/r:0,i=Math.max(0,Math.min(1,n));t.style.transform=`scaleX(${i})`}function l(){const t=window.scrollY||e.scrollTop||0;t>n?i=1:t<n&&(i=-1),n=t,document.body.classList.toggle("scroll-up",i<0&&t>0),s(t),o=!1}window.addEventListener("scroll",(()=>{o||(o=!0,requestAnimationFrame(l))}),{passive:!0}),window.addEventListener("resize",(()=>{window.debouncer.schedule("recalc-max-scroll",(()=>{a(),s(window.scrollY||e.scrollTop||0)}),20)})),a(),s(n),window.decodeHTMLEntities=function(e){return window.decodeHelper||(window.decodeHelper=document.createElement("textarea")),window.decodeHelper.innerHTML=e,window.decodeHelper.value},window.focusNextElement=function(){if(document.activeElement&&document.activeElement.form){var e=Array.prototype.filter.call(document.activeElement.form.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'),(function(e){return e.offsetWidth>0||e.offsetHeight>0||e===document.activeElement})),t=e.indexOf(document.activeElement);if(t>-1)(e[t+1]||e[0]).focus()}}})(); |
| | |
| | | <?php |
| | | namespace JVBase\base; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | /** |
| | | * JVB_MEMBERSHIP defines the structure of the directory |
| | | * Options include: |
| | |
| | | { |
| | | protected bool $member_content = true; |
| | | protected bool $invitable = false; |
| | | protected bool $member_verified = false; |
| | | protected array $can_invite = []; |
| | | protected bool $notifications = true; |
| | | protected bool $member_expires = false; |
| | |
| | | return false; |
| | | } |
| | | |
| | | public function setInvitable(array $invitable):void |
| | | { |
| | | $users = Registrar::getRegistered('user'); |
| | | |
| | | $invitable = array_filter($invitable, function($role) use ($users) { |
| | | if (!in_array($role, $users)) { |
| | | error_log('[Membership]::set Invitable role '.$role.' does not exist.'); |
| | | return false; |
| | | } |
| | | return true; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | $allowed = []; |
| | | foreach ($invitable as $inviter => $canInvites) { |
| | | $canInvites = array_filter($canInvites, function($role) use ($users){ |
| | | if (!in_array($role, $users)) { |
| | | error_log('[Membership]::set Invitable role '.$role.' does not exist.'); |
| | | return false; |
| | | } |
| | | return true; |
| | | }); |
| | | if (!empty($canInvites)) { |
| | | $allowed[$inviter] = $canInvites; |
| | | } |
| | | } |
| | | if (!empty($allowed)) { |
| | | $this->can_invite = $allowed; |
| | | } |
| | | } |
| | | |
| | | public function getInviters():array |
| | | { |
| | | return $this->can_invite; |
| | |
| | | return array_key_exists($role, $this->can_invite) ? $this->can_invite[$role] : []; |
| | | } |
| | | |
| | | public function setApprovers(array $roles):void |
| | | { |
| | | $allowed = Registrar::getRegistered('users'); |
| | | $this->can_approve = array_filter($roles, function($role) use ($allowed){ |
| | | return in_array($role, $allowed); |
| | | }); |
| | | } |
| | | public function getApprovers():array |
| | | { |
| | | return $this->can_approve; |
| | |
| | | } |
| | | |
| | | |
| | | public function setCanSeeForum(array $roles):void |
| | | { |
| | | $allowed = Registrar::getRegistered('users'); |
| | | $this->member_only = array_filter($roles, function($role) use ($allowed){ |
| | | return in_array($role, $allowed); |
| | | }); |
| | | } |
| | | public function canSeeForum(string $role):bool |
| | | { |
| | | return $this->forum && (empty($this->member_only) || in_array($role, $this->member_only)); |
| File was renamed from base/seo.php |
| | |
| | | return $class; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | /** |
| | | * JVB_SCHEMA: Site-wide schema configuration |
| | | * |
| | | * Structure: |
| | | * - business: LocalBusiness/Organization for home page |
| | | * - website: WebSite schema configuration |
| | | * - actions: PotentialAction definitions |
| | | * - attribution: Developer/maintainer info |
| | | */ |
| | | |
| | | |
| | | /** |
| | | JVB_CONTENT['artwork'] = [ |
| | | 'singular' => 'Artwork', |
| | | 'plural' => 'Artworks', |
| | | // ... other config |
| | | |
| | | 'seo' => [ |
| | | 'meta' => [ |
| | | 'title' => '{{post_title}} by {{linked_user.display_name}} | {{site_title}}', |
| | | 'description' => '{{style.primary.name}} artwork by {{linked_user.display_name}}. {{short_bio|default:View this piece and more.}}', |
| | | 'archive_title' => 'Artwork Gallery', |
| | | 'archive_description' => 'Browse our collection of tattoo artwork and designs.', |
| | | ], |
| | | 'schema' => [ |
| | | 'type' => 'VisualArtwork', |
| | | 'mappings' => [ |
| | | 'artform' => 'style.primary', // DefinedTerm from taxonomy |
| | | 'creator' => 'linked_user', // Person from linked user |
| | | 'image' => 'featured_image', // ImageObject |
| | | 'artMedium' => 'medium.names:3', // Comma-separated term names |
| | | ], |
| | | 'overrides' => [ |
| | | 'inLanguage' => 'en', |
| | | ] |
| | | ] |
| | | ] |
| | | ]; |
| | | |
| | | JVB_CONTENT['artist'] = [ |
| | | 'singular' => 'Artist', |
| | | 'plural' => 'Artists', |
| | | |
| | | 'seo' => [ |
| | | 'meta' => [ |
| | | 'title' => '{{post_title}} - {{artist_type|default:Tattoo Artist}} in {{city.primary.name|default:Edmonton}}', |
| | | 'description' => '{{short_bio|truncate:155}}', |
| | | ], |
| | | 'schema' => [ |
| | | 'type' => 'Person', |
| | | 'mappings' => [ |
| | | 'image' => 'image_portrait', |
| | | 'jobTitle' => 'artist_type', |
| | | 'worksFor' => 'shop.primary', // LocalBusiness reference |
| | | 'knowsAbout' => 'style.names', // Array of style names |
| | | 'areaServed' => 'city.primary.name', |
| | | ] |
| | | ] |
| | | ] |
| | | ]; |
| | | |
| | | JVB_TAXONOMY['shop'] = [ |
| | | 'singular' => 'Shop', |
| | | 'plural' => 'Shops', |
| | | |
| | | 'seo' => [ |
| | | 'meta' => [ |
| | | 'title' => '{{term_name}} - Tattoo Shop in {{city.primary.name|default:Edmonton}}', |
| | | 'description' => '{{tagline|default:Visit}} {{term_name}}. {{short_bio|truncate:120}}', |
| | | ], |
| | | 'schema' => [ |
| | | 'type' => 'TattooParlor', // or LocalBusiness |
| | | 'mappings' => [ |
| | | 'address' => 'location', |
| | | 'telephone' => 'phone', |
| | | 'email' => 'email', |
| | | 'openingHoursSpecification' => 'hours', |
| | | 'image' => 'image', |
| | | 'priceRange' => 'price_range', |
| | | 'paymentAccepted' => 'payment_accepted', |
| | | ], |
| | | 'overrides' => [ |
| | | 'additionalTypeTrait' => 'https://schema.org/TattooParlor', |
| | | ] |
| | | ] |
| | | ] |
| | | ]; |
| | | |
| | | JVB_TAXONOMY['style'] = [ |
| | | 'singular' => 'Style', |
| | | 'plural' => 'Styles', |
| | | |
| | | 'seo' => [ |
| | | 'meta' => [ |
| | | 'title' => '{{term_name}} Tattoos in Edmonton | {{site_title}}', |
| | | 'description' => '{{tagline|default:Explore}} {{term_name}} tattoo artists and designs. {{characteristics|strip|truncate:100}}', |
| | | ], |
| | | 'schema' => [ |
| | | 'type' => 'DefinedTerm', |
| | | 'mappings' => [ |
| | | 'alternateName' => 'alternate_name', |
| | | ] |
| | | ] |
| | | ] |
| | | ]; |
| | | |
| | | **/ |
| | |
| | | protected static Membership $membershipConfig; |
| | | protected static Site $instance; |
| | | protected static string $icons = 'regular'; |
| | | /** |
| | | * @var array Dashboard config |
| | | */ |
| | | protected static string $dashboardTitle; |
| | | |
| | | /** |
| | | * @var bool $is_directory Whether this is a membership directory |
| | | */ |
| | | protected static bool $is_directory = false; |
| | | protected static string $directory_singular; |
| | | protected static string $directory_plural; |
| | | /** |
| | | * @var bool Whether to add a help menu to the header |
| | | */ |
| | | protected static bool $help_menu = false; |
| | | /** |
| | | * @var bool $has_membership Whether this site has membership |
| | | */ |
| | |
| | | * @var bool $referrals Whether to implement a referral system, with rewards |
| | | */ |
| | | protected static bool $referrals = false; |
| | | protected static string $defaultReferralRole = BASE.'client'; |
| | | /** |
| | | * @var bool $magic_link Whether users can login without a password |
| | | */ |
| | |
| | | } |
| | | return self::$membershipConfig; |
| | | } |
| | | |
| | | public function setDashboardTitle(string $title):void |
| | | { |
| | | self::$dashboardTitle = $title; |
| | | } |
| | | |
| | | public static function dashboardTitle():bool|string |
| | | { |
| | | return self::$dashboardTitle ?? false; |
| | | } |
| | | |
| | | public function setDirectorySingular(string $title):void |
| | | { |
| | | self::$directory_singular = $title; |
| | | } |
| | | public static function getDirectorySingular():bool|string |
| | | { |
| | | return self::$directory_singular ?? false; |
| | | } |
| | | |
| | | public function setDirectoryPlural(string $title):void |
| | | { |
| | | self::$directory_plural = $title; |
| | | } |
| | | public static function getDirectoryPlural():bool|string |
| | | { |
| | | return self::$directory_plural ?? false; |
| | | } |
| | | |
| | | public static function setDefaultReferralRole(string $role):void |
| | | { |
| | | self::$defaultReferralRole = $role; |
| | | } |
| | | public static function getDefaultReferralRole():string |
| | | { |
| | | return self::$defaultReferralRole; |
| | | } |
| | | } |
| | | |
| | | |
| | | $defaults = [ |
| | | 'icons' => 'light', |
| | | 'directory' => false, //as in, a membership directory |
| | | 'membership' => false, |
| | | 'has_map' => false, //TODO: migrate to integrations['gmb'] |
| | | 'dashboard' => false, |
| | | 'support' => false, |
| | | 'feed_block' => false, |
| | | 'email_notifications' => false, |
| | | 'integrations' => [ |
| | | 'bluesky' => false, |
| | | 'cloudflare' => false, |
| | | 'facebook' => false, |
| | | 'maps' => false, |
| | | 'gmb' => false, |
| | | 'helcim' => false, |
| | | 'instagram' => false, |
| | | 'square' => false, |
| | | 'umami' => false, |
| | | ], |
| | | 'is_restaurant' => false, |
| | | 'limit_hours' => false, |
| | | 'enthusiast' => false, |
| | | 'favourites' => false, //optional flag to allow enthusiasts, but not favourites |
| | | ]; |
| | | |
| | | $jvb_site = array_merge($defaults, apply_filters('jvb_site', [])); |
| | | |
| | | define('JVB_SITE', $jvb_site); |
| | |
| | | function jvbDefineSite() { |
| | | Site::getInstance(); |
| | | } |
| | | require(JVB_DIR.'/base/content.php'); |
| | | //require(JVB_DIR.'/base/content.php'); |
| | | require(JVB_DIR.'/base/email.php'); |
| | | require(JVB_DIR.'/base/taxonomies.php'); |
| | | require(JVB_DIR.'/base/users.php'); |
| | | //require(JVB_DIR.'/base/taxonomies.php'); |
| | | //require(JVB_DIR.'/base/users.php'); |
| | | require(JVB_DIR.'/base/Login.php'); |
| | | require(JVB_DIR.'/base/Membership.php'); |
| | | require(JVB_DIR.'/base/options.php'); |
| | | require(JVB_DIR.'/base/seo.php'); |
| | | require(JVB_DIR.'/base/SchemaHelper.php'); |
| | | |
| | | $base = apply_filters('jvb_base', 'jvb_'); |
| | | $base = (str_ends_with($base, '_')) ? $base : $base.'_'; |
| | |
| | | * - rewrites = (array) key = associated taxonomy; value = what to append to rewrite base |
| | | */ |
| | | $defaults = []; |
| | | if (!empty(JVB_SITE) && array_key_exists('has_support', JVB_SITE) && JVB_SITE['has_support'] === true) { |
| | | $defaults = array_merge($defaults, jvb_support_post_type()); |
| | | } |
| | | //if (!empty(JVB_SITE) && array_key_exists('has_support', JVB_SITE) && JVB_SITE['has_support'] === true) { |
| | | // $defaults = array_merge($defaults, jvb_support_post_type()); |
| | | //} |
| | | $content = array_merge($defaults, apply_filters('jvb_content', [])); |
| | | define('JVB_CONTENT', $content); |
| | | |
| | |
| | | */ |
| | | |
| | | $defaults = []; |
| | | if (array_key_exists('enthusiast', JVB_SITE) && JVB_SITE['enthusiast'] === true) { |
| | | $defaults['enthusiast'] = [ |
| | | 'label' => 'Enthusiast', |
| | | 'has_dashboard' => true, |
| | | 'can_create' => [], |
| | | 'can_register' => true, |
| | | 'can_favourite' => true, |
| | | 'keep_stats' => true, |
| | | 'join_text' => 'Save your favourites. Get Notified.', |
| | | 'join_title' => 'Welcome to the Scene.', |
| | | 'join_description' => [ |
| | | 'Save your favourites for easy access', |
| | | 'Get notified when your favourite artists add new content', |
| | | 'Stay in the loop with local flash days and events', |
| | | 'Discover styles and artists that match your vision' |
| | | ], |
| | | 'join_bonus' => '<strong>BONUS: </strong>Everything\'s free. And always will be. We work with partners vetted by the community to keep the lights on.', |
| | | ]; |
| | | } |
| | | //if (array_key_exists('enthusiast', JVB_SITE) && JVB_SITE['enthusiast'] === true) { |
| | | // $defaults['enthusiast'] = [ |
| | | // 'label' => 'Enthusiast', |
| | | // 'has_dashboard' => true, |
| | | // 'can_create' => [], |
| | | // 'can_register' => true, |
| | | // 'can_favourite' => true, |
| | | // 'keep_stats' => true, |
| | | // 'join_text' => 'Save your favourites. Get Notified.', |
| | | // 'join_title' => 'Welcome to the Scene.', |
| | | // 'join_description' => [ |
| | | // 'Save your favourites for easy access', |
| | | // 'Get notified when your favourite artists add new content', |
| | | // 'Stay in the loop with local flash days and events', |
| | | // 'Discover styles and artists that match your vision' |
| | | // ], |
| | | // 'join_bonus' => '<strong>BONUS: </strong>Everything\'s free. And always will be. We work with partners vetted by the community to keep the lights on.', |
| | | // ]; |
| | | //} |
| | | |
| | | $users = array_merge($defaults, apply_filters('jvb_user', [])); |
| | | define('JVB_USER', $users); |
| | |
| | | return (array_key_exists($key, $config) && $config[$key] === true); |
| | | } |
| | | |
| | | function jvbUserIsVerified():bool |
| | | { |
| | | $membership = Site::membership(); |
| | | |
| | | return !($membership && $membership->has('member_verified')) || current_user_can('skip_moderation'); |
| | | } |
| | | |
| | | function jvbUserTypes():array |
| | | { |
| | | $types = get_option(BASE.'user_types'); |
| | | if (JVB_TESTING) { |
| | | $types = false; |
| | | } |
| | | if ($types === false) { |
| | | $types = []; |
| | | foreach (JVB_USER as $type => $config) { |
| | | if (array_key_exists('profile', $config)) { |
| | | $types[$type] = BASE.$config['profile']; |
| | | } |
| | | } |
| | | update_option(BASE.'user_types', $types); |
| | | } |
| | | return $types; |
| | | return Registrar::getFeatured('profile_link', 'user'); |
| | | } |
| | | |
| | | function isJVBContentTax():bool |
| | | { |
| | | return is_tax(array_map(function ($tax) { |
| | | return jvbCheckBase($tax); |
| | | }, Registrar::getFeatured('is_content', 'term'))); |
| | | } |
| | | |
| | | function taxIsJVBContentTax($tax):bool |
| | | { |
| | | $allowed = Registrar::getFeatured('is_content', 'term'); |
| | | $tax = jvbNoBase($tax); |
| | | return in_array($tax, $allowed); |
| | | } |
| | | |
| | | function jvbIsOpen():bool |
| | | { |
| | | |
| | | if (!jvbCheck('limit_hours', JVB_SITE)) { |
| | | return true; |
| | | } |
| | | if (get_option(BASE.'open_to_public') !== '1') { |
| | | return false; |
| | | } |
| | | //Check if today_hours is set |
| | | if (get_option(BASE.'today_hours')) { |
| | | return jvbIsTimeBetween(); |
| | | } |
| | | //Default to the stored settings |
| | | return jvbIsCurrentlyOpen(); |
| | | } |
| | | //function jvbIsOpen():bool |
| | | //{ |
| | | // |
| | | // if (!jvbCheck('limit_hours', JVB_SITE)) { |
| | | // return true; |
| | | // } |
| | | // if (get_option(BASE.'open_to_public') !== '1') { |
| | | // return false; |
| | | // } |
| | | // //Check if today_hours is set |
| | | // if (get_option(BASE.'today_hours')) { |
| | | // return jvbIsTimeBetween(); |
| | | // } |
| | | // //Default to the stored settings |
| | | // return jvbIsCurrentlyOpen(); |
| | | //} |
| | | |
| | | |
| | | function jvbTermHasPosts(int $termID, string $taxonomy):bool |
| | |
| | | } |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\registry\TaxonomyRegistrar; |
| | | use WP_Error; |
| | | use Exception; |
| | | |
| | |
| | | // Handle individual taxonomy rebuild |
| | | if (isset($_POST['rebuild_taxonomy']) && wp_verify_nonce($_POST['_wpnonce'], 'rebuild_taxonomy')) { |
| | | $taxonomy = sanitize_text_field($_POST['taxonomy']); |
| | | $registrar = Registrar::getInstance($taxonomy)); |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | if ($registrar && $registrar->hasFeature('is_content')) { |
| | | $results = $this->rebuildCustomTable($taxonomy); |
| | | // Store results in transient to display after redirect |
| | |
| | | ]; |
| | | |
| | | // Check if this is a content taxonomy |
| | | $registrar = Registrar::getInstance($taxonomy)); |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | if (!$registrar->hasFeature('is_content')) { |
| | | $results['messages'][] = "Taxonomy {$taxonomy} is not a content taxonomy"; |
| | | return $results; |
| | |
| | | require(JVB_DIR . '/inc/admin/Integrations.php'); |
| | | require(JVB_DIR . '/inc/admin/SEOAdmin.php'); |
| | | |
| | | if (!empty(Registrar::getFeatured('is_content', 'term'))) { |
| | | require(JVB_DIR . '/inc/admin/ContentTaxonomy.php'); |
| | | add_action('init', 'jvb_maybe_setup_content_taxonomy', 1); |
| | | function jvb_maybe_setup_content_taxonomy(): void |
| | | { |
| | | if (!empty(Registrar::getFeatured('is_content', 'term'))) { |
| | | require(JVB_DIR . '/inc/admin/ContentTaxonomy.php'); |
| | | } |
| | | } |
| | |
| | | use DateTime; |
| | | use DOMDocument; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\SEO\BreadcrumbManager; |
| | | use WP_Block; |
| | | use WP_Query; |
| | | |
| | |
| | | { |
| | | $this->cache = Cache::for('blocks', WEEK_IN_SECONDS); |
| | | $this->cache->connect('post')->connect('taxonomy'); |
| | | add_filter('render_block', [$this, 'render'], 900, 3); |
| | | add_filter('render_block', [$this, 'render'], 900, 2); |
| | | |
| | | add_action('init', [$this, 'registerBlockStyles']); |
| | | } |
| | |
| | | ); |
| | | } |
| | | |
| | | public function render(string $content, array $block, WP_Block $instance) |
| | | public function render(string $content, array $block) |
| | | { |
| | | $blockName = $this->sanitizeBlockName($block); |
| | | $method = 'render_'.$blockName; |
| | |
| | | '</h'.$level.'>'; |
| | | } |
| | | |
| | | public function render_core_list(array $block):string |
| | | public function render_core_list(array $block, string $content):string |
| | | { |
| | | $tag = (array_key_exists('ordered', $block['attrs'])) ? 'ol' : 'ul'; |
| | | return '<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.$this->innerBlocks($block).'</'.$tag.'>'; |
| | |
| | | aria-expanded="false">'. |
| | | jvbIcon('list', ['title'=>'Toggle Menu']). |
| | | jvbIcon('x', ['title'=>'Toggle Menu']). |
| | | '</button>'; |
| | | '</button>'; |
| | | $class = ($toggle === '') ? |
| | | $this->getClassesAndStyles($block['attrs'], ['mobile']) : |
| | | $this->getClassesAndStyles($block['attrs']); |
| | |
| | | '<nav><ul>'.jvbNotificationMenu().jvbHelpMenu().'</ul></nav>' : |
| | | ''; |
| | | |
| | | |
| | | //Allows to add custom items to a menu, based on the menu name |
| | | $helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, get_the_title($ID)); |
| | | $main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), get_the_title($ID), $block)); |
| | |
| | | |
| | | |
| | | if (($isHeaderTemplate || $isFooterTemplate)) { |
| | | $innerContent = $content; |
| | | |
| | | $tag = $isHeaderTemplate ?: $isFooterTemplate ?: 'div'; |
| | | |
| | |
| | | jvbIcon('sun-dim', ['title'=> 'Light Mode']). |
| | | jvbIcon('moon', ['title'=>'Dark Mode']). |
| | | '</span></label>' : ''; |
| | | $breadcrumbs = jvbBuildBreadcrumbs(); |
| | | $breadcrumbs = BreadcrumbManager::getInstance()->renderNavigation(); |
| | | $afterHeader = apply_filters('jvbBelowHeader', $afterHeader); |
| | | |
| | | if ($afterHeader !== '') { |
| | |
| | | } |
| | | $footerText = jvbRandomFooterText(); |
| | | } |
| | | // jvbDump($beforeHeader,'beforeHeader'); |
| | | // jvbDump('<'.$tag.$this->getClassesAndStyles($block['attrs']).'>','tag'); |
| | | // jvbDump($themeSwitch,'themeSwitch'); |
| | | // jvbDump($this->inside($block, $tag, $content),'inside'); |
| | | // jvbDump($footerText,'footerText'); |
| | | // jvbDump($afterHeader, 'afterheader'); |
| | | // jvbDump($breadcrumbs, 'breadcrumbs'); |
| | | |
| | | return $beforeHeader.'<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'. |
| | | $content = $beforeHeader.'<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'. |
| | | $themeSwitch . |
| | | $this->inside($block, $tag, $content) . |
| | | $this->inside($block, false, $innerContent). |
| | | // $this->innerBlocks($block). |
| | | // $innerContent. |
| | | $footerText.'</'.$tag.'>'.$afterHeader.$breadcrumbs; |
| | | } |
| | | |
| | |
| | | { |
| | | $content = ''; |
| | | foreach ($block['innerBlocks'] as $b) { |
| | | $method = 'render_'.$this->sanitizeBlockName($b); |
| | | $function = BASE.$method; |
| | | |
| | | $content .= $before; |
| | | if (function_exists($function)) { |
| | | $content .= $function($b, ''); |
| | | } else if (method_exists($this, $method)) { |
| | | $content .= $this->$method($b, ''); |
| | | } else { |
| | | $content .= render_block($b); |
| | | } |
| | | $content .= $after; |
| | | $content .= $this->render('', $b); |
| | | } |
| | | return $content; |
| | | } |
| | | |
| | | public function inside(array $block, mixed $tag = false, mixed $o = false):string |
| | | { |
| | | if (!$o) { |
| | | $o = trim($block['innerHTML']); |
| | | } |
| | | if (!$tag) { |
| | | //check to see if there was one dynamically set first |
| | | $tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : ''; |
| | | $tag = ($tag == '') ? str_replace('<', '', strtok($o, '>')) : ''; |
| | | $tag = (str_contains($tag, ' class')) ? strtok($tag, ' class') : $tag; |
| | | $tag = trim($tag); |
| | | } |
| | | if (!str_starts_with($o, '<'.$tag)) { |
| | | public function inside(array $block, mixed $tag = false, mixed $o = false): string |
| | | { |
| | | if (!$o) { |
| | | $o = trim($block['innerHTML']); |
| | | } |
| | | |
| | | $dom = new \DOMDocument(); |
| | | |
| | | @$dom->loadHTML('<?xml encoding="utf-8"?>' . $o, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); |
| | | |
| | | // Find the real outermost element |
| | | $root = null; |
| | | foreach ($dom->childNodes as $node) { |
| | | if ($node->nodeType === XML_ELEMENT_NODE) { |
| | | $root = $node; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (!$root) { |
| | | return $o; |
| | | } |
| | | |
| | | $len = strlen('</'.$tag.'>'); |
| | | // Only enforce tag match if explicitly provided |
| | | if ($tag && strtolower($root->nodeName) !== strtolower($tag)) { |
| | | return $o; |
| | | } |
| | | |
| | | return substr_replace( |
| | | str_replace( |
| | | strtok($o, '>').'>', |
| | | ' ', |
| | | $o |
| | | ), |
| | | '', |
| | | -$len, |
| | | $len |
| | | ); |
| | | } |
| | | $inner = ''; |
| | | foreach ($root->childNodes as $child) { |
| | | $inner .= $dom->saveHTML($child); |
| | | } |
| | | |
| | | return trim($inner); |
| | | } |
| | | |
| | | /** |
| | | * Extract content from a specific nested element |
| | |
| | | //require(JVB_DIR . '/inc/blocks/RegisterBlocks.php'); |
| | | require(JVB_DIR . '/inc/blocks/CustomBlocks.php'); |
| | | |
| | | if (Site::has('feed_block')) { |
| | | require(JVB_DIR . '/inc/blocks/FeedBlock.php'); |
| | | new JVBase\blocks\FeedBlock(); |
| | | |
| | | add_action('init', 'jvb_conditionally_load_blocks', 1); |
| | | function jvb_conditionally_load_blocks(): void |
| | | { |
| | | if (Site::has('feed_block')) { |
| | | require(JVB_DIR . '/inc/blocks/FeedBlock.php'); |
| | | new JVBase\blocks\FeedBlock(); |
| | | } |
| | | |
| | | if (Site::has('is_restaurant')) { |
| | | require(JVB_DIR . '/inc/blocks/MenuBlock.php'); |
| | | new JVBase\blocks\MenuBlock(); |
| | | } |
| | | |
| | | if (Site::has('faq')) { |
| | | require(JVB_DIR . '/inc/blocks/FAQBlock.php'); |
| | | new JVBase\blocks\FAQBlock(); |
| | | } |
| | | |
| | | if (!empty(Registrar::getFeatured('is_glossary'))) { |
| | | require(JVB_DIR . '/inc/blocks/GlossaryBlock.php'); |
| | | new JVBase\blocks\GlossaryBlock(); |
| | | } |
| | | |
| | | if (!empty(Registrar::getFeatured('is_timeline'))) { |
| | | require(JVB_DIR . '/inc/blocks/TimelineBlock.php'); |
| | | new JVBase\blocks\TimelineBlock(); |
| | | } |
| | | |
| | | if (Site::hasIntegration('gmb')) { |
| | | require(JVB_DIR . '/build/gmbreviews/render.php'); |
| | | register_block_type( |
| | | JVB_DIR . '/build/gmbreviews', |
| | | [ |
| | | 'render_callback' => 'jvbRenderGMBReviewsBlock' |
| | | ]); |
| | | } |
| | | // if (jvbSiteUsesFeedBlock()) { |
| | | // register_block_type( |
| | | // JVB_DIR . '/build/feed', |
| | | // [ |
| | | // 'render_callback' => 'jvbRenderFeedBlock' |
| | | // ] |
| | | // ); |
| | | // } |
| | | if (!empty(Registrar::getFeatured('show_directory'))) { |
| | | register_block_type( |
| | | JVB_DIR . '/build/list', |
| | | [ |
| | | 'render_callback' => 'jvbRenderListBlock' |
| | | ] |
| | | ); |
| | | } |
| | | register_block_type( |
| | | JVB_DIR . '/build/drawer-menu', |
| | | ); |
| | | } |
| | | |
| | | if (Site::has('is_restaurant')) { |
| | | require(JVB_DIR . '/inc/blocks/MenuBlock.php'); |
| | | new JVBase\blocks\MenuBlock(); |
| | | } |
| | | |
| | | if (Site::has('faq')) { |
| | | require(JVB_DIR . '/inc/blocks/FAQBlock.php'); |
| | | new JVBase\blocks\FAQBlock(); |
| | | } |
| | | |
| | | if (!empty(Registrar::getFeatured('is_glossary'))) { |
| | | require(JVB_DIR . '/inc/blocks/GlossaryBlock.php'); |
| | | new JVBase\blocks\GlossaryBlock(); |
| | | } |
| | | |
| | | if (!empty(Registrar::getFeatured('is_timeline'))) { |
| | | require(JVB_DIR . '/inc/blocks/TimelineBlock.php'); |
| | | new JVBase\blocks\TimelineBlock(); |
| | | } |
| | | |
| | | require(JVB_DIR . '/inc/blocks/SummaryBlock.php'); |
| | | new JVBase\blocks\SummaryBlock(); |
| | |
| | | ]); |
| | | } |
| | | add_filter('block_categories_all', 'jvbRegisterBlockCategory'); |
| | | |
| | | if (Site::hasIntegration('gmb')) { |
| | | require(JVB_DIR . '/build/gmbreviews/render.php'); |
| | | } |
| | | function jvbRegisterBlocks():void |
| | | { |
| | | if (Site::hasIntegration('gmb')) { |
| | | register_block_type( |
| | | JVB_DIR . '/build/gmbreviews', |
| | | [ |
| | | 'render_callback' => 'jvbRenderGMBReviewsBlock' |
| | | ]); |
| | | } |
| | | // if (jvbSiteUsesFeedBlock()) { |
| | | // register_block_type( |
| | | // JVB_DIR . '/build/feed', |
| | | // [ |
| | | // 'render_callback' => 'jvbRenderFeedBlock' |
| | | // ] |
| | | // ); |
| | | // } |
| | | if (!empty(Registrar::getFeatured('show_directory'))) { |
| | | register_block_type( |
| | | JVB_DIR . '/build/list', |
| | | [ |
| | | 'render_callback' => 'jvbRenderListBlock' |
| | | ] |
| | | ); |
| | | } |
| | | register_block_type( |
| | | JVB_DIR . '/build/drawer-menu', |
| | | ); |
| | | } |
| | | add_action('init', 'jvbRegisterBlocks'); |
| | |
| | | } |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\RoleManager; |
| | | use JVBase\meta\Form; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | |
| | | 'label' => 'Scrapped', |
| | | ] |
| | | ]; |
| | | $permission = ($content === 'news') ? 'update' : $content; |
| | | $permission = JVB_CONTENT[$content]['plural']??$content.'s'; |
| | | |
| | | $canPublish = current_user_can("publish_{$permission}"); |
| | | $permission = JVB()->roles()->getContentPlural($content); |
| | | $canPublish = current_user_can("publish_{$permission}") && current_user_can('skip_moderation'); |
| | | $out = '<div class="filters">'; |
| | | if (!empty($statusFilters)) { |
| | | $out .= '<div class="status">'; |
| | |
| | | */ |
| | | function jvbGetBulkActions(string $content):array |
| | | { |
| | | |
| | | $permission = ($content === 'news') ? 'update' : $content; |
| | | $permission = JVB_CONTENT[$content]['plural']??$content.'s'; |
| | | $bulkActions = [ |
| | | 'publish' => [ |
| | | 'icon' => 'show', |
| | |
| | | 'disabled' => true, |
| | | ] |
| | | ]; |
| | | if (current_user_can("publish_{$permission}")) { |
| | | $permission = JVB()->roles()->getContentPlural($content); |
| | | if (current_user_can('skip_moderation') && current_user_can("publish_{$permission}")) { |
| | | $bulkActions['publish']['disabled'] = false; |
| | | } |
| | | if (current_user_can("edit_{$permission}")) { |
| | |
| | | string $postType = '', |
| | | bool $prefix = false |
| | | ):void { |
| | | switch ($contentType) { |
| | | case 'post': |
| | | $settings = JVB_CONTENT; |
| | | break; |
| | | case 'term': |
| | | $settings = JVB_TAXONOMY; |
| | | break; |
| | | case 'user': |
| | | $settings = JVB_USER; |
| | | break; |
| | | default: |
| | | return; |
| | | } |
| | | $sections = $settings[$postType]['sections']??[]; |
| | | if (empty($sections)) { |
| | | $registrar = Registrar::getInstance($postType); |
| | | if (!$registrar || empty($registrar->getSections())) { |
| | | return; |
| | | } |
| | | |
| | | echo '<div class="container">'; |
| | | $nav = '<nav class="tabs row start" role="tablist">'; |
| | | $i = 1; |
| | | foreach ($sections as $slug => $section) { |
| | | foreach ($registrar->getSections() as $slug => $section) { |
| | | $nav .= '<button type="button" class="tab'; |
| | | |
| | | $ariaActive = 'false'; |
| | |
| | | data-object-id="<?=$ID?>" data-content-type="<?=$postType?>"> |
| | | <?php |
| | | $i = 0; |
| | | foreach ($sections as $slug => $section) { |
| | | foreach ($registrar->getSections() as $slug => $section) { |
| | | $tabName = ($prefix) ? $ID.'-'.$slug : $slug; |
| | | |
| | | $class = ($i == 0) ? ' active' : ''; |
| | |
| | | $types = get_post_meta($postID, BASE.'content_types', true); |
| | | if ($types === '') { |
| | | $types = []; |
| | | $link = (int) get_post_meta($postID, BASE.'link', true); |
| | | $link = (int) get_post_meta($postID, BASE.'profile_link', true); |
| | | if ($link === '' || !is_numeric($link)) { |
| | | return $types; |
| | | } |
| | |
| | | if (!$user) { |
| | | return []; |
| | | } |
| | | $id = (int) get_user_meta($userID, BASE.'link', true); |
| | | $id = (int) get_user_meta($userID, BASE.'profile_link', true); |
| | | |
| | | $meta = Meta::forPost($id); |
| | | $artist = $meta->getAll(['first_name','type','city','shop']); |
| | |
| | | } |
| | | $user = ($ID === 0) ? wp_get_current_user() : get_userdata($ID); |
| | | return array_values(array_intersect( |
| | | array_keys(array_merge(JVB_USER, ['administrator'])), |
| | | array_keys(array_merge(Registrar::getRegistered('user'), ['administrator'])), |
| | | array_map(function ($role) { |
| | | return jvbNoBase($role); |
| | | }, |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Form; |
| | | use JVBase\base\Site; |
| | | use JVBase\utility\Image; |
| | |
| | | </a> |
| | | </li> |
| | | <li class="notifications has-submenu"> |
| | | <button class="toggle notifications" type="button" title="Toggle Notifications" aria-expanded="false" aria-controls="notifications-dropdown"> |
| | | <button class="toggle notifications" type="button" data-action="toggle-submenu" title="Toggle Notifications" aria-expanded="false" aria-controls="notifications-dropdown"> |
| | | <?= jvbIcon('bell', ['title'=> 'No Notifications'])?> |
| | | <?= jvbIcon('bell-ringing', ['title'=> 'New Notifications']) ?> |
| | | <span class="notification-badge" aria-label="Notifications"> |
| | | </span> |
| | | </button> |
| | | <ul class="notifications-preview submenu"> |
| | | <ul class="notifications-preview submenu" id="notifications-dropdown"> |
| | | <li id="view-all"><a href="<?=get_home_url(null, '/dash/notifications/')?>" class="view-all">View All Notifications</a></li> |
| | | </ul> |
| | | <template class="notificationItem"> |
| | |
| | | */ |
| | | function jvbHelpMenu():string |
| | | { |
| | | if (!Site::has('helpMenu')) { |
| | | if (!Site::has('help_menu')) { |
| | | return ''; |
| | | } |
| | | $out = get_option(BASE.'help_menu'); |
| | | |
| | | if ($out === false) { |
| | | $open = '<li><a href="'; |
| | | $mid = '">'; |
| | | $close = '</a></li>'; |
| | | $cache = Cache::for('help_menu'); |
| | | $out = $cache->remember( |
| | | 'help_menu', |
| | | function () { |
| | | $open = '<li><a href="'; |
| | | $mid = '">'; |
| | | $close = '</a></li>'; |
| | | |
| | | $titles = array( |
| | | 'About', |
| | | 'Contact', |
| | | 'Request a Feature', |
| | | 'Technical Issue', |
| | | ); |
| | | $links = ''; |
| | | foreach ($titles as $t) { |
| | | $page = new WP_Query(array( |
| | | 'post_type' => 'page', |
| | | 'title' => $t, |
| | | 'fields' => 'ids' |
| | | )); |
| | | if ($page->have_posts()) { |
| | | $end = ($t == 'About') ? '<li><a href="'.get_home_url(null, '/directory/partners/').'" title="View our Partners">Partners</a></li>' : ''; |
| | | $links .= $open.get_the_permalink($page->posts[0]).'" title="'.$t.$mid.$t.$close.$end; |
| | | } |
| | | wp_reset_postdata(); |
| | | } |
| | | $titles = array( |
| | | 'About', |
| | | 'Contact', |
| | | 'Request a Feature', |
| | | 'Technical Issue', |
| | | ); |
| | | $links = ''; |
| | | foreach ($titles as $t) { |
| | | $page = new WP_Query(array( |
| | | 'post_type' => 'page', |
| | | 'title' => $t, |
| | | 'fields' => 'ids' |
| | | )); |
| | | if ($page->have_posts()) { |
| | | $end = ($t == 'About') ? '<li><a href="'.get_home_url(null, '/directory/partners/').'" title="View our Partners">Partners</a></li>' : ''; |
| | | $links .= $open.get_the_permalink($page->posts[0]).'" title="'.$t.$mid.$t.$close.$end; |
| | | } |
| | | wp_reset_postdata(); |
| | | } |
| | | |
| | | $out = '<li class="has-submenu"> |
| | | <button class="toggle quick-help" type="button" title="Toggle Quick Help Menu" aria-expanded="false" aria-controls="quick-help" aria-label="Toggle Quick Help Menu">'.jvbIcon('question', ['title'=> 'Quick Help']).'</button> |
| | | <ul class="submenu"> |
| | | return '<li class="has-submenu"> |
| | | <button class="toggle quick-help" data-action="toggle-submenu" type="button" title="Toggle Quick Help Menu" aria-expanded="false" aria-controls="quick-help" aria-label="Toggle Quick Help Menu">'.jvbIcon('question', ['title'=> 'Quick Help']).'</button> |
| | | <ul class="submenu" id="quick-help"> |
| | | '.$links.' |
| | | </ul> |
| | | </li>'; |
| | | |
| | | update_option(BASE.'help_menu', $out); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | if (is_user_logged_in()) { |
| | | $out .= '<li><a href="'.wp_logout_url(get_home_url()).'" title="Logout">'.jvbIcon('sign-out').'<span class="screen-reader-text">Logout</span></a></li>'; |
| | |
| | | } |
| | | |
| | | // Get the role from options with proper fallback |
| | | $role = $options['default_role'] ?? get_option(BASE . 'client_import_role', JVB_USER); |
| | | $role = $options['default_role'] ?? get_option(BASE . 'client_import_role', BASE.'client'); |
| | | |
| | | // Ensure role exists, fallback to JVB_USER if not |
| | | // Ensure role exists |
| | | if (!get_role($role)) { |
| | | $role = JVB_USER; |
| | | return new WP_Error('invalid_role', 'Invalid role'); |
| | | } |
| | | |
| | | // Create user |
| | |
| | | // Cloudflare Turnstile uses POST parameters, not headers |
| | | return []; |
| | | } |
| | | |
| | | protected function getApiUrl(string $endpoint, ?string $baseKey = null): string |
| | | { |
| | | // Only used for direct API calls, not through base class methods |
| | | return 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; |
| | | } |
| | | } |
| | |
| | | |
| | | // Set user role (assuming you have a customer role defined) |
| | | $user = new \WP_User($user_id); |
| | | $user->set_role(BASE.'foodie'); // Or whatever role from JVB_USER |
| | | $user->set_role(BASE.'foodie'); // Or whatever role |
| | | |
| | | // Generate password reset key |
| | | $reset_key = get_password_reset_key($user); |
| | |
| | | wp_enqueue_script('jvb-square-checkout'); |
| | | |
| | | wp_localize_script('jvb-square-checkout', 'squareConfig', [ |
| | | 'isOpen' => jvbIsOpen(), |
| | | //TODO 'isOpen' => jvbIsOpen(), |
| | | 'application_id' => $this->credentials['client_id'] ?? '', |
| | | 'location_id' => $this->locationId, |
| | | 'environment' => $this->environment, |
| | |
| | | 'menu_title' => 'JakeVan', |
| | | 'capability' => 'manage_options', |
| | | 'menu_slug' => BASE . 'settings', |
| | | 'icon' => jvbCSSIcon('settings'), |
| | | 'icon' => jvbCSSIcon('gear-six'), |
| | | 'position' => 0 |
| | | ]; |
| | | $this->subpages = get_option(BASE.'adminSubpage', []); |
| | |
| | | |
| | | |
| | | use DateTime; |
| | | use JVBase\base\Site; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\managers\CustomTable; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | |
| | | protected array $tables =[]; |
| | | protected int $expiresAt = 21; //days for request |
| | | protected int $requiredVotes = 3; //Number of votes before a term is approved |
| | | protected bool $hasApproval = false; |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | if (empty($this->tables)) { |
| | | return; |
| | | } |
| | | |
| | | $this->hasApproval = Site::membership() && Site::membership()->has('member_verified'); |
| | | if ($this->hasApproval) { |
| | | add_action('user_register', [$this, 'handleRegistration'], 10, 2); |
| | | } |
| | | |
| | | add_action('jvb_cleanup_expired_approvals', [$this, 'cleanupExpired']); |
| | | } |
| | | |
| | | protected function defineTables():void |
| | |
| | | $types = Registrar::getFeatured('approve_new'); |
| | | foreach ($types as $type) { |
| | | $requests = CustomTable::for("approval_{$type}_requests"); |
| | | $registrar = Registrar::getInstance($type); |
| | | |
| | | $requests->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | |
| | | ]); |
| | | $base = BASE; |
| | | $requests->setConstraints([ |
| | | "CONSTRAINTS `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`) |
| | | "CONSTRAINT `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$requests->getUserTable()}` (`ID`) ON DELETE CASCADE", |
| | | "CONSTRAINTS `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`) |
| | | "CONSTRAINT `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`) |
| | | REFERENCES `{$requests->getTermTable()}` (`term_id`) ON DELETE CASCADE" |
| | | ]); |
| | | $requests->defineTable(); |
| | |
| | | |
| | | $votes->setConstraints([ |
| | | "CONSTRAINT `{$base}{$type}_user_approval_request` FOREIGN KEY (`request_id`) |
| | | REFERENCES `{$requests->getFullTableName()} (`id`) ON DELETE CASCADE", |
| | | REFERENCES `{$requests->getFullTableName()}` (`id`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$votes->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | |
| | | ]; |
| | | } |
| | | } |
| | | /** |
| | | * Handler for user registration |
| | | * |
| | | * @param int $user_id New user ID |
| | | * @param object $user the new user object |
| | | * |
| | | * @return void |
| | | */ |
| | | public function handleRegistration(int $user_id, object $user): void |
| | | { |
| | | $registrar = Registrar::getInstance(jvbUserRole($user_id)); |
| | | |
| | | if ($registrar && $registrar->hasFeature('approve_new')) { |
| | | $user->add_cap('skip_moderation', false); |
| | | $this->createApproval($user_id, $registrar->getSlug(), $user->display_name); |
| | | } |
| | | } |
| | | public function canApprove(int $userID, string $type):bool |
| | | { |
| | | $type = jvbNoBase($type); |
| | |
| | | return $this->response(false, 'Invalid vote'); |
| | | } |
| | | |
| | | |
| | | |
| | | $response = $votes->findOrCreate([ |
| | | 'request_id' => $request_id, |
| | | 'user_id' => $userID |
| | |
| | | if (!$response) { |
| | | return $this->response(false, 'Could not store vote for some reason.'); |
| | | } |
| | | $approvers = $request['approved_by']; |
| | | $rejectors = $request['rejected_By']; |
| | | $both = array_merge($approvers, $rejectors); |
| | | |
| | | //See if the user has already voted |
| | | //If the user is changing their vote, proceed. If it's the same, we can bail early. |
| | | if (in_array($userID, $both)) { |
| | | switch ($vote) { |
| | | case 'reject': |
| | | if (in_array($userID, $approvers)) { |
| | | unset($approvers[array_search($userID, $approvers)]); |
| | | } else { |
| | | return $this->response(true, 'You already voted this way.'); |
| | | } |
| | | break; |
| | | case 'approve': |
| | | if (in_array($userID, $rejectors)) { |
| | | unset($rejectors[array_search($userID, $rejectors)]); |
| | | } else { |
| | | return $this->response(true, 'You already voted this way.'); |
| | | } |
| | | } |
| | | } |
| | | |
| | | switch ($vote) { |
| | | case 'reject': |
| | | $rejectors[] = $userID; |
| | | $request['rejections']++; |
| | | break; |
| | | case 'approve': |
| | | $approvers[] = $userID; |
| | | $request['approvals']++; |
| | | break; |
| | | default: |
| | |
| | | $this->finalizeApproval($request_id, $type); |
| | | } |
| | | $updated = $requests->update([ |
| | | 'rejections'=> $request['rejections'], |
| | | 'approvals' => $request['approvals'] |
| | | 'rejections' => $request['rejections'], |
| | | 'approvals' => $request['approvals'], |
| | | 'rejected_by' => $rejectors, |
| | | 'approved_by' => $approvers |
| | | ], |
| | | [ |
| | | 'request_id' => $request_id |
| | |
| | | $updatedID = $term['term_id']; |
| | | } |
| | | } elseif ($registrar->getType() === 'user') { |
| | | |
| | | $user = get_userdata($request['user_id']); |
| | | $user->add_cap('skip_moderation', true); |
| | | update_user_meta($request['user_id'], BASE.'verification_date', current_time('mysql')); |
| | | } |
| | | |
| | | $updates = [ |
| | |
| | | ]); |
| | | |
| | | |
| | | JVB()->notification()->addNotification($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | JVB()->notification()->notify($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | } |
| | | public function finalizeDenial(int $request_id, string $type):void |
| | | { |
| | |
| | | 'request_id' => $request_id |
| | | ]); |
| | | |
| | | JVB()->notification()->addNotification($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | JVB()->notification()->notify($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.'); |
| | | } |
| | | |
| | | public function getApprovalRequests(int $userID, ?string $type = null):array |
| | |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Create artist approval request |
| | | * |
| | | * @param int $user_id User ID to be approved |
| | | * |
| | | * @return int|false Request ID or false on failure |
| | | */ |
| | | /** |
| | | * Create artist approval request - REFACTORED |
| | | */ |
| | | public function createArtistApprovalRequest(int $user_id): int|false |
| | | { |
| | | $userRole = jvbUserRole($user_id); |
| | | $table = $this->tables[$userRole]['requests']??false; |
| | | if (!$table) { |
| | | return false; |
| | | } |
| | | return $table->transaction(function($table) use ($user_id) { |
| | | // Check for existing request |
| | | $existing = $table->where(['user_id' => $user_id])->first(); |
| | | |
| | | if ($existing) { |
| | | return $existing->id; |
| | | } |
| | | |
| | | $user_data = get_userdata($user_id); |
| | | |
| | | return $table->create([ |
| | | 'user_id' => $user_id, |
| | | 'status' => 'pending', |
| | | 'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')), |
| | | 'current_approvals' => 0, |
| | | 'current_rejections' => 0, |
| | | 'required_approvals' => 3, // From config |
| | | 'approved_by' => json_encode([]), |
| | | 'rejected_by' => json_encode([]), |
| | | ]); |
| | | }); |
| | | } |
| | | |
| | | public function getRequest(int $requestID, string $type):array|false |
| | | { |
| | | $table = $this->requests($type); |
| | | if (!$table) { |
| | | return false; |
| | | } |
| | | return $table->get(['id' => $requestID])??false; |
| | | } |
| | | public function getVotes(int $requestID, string $type):array|false |
| | | { |
| | | $table = $this->votes($type); |
| | | if (!$table) { |
| | | return false; |
| | | } |
| | | return $table->getMany(['request_id' => $requestID])??false; |
| | | } |
| | | public function cleanupExpired():void |
| | | { |
| | | $now = current_time('mysql'); |
| | | foreach ($this->tables as $type => $tables) { |
| | | $tables['requests']->query( |
| | | "UPDATE {table} |
| | | SET status = 'expired' |
| | | WHERE status = 'pending' |
| | | AND expires_at <%s", |
| | | [$now] |
| | | ); |
| | | } |
| | | } |
| | | } |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\ui\CRUDSkeleton; |
| | | |
| | |
| | | $this->skeleton->addCapabilities(['view', 'edit', 'create', 'delete']); |
| | | |
| | | $plural = strtolower($this->registrar->getPlural() ?? $this->content . 's'); |
| | | $canPublish = jvbUserIsVerified() && user_can($this->user_id, "publish_{$plural}"); |
| | | $canPublish = $this->userIsVerified() && user_can($this->user_id, "publish_{$plural}"); |
| | | $this->skeleton->userCanPublish($canPublish); |
| | | |
| | | // Bulk actions |
| | |
| | | add_filter('jvbAdditionalActions', [$this, 'createItem']); |
| | | } |
| | | |
| | | protected function userIsVerified():bool { |
| | | $membership = Site::membership(); |
| | | |
| | | return !($membership && $membership->has('member_verified')) || current_user_can('skip_moderation'); |
| | | } |
| | | /** |
| | | * Setup uploader configuration |
| | | */ |
| | |
| | | $this->cache->flush(); |
| | | $this->user = wp_get_current_user(); |
| | | $this->role = jvbUserRole($this->user->ID); |
| | | $this->userLink = (int)get_user_meta($this->user->ID, BASE.'link', true); |
| | | $this->userLink = (int)get_user_meta($this->user->ID, BASE.'profile_link', true); |
| | | $this->baseURL = get_home_url(null, '/dash'); |
| | | |
| | | add_action('template_redirect', [$this, 'handleRedirects']); |
| | |
| | | <!DOCTYPE html> |
| | | <html <?php language_attributes(); ?>> |
| | | <head> |
| | | <title><?= (array_key_exists('dashboard_title', JVB_SITE)) ? JVB_SITE['dashboard_title'] : 'Dashboard | '.get_bloginfo('name') ?></title> |
| | | <title><?= Site::dashboardTitle()??'Dashboard | '.get_bloginfo('name') ?></title> |
| | | <meta charset="<?php bloginfo('charset'); ?>"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | | <?php |
| | |
| | | |
| | | jvb_register_do_once('buildDirectories', [$this, 'activate']); |
| | | add_action('init', [$this, 'registerDirectories']); |
| | | add_action('render_block', [$this, 'renderBlock'], 998, 3); |
| | | add_filter('render_block', [$this, 'renderBlock'], 998, 3); |
| | | } |
| | | |
| | | public function registerDirectories():void |
| | | { |
| | | |
| | | $singular = (array_key_exists('directory_label', JVB_SITE)) ? JVB_SITE['directory_label'][0] : 'Directory'; |
| | | $plural = (array_key_exists('directory_label', JVB_SITE)) ? JVB_SITE['directory_label'][1] : 'Directories'; |
| | | $singular = !empty(Site::getDirectorySingular())?:'Directory'; |
| | | $plural = !empty(Site::getDirectorySingular())?: 'Directories'; |
| | | $config = [ |
| | | 'labels' => [ |
| | | 'name' => $plural, |
| | |
| | | |
| | | public function referAs($plural = false):string |
| | | { |
| | | if (!empty(JVB_SITE) && array_key_exists('directory_label', JVB_SITE)) { |
| | | return ($plural) ? JVB_SITE['directory_label'][1] : JVB_SITE['directory_label'][0]; |
| | | } |
| | | return ($plural) ? 'Directories' : 'Directory'; |
| | | return ($plural) ? Site::getDirectoryPlural()??'Directories' : Site::getDirectorySingular()??'Directory'; |
| | | } |
| | | |
| | | /***************************************************** |
| | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`unique_share_user` (`list_id`, `user_id`)'], |
| | | ['key'=> 'UNIQUE', 'value' => '`unique_share_email` (`list_id`, `email`'], |
| | | ['key'=> 'UNIQUE', 'value' => '`unique_share_email` (`list_id`, `email`)'], |
| | | '`list_shares` (`list_id`)', |
| | | '`list_user` (`list_id`, `user_id`)', |
| | | '`status_index` (`status`)' |
| | |
| | | { |
| | | $this->source = $source; |
| | | $this->cache = Cache::for('icons_' . $source, WEEK_IN_SECONDS); |
| | | $this->style = (array_key_exists('icons', JVB_SITE) && in_array(JVB_SITE['icons'], $this->styles)) |
| | | ? JVB_SITE['icons'] |
| | | : 'regular'; |
| | | $this->style = Site::icon(); |
| | | |
| | | $this->addMap(); |
| | | |
| | |
| | | */ |
| | | public function get(string $name, array $options = []): string |
| | | { |
| | | if ($name === '') { |
| | | if (empty($name)) { |
| | | //No icon requested |
| | | return ''; |
| | | } |
| | |
| | | if (!$style) { |
| | | $style = $this->style; |
| | | } |
| | | |
| | | $icon = $this->map[$icon] ?? $icon; |
| | | |
| | | // Validate icon exists |
| | | if (!$this->iconExists($icon, $style)) { |
| | | error_log('[IconsManager] Icon not found: ' . $icon); |
| | | return ''; |
| | | } |
| | | $svg = $this->getEncodedSVG($icon, $style); |
| | | if ($svg !== '') { |
| | | return "data:image/svg+xml;base64,{$svg}"; |
| | |
| | | 'name' => 'varchar(255) NOT NULL', |
| | | 'email' => 'varchar(255) NOT NULL', |
| | | 'invitation_token' => 'varchar(255) NOT NULL', |
| | | 'invited_role' => 'varchar(50) NOT NULL COMMENT \'Role being invited to\'', |
| | | 'status' => 'ENUM(\'pending\',\'accepted\',\'rejected\',\'expired\',\'revoked\') DEFAULT \'pending\'', |
| | | 'inviters' => 'JSON NOT NULL COMMENT \'Array of {user_id, invited_at}\'', |
| | | 'invited_role' => 'varchar(50) NOT NULL', //Role being invited to |
| | | 'status' => "ENUM('pending','accepted','rejected','expired','revoked') DEFAULT 'pending'", |
| | | 'inviters' => 'JSON NOT NULL', // Array of {user_id, invited_at} |
| | | 'new_user_id' => $table->getUserIDType().' DEFAULT NULL', |
| | | 'expires_at' => 'datetime NOT NULL', |
| | | 'accepted_at' => 'datetime DEFAULT NULL', |
| | |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE KEY', 'value' => '`unique_email_role` (`email`, `invited_role`)'], |
| | | ['key' => 'UNIQUE', 'value' => '`unique_email_role` (`email`, `invited_role`)'], |
| | | '`token_lookup` (`invitation_token`)', |
| | | '`status_expiry` (`status`, `expires_at`)', |
| | | '`role_status` (`invited_role`, `status`)', |
| | |
| | | protected function defineTable():bool |
| | | { |
| | | $table = CustomTable::for('karma_'.$this->key); |
| | | [$type, $table, $column] = $this->getReferenceTable($table); |
| | | [$type, $referenceTable, $column] = $this->getReferenceTable($table); |
| | | if (!$type) { |
| | | error_log('[KarmaManager]::defineTable Attempted to build reference for invalid table: '.$this->references); |
| | | CustomTable::destroyInstance('karma_'.$this->key); |
| | |
| | | 'item_id' => "{$type} NOT NULL", |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'content' => 'varchar(255) NOT NULL', |
| | | 'vote' => "ENUM('up,'down') NOT NULL", |
| | | 'vote' => "ENUM('up','down') NOT NULL", |
| | | 'created_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | | 'updated_at'=> 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' |
| | | ]); |
| | |
| | | ]); |
| | | $base = BASE; |
| | | $table->setConstraints([ |
| | | "CONSTRAINT `{$base}kt_{$type}_item_id` FOREIGN KEY (`item_id`) |
| | | REFERENCES `{$table}` (`{$column}`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}kt_{$type}_user_id` FOREIGN KEY (`user_id`) |
| | | "CONSTRAINT `{$base}kt_{$this->key}_{$type}_item_id` FOREIGN KEY (`item_id`) |
| | | REFERENCES `{$referenceTable}` (`{$column}`) ON DELETE CASCADE", |
| | | "CONSTRAINT `{$base}kt_{$this->key}_{$type}_user_id` FOREIGN KEY (`user_id`) |
| | | REFERENCES `{$table->getUserTable()}` (`ID`) ON DELETE CASCADE" |
| | | ]); |
| | | $table->defineTable(); |
| | |
| | | use JVBase\forms\TaxonomySelector; |
| | | use JVBase\meta\Form; |
| | | |
| | | use WP_Error; |
| | | use JVBase\registrar\Registrar;use WP_Error; |
| | | use WP_User; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | 'hint' => 'Have a referral code? Paste it here!' |
| | | ]; |
| | | } |
| | | if (count(JVB_USER) > 1) { |
| | | foreach (JVB_USER as $slug => $config) { |
| | | if (!array_key_exists('can_register', $config) || !$config['can_register']) { |
| | | continue; |
| | | } |
| | | $icon = $config['icon'] ?? ''; |
| | | $canRegister = Registrar::getFeatured('can_register', 'user'); |
| | | if (!empty($canRegister)) { |
| | | foreach ($canRegister as $role) { |
| | | $registrar = Registrar::getInstance($role); |
| | | $config = $registrar->getConfig('register'); |
| | | $icon = $registrar->getIcon('user'); |
| | | $icon = ($icon !== '') ? jvbIcon($icon) : ''; |
| | | $select[$slug] = '<span class="label">'.$icon.$config['label'].'</span><span class="text">'.$config['register']['text']??''.'</span>'; |
| | | if (!empty($config['register']['fields']??[])){ |
| | | foreach ($config['register']['fields'] as $field) { |
| | | $select[$role] = '<span class="label">'.$icon.$registrar->getSingular().'</span><span class="text">'.$config['description']??Site::login()->getDescription('register')??''.'</span>'; |
| | | if (!empty($config['fields'])){ |
| | | foreach ($config['fields'] as $field) { |
| | | $field['condition'] = [ |
| | | 'field' => 'user_select', |
| | | 'value' => $slug, |
| | | 'value' => $role, |
| | | 'operator' => '==' |
| | | ]; |
| | | $fields[] = $field; |
| | |
| | | } |
| | | } |
| | | } |
| | | public function loginUrl(string $login_url, string $redirect, bool $force_reauth):string |
| | | public function loginUrl(string $login_url, ?string $redirect, bool $force_reauth):string |
| | | { |
| | | // This will append /custom-login/ to you main site URL as configured in general settings (ie https://domain.com/custom-login/) |
| | | $login_url = site_url( '/login/', 'login' ); |
| | |
| | | */ |
| | | public function handleLinkChange(int $meta_id, int $object_id, string $meta_key, string|null $meta_value = null):void |
| | | { |
| | | // Only proceed for BASE.'link' meta key |
| | | // Only proceed for BASE.'profile_link' meta key |
| | | if ($meta_key !== BASE . 'link') { |
| | | return; |
| | | } |
| | |
| | | return $this->notifications->markActioned($userID, $notification_id, $result); |
| | | } |
| | | |
| | | public function getNotificationTypes(bool $all = false):array |
| | | { |
| | | return $this->notifications->getNotificationTypes($all); |
| | | } |
| | | |
| | | /************************************************ |
| | | * CONTENT NOTIFICATIONS |
| | | * These are pooled notifications of new content for: |
| | |
| | | $this->types = $types; |
| | | } |
| | | |
| | | public function getNotificationTypes(bool $all = false):array |
| | | { |
| | | return ($all) ? $this->types : array_keys($this->types); |
| | | } |
| | | |
| | | private function defineTable():void |
| | | { |
| | | $table = CustomTable::for('notifications_main'); |
| | | |
| | | $typeEnum = implode(',',array_map(function($type) { return '`'.$type.'`';}, array_keys($this->types))); |
| | | $typeEnum = implode(',',array_map(function($type) { return "'{$type}'";}, array_keys($this->types))); |
| | | |
| | | $table->setColumns([ |
| | | 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', |
| | |
| | | '`user_status` (`for_user`, `status`)', |
| | | '`target_lookup` (`target_id`, `target_type`)', |
| | | '`unread_notifications` (`for_user`, `status`, `created_at`)', |
| | | '`requires_action` (`for_user`, `requires_action`, `action_taken`)', |
| | | '`requires_action` (`for_user`, `action_taken`)', |
| | | '`from_user_lookup` (`for_user`, `from_user`, `target_id`, `target_type`, `type`)' |
| | | ]); |
| | | |
| | |
| | | 'referee_reward_type' => 'percentage', // 'percentage' or 'fixed' |
| | | 'referee_reward_amount' => 20, // 20% or $20 |
| | | 'referee_reward_applies_to' => 'first_order', // 'first_order' or 'all_orders' |
| | | 'referral_role' => BASE.'client' |
| | | ]; |
| | | |
| | | protected string $role = BASE.'client'; |
| | | protected string $role; |
| | | |
| | | protected array $settings; |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | $this->role = Site::getDefaultReferralRole(); |
| | | $this->default_settings['referral_role'] = $this->role; |
| | | global $wpdb; |
| | | $this->wpdb = $wpdb; |
| | | $this->cache = Cache::for('referrals', WEEK_IN_SECONDS); |
| | |
| | | update_option(BASE . 'referral_page_id', $page_id); |
| | | |
| | | // Save client import role |
| | | $import_role = sanitize_text_field($post_data[BASE . 'referral_role'] ?? JVB_USER); |
| | | $import_role = sanitize_text_field($post_data[BASE . 'referral_role'] ?? Site::getDefaultReferralRole()); |
| | | update_option(BASE . 'referral_role', $import_role); |
| | | |
| | | // Save reward settings |
| | |
| | | { |
| | | private array $roles; |
| | | private array $content; |
| | | private array $subTypes = []; |
| | | |
| | | public function __construct() |
| | | { |
| | |
| | | Registrar::getFeatured('is_content', 'term') |
| | | )); |
| | | add_action('set_user_role', [$this, 'updateRoles'], 10, 3); |
| | | |
| | | $this->checkRoleSubTypes(); |
| | | } |
| | | |
| | | public function checkRoleSubTypes():void |
| | | { |
| | | foreach ($this->roles as $role) { |
| | | $registrar = Registrar::getInstance($role); |
| | | if ($registrar->getUserSubtype()){ |
| | | $this->subTypes[jvbCheckBase($registrar->getUserSubtype())] = $registrar->getCreatable(); |
| | | } |
| | | } |
| | | if (!empty($this->subTypes)) { |
| | | add_action('set_object_terms', [$this, 'maybeSwitchPermissions'],10, 6); |
| | | } |
| | | } |
| | | |
| | | public function updateRoles(int $userID, string $role, array $oldRoles):void |
| | | { |
| | | if (doing_action('set_user_role') > 1) { |
| | |
| | | $roles = array_keys($user->roles); |
| | | |
| | | foreach ($roles as $role) { |
| | | $role = jvbNoBase($role); |
| | | $registrar = Registrar::getInstance($role); |
| | | if (!$registrar) { |
| | | return false; |
| | |
| | | if (!in_array($type, $this->roles)) { |
| | | return false; |
| | | } |
| | | $link = get_user_meta($user->ID, BASE.'link', true); |
| | | $link = get_user_meta($user->ID, BASE.'profile_link', true); |
| | | if ($link === '') { |
| | | $type = BASE.$type; |
| | | $name = $user->display_name; |
| | |
| | | 'post_author' => $user->ID, |
| | | ]); |
| | | if ($link) { |
| | | update_user_meta($user->ID, BASE.'link', $link); |
| | | update_post_meta($link, BASE.'link', $user->ID); |
| | | update_user_meta($user->ID, BASE.'profile_link', $link); |
| | | update_post_meta($link, BASE.'profile_link', $user->ID); |
| | | } |
| | | } |
| | | return $link; |
| | |
| | | if ($registrar->hasFeature('has_dashboard') ?? false) { |
| | | $admin_role = get_role('administrator'); |
| | | if ($admin_role) { |
| | | $admin_role->add_cap("manage_{$role_name}s", true); |
| | | $admin_role->add_cap("edit_{$role_name}_settings", true); |
| | | $admin_role->add_cap("manage_{$role_name}s"); |
| | | $admin_role->add_cap("edit_{$role_name}_settings"); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | return [ |
| | | "edit_{$content}", |
| | | "edit_published_{$content}", |
| | | "read_{$content}", |
| | | "delete_{$content}", |
| | | "delete_published_{$content}", |
| | | "edit_{$plural}", |
| | | "edit_others_{$plural}", |
| | | "publish_{$plural}", |
| | | "read_private_{$plural}", |
| | | "edit_private_{$plural}", |
| | | "delete_private_{$plural}", |
| | | ]; |
| | | } |
| | | protected function getOthersCapabilities(string $content):array |
| | |
| | | return [ |
| | | "edit_others_{$plural}", |
| | | "delete_others_{$plural}", |
| | | "read_private_{$plural}", |
| | | "edit_private_{$plural}", |
| | | "delete_private_{$plural}", |
| | | ]; |
| | | } |
| | | |
| | |
| | | return str_replace(' ', '_', $content.'s'); |
| | | } |
| | | |
| | | public function activate(): void |
| | | public static function activate(): void |
| | | { |
| | | error_log('[RoleManager]::activate'); |
| | | error_log('Registering roles...'); |
| | | $instance = new self; |
| | | foreach (Registrar::getRegistered('user') as $role) { |
| | | $this->registerRole($role); |
| | | $instance->registerRole($role); |
| | | } |
| | | error_log('Roles registered!'); |
| | | error_log('Removing unneeded roles...'); |
| | | remove_role('contributor'); |
| | | remove_role('author'); |
| | | remove_role('editor'); |
| | | error_log('Roles removed!'); |
| | | |
| | | error_log('Adding Admin Capabilities'); |
| | | $instance->addAdminCaps(); |
| | | error_log('Ensuring Existing User\'s Roles...'); |
| | | $instance->ensureRoleCaps(); |
| | | error_log('Roles activated!'); |
| | | } |
| | | protected function addAdminCaps():void |
| | | { |
| | | $users = get_users(['role' => 'administrator']); |
| | | foreach (array_merge(Registrar::getRegistered('post'), Registrar::getFeatured('is_content')) as $slug) { |
| | | $this->grantRoleCapabilities('administrator', $slug); |
| | | $this->grantRoleOthersCapabilities('administrator', $slug); |
| | | |
| | | foreach ($users as $user) { |
| | | $this->grantContent($user, $slug); |
| | | $this->grantOthersContent($user, $slug); |
| | | } |
| | | } |
| | | } |
| | | protected function ensureRoleCaps():void |
| | | { |
| | | $roles = Registrar::getRegistered('user'); |
| | | foreach ($roles as $role) { |
| | | $registrar = Registrar::getInstance($role); |
| | | $creatable = $registrar->getCreatable(); |
| | | $manageable = $registrar->getManageOthers(); |
| | | if (!empty($creatable) || !empty($manageable)) { |
| | | $users = get_users(['role' => jvbCheckBase($role)]); |
| | | foreach ($users as $user) { |
| | | foreach ($creatable as $slug) { |
| | | $this->grantContent($user, $slug); |
| | | } |
| | | foreach ($manageable as $slug) { |
| | | $this->grantOthersContent($user, $slug); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /****************************************************************** |
| | | * OWNABLE and MANAGABLE terms (ie: tattoo shops) |
| | |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public function maybeSwitchPermissions(int $object_id, array $terms, array $tt_ids, string $taxonomy, bool $append, array $old_tt_ids):void |
| | | { |
| | | //This shouldn't happen, but whatever |
| | | if (empty($this->subTypes)) { |
| | | return; |
| | | } |
| | | |
| | | if (!in_array($taxonomy, array_keys($this->subTypes))) { |
| | | return; |
| | | } |
| | | |
| | | $new = array_diff($tt_ids, $old_tt_ids); |
| | | $old = array_diff($old_tt_ids, $tt_ids); |
| | | |
| | | $userID = (int)get_post_meta($object_id, BASE.'profile_link',true); |
| | | if ($userID === 0) { |
| | | return; |
| | | } |
| | | $user = get_userdata($userID); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | //Revoke old first |
| | | if (!empty($old)) { |
| | | $old = array_filter(array_map(function ($id) use ($taxonomy) { |
| | | $termID = $this->getTermIDFromTTID($id); |
| | | return $this->getRootTermSlug($termID, $taxonomy); |
| | | }, $old)); |
| | | foreach ($old as $slug) { |
| | | if (!array_key_exists($slug, $this->subTypes[$taxonomy])) { |
| | | error_log('[RoleManager]::maybeSwitchPermissions Could not find creatable types for role subtype '.$slug); |
| | | continue; |
| | | } |
| | | foreach ($this->subTypes[$taxonomy][$slug] ?? [] as $s) { |
| | | $this->grantContent($user, $s, false); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (!empty($new)) { |
| | | $new = array_filter(array_map(function ($id) use ($taxonomy){ |
| | | $termID = $this->getTermIDFromTTID($id); |
| | | return $this->getRootTermSlug($termID, $taxonomy); |
| | | }, $new)); |
| | | foreach ($new as $slug) { |
| | | if (!array_key_exists($slug, $this->subTypes[$taxonomy])) { |
| | | error_log('[RoleManager]::maybeSwitchPermissions Could not find creatable types for role subtype '.$slug); |
| | | continue; |
| | | } |
| | | foreach ($this->subTypes[$taxonomy][$slug] ?? [] as $s) { |
| | | $this->grantContent($user, $s); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Helper function to get term_id from term_taxonomy_id |
| | | * @param int $tt_id |
| | | * |
| | | * @return int |
| | | */ |
| | | private function getTermIDFromTTID(int $tt_id):int |
| | | { |
| | | global $wpdb; |
| | | return $wpdb->get_var($wpdb->prepare( |
| | | "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id = %d", |
| | | $tt_id |
| | | )); |
| | | } |
| | | |
| | | private function getRootTermSlug(int $termID, string $taxonomy): string|false |
| | | { |
| | | $term = get_term($termID, $taxonomy); |
| | | if (!$term || is_wp_error($term)) { |
| | | return false; |
| | | } |
| | | while ($term->parent !== 0) { |
| | | $term = get_term($term->parent, $taxonomy); |
| | | if (!$term || is_wp_error($term)) { |
| | | return false; |
| | | } |
| | | } |
| | | return $term->slug; |
| | | } |
| | | } |
| | |
| | | */ |
| | | class SEOAdminPage |
| | | { |
| | | private ConfigManager $config; |
| | | // private SchemaBuilder $registry; |
| | | |
| | | public function __construct() |
| | |
| | | <?php |
| | | //New System |
| | | require(JVB_DIR . '/inc/managers/SEO/render/_setup.php'); |
| | | |
| | | |
| | | //require(JVB_DIR . '/inc/managers/SEO/schemas/_setup.php'); |
| | | // |
| | | //require(JVB_DIR . '/inc/managers/SEO/FieldBuilder.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/FieldOverrideBuilder.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/TypeBuilder.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/SchemaBuilder.php'); |
| | | //require(JVB_DIR.'/base/seo.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/ConfigManager.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/BreadcrumbManager.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/SchemaFieldHelpers.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/SchemaReferenceBuilder.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/TemplateResolver.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/SchemaOutputManager.php'); |
| | | require(JVB_DIR . '/inc/managers/SEO/SEOAdminPage.php'); |
| | | //require(JVB_DIR . '/inc/managers/SEO/SEOAdminPage.php'); |
| | |
| | | return $this->response(false, 'Term does not exist'); |
| | | } |
| | | |
| | | $profile = get_user_meta($userID, BASE.'link', true); |
| | | $profile = get_user_meta($userID, BASE.'profile_link', true); |
| | | if (empty($profile)) { |
| | | return $this->response(false, 'No Profile found'); |
| | | } |
| | |
| | | <?php |
| | | |
| | | use JVBase\managers\ApprovalManager; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\managers\IconsManager; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | require(JVB_DIR . '/inc/managers/UserTermsManager.php'); |
| | | } |
| | | |
| | | if (Site::has('notifications')) { |
| | | require(JVB_DIR . '/inc/managers/Notifications/Content.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/EmailDigests.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/Notifications.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/Preferences.php'); |
| | | require(JVB_DIR . '/inc/managers/NotificationManager.php'); |
| | | if (!empty(Registrar::getFeatured('approve_new'))) { |
| | | require(JVB_DIR . '/inc/managers/ApprovalManager.php'); |
| | | } |
| | | |
| | | |
| | | $membership = Site::membership(); |
| | | if ($membership) { |
| | | if ($membership->has('notifications')) { |
| | | require(JVB_DIR . '/inc/managers/Notifications/Content.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/EmailDigests.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/Notifications.php'); |
| | | require(JVB_DIR . '/inc/managers/Notifications/Preferences.php'); |
| | | require(JVB_DIR . '/inc/managers/NotificationManager.php'); |
| | | } |
| | | if ($membership->has('forum') && !empty(Registrar::getFeatured('is_content', 'term'))) { |
| | | require(JVB_DIR . '/inc/managers/NewsRelationships.php'); |
| | | } |
| | |
| | | if (Site::has('referrals')) { |
| | | require(JVB_DIR . '/inc/managers/ReferralManager.php'); |
| | | } |
| | | |
| | | if (!empty(Registrar::getFeatured('karma'))) { |
| | | require(JVB_DIR . '/inc/managers/KarmaManager.php'); |
| | | } |
| | | if (Site::has('favourites')) { |
| | | require(JVB_DIR . '/inc/managers/FavouritesManager.php'); |
| | | } |
| | | } |
| | | |
| | | add_action('init', 'jvb_load_karma', 1); |
| | | function jvb_load_karma():void |
| | | { |
| | | } |
| | | require(JVB_DIR . '/inc/managers/ErrorHandler.php'); |
| | | require(JVB_DIR . '/inc/managers/queue/_setup.php'); |
| | |
| | | require(JVB_DIR . '/inc/meta/Registry.php'); |
| | | require(JVB_DIR . '/inc/meta/Sanitizer.php'); |
| | | |
| | | //OLD SYSTEM |
| | | // require(JVB_DIR . '/inc/meta/MetaManager.php'); |
| | |
| | | |
| | | 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.'.') |
| | | }; |
| | | $method = 'add'.implode('',array_map('ucfirst',explode('_', $name))).'Field'; |
| | | if (method_exists($this, $method)) { |
| | | $this->$method(); |
| | | } else { |
| | | error_log('[Field]addCommon: No configuration found for '.$name.'.'); |
| | | } |
| | | return $this; |
| | | } |
| | | |
| | | protected function addWikiField():void |
| | | protected function addWikiField(?string $label = null):void |
| | | { |
| | | $this->addField( |
| | | 'wiki', |
| | | [ |
| | | 'type' => 'url', |
| | | 'label' => 'Wikipedia Page', |
| | | 'label' => $label ?: 'Wikipedia Page', |
| | | 'description' => 'For the schema', |
| | | 'quickEdit' => true, |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addLinksField():void |
| | | protected function addLinksField(?string $label = null):void |
| | | { |
| | | $this->addField( |
| | | 'links', |
| | |
| | | 'type' => 'repeater', |
| | | 'quickEdit' => true, |
| | | 'add_label' => 'title', |
| | | 'label' => 'Online Links', |
| | | 'label' => $label ?:'Online Links', |
| | | 'description' => 'These are listed publicly on the website', |
| | | 'fields' => [ |
| | | 'url' => [ |
| | |
| | | 'type' => 'text', |
| | | 'label' => 'Tracker', |
| | | 'description' => 'If you are set up to track link referrals, add what comes after the ? here.', |
| | | 'default' => 'ref=edmonton_ink' |
| | | ], |
| | | ], |
| | | 'section' => 'contact' |
| | |
| | | ); |
| | | } |
| | | |
| | | protected function addReviewField():void |
| | | protected function addReviewField(?string $label = null):void |
| | | { |
| | | $this->addField( |
| | | 'reviews', |
| | | [ |
| | | 'type' => 'repeater', |
| | | 'add_label' => 'name', |
| | | 'label' => 'Reviews', |
| | | 'label' => $label ?: 'Reviews', |
| | | 'fields' => [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | |
| | | ); |
| | | } |
| | | |
| | | protected function addAlternateName():void |
| | | protected function addAlternateNameField():void |
| | | { |
| | | $this->addField( |
| | | 'alternate_name', |
| | |
| | | ] |
| | | ); |
| | | } |
| | | protected function addKeywords():void |
| | | protected function addKeywordsField():void |
| | | { |
| | | $this->addField( |
| | | 'keywords', |
| | |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addOutsidePhotoField():void |
| | | { |
| | | $this->addField( |
| | | 'outside_photo', |
| | | [ |
| | | 'type' => 'image', |
| | | 'limit' => 1, |
| | | 'label' => __('Outside Photo', 'jvb') |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addSloganField():void |
| | | { |
| | | $this->addField( |
| | | 'slogan', |
| | | [ |
| | | 'type' => 'text', |
| | | 'label' => __('Tagline or Slogan', 'jvb') |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addPaymentField():void |
| | | { |
| | | $this->addField( |
| | | 'payment_accepted', |
| | | [ |
| | | 'type' => 'set', |
| | | 'label' => __('Payment Accepted', 'jvb'), |
| | | 'options' => [ |
| | | 'Cash' => 'Cash', |
| | | 'Credit Card' => 'Credit Card', |
| | | 'Debit' => 'Debit', |
| | | 'Google Pay' => 'Google Pay', |
| | | 'Apple Pay' => 'Apple Pay', |
| | | 'PayPal' => 'PayPal', |
| | | 'Interac' => 'Interac', |
| | | 'AMEX' => 'AMEX', |
| | | ], |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | protected function addAmenitiesField():void |
| | | { |
| | | $this->addField( |
| | | 'amenities', |
| | | [ |
| | | 'type' => 'set', |
| | | 'label' => __('Amenities', 'jvb'), |
| | | 'options' => [ |
| | | 'Wheelchair Accessible' => 'Wheelchair Accessible', |
| | | 'Free Parking' => 'Free Parking', |
| | | 'Private Rooms' => 'Private Rooms', |
| | | 'Air Conditioning' => 'Air Conditioning', |
| | | 'WiFi' => 'WiFi', |
| | | 'Gender Neutral Restroom' => 'Gender Neutral Restroom', |
| | | 'LGBTQ+ Friendly' => 'LGBTQ+ Friendly', |
| | | 'Sterilization Room' => 'Sterilization Room', |
| | | 'Refreshments Available' => 'Refreshments Available', |
| | | 'Street Level Access' => 'Street Level Access', |
| | | 'Single Use Needles' => 'Single Use Needles', |
| | | 'Consultation Room' => 'Consultation Room', |
| | | 'Aftercare Products Available' => 'Aftercare Products Available', |
| | | 'Walk-Ins Welcome' => 'Walk-Ins Welcome', |
| | | ] |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | |
| | | protected string $type; |
| | | protected string $singular; |
| | | protected string $plural; |
| | | protected string $profile; |
| | | protected string $description =''; |
| | | protected Fields $fields; |
| | | protected array $sections = []; |
| | |
| | | * @var array slugs of other user roles this role can manage |
| | | */ |
| | | protected array $manage_others = []; |
| | | /** |
| | | * @var string The slug of the taxonomy that defines different access points |
| | | * Example, for edmonton.ink, we have artist_type, and $this->>can_create would be: |
| | | * [ |
| | | * 'piercer' => ['piercings', 'artwork', 'events'], |
| | | * 'tattoo_artist'=> ['tattoos', 'artwork', 'events'], |
| | | * 'artist' => ['artwork'] |
| | | * ] |
| | | */ |
| | | protected string $user_subtype; |
| | | |
| | | /** Configs **/ |
| | | protected Breadcrumbs $breadcrumbs; |
| | |
| | | // $this->initClasses(); |
| | | $this->setFields(); |
| | | |
| | | add_action('init', [$this, 'register'], 0); |
| | | add_action('init', [$this, 'register'], 2); |
| | | add_filter('jvbDashboardPage', [$this, 'renderDashPage'], 10, 3); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | if ($this->karma) { |
| | | $this->karmaManager = KarmaManager::for($this->slug); |
| | | $this->karmaManager = KarmaManager::for($this->slug, $this->type); |
| | | } |
| | | } |
| | | public static function getInstance(string $slug):Registrar|false |
| | |
| | | * FLAGGED FEATURES |
| | | *****************************************************************/ |
| | | |
| | | public function profile(?string $slug = null, ?string $singular = null, ?string $plural = null):self |
| | | { |
| | | if (!$slug) { |
| | | $slug = $this->slug.'_profile'; |
| | | } |
| | | if (!$singular) { |
| | | $singular = $this->singular; |
| | | } |
| | | if (!$plural) { |
| | | $plural = $this->plural; |
| | | } |
| | | $this->profile_link = true; |
| | | |
| | | $this->profile = $slug; |
| | | |
| | | return Registrar::forPost($slug, $singular, $plural); |
| | | } |
| | | public function getProfile():self|false |
| | | { |
| | | if (!$this->profile_link) { |
| | | return false; |
| | | } |
| | | return self::getInstance($this->profile); |
| | | } |
| | | |
| | | public function setUserSubtype(string $type):self |
| | | { |
| | | $this->user_subtype = sanitize_text_field($type); |
| | | return $this; |
| | | } |
| | | public function getUserSubtype():string|false |
| | | { |
| | | return $this->user_subtype?:false; |
| | | } |
| | | } |
| | |
| | | require_once(JVB_DIR . '/inc/registrar/config/Directory.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/Feed.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/SEO.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/Section.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/Integration.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/Register.php'); |
| | | require_once(JVB_DIR . '/inc/registrar/config/seo/_setup.php'); |
| | | |
| | | require_once(JVB_DIR . '/inc/registrar/fields/Field.php'); |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\registrar\config; |
| | | |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | final class Register extends Config { |
| | | public string $title; |
| | | public string $description; |
| | | public array $fields = [ |
| | | 'username' => [ |
| | | 'type' => 'email', |
| | | 'label' => 'Your Email', |
| | | 'required' => true, |
| | | ], |
| | | 'password' => [ |
| | | 'type' => 'text', |
| | | 'subtype' => 'password', |
| | | 'label' => 'Your Password', |
| | | ], |
| | | 'password_check' => [ |
| | | 'type' => 'text', |
| | | 'subtype' => 'password', |
| | | 'label' => 'Verify Password', |
| | | ], |
| | | ]; |
| | | |
| | | public function __construct() { |
| | | } |
| | | |
| | | public function setTitle(string $title): self { |
| | | $this->title = $title; |
| | | return $this; |
| | | } |
| | | public function getTitle():string |
| | | { |
| | | return $this->title; |
| | | } |
| | | |
| | | public function setDescription(string|array $desc):self |
| | | { |
| | | if (is_array($desc)) { |
| | | $desc = implode('', array_map(function ($p) { return '<p>'.$p.'</p>';}, $desc)); |
| | | } else if (!str_contains('<p>', $desc)) { |
| | | $desc = '<p>'.$desc.'</p>'; |
| | | } |
| | | $this->description = $desc; |
| | | return $this; |
| | | } |
| | | public function getDescription():string|array |
| | | { |
| | | return $this->description; |
| | | } |
| | | |
| | | public function setFields(array $fields):self |
| | | { |
| | | $this->fields = array_merge($this->fields, $fields); |
| | | return $this; |
| | | } |
| | | |
| | | public function getFields():array |
| | | { |
| | | return $this->fields; |
| | | } |
| | | |
| | | function getConfig(): array |
| | | { |
| | | return [ |
| | | 'title' => $this->title ?? false, |
| | | 'description' => $this->description??false, |
| | | 'fields' => $this->fields |
| | | ]; |
| | | } |
| | | } |
| | |
| | | protected string $description = ''; |
| | | protected string $icon = ''; |
| | | protected array $fields = []; |
| | | protected Registrar $registrar; |
| | | |
| | | public function __construct(string $title) { |
| | | public function __construct(string $title, Registrar $registrar) { |
| | | $this->title = $title; |
| | | $this->slug = sanitize_title($title); |
| | | $this->registrar = $registrar; |
| | | } |
| | | |
| | | public function setTitle(string $title): self { |
| | |
| | | |
| | | protected function checkFields(string|array $fields):string|array |
| | | { |
| | | $allFields = Registrar::getInstance($this->slug)->getFields(); |
| | | $allFields = $this->registrar->getFields(); |
| | | if (is_array($fields)){ |
| | | foreach ($fields as $index =>$fieldName){ |
| | | if (!array_key_exists($fieldName, $allFields)){ |
| | |
| | | |
| | | $class = JVB()->schemaHelper()::classFromConfig($config, $meta); |
| | | $class->setAuthor(JVB()->seo()->getCreator(true)); |
| | | |
| | | $class = apply_filters('jvb_single_'.$this->slug.'_schema_output', $class, $ID); |
| | | return $class->outputSchema(); |
| | | } |
| | | ); |
| | |
| | | protected bool $quill; // whether to use quill |
| | | protected int $maxLength; // of characters |
| | | protected string $subtype; |
| | | protected array $condition; |
| | | protected array $allowedSubtype = ['text', 'url','number','tel','email','number']; |
| | | /** |
| | | * @var ?bool For timeline post types. Indicates whether all posts get this field, or just the parent |
| | |
| | | $method = 'set'.implode('',array_map('ucfirst',explode('_', $key)));; |
| | | $class->$method($value); |
| | | } else { |
| | | error_log('Instance: '.print_r($class, true)); |
| | | // error_log('Instance: '.print_r($class, true)); |
| | | error_log('[JVBase\registrar\Field] Invalid key for '.$name.': '.$key); |
| | | } |
| | | } |
| | |
| | | return $this->hint; |
| | | } |
| | | |
| | | protected function setType(string $type):void{ |
| | | public function setType(string $type):void{ |
| | | $allowed = array_keys(MetaTypeManager::getTypes()); |
| | | if (!in_array($type, $allowed)) { |
| | | error_log('[JVBase\registrar\Field] Invalid type attempted '.$type); |
| | |
| | | $this->type = $type; |
| | | } |
| | | |
| | | protected function setLabel(string $label):void{ |
| | | public function setLabel(string $label):void{ |
| | | $this->label = $label; |
| | | } |
| | | protected function setRequired(bool $required):void{ |
| | | public function setRequired(bool $required):void{ |
| | | $this->required = $required; |
| | | } |
| | | protected function setHidden(bool $hidden):void{ |
| | | public function setHidden(bool $hidden):void{ |
| | | $this->hidden = $hidden; |
| | | } |
| | | protected function setQuickEdit(bool $quickEdit):void{ |
| | | public function setQuickEdit(bool $quickEdit):void{ |
| | | $this->quickEdit = $quickEdit; |
| | | } |
| | | protected function setDefault(mixed $default):void |
| | | public function setDefault(mixed $default):void |
| | | { |
| | | $this->default = $default; |
| | | } |
| | | |
| | | protected function setQuill(bool $quill):void |
| | | public function setQuill(bool $quill):void |
| | | { |
| | | $this->quill = $quill; |
| | | } |
| | |
| | | return $this->section??null; |
| | | } |
| | | |
| | | protected function setMaxLength(int $maxLength):void |
| | | public function setMaxLength(int $maxLength):void |
| | | { |
| | | $this->maxLength = $maxLength; |
| | | } |
| | |
| | | { |
| | | return $this->subtype; |
| | | } |
| | | |
| | | public function setCondition(array $condition):void |
| | | { |
| | | $required = ['field', 'operator', 'value']; |
| | | foreach ($required as $field) { |
| | | if (!array_key_exists($field, $condition)) { |
| | | error_log('[Field]::setCondition Required condition '.$field.' not found'); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | $this->condition = $condition; |
| | | } |
| | | public function getCondition(array $condition):array |
| | | { |
| | | return $this->condition; |
| | | } |
| | | } |
| | |
| | | protected string $add_label = 'Add Row'; |
| | | protected string $row_label = 'New Item'; |
| | | |
| | | public function setAdd_label(string $label):void |
| | | public function setAddLabel(string $label):void |
| | | { |
| | | $this->add_label = $label; |
| | | } |
| | | public function getAdd_label():string |
| | | public function getAddLabel():string |
| | | { |
| | | return $this->add_label; |
| | | } |
| | |
| | | } |
| | | return true; |
| | | } |
| | | public function setRow_label(string $label):void |
| | | public function setRowLabel(string $label):void |
| | | { |
| | | if (str_contains($label, '{{')) { |
| | | if (!$this->checkFieldVariables($label)) { |
| | |
| | | |
| | | $this->row_label = $label; |
| | | } |
| | | public function getRow_label():string |
| | | public function getRowLabel():string |
| | | { |
| | | return $this->row_label; |
| | | } |
| | |
| | | 'max_participants' => [ |
| | | 'type' => 'number', |
| | | 'label' => __('Maximum Participants', 'jvb'), |
| | | 'bulkEdit'=> true, |
| | | ], |
| | | 'is_free' => [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Free Event?', |
| | | 'default' => 1, |
| | | 'bulkEdit'=> true, |
| | | ], |
| | | 'cost' => [ |
| | | 'type' => 'number', |
| | |
| | | 'operator' => '!=', |
| | | 'value' => 1 |
| | | ], |
| | | 'bulkEdit'=> true, |
| | | ], |
| | | 'ticket_url' => [ |
| | | 'type' => 'url', |
| | |
| | | 'type' => 'true_false', |
| | | 'label' => 'Drop-ins Welcome?', |
| | | 'default' => true, |
| | | 'bulkEdit'=> true, |
| | | ], |
| | | 'recurrence' => [ |
| | | 'type' => 'group', |
| | |
| | | |
| | | $table->setKeys([ |
| | | ['key' => 'PRIMARY', 'value' => '(`id`)'], |
| | | ['key' => 'UNIQUE', 'value' => '(`profile_id`, `term_id`, `start_date`)'], |
| | | ['key' => 'UNIQUE', 'value' => 'profile_term_date (`profile_id`, `term_id`, `start_date`)'], |
| | | 'content_role (`term_id`, `role`)', |
| | | 'user_id (`user_id`)', |
| | | 'profile_id (`profile_id`)', |
| | |
| | | return; |
| | | } |
| | | |
| | | $user = get_post_meta($object_id, BASE.'link', true); |
| | | $user = get_post_meta($object_id, BASE.'profile_link', true); |
| | | if (empty($author)) { |
| | | $user = get_post($object_id)->post_author??false; |
| | | if (!$user) { |
| | |
| | | exit; |
| | | } |
| | | |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\managers\CustomTable; |
| | | class MakeVerification { |
| | | protected CustomTable $table; |
| | |
| | | { |
| | | // $this->defineTables(); |
| | | } |
| | | |
| | | } |
| | |
| | | } |
| | | |
| | | //get the user's profile id |
| | | $profileID = get_user_meta($userID, BASE.'link', true); |
| | | $profileID = get_user_meta($userID, BASE.'profile_link', true); |
| | | if (!$profileID) { |
| | | return false; |
| | | } |
| | |
| | | |
| | | protected function checkContent(string $content, bool $bool = false):string|bool |
| | | { |
| | | $result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??''; |
| | | if ($bool) { |
| | | return $result !== ''; |
| | | } |
| | | return $result; |
| | | return (bool)Registrar::getInstance($content); |
| | | } |
| | | |
| | | |
| | |
| | | parent::__construct(); |
| | | |
| | | $this->initTypes(); |
| | | |
| | | if ($this->hasMemberApproval) { |
| | | add_action('user_register', [$this, 'handleNewUserRegistration'], 10, 2); |
| | | } |
| | | |
| | | add_action('jvb_cleanup_expired_approvals', [$this, 'cleanupExpiredApprovals']); |
| | | } |
| | | |
| | | protected function initTypes():void |
| | | { |
| | | $this->userTypes = []; |
| | | $this->termTypes = []; |
| | | $this->allTypes = []; |
| | | if ($this->hasMemberApproval) { |
| | | $this->userTypes = Registrar::getFeatured('approve_new', 'user'); |
| | | $this->allTypes = $this->userTypes; |
| | |
| | | ->register(); |
| | | } |
| | | |
| | | /** |
| | | * Handler for user registration |
| | | * |
| | | * @param int $user_id New user ID |
| | | * @param object $user the new user object |
| | | * |
| | | * @return void |
| | | */ |
| | | public function handleNewUserRegistration(int $user_id, object $user): void |
| | | { |
| | | $intersect = array_intersect( |
| | | array_map(fn($role) => BASE.$role, $this->userTypes), |
| | | (array) $user->roles |
| | | ); |
| | | |
| | | if (!empty($intersect)) { |
| | | $user->add_cap('skip_moderation', false); |
| | | $this->createArtistApprovalRequest($user_id); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * @param WP_REST_Request $request |
| | |
| | | */ |
| | | protected function handleVote(string $type, string $vote, int $request_id, int $user_id, string $notes = ''): bool |
| | | { |
| | | if (!in_array($vote, ['approve', 'reject'])) { |
| | | if (!in_array($vote, ['approve', 'reject', 'dismiss'])) { |
| | | return false; |
| | | } |
| | | |
| | | $requestTable = $this->getTableName($type, 'requests'); |
| | | $voteTable = $this->getTableName($type, 'votes'); |
| | | |
| | | $requests = CustomTable::for($requestTable); |
| | | $votes = CustomTable::for($voteTable); |
| | | |
| | | try { |
| | | return $requests->transaction(function($requests) use ($votes, $request_id, $user_id, $vote, $notes, $type) { |
| | | // Get the approval request |
| | | $request = $requests->where(['id' => $request_id])->first(); |
| | | |
| | | if (!$request || $request->status !== 'pending') { |
| | | throw new Exception("Invalid approval request"); |
| | | } |
| | | |
| | | // Check if user already voted |
| | | $existingVote = $votes->where([ |
| | | 'request_id' => $request_id, |
| | | 'user_id' => $user_id |
| | | ])->first(); |
| | | |
| | | if ($existingVote) { |
| | | if ($existingVote->vote !== $vote) { |
| | | // Update vote |
| | | $votes->where(['id' => $existingVote->id]) |
| | | ->updateResults(['vote' => $vote]); |
| | | return true; |
| | | } |
| | | throw new Exception("User has already voted on this request"); |
| | | } |
| | | |
| | | // Insert new vote |
| | | $votes->create([ |
| | | 'request_id' => $request_id, |
| | | 'user_id' => $user_id, |
| | | 'vote' => $vote, |
| | | 'notes' => $notes, |
| | | ]); |
| | | |
| | | // Update request based on vote type |
| | | $user = get_userdata($user_id); |
| | | |
| | | if ($vote === 'approve') { |
| | | $this->handleApproval($requests, $request, $request_id, $user, $type); |
| | | } else { |
| | | $this->handleRejection($requests, $request, $request_id, $user, $type); |
| | | } |
| | | |
| | | return true; |
| | | }); |
| | | } catch (Exception $e) { |
| | | $this->logError('handleVote', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id, |
| | | 'request_id' => $request_id, |
| | | 'vote' => $vote |
| | | ]); |
| | | return false; |
| | | } |
| | | $result = JVB()->approvals()->markApproval($request_id, $user_id, $type, $vote, $notes); |
| | | return $result['success']; |
| | | } |
| | | |
| | | /** |
| | | * Handle approval vote logic |
| | | */ |
| | | protected function handleApproval(CustomTable $table, object $request, int $request_id, $user, string $type): void |
| | | { |
| | | $approvers = json_decode($request->approved_by, true) ?: []; |
| | | $approvers[$user->ID] = [ |
| | | 'name' => $user->display_name, |
| | | 'voted' => current_time('mysql') |
| | | ]; |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'current_approvals' => $request->current_approvals + 1, |
| | | 'approved_by' => json_encode($approvers), |
| | | 'expires_at' => $this->rebuildExpiryDate() |
| | | ]); |
| | | |
| | | // Check if threshold met |
| | | if ($request->current_approvals + 1 >= $request->required_approvals) { |
| | | match ($type) { |
| | | 'term' => $this->makeTermLive($request), |
| | | default => $this->completeVerification($request_id, $type), |
| | | }; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle rejection vote logic |
| | | */ |
| | | protected function handleRejection(CustomTable $table, object $request, int $request_id, $user, string $type): void |
| | | { |
| | | $rejecters = json_decode($request->rejected_by, true) ?: []; |
| | | $rejecters[$user->ID] = [ |
| | | 'name' => $user->display_name, |
| | | 'voted' => current_time('mysql') |
| | | ]; |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'current_rejections' => $request->current_rejections + 1, |
| | | 'rejected_by' => json_encode($rejecters), |
| | | 'expires_at' => $this->rebuildExpiryDate() |
| | | ]); |
| | | |
| | | // Check if threshold met |
| | | if ($request->current_rejections + 1 >= $request->required_approvals) { |
| | | match ($type) { |
| | | 'term' => $this->makeTermUnalive($request), |
| | | default => $this->denyVerification($request_id, $type), |
| | | }; |
| | | } |
| | | } |
| | | protected function rebuildExpiryDate() |
| | | { |
| | | return date('Y-m-d H:i:s', strtotime("+{$this->expiryDays} days", time())); |
| | | } |
| | | |
| | | /** |
| | | * @param string $type user/artist or term |
| | | * @param array $request |
| | | * |
| | | * @return bool|int |
| | | */ |
| | | protected function createApprovalRequest(string $type, array $request): int |
| | | { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | |
| | | $id = CustomTable::for($tableName)->create($request); |
| | | |
| | | if (!$id) { |
| | | throw new Exception('Failed to create approval request'); |
| | | } |
| | | |
| | | return $id; |
| | | } |
| | | |
| | | /************* |
| | | * Artist Approvals |
| | | ************/ |
| | | /** |
| | | * Create artist approval request |
| | | * |
| | | * @param int $user_id User ID to be approved |
| | | * |
| | | * @return int|false Request ID or false on failure |
| | | */ |
| | | /** |
| | | * Create artist approval request - REFACTORED |
| | | */ |
| | | public function createArtistApprovalRequest(int $user_id): int|false |
| | | { |
| | | $userRole = jvbUserRole($user_id); |
| | | $tableName = $this->getTableName($userRole, 'requests'); |
| | | $table = CustomTable::for($tableName); |
| | | |
| | | try { |
| | | return $table->transaction(function($table) use ($user_id) { |
| | | // Check for existing request |
| | | $existing = $table->where(['user_id' => $user_id])->first(); |
| | | |
| | | if ($existing) { |
| | | return $existing->id; |
| | | } |
| | | |
| | | $user_data = get_userdata($user_id); |
| | | |
| | | return $table->create([ |
| | | 'user_id' => $user_id, |
| | | 'status' => 'pending', |
| | | 'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')), |
| | | 'current_approvals' => 0, |
| | | 'current_rejections' => 0, |
| | | 'required_approvals' => 3, // From config |
| | | 'approved_by' => json_encode([]), |
| | | 'rejected_by' => json_encode([]), |
| | | ]); |
| | | }); |
| | | } catch (Exception $e) { |
| | | $this->logError('createArtistApprovalRequest', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id |
| | | ]); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Mark an artist as verified |
| | | * |
| | | * @param int $user_id The user to verify |
| | | * @param int $verified_by ID of user who verified them (optional) |
| | | * |
| | | * @return bool Success status |
| | | */ |
| | | public function verifyArtist(int $user_id, int $verified_by = 0):bool |
| | | { |
| | | $user = get_userdata($user_id); |
| | | |
| | | // Check if user has the artist role |
| | | if (!array_intersect(array_map(function ($role) { return BASE.$role; }, $this->userTypes), $user->roles)) { |
| | | return false; |
| | | } |
| | | |
| | | // Add the capability |
| | | $user->add_cap('skip_moderation', true); |
| | | |
| | | // Store verification metadata |
| | | update_user_meta($user_id, BASE . 'verification_date', current_time('mysql')); |
| | | if ($verified_by) { |
| | | update_user_meta($user_id, BASE . 'verified_by', $verified_by); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Mark an artist as verified |
| | | * |
| | | * @param int $user_id The user to verify |
| | | * @param int $verified_by ID of user who verified them (optional) |
| | | * |
| | | * @return bool Success status |
| | | */ |
| | | public function unverifyArtist(int $user_id, int $verified_by = 0):bool |
| | | { |
| | | $user = get_userdata($user_id); |
| | | |
| | | // Check if user has the artist role |
| | | if (!array_intersect(array_map(function ($role) { return BASE.$role; }, $this->userTypes), $user->roles)) { |
| | | return false; |
| | | } |
| | | |
| | | // Add the capability |
| | | $user->add_cap('skip_moderation', false); |
| | | |
| | | // Store verification metadata |
| | | update_user_meta($user_id, BASE . 'unverification_date', current_time('mysql')); |
| | | if ($verified_by) { |
| | | update_user_meta($user_id, BASE . 'unverified_by', $verified_by); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Record an approval vote for an artist |
| | | * |
| | | * @param int $user_id User casting the approval vote |
| | |
| | | return $this->handleVote(jvbUserRole($user_id), $vote, $request_id, $user_id, $notes); |
| | | } |
| | | |
| | | /** |
| | | * Complete verification - REFACTORED |
| | | */ |
| | | protected function completeVerification(int $request_id, string $type = 'artist'): void |
| | | { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | $table = CustomTable::for($tableName); |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'status' => 'approved', |
| | | 'approved_at' => current_time('mysql') |
| | | ]); |
| | | |
| | | $request = $table->where(['id' => $request_id])->first(); |
| | | |
| | | if ($request && $request->user_id) { |
| | | $user = new \WP_User($request->user_id); |
| | | $user->add_cap('skip_moderation', true); |
| | | |
| | | JVB()->notification()->addNotification( |
| | | $request->user_id, |
| | | 'approval_granted', |
| | | ['message' => 'Your account has been verified!'] |
| | | ); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | protected function denyVerification(int $request_id, string $type = 'artist'): void |
| | | { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | $table = CustomTable::for($tableName); |
| | | |
| | | $table->where(['id' => $request_id])->updateResults([ |
| | | 'status' => 'rejected', |
| | | 'rejected_at' => current_time('mysql') |
| | | ]); |
| | | |
| | | $request = $table->where(['id' => $request_id])->first(); |
| | | |
| | | if ($request && $request->user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $request->user_id, |
| | | 'approval_denied', |
| | | ['message' => 'Your verification request was not approved.'] |
| | | ); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | } |
| | | |
| | | /** |
| | | * Get verification details for a request |
| | |
| | | */ |
| | | public function getVerificationDetails(int $requestID, string $type): array|false |
| | | { |
| | | $requestTable = CustomTable::for($this->getTableName($type, 'requests')); |
| | | $voteTable = CustomTable::for($this->getTableName($type, 'votes')); |
| | | |
| | | $request = $requestTable->where(['id' => $requestID])->first(ARRAY_A); |
| | | |
| | | $request = JVB()->approvals()->getRequest($requestID, $type); |
| | | if (!$request) { |
| | | return false; |
| | | } |
| | | |
| | | // Get the votes for this request |
| | | $votes = $voteTable |
| | | ->where(['request_id' => $request['id']]) |
| | | ->orderBy('created_at', 'ASC') |
| | | ->getResults(ARRAY_A); |
| | | $votes = JVB()->approvals()->getVotes($requestID, $type); |
| | | |
| | | // Join with user data for display names |
| | | foreach ($votes as &$vote) { |
| | | $user = get_userdata($vote['user_id']); |
| | | $vote['approver_name'] = $user ? $user->display_name : 'Unknown'; |
| | | $vote['approver_name'] = $user ? jvbGetUsername($vote['user_id']) : 'Someone'; |
| | | } |
| | | |
| | | return [ |
| | |
| | | ]; |
| | | } |
| | | |
| | | /************* |
| | | * Term Approvals |
| | | ************/ |
| | | public function voteForTerm(int $user_id, int $request_id, string $vote, string $notes = ''):bool |
| | | { |
| | | return $this->handleVote('term', $vote, $user_id, $request_id, $notes); |
| | | } |
| | | |
| | | /** |
| | | * Publish an approved term |
| | | * |
| | | * @param object $request Approval request object |
| | | * |
| | | * @return boolean Success or failure |
| | | */ |
| | | protected function makeTermLive(object $request): bool |
| | | { |
| | | try { |
| | | $taxonomy = $request->taxonomy; |
| | | $term_name = $request->name; |
| | | $parent = $request->parent; |
| | | |
| | | $result = wp_insert_term($term_name, $taxonomy, [ |
| | | 'parent' => $parent |
| | | ]); |
| | | |
| | | if (is_wp_error($result)) { |
| | | throw new Exception($result->get_error_message()); |
| | | } |
| | | |
| | | $term_id = $result['term_id']; |
| | | |
| | | // Update request status |
| | | CustomTable::for($this->getTableName('term', 'requests')) |
| | | ->where(['id' => $request->id]) |
| | | ->updateResults([ |
| | | 'status' => 'approved', |
| | | 'created_term' => $term_id |
| | | ]); |
| | | |
| | | $userIDs = []; |
| | | $approvedBy = []; |
| | | $approvers = json_decode($request->approved_by, true) ?: []; |
| | | $requesters = json_decode($request->requested_by, true) ?: []; |
| | | $rejectors = json_decode($request->rejected_by, true) ?: []; |
| | | |
| | | foreach (array_merge($requesters, $approvers, $rejectors) as $user_id => $info) { |
| | | $userIDs[] = $user_id; |
| | | } |
| | | foreach ($approvers as $user_id => $info) { |
| | | $approvedBy[] = $info['name']; |
| | | } |
| | | |
| | | $approvedBy = jvbCommaList($approvedBy); |
| | | |
| | | JVB()->notification()->addNotification( |
| | | $userIDs, |
| | | 'term_approved', |
| | | [ |
| | | 'term_id' => $term_id, |
| | | 'term_name' => $term_name, |
| | | 'taxonomy' => $taxonomy, |
| | | 'approved_by' => $approvedBy |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | $this->logError('makeTermLive', [ |
| | | 'error' => $e->getMessage(), |
| | | 'request_id' => $request->id, |
| | | 'term_name' => $term_name ?? '', |
| | | 'taxonomy' => $taxonomy ?? '' |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | /** |
| | | * Reject a proposed term |
| | | * |
| | | * @param object $request request object |
| | | * |
| | | * @return boolean Success or failure |
| | | */ |
| | | protected function makeTermUnalive(object $request): bool |
| | | { |
| | | try { |
| | | // Update request status |
| | | CustomTable::for($this->getTableName('term', 'requests')) |
| | | ->where(['id' => $request->id]) |
| | | ->updateResults([ |
| | | 'status' => 'rejected' |
| | | ]); |
| | | |
| | | $userIDs = []; |
| | | $rejectedBy = []; |
| | | |
| | | $approvers = json_decode($request->approved_by, true) ?: []; |
| | | $requesters = json_decode($request->requested_by, true) ?: []; |
| | | $rejectors = json_decode($request->rejected_by, true) ?: []; |
| | | |
| | | foreach (array_merge($requesters, $approvers, $rejectors) as $user_id => $info) { |
| | | $userIDs[] = $user_id; |
| | | } |
| | | foreach ($rejectors as $user_id => $info) { |
| | | $rejectedBy[] = $info['name']; |
| | | } |
| | | |
| | | $rejectedBy = jvbCommaList($rejectedBy); |
| | | |
| | | JVB()->notification()->addNotification( |
| | | $userIDs, |
| | | 'term_rejected', |
| | | [ |
| | | 'term_name' => $request->name, |
| | | 'taxonomy' => $request->taxonomy, |
| | | 'rejected_by' => $rejectedBy |
| | | ] |
| | | ); |
| | | |
| | | return true; |
| | | } catch (Exception $e) { |
| | | $this->logError('makeTermUnalive', [ |
| | | 'error' => $e->getMessage(), |
| | | 'request_id' => $request->id, |
| | | 'term_name' => $request->name ?? '', |
| | | 'taxonomy' => $request->taxonomy ?? '' |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Create a new term approval request |
| | |
| | | int $parent = 0, |
| | | int $required_approvals = 3 |
| | | ): int|false { |
| | | $table = CustomTable::for($this->getTableName('term', 'requests')); |
| | | |
| | | try { |
| | | return $table->transaction(function($table) use ($user_id, $taxonomy, $name, $parent, $required_approvals) { |
| | | // Check for existing request |
| | | $existing = $table->where([ |
| | | 'name' => $name, |
| | | 'taxonomy' => $taxonomy, |
| | | 'parent' => $parent, |
| | | 'status' => 'pending' |
| | | ])->first(); |
| | | |
| | | if ($existing) { |
| | | $requestedBy = json_decode($existing->requested_by, true) ?: []; |
| | | |
| | | if (isset($requestedBy[$user_id])) { |
| | | return (int)$existing->id; |
| | | } |
| | | |
| | | $requestedBy[$user_id] = get_userdata($user_id)->display_name; |
| | | |
| | | $table->where(['id' => $existing->id])->updateResults([ |
| | | 'requested_by' => json_encode($requestedBy) |
| | | ]); |
| | | |
| | | return (int)$existing->id; |
| | | } |
| | | |
| | | // Create new request |
| | | return $this->createApprovalRequest('term', [ |
| | | 'taxonomy' => $taxonomy, |
| | | 'name' => $name, |
| | | 'parent' => $parent ?: null, |
| | | 'status' => 'pending', |
| | | 'required_approvals' => $required_approvals, |
| | | 'current_approvals' => 0, |
| | | 'current_rejections' => 0, |
| | | 'requested_by' => json_encode([$user_id => get_userdata($user_id)->display_name]), |
| | | 'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')), |
| | | ]); |
| | | }); |
| | | } catch (Exception $e) { |
| | | $this->logError('createTermApprovalRequest', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id, |
| | | 'taxonomy' => $taxonomy, |
| | | 'name' => $name |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | /** |
| | | * Clean up expired approval requests and notify admin |
| | | * |
| | | * @return void |
| | | */ |
| | | public function cleanupExpiredApprovals(): void |
| | | { |
| | | $now = current_time('mysql'); |
| | | |
| | | foreach ($this->allTypes as $type) { |
| | | $tableName = $this->getTableName($type, 'requests'); |
| | | |
| | | CustomTable::for($tableName)->query( |
| | | "UPDATE {table} |
| | | SET status = 'expired' |
| | | WHERE status = 'pending' |
| | | AND expires_at < %s", |
| | | [$now] |
| | | ); |
| | | } |
| | | |
| | | $this->cache->flush(); |
| | | $result = JVB()->approvals()->createApproval( |
| | | $user_id, |
| | | $taxonomy, |
| | | $name, |
| | | $parent |
| | | ); |
| | | return $result['success']; |
| | | } |
| | | |
| | | protected function getTableName(string $type, string $suffix): string |
| | |
| | | |
| | | private function getUserApprovals(string $status = 'pending'): array |
| | | { |
| | | |
| | | $table = CustomTable::for($this->getTableName('artist', 'requests')); |
| | | |
| | | $query = $table; |
| | |
| | | protected function getAuthorData(WP_Post $post) |
| | | { |
| | | $author = $post->post_author; |
| | | $userLink = get_user_meta($author, BASE.'link', true); |
| | | $userLink = get_user_meta($author, BASE.'profile_link', true); |
| | | return $this->cache->remember( |
| | | $userLink, |
| | | function () use ($userLink, $author) { |
| | |
| | | } |
| | | |
| | | // Check if role is valid and can register |
| | | $role_config = JVB_USER[$user_select] ?? null; |
| | | $registrar = Registrar::getInstance($user_select); |
| | | |
| | | if (!$role_config) { |
| | | if (!$registrar) { |
| | | return new WP_Error('invalid_role', 'Invalid role selected.'); |
| | | } |
| | | |
| | | if (!($role_config['can_register'] ?? false)) { |
| | | if (!($registrar->hasFeature('can_register') ?? false)) { |
| | | return new WP_Error('role_not_allowed', 'This role cannot be selected during registration.'); |
| | | } |
| | | |
| | | return BASE . $user_select; |
| | | return $registrar->getBased(); |
| | | } |
| | | |
| | | /** |
| | |
| | | public function init(): void |
| | | { |
| | | $this->manager = JVB()->notification(); |
| | | $this->notification_types = $this->manager->getNotificationTypes(); |
| | | $this->notification_types = $this->manager->getNotificationTypes(true); |
| | | } |
| | | |
| | | /** |
| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\importers\JaneAppClientImporter; |
| | | use JVBase\importers\JaneAppSalesImporter; |
| | | use JVBase\managers\CustomTable; |
| | |
| | | |
| | | // Import using JaneAppClientImporter |
| | | $importer = new JaneAppClientImporter(); |
| | | $default_role = get_option(BASE . 'referral_role', JVB_USER); |
| | | $default_role = get_option(BASE . 'referral_role', Site::getDefaultReferralRole()); |
| | | |
| | | $options = [ |
| | | 'update_existing' => true, |
| | |
| | | if ($request->get_param('main_context') && in_array(jvbCheckBase(json_decode($request->get_param('main_context'), true)['context']), jvbUserTypes())) { |
| | | |
| | | $main_context = json_decode($request->get_param('main_context'), true); |
| | | $userID = get_post_meta($main_context['id'], BASE.'link', true); |
| | | $userID = get_post_meta($main_context['id'], BASE.'profile_link', true); |
| | | $manager = new UserTermsManager(); |
| | | $related = $manager->fetchUserTerms($userID, $taxonomy); |
| | | |
| | |
| | | !array_key_exists('post_id', $data) && |
| | | !array_key_exists('term_id', $data) && |
| | | !array_key_exists('item_id', $data)) { |
| | | $args['post_id'] = (int)get_user_meta((int) $value, BASE.'link', true); |
| | | $args['post_id'] = (int)get_user_meta((int) $value, BASE.'profile_link', true); |
| | | } |
| | | } |
| | | break; |
| | |
| | | } elseif (array_key_exists('term_id', $data)) { |
| | | $meta = Meta::forTerm($data['term_id']); |
| | | } else { |
| | | $link = (int)get_user_meta($data['user'], BASE.'link'); |
| | | $link = (int)get_user_meta($data['user'], BASE.'profile_link'); |
| | | $meta = Meta::forPost($link); |
| | | } |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\ui; |
| | | |
| | | use JVBase\base\Site; |
| | | use JVBase\managers\UserTermsManager; |
| | | use JVBase\meta\Form; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | |
| | | protected function taxConfig(string $taxonomy, string $label = ''):array |
| | | { |
| | | $isVerified = jvbUserIsVerified(); |
| | | $isVerified = $this->userIsVerified(); |
| | | $label = ($label === '') ? Registrar::getInstance($taxonomy)->getPlural() : $label; |
| | | return [ |
| | | 'type' => 'taxonomy', |
| | |
| | | ]; |
| | | } |
| | | |
| | | protected function userIsVerified():bool |
| | | { |
| | | $membership = Site::membership(); |
| | | |
| | | return !($membership && $membership->has('member_verified')) || current_user_can('skip_moderation'); |
| | | } |
| | | |
| | | public function addSearch():self |
| | | { |
| | | $this->hasSearch = true; |
| | |
| | | <?php |
| | | |
| | | require(JVB_DIR.'/inc/utility/Checker.php'); |
| | | require(JVB_DIR.'/inc/utility/Features.php'); |
| | | require(JVB_DIR.'/inc/utility/helpers.php'); |
| | | require(JVB_DIR.'/inc/utility/Validator.php'); |
| | | require(JVB_DIR.'/inc/utility/Image.php'); |
| | | |
| | | |
| | |
| | | |
| | | require(JVB_DIR.'/base/_setup.php'); |
| | | |
| | | if (empty(JVB_SITE)) { |
| | | return; |
| | | } |
| | | require(JVB_DIR.'/inc/utility/setup.php'); |
| | | require(JVB_DIR.'/checks.php'); |
| | | |
| | | |
| | | require(JVB_DIR . '/inc/registry/_setup.php'); |
| | | |
| | | require(JVB_DIR . '/inc/registrar/_setup.php'); |
| | | require(JVB_DIR . '/activate.php'); |
| | | |
| | | require(JVB_DIR . '/inc/helpers/all.php'); |
| | |
| | | "start": "wp-scripts start" |
| | | }, |
| | | "devDependencies": { |
| | | "@wordpress/scripts": "^31.2.0" |
| | | "@wordpress/scripts": "^19.2.4" |
| | | } |
| | | } |