Jake Vanderwerf
2026-08-28 a9654f00c8fbc9934289d761843ddd071025013f
=PHPStorm Code Inspection: fixed class reference errors (from getting rid of JVB() and replacing with direct class::getInstance; as well as general code sniffing, etc
242 files modified
1 files added
3585 ■■■■■ changed files
JVBase.php 35 ●●●●● patch | view | raw | blame | history
activate.php 5 ●●●●● patch | view | raw | blame | history
cleanup.php 50 ●●●●● patch | view | raw | blame | history
inc/admin/ContentTaxonomy.php 14 ●●●●● patch | view | raw | blame | history
inc/admin/Integrations.php 1 ●●●●● patch | view | raw | blame | history
inc/blocks/CustomBlocks.php 5 ●●●●● patch | view | raw | blame | history
inc/blocks/FAQBlock.php 1 ●●●●● patch | view | raw | blame | history
inc/blocks/FeedBlock.php 159 ●●●●● patch | view | raw | blame | history
inc/blocks/FeedBlockOld.php 19 ●●●●● patch | view | raw | blame | history
inc/blocks/FormBlock.php 7 ●●●●● patch | view | raw | blame | history
inc/blocks/GMBReviewBlock.php 3 ●●●●● patch | view | raw | blame | history
inc/blocks/MenuBlock.php 7 ●●●●● patch | view | raw | blame | history
inc/blocks/SummaryBlock.php 1 ●●●●● patch | view | raw | blame | history
inc/blocks/TimelineBlock.php 2 ●●●●● patch | view | raw | blame | history
inc/helpers/all.php 2 ●●●●● patch | view | raw | blame | history
inc/helpers/breadcrumbs.php 1 ●●●●● patch | view | raw | blame | history
inc/helpers/dashboard.php 10 ●●●●● patch | view | raw | blame | history
inc/helpers/forms.php 2 ●●●●● patch | view | raw | blame | history
inc/helpers/renderFields.php 59 ●●●●● patch | view | raw | blame | history
inc/helpers/time.php 2 ●●●●● patch | view | raw | blame | history
inc/importers/JaneAppClientImporter.php 3 ●●●●● patch | view | raw | blame | history
inc/importers/JaneAppSalesImporter.php 3 ●●●●● patch | view | raw | blame | history
inc/integrations/EndpointField.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/Integrations.php 7 ●●●●● patch | view | raw | blame | history
inc/integrations/Requests.php 6 ●●●●● patch | view | raw | blame | history
inc/integrations/SyncTo.php 4 ●●●●● patch | view | raw | blame | history
inc/integrations/SyncToPost.php 4 ●●●●● patch | view | raw | blame | history
inc/integrations/SyncToTerm.php 3 ●●●●● patch | view | raw | blame | history
inc/integrations/SyncToUser.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/_Base.php 4 ●●●●● patch | view | raw | blame | history
inc/integrations/services/BlueSky.php 5 ●●●●● patch | view | raw | blame | history
inc/integrations/services/BlueSkyOld.php 5 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Facebook.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/services/FacebookOld.php 34 ●●●●● patch | view | raw | blame | history
inc/integrations/services/GoogleMaps.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/services/GoogleMapsOld.php 35 ●●●●● patch | view | raw | blame | history
inc/integrations/services/GoogleMyBusiness.php 3 ●●●●● patch | view | raw | blame | history
inc/integrations/services/GoogleMyBusinessOld.php 150 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Helcim.php 4 ●●●●● patch | view | raw | blame | history
inc/integrations/services/HelcimOld.php 24 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Instagram.php 3 ●●●●● patch | view | raw | blame | history
inc/integrations/services/InstagramOld.php 29 ●●●●● patch | view | raw | blame | history
inc/integrations/services/PostMark.php 2 ●●●●● patch | view | raw | blame | history
inc/integrations/services/PostMarkOld.php 17 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Square.php 4 ●●●●● patch | view | raw | blame | history
inc/integrations/services/SquareOld.php 4 ●●●●● patch | view | raw | blame | history
inc/integrations/services/Umami.php 1 ●●●●● patch | view | raw | blame | history
inc/integrations/services/UmamiOld.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/AdminPages.php 15 ●●●●● patch | view | raw | blame | history
inc/managers/Approvals.php 6 ●●●●● patch | view | raw | blame | history
inc/managers/Cache.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/CacheBack.php 12 ●●●●● patch | view | raw | blame | history
inc/managers/CustomTable.php 133 ●●●●● patch | view | raw | blame | history
inc/managers/Dashboard/DashboardPage.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/Emails.php 15 ●●●●● patch | view | raw | blame | history
inc/managers/Errors.php 5 ●●●●● patch | view | raw | blame | history
inc/managers/Favourites.php 5 ●●●●● patch | view | raw | blame | history
inc/managers/IconsManager.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/ImageGenerator.php 63 ●●●●● patch | view | raw | blame | history
inc/managers/InvitationsManager.php 8 ●●●●● patch | view | raw | blame | history
inc/managers/KarmaManager.php 9 ●●●●● patch | view | raw | blame | history
inc/managers/Login.php 62 ●●●●● patch | view | raw | blame | history
inc/managers/MagicLinks.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/NewsRelationships.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/Notifications/EmailDigests.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/Notifications/Notifications.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/OperationQueue.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/Referrals.php 13 ●●●●● patch | view | raw | blame | history
inc/managers/Responses.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/Roles.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/SEOAdminPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/_edmonotonink.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/DataType/Date.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/SchemaOutput.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Action/Action.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Action/InteractAction.php 14 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/DefinedTermSet.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/HowTo.php 6 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/HowToSection.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/HowToStep.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/AboutPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/CheckoutPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/ContactPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/FAQPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/ItemPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/MedicalWebPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/QAPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/RealEstateListing.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/CreativeWork/WebPage/SearchResultsPage.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/AggregateOffer.php 17 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/CategoryCode.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/DefinedTerm.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/EntryPoint.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/ActionStatusType.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/AdultOrientedEnumeration.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/BusinessEntityType.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/BusinessFunction.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/CertificationStatusEnumeration.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php 5 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/DeliveryMethod.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/DigitalPlatformEnumeration.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/EventStatusType.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/OrderStatusType.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/SizeGroupEnumeration.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/SizeSystemEnumeration.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/StatusEnumeration.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/WarrantyScope.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Enumeration/WearableSizeGroupEnumeration.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/HowToItem.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/HowToTool.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/MenuItem.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/PaymentMethod.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Quantity/DurationOfWarranty.php 22 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Quantity/_setup.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/Reservation/Reservation.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/StructuredValue/PropertyValue.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/StructuredValue/ServicePeriod.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/StructuredValue/StructuredValue.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Intangible/StructuredValue/WarrantyPromise.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/AnimalShelter.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/AutomotiveBusiness.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/ChildCare.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/Dentist.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/DryCleaningOrLaundry.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/EmergencyService.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/EntertainmentBusiness.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FinancialService.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Bakery.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/BarOrPub.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Brewery.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/CafeOrCoffeeShop.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Distillery.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/FastFoodRestaurant.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/IceCreamShop.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Restaurant.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Winery.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/GovernmentOffice.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/HealthAndBeautyBusiness.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/HomeAndConstructionBusiness.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/InternetCafe.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/LegalService.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/Library.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/LocalBusiness.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/LodgingBusiness.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/ProfessionalService.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/RadioStation.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/RealEstateAgent.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/RecyclingCenter.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/SelfStorage.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/ShoppingCenter.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/SportsActivityLocation.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/Store.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Organization/LocalBusiness/TelevisionStation.php 7 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Place/AdministrativeArea/AdministrativeArea.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Place/AdministrativeArea/Country.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Thing/Place/AdministrativeArea/State.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/ThingSchema.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/audioTrait.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/byMonthWeekTrait.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/certificationIdentificationTrait.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/creatorTrait.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/dateCreatedTrait.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/deathDateTrait.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/durationOfWarrantyTrait.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/foundingDateTrait.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/hasMenuTrait.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/homeLocationTrait.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/isVariantOfTrait.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/occupationalCategoryTrait.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/offersTrait.php 3 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/skuTrait.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/validThroughTrait.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/SEO/render/Traits/_Properties/videoTrait.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/TaxonomyRelationships.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/UserTermsManager.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/VerifyEntryManager.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/_setup.php 2 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Metrics.php 13 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Processor.php 8 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Queue.php 11 ●●●●● patch | view | raw | blame | history
inc/managers/queue/Storage.php 5 ●●●●● patch | view | raw | blame | history
inc/managers/queue/TypeRegistry.php 4 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/ContentExecutor.php 21 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/ContentTermExecutor.php 25 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/IntegrationExecutor.php 1 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/InvitationExecutor.php 8 ●●●●● patch | view | raw | blame | history
inc/managers/queue/executors/UploadExecutor.php 10 ●●●●● patch | view | raw | blame | history
inc/meta/Form.php 25 ●●●●● patch | view | raw | blame | history
inc/meta/Group.php 2 ●●●●● patch | view | raw | blame | history
inc/meta/Item.php 7 ●●●●● patch | view | raw | blame | history
inc/meta/Meta.php 2 ●●●●● patch | view | raw | blame | history
inc/meta/Registry.php 11 ●●●●● patch | view | raw | blame | history
inc/meta/Storage.php 15 ●●●●● patch | view | raw | blame | history
inc/registrar/Fields.php 2 ●●●●● patch | view | raw | blame | history
inc/registrar/Posts.php 11 ●●●●● patch | view | raw | blame | history
inc/registrar/Registrar.php 15 ●●●●● patch | view | raw | blame | history
inc/registrar/config/Dashboard.php 2 ●●●●● patch | view | raw | blame | history
inc/registrar/config/Directory.php 2 ●●●●● patch | view | raw | blame | history
inc/registrar/config/seo/Helpers.php 2 ●●●●● patch | view | raw | blame | history
inc/registrar/config/seo/Meta.php 3 ●●●●● patch | view | raw | blame | history
inc/registrar/config/seo/Resolver.php 5 ●●●●● patch | view | raw | blame | history
inc/registrar/config/seo/Schema.php 2 ●●●●● patch | view | raw | blame | history
inc/registrar/fields/Field.php 2 ●●●●● patch | view | raw | blame | history
inc/registrar/fields/RepeaterField.php 4 ●●●●● patch | view | raw | blame | history
inc/registrar/helpers/HideSingle.php 2 ●●●●● patch | view | raw | blame | history
inc/registrar/helpers/MakeTimelineType.php 4 ●●●●● patch | view | raw | blame | history
inc/rest/Response.php 3 ●●●●● patch | view | raw | blame | history
inc/rest/Rest.php 3 ●●●●● patch | view | raw | blame | history
inc/rest/RestRouteManager.php 12 ●●●●● patch | view | raw | blame | history
inc/rest/_setup.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/AdminRoutes.php 36 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ApprovalRoutes.php 8 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ContentRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ContentTermsRoutes.php 50 ●●●●● patch | view | raw | blame | history
inc/rest/routes/FavouritesRoutes.php 41 ●●●●● patch | view | raw | blame | history
inc/rest/routes/FeedRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/FormRoutes.php 25 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ImporterRoutes.php 9 ●●●●● patch | view | raw | blame | history
inc/rest/routes/Invitations.php 1 ●●●●● patch | view | raw | blame | history
inc/rest/routes/LoginRoutes.php 34 ●●●●● patch | view | raw | blame | history
inc/rest/routes/MagicLinkRoutes.php 1 ●●●●● patch | view | raw | blame | history
inc/rest/routes/NewsRoutes.php 1 ●●●●● patch | view | raw | blame | history
inc/rest/routes/NotificationsRoutes.php 25 ●●●●● patch | view | raw | blame | history
inc/rest/routes/OptionsRoutes.php 4 ●●●●● patch | view | raw | blame | history
inc/rest/routes/QueueRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ReferralRoutes.php 30 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ResponseRoutes.php 3 ●●●●● patch | view | raw | blame | history
inc/rest/routes/SEORoutes.php 1 ●●●●● patch | view | raw | blame | history
inc/rest/routes/SettingsRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/rest/routes/ShopRoutes.php 191 ●●●●● patch | view | raw | blame | history
inc/rest/routes/TermRoutes.php 8 ●●●●● patch | view | raw | blame | history
inc/rest/routes/UserRoutes.php 2 ●●●●● patch | view | raw | blame | history
inc/ui/CRUDSkeleton.php 2 ●●●●● patch | view | raw | blame | history
inc/ui/Checkout.php 1 ●●●●● patch | view | raw | blame | history
inc/ui/Navigation.php 3 ●●●●● patch | view | raw | blame | history
inc/users/UserSettings.php 1256 ●●●●● patch | view | raw | blame | history
inc/utility/Video.php 2 ●●●●● patch | view | raw | blame | history
jvb.php 11 ●●●●● patch | view | raw | blame | history
src/forms/render.php 2 ●●●●● patch | view | raw | blame | history
src/summary/render.php 27 ●●●●● patch | view | raw | blame | history
templates/dashboard/sections/shop.php 2 ●●●●● patch | view | raw | blame | history
templates/email/notification-digest.php 19 ●●●●● patch | view | raw | blame | history
JVBase.php
@@ -1,43 +1,8 @@
<?php
namespace JVBase;
use JVBase\admin\SEOAdmin;
use JVBase\base\SchemaHelper;
use JVBase\base\Site;
use JVBase\blocks\CustomBlocks;
use JVBase\managers\Approvals;
use JVBase\managers\DirectoryManager;
use JVBase\managers\Favourites;
use JVBase\managers\InvitationsManager;
use JVBase\managers\Login;
use JVBase\managers\NotificationManager;
use JVBase\managers\queue\Queue;
use JVBase\managers\Referrals;
use JVBase\managers\SEO\render\SchemaOutput;
use JVBase\managers\TaxonomyRelationships;
use JVBase\managers\UserTermsManager;
use JVBase\registrar\Registrar;
use JVBase\rest\routes\AdminRoutes;
use JVBase\rest\routes\ApprovalRoutes;
use JVBase\rest\routes\ContentRoutes;
use JVBase\rest\routes\ContentTermsRoutes;
use JVBase\rest\routes\ErrorRoutes;
use JVBase\rest\routes\FavouritesRoutes;
use JVBase\rest\routes\FeedRoutes;
use JVBase\rest\routes\FormRoutes;
use JVBase\rest\routes\Invitations;
use JVBase\rest\routes\LoginRoutes;
use JVBase\rest\routes\NewsRoutes;
use JVBase\rest\routes\NotificationsRoutes;
use JVBase\rest\routes\OptionsRoutes;
use JVBase\rest\routes\ReferralRoutes;
use JVBase\rest\routes\ResponseRoutes;
use JVBase\rest\routes\SEORoutes;
use JVBase\rest\routes\SettingsRoutes;
use JVBase\rest\routes\TermRoutes;
use JVBase\rest\routes\UploadRoutes;
use JVBase\rest\routes\VoteRoutes;
use UserRoutes;
if (!defined('ABSPATH')) {
    exit;
activate.php
@@ -3,9 +3,6 @@
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use JVBase\base\Site;
use JVBase\managers\DashboardManager;
use JVBase\managers\DirectoryManager;
use JVBase\managers\queue\Queue;
use JVBase\managers\Referrals;
use JVBase\managers\Roles;
use JVBase\registrar\Registrar;
@@ -81,7 +78,7 @@
        ];
        foreach ($capabilities as $cap) {
            $role->add_cap($cap, true);
            $role->add_cap($cap);
        }
        foreach ($users as $user) {
cleanup.php
@@ -71,7 +71,7 @@
    // Remove third-party styles
    wp_deregister_style('akismet-widget-style-inline-css');
}
add_action('wp_enqueue_scripts', 'jvbRemoveBlockAssets', 9999);
//add_action('wp_enqueue_scripts', 'jvbRemoveBlockAssets', 9999);
/*******************************************************************************
WORDPRESS HEAD CLEANUP
@@ -233,7 +233,7 @@
        }
    }
}
add_action('before_delete_post', 'jvbCleanupPostDeletion');
//add_action('before_delete_post', 'jvbCleanupPostDeletion');
/*******************************************************************************
THEME CUSTOMIZATION
@@ -246,24 +246,24 @@
function jvbBodyClasses(array $classes): array
{
    // Start with empty array - only add what we need
    $clean_classes = [];
    $classes = [];
    // Add contextual classes
    if (is_front_page()) {
        $clean_classes[] = 'home';
        $classes[] = 'home';
    }
    if (function_exists('jvbIsDirectory') && jvbIsDirectory()) {
        $clean_classes[] = 'is-directory';
        $classes[] = 'is-directory';
    } elseif (is_tax()) {
        $clean_classes[] = str_replace(BASE, '', get_queried_object()->taxonomy);
        $classes[] = str_replace(BASE, '', get_queried_object()->taxonomy);
    } elseif (is_singular() && !is_singular('page')) {
        $clean_classes[] = str_replace(BASE, '', get_queried_object()->post_type);
        $classes[] = str_replace(BASE, '', get_queried_object()->post_type);
    } elseif (is_post_type_archive()) {
        $clean_classes[] = str_replace(BASE, '', get_queried_object()->name);
        $classes[] = str_replace(BASE, '', get_queried_object()->name);
    }
    return $clean_classes;
    return $classes;
}
add_filter('body_class', 'jvbBodyClasses');
@@ -295,19 +295,19 @@
// Hide existing comments
add_filter('comments_array', '__return_empty_array', 10, 2);
// Remove comments page in menu
add_action('admin_menu', function () {
    remove_menu_page('edit-comments.php');
});
// Remove comments links from admin bar
add_action('init', function () {
    if (is_admin_bar_showing()) {
        remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60);
    }
});
// Remove comments links from admin bar
add_action('add_admin_bar_menus', function () {
    remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60);
});
//
//// Remove comments page in menu
//add_action('admin_menu', function () {
//  remove_menu_page('edit-comments.php');
//});
//
//// Remove comments links from admin bar
//add_action('init', function () {
//  if (is_admin_bar_showing()) {
//      remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60);
//  }
//});
//// Remove comments links from admin bar
//add_action('add_admin_bar_menus', function () {
//  remove_action('admin_bar_menu', 'wp_admin_bar_comments_menu', 60);
//});
inc/admin/ContentTaxonomy.php
@@ -5,6 +5,7 @@
    exit;
}
use JVBase\managers\queue\Queue;
use JVBase\registrar\Registrar;
use WP_Error;
use Exception;
@@ -377,18 +378,17 @@
    public function processOperation(WP_Error $result, object $operation, array $data):WP_Error|array
    {
        switch ($operation->type) {
            case 'sync_content_taxonomy_tables':
                return $this->processContentTaxonomyTableSync($operation, $data);
            default:
                return $result;
        }
        return match ($operation->type) {
            'sync_content_taxonomy_tables' => $this->processContentTaxonomyTableSync($operation, $data),
            default => $result,
        };
    }
    public function processContentTaxonomyTableSync(object $operation, array $data):array
    {
        //TODO: This won't work currently
        $taxonomy = $data['taxonomy'];
        $registrar = new TaxonomyRegistrar($taxonomy, JVB_TAXONOMY[$taxonomy]);
        $registrar = Registrar::getInstance($taxonomy);
        $results = ['errors' => 0, 'success' => 0];
        foreach ($data['terms'] as $term) {
            try {
inc/admin/Integrations.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\admin;
use JVBase\base\Site;
use JVBase\integrations\ServiceMap;
use JVBase\managers\IconsManager;
use JVBase\registrar\Registrar;
inc/blocks/CustomBlocks.php
@@ -168,8 +168,7 @@
        }
    public function prerender(?string $content, array $block, ?WP_Block $parent = null):?string
    {
        $result = $this->checkMethods($content, $block, $parent, true);
        return $result;
        return $this->checkMethods($content, $block, $parent, true);
    }
    public function render(string $content, array $block):string
@@ -554,7 +553,7 @@
        }
        if (array_key_exists('focalPoint', $attrs)) {
            $attrs['isObjectPosition'] = true;
            $style = $this->getFocalPointStyle($attrs['focalPoint'], $attrs);
            $style = implode(';',$this->getFocalPointStyle($attrs['focalPoint'], $attrs));
            $style .= ';object-fit:none;';
            $imgLink = str_replace('<img', sprintf(
                '<img style="%s"',
inc/blocks/FAQBlock.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\blocks;
use JVBase\managers\Cache;
use JVBase\managers\DirectoryManager;
use JVBase\registrar\Registrar;
use WP_Block;
inc/blocks/FeedBlock.php
@@ -6,8 +6,6 @@
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
use JVBase\ui\CRUDSkeleton;
use WP_Block;
use WP_Query;
if (!defined('ABSPATH')) {
@@ -794,97 +792,94 @@
        if (!$isTemplate && empty($value)) {
            return '';
        }
        switch ($fieldName) {
            case 'post_title':
                return sprintf(
                    '<h3%s>%s</h3>',
                    $data,
                    $meta && !empty($value) ? $value : '',
                );
            case 'post_date':
            case 'post_modified':
                return sprintf(
                    '<time%s%s>%s</time>',
                    $data,
                    $meta && !empty($value) ? date('c', strtotime($value)) : '',
                    $meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
                );
        }
        return match ($fieldType) {
            'date' => sprintf(
        return match ($fieldName) {
            'post_title' => sprintf(
                '<h3%s>%s</h3>',
                $data,
                $meta && !empty($value) ? $value : '',
            ),
            'post_date', 'post_modified' => sprintf(
                '<time%s%s>%s</time>',
                $data,
                $meta && !empty($value) ? date('c', strtotime($value)) : '',
                $meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
            ),
            'datetime' => sprintf(
                '<time%s%s>%s</time>',
                $data,
                $meta && !empty($value) ? date('c', strtotime($value)) : '',
                $meta && !empty($value) ? date('F j, Y at h:iA', strtotime($value)) : '',
            ),
            'time' => sprintf(
                '<time%s%s>%s</time>',
                $data,
                $value,
                $value
            ),
            'upload' => empty($value) ? sprintf(
                '<img%s width="300px" height="300px" loading="lazy" decoding="async">',
                $data
            ) :
                str_replace('<img', '<img' . $data, jvbFormatImage($value)),
            'selector' => sprintf(
                '<ul%s class="terms %s">%s</ul>',
                $data,
                $config['taxonomy']??$config['post_type']??$config['role']??'',
                empty($value) ? sprintf('<li><a>%s</a></li>',
                    $this->iconFor($config)
            default => match ($fieldType) {
                'date' => sprintf(
                    '<time%s%s>%s</time>',
                    $data,
                    $meta && !empty($value) ? date('c', strtotime($value)) : '',
                    $meta && !empty($value) ? date('F j, Y', strtotime($value)) : '',
                ),
                'datetime' => sprintf(
                    '<time%s%s>%s</time>',
                    $data,
                    $meta && !empty($value) ? date('c', strtotime($value)) : '',
                    $meta && !empty($value) ? date('F j, Y at h:iA', strtotime($value)) : '',
                ),
                'time' => sprintf(
                    '<time%s%s>%s</time>',
                    $data,
                    $value,
                    $value
                ),
                'upload' => empty($value) ? sprintf(
                    '<img%s width="300px" height="300px" loading="lazy" decoding="async">',
                    $data
                ) :
                    implode('', array_filter(array_map(function ($ID) use ($config) {
                        $type = $config['subtype'];
                        $taxonomy = isset($config['taxonomy']) ? jvbCheckBase($config['taxonomy']) : '';
                    str_replace('<img', '<img' . $data, jvbFormatImage($value)),
                'selector' => sprintf(
                    '<ul%s class="terms %s">%s</ul>',
                    $data,
                    $config['taxonomy'] ?? $config['post_type'] ?? $config['role'] ?? '',
                    empty($value) ? sprintf('<li><a>%s</a></li>',
                        $this->iconFor($config)
                    ) :
                        implode('', array_filter(array_map(function ($ID) use ($config) {
                            $type = $config['subtype'];
                            $taxonomy = isset($config['taxonomy']) ? jvbCheckBase($config['taxonomy']) : '';
                        $item = match ($type) {
                            'taxonomy' => get_term($ID, $taxonomy),
                            'user' => get_userdata($ID),
                            'post' => get_post($ID),
                            default => false,
                        };
                            $item = match ($type) {
                                'taxonomy' => get_term($ID, $taxonomy),
                                'user' => get_userdata($ID),
                                'post' => get_post($ID),
                                default => false,
                            };
                        if (!$item || is_wp_error($item)) {
                            return '';
                        }
                            if (!$item || is_wp_error($item)) {
                                return '';
                            }
                        $icon = $this->iconFor($config);
                            $icon = $this->iconFor($config);
                        $name = match ($type) {
                            'taxonomy' => $item->name,
                            'user' => $item->display_name,
                            'post' => $item->post_title,
                            default => '',
                        };
                        $url = match ($type) {
                            'taxonomy' => get_term_link((int)$ID, $taxonomy),
                            'user' => get_the_permalink(get_user_meta($ID, BASE . 'userLink', true)),
                            'post' => get_the_permalink($ID),
                            default => ''
                        };
                        return sprintf(
                            '<li><a href="%s">%s%s</a></li>',
                            $url,
                            !empty($icon) ? jvbIcon($icon) : '',
                            $name
                        );
                    }, explode(',', $value))))
            ),
            default => sprintf(
                '<p%s>%s</p>',
                $data,
                $value
            ),
                            $name = match ($type) {
                                'taxonomy' => $item->name,
                                'user' => $item->display_name,
                                'post' => $item->post_title,
                                default => '',
                            };
                            $url = match ($type) {
                                'taxonomy' => get_term_link((int)$ID, $taxonomy),
                                'user' => get_the_permalink(get_user_meta($ID, BASE . 'userLink', true)),
                                'post' => get_the_permalink($ID),
                                default => ''
                            };
                            return sprintf(
                                '<li><a href="%s">%s%s</a></li>',
                                $url,
                                !empty($icon) ? jvbIcon($icon) : '',
                                $name
                            );
                        }, explode(',', $value))))
                ),
                default => sprintf(
                    '<p%s>%s</p>',
                    $data,
                    $value
                ),
            },
        };
    }
    protected function iconFor(array $fieldConfig):string
    {
inc/blocks/FeedBlockOld.php
@@ -481,17 +481,14 @@
    protected function defaultFieldTemplate(string $fieldType, string $fieldName):string
    {
        $data = ' data-field="'.$fieldName.'"';
        switch ($fieldName) {
            case 'post_title':
                return '<h3'.$data.'></h3>';
            case 'post_date':
            case 'post_modified':
                return '<time'.$data.'></time>';
        }
        return match($fieldType) {
            'upload'    => '<img'.$data.' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
            'taxonomy'  => '<ul'.$data.'><li><a><i></i></a></li></ul>',
            default =>  '<p'.$data.'></p>',
        return match ($fieldName) {
            'post_title' => '<h3' . $data . '></h3>',
            'post_date', 'post_modified' => '<time' . $data . '></time>',
            default => match ($fieldType) {
                'upload' => '<img' . $data . ' width="300px" height="300px" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 800px" loading="lazy" decoding="async">',
                'taxonomy' => '<ul' . $data . '><li><a><i></i></a></li></ul>',
                default => '<p' . $data . '></p>',
            },
        };
    }
    /**
inc/blocks/FormBlock.php
@@ -91,10 +91,9 @@
        $forms = apply_filters('jvb_register_forms', $default_forms);
        // Process forms to ensure they have proper structure
        $processed_forms = [];
        foreach ($forms as $form_key => $form_config) {
            $processed_forms[$form_key] = $this->processFormConfig($form_config);
        }
        $processed_forms = array_map(function ($form_config) {
            return $this->processFormConfig($form_config);
        }, $forms);
        return $processed_forms;
    }
inc/blocks/GMBReviewBlock.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\blocks;
use Exception;
use JVBase\base\Site;
use JVBase\integrations\services\GoogleMyBusiness;
//use JVBase\managers\Cache;
@@ -246,7 +247,7 @@
            <?php
            return ob_get_clean();
        } catch (\Exception $e) {
        } catch (Exception $e) {
            error_log('[GMB Reviews Block] Error: ' . $e->getMessage());
            return '';
        }
inc/blocks/MenuBlock.php
@@ -4,10 +4,7 @@
use JVBase\base\Options;
use JVBase\base\Site;
use JVBase\managers\Cache;
use JVBase\forms\TaxonomySelector;
use JVBase\meta\Form;
use JVBase\meta\Meta;
use JVBase\meta\Render;
use WP_Block;
use WP_Query;
@@ -219,7 +216,7 @@
   data-quantity="1"
   data-product_id="%d"
   data-variation_id="%d"
   class="add_to_cart_button ajax_add_to_cart add-to-cart">
   class="btn chip add_to_cart_button ajax_add_to_cart add-to-cart">
   %s
   <span>Add to Cart</span>
</a>', //Note: we had 3 %s%s%s before the last </div> last time. This was to add optional ingredients or preparation time duration, not default in WooCommerce
@@ -259,7 +256,7 @@
                '<a href="?add-to-cart=%d"
                   data-quantity="1"
                   data-product_id="%d"
                   class="add_to_cart_button ajax_add_to_cart add-to-cart">
                   class="btn chip add_to_cart_button ajax_add_to_cart add-to-cart">
                   %s
                    <span>Add to Cart</span>
                </a>',
inc/blocks/SummaryBlock.php
@@ -2,7 +2,6 @@
namespace JVBase\blocks;
use JVBase\managers\Cache;
use JVBase\forms\TaxonomySelector;
use WP_Block;
if (!defined('ABSPATH')) {
inc/blocks/TimelineBlock.php
@@ -230,6 +230,6 @@
    public function renderGallery():void
    {
        jvbRenderGallery(true);
        jvbRenderGallery();
    }
}
inc/helpers/all.php
@@ -1,7 +1,5 @@
<?php
use JVBase\base\Site;
if (!defined('ABSPATH')) {
    exit;
}
inc/helpers/breadcrumbs.php
@@ -6,6 +6,7 @@
 * Use BreadcrumbManager directly for new code
 */
use JVBase\managers\DirectoryManager;
use JVBase\managers\SEO\BreadcrumbManager;
if (!defined('ABSPATH')) {
inc/helpers/dashboard.php
@@ -1,5 +1,6 @@
<?php
use JVBase\managers\Roles;
use JVBase\registrar\Registrar;
if (!defined('ABSPATH')) {
@@ -49,12 +50,15 @@
 * Checks if the logged in user is one of us
 * @return bool
 */
function isOurPeople():bool
function isOurPeople(?int $userID = null):bool
{
    if (!is_user_logged_in()) {
    if (is_null($userID) && !is_user_logged_in()) {
        return false;
    }
    $user = wp_get_current_user();
    $user = is_null($userID) ? wp_get_current_user() : get_userdata($userID);
    if ($user || is_wp_error($user)) {
        return false;
    }
    $registered = Registrar::withFeature('has_dashboard', 'user');
    $mapped = array_map(fn($role) => BASE.$role,$registered);
    return count(
inc/helpers/forms.php
@@ -6,7 +6,7 @@
    exit;
}
function jvbRenderForm(string $endpoint, array $fields, array $options = [], bool $return = false):mixed
function jvbRenderForm(string $endpoint, array $fields, array $options = [], bool $return = false): string|bool
{
    ob_start();
    ?>
inc/helpers/renderFields.php
@@ -90,48 +90,21 @@
        $out = '<ul class="contact">';
        foreach ($links as $link) {
            $url = $link['url'];
            switch (true) {
                case strpos($url, 'facebook'):
                    $i = jvbIcon('facebook-logo');
                    break;
                case strpos($url, 'instagram'):
                    $i = jvbIcon('instagram-logo');
                    break;
                case strpos($url, 'tiktok'):
                    $i = jvbIcon('tiktok-logo');
                    break;
                case strpos($url, 'x.com'):
                    $i = jvbIcon('x');
                    break;
                case strpos($url, 'youtube'):
                    $i = jvbIcon('youtube-logo');
                    break;
                case strpos($url, 'mastadon'):
                    $i = jvbIcon('mastadon-logo');
                    break;
                case strpos($url, 'fediverse'):
                case strpos($url, 'bluesky'):
                    $i = jvbIcon('fediverse-logo');
                    break;
                case strpos($url, 'linktree'):
                    $i = jvbIcon('linktree-logo');
                    break;
                case strpos($url, 'snapchat'):
                    $i = jvbIcon('snapchat-logo');
                    break;
                case strpos($url, 'twitch'):
                    $i = jvbIcon('twitch-logo');
                    break;
                case strpos($url, 'threads'):
                    $i = jvbIcon('threads-logo');
                    break;
                case strpos($url, 'twitter'):
                    $i = jvbIcon('twitter-logo');
                    break;
                default:
                    $i = jvbIcon('link');
                    break;
            }
            $i = match (true) {
                strpos($url, 'facebook') => jvbIcon('facebook-logo'),
                strpos($url, 'instagram') => jvbIcon('instagram-logo'),
                strpos($url, 'tiktok') => jvbIcon('tiktok-logo'),
                strpos($url, 'x.com') => jvbIcon('x'),
                strpos($url, 'youtube') => jvbIcon('youtube-logo'),
                strpos($url, 'mastadon') => jvbIcon('mastadon-logo'),
                strpos($url, 'fediverse'), strpos($url, 'bluesky') => jvbIcon('fediverse-logo'),
                strpos($url, 'linktree') => jvbIcon('linktree-logo'),
                strpos($url, 'snapchat') => jvbIcon('snapchat-logo'),
                strpos($url, 'twitch') => jvbIcon('twitch-logo'),
                strpos($url, 'threads') => jvbIcon('threads-logo'),
                strpos($url, 'twitter') => jvbIcon('twitter-logo'),
                default => jvbIcon('link'),
            };
            $tracker = (array_key_exists('tracker', $link)) ? '?'.$link['tracker'] : '?source=edmontonInk';
            $out .= '<li><a href="'.$link['url'].$tracker.'" target="_blank">'.$i.$link['title'].'</a></li>';
        }
@@ -319,7 +292,7 @@
                    $aClose = '</a>';
                }
                if ($review['date'] !== '') {
                    $date = new Date('M j, Y', strtotime($review['date']));
                    $date = date('M j, Y', strtotime($review['date']));
                }
                $out .= '<cite class="row x-btw">';
inc/helpers/time.php
@@ -174,7 +174,7 @@
    $out = '';
    if (!empty($hours) && jvbHasOperatingHours($hours)) {
        $out = jvbGetFormattedHours($hours, true, true);
        $out = jvbGetFormattedHours($hours);
    } else {
        $out = '<p class="no-hours">Hours not available</p>';
    }
inc/importers/JaneAppClientImporter.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\importers;
use Exception;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -125,7 +126,7 @@
            // Commit transaction
            $this->wpdb->query('COMMIT');
        } catch (\Exception $e) {
        } catch (Exception $e) {
            // Rollback on error
            $this->wpdb->query('ROLLBACK');
            fclose($handle);
inc/importers/JaneAppSalesImporter.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\importers;
use Exception;
use WP_Error;
use JVBase\managers\Referrals;
@@ -154,7 +155,7 @@
            // Commit transaction
            $this->wpdb->query('COMMIT');
        } catch (\Exception $e) {
        } catch (Exception $e) {
            // Rollback on error
            $this->wpdb->query('ROLLBACK');
            fclose($handle);
inc/integrations/EndpointField.php
@@ -2,7 +2,6 @@
namespace JVBase\integrations;
use Closure;
use JVBase\meta\Meta;
use JVBase\meta\Validator;
if (!defined('ABSPATH')) {
inc/integrations/Integrations.php
@@ -3,6 +3,7 @@
use Exception;
use JVBase\managers\Cache;
use JVBase\managers\queue\Queue;
use JVBase\managers\UploadManager;
use JVBase\registrar\Registrar;
use WP_Error;
@@ -177,7 +178,7 @@
            return $this->response(false, 'Failed to save credentials to database');
        } catch (\Exception $e) {
        } catch (Exception $e) {
            // Revert to old credentials on any error
            $old = Auth::getInstance()->getCredentials(
                $this->service_name,
@@ -476,7 +477,7 @@
                'message' => 'Connection established successfully'
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => 'Error: ' . $e->getMessage()
@@ -608,7 +609,7 @@
            return $jpegVersion;
        }
        $uploader = UploadManager::getInstance();
        $converted = $uploader->convertImageTo($imgID, 'jpeg', 80, false);
        $converted = $uploader->convertImageTo($imgID, 'jpeg');
        if ($converted && !is_wp_error($converted)) {
            update_post_meta($imgID, BASE . 'jpeg_version', $converted['attachment_id']);
            return $converted['attachment_id'];
inc/integrations/Requests.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\integrations;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
}
@@ -23,7 +25,7 @@
     * Child classes inspect the wp_remote_request response,
     * decode it, and handle/log errors as needed
     */
    abstract protected function handleResponse(array|\WP_Error $response, string $method, string $endpoint): array;
    abstract protected function handleResponse(array|WP_Error $response, string $method, string $endpoint): array;
    protected function initializeRequestLimiter():void
    {
@@ -75,7 +77,7 @@
            return [];
        }
        if ($this->requestLimiter && !$this->awaitRateLimit($endpoint)) {
            $error = new \WP_Error('rate_limit_exceeded', "Rate limit exceeded for {$endpoint}");
            $error = new WP_Error('rate_limit_exceeded', "Rate limit exceeded for {$endpoint}");
            return $this->handleResponse($error, $method, $endpoint);
        }
inc/integrations/SyncTo.php
@@ -3,12 +3,8 @@
use Exception;
use JVBase\meta\Meta;
use JVBase\meta\Sanitizer;
use JVBase\registrar\Registrar;
use WP_Error;
use WP_Post;
use WP_Term;
use WP_User;
if (!defined('ABSPATH')) {
    exit;
inc/integrations/SyncToPost.php
@@ -22,7 +22,7 @@
    }
    public function removeSavePost():void
    {
        remove_action('save_post', [$this, 'handleSavePost'], 20, 3);
        remove_action('save_post', [$this, 'handleSavePost'], 20);
    }
    public function handleSavePost(int $postID, WP_Post $post, bool $update):void
    {
@@ -104,7 +104,7 @@
    }
    public function removeDeletePost():void
    {
        remove_action('before_delete_post', [$this, 'handleSavePost'], 20, 3);
        remove_action('before_delete_post', [$this, 'handleSavePost'], 20);
    }
    public function handleDeletePost(int $postID):void
    {
inc/integrations/SyncToTerm.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\integrations;
use JVBase\managers\queue\Queue;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
@@ -25,7 +26,7 @@
        if (empty($this->syncTaxonomies)) {
            return;
        }
        remove_action('saved_term', [$this, 'handleSaveTerm'], 20, 3);
        remove_action('saved_term', [$this, 'handleSaveTerm'], 20);
    }
    protected function handleSaveTerm(int $termID, int $tt_id, string $taxonomy, bool $update, array $args):void
    {
inc/integrations/SyncToUser.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\integrations;
use JVBase\managers\queue\Queue;
use JVBase\meta\Meta;
use JVBase\meta\Sanitizer;
use JVBase\registrar\Registrar;
inc/integrations/_Base.php
@@ -138,10 +138,6 @@
    protected function initializeRateLimiters():void
    {
        $key = $this->service_name;
        if (!is_null($this->userID)) {
            $key .= '_'.$this->userID;
        }
        if (method_exists($this, 'initializeRequestLimiter')) {
            $this->initializeRequestLimiter();
        }
inc/integrations/services/BlueSky.php
@@ -1,7 +1,10 @@
<?php
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Auth;
use JVBase\integrations\Integrations;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
}
@@ -404,7 +407,7 @@
        return 'https://bsky.social/xrpc/';
    }
    protected function processIntegrationAction(string $action, array $data):\WP_Error|array
    protected function processIntegrationAction(string $action, array $data): WP_Error|array
    {
        return [];
    }
inc/integrations/services/BlueSkyOld.php
@@ -1,7 +1,10 @@
<?php
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Auth;
use JVBase\integrations\Integrations;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
}
@@ -404,7 +407,7 @@
        return 'https://bsky.social/xrpc/';
    }
    protected function processIntegrationAction(string $action, array $data):\WP_Error|array
    protected function processIntegrationAction(string $action, array $data): WP_Error|array
    {
        return [];
    }
inc/integrations/services/Facebook.php
@@ -17,7 +17,6 @@
use JVBase\integrations\SyncFrom;
use JVBase\integrations\SyncTo;
use JVBase\integrations\UserConnection;
use JVBase\meta\Meta;
use JVBase\meta\Sanitizer;
use WP_Error;
inc/integrations/services/FacebookOld.php
@@ -5,6 +5,7 @@
 */
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Integrations;
use JVBase\meta\Meta;
use WP_Post;
@@ -29,18 +30,8 @@
    private string $page_id = '';
    private string $page_access_token = '';
    private array $permissions = [];
    private array $pages = [];
    // Post type mapping for Facebook
    private const FB_POST_TYPES = [
        'post' => 'feed',
        'photo' => 'photos',
        'video' => 'videos',
        'event' => 'events',
        'offer' => 'offers',
        'note' => 'notes',
        'milestone' => 'milestones'
    ];
    protected static array $instances = [];
    public static function getInstance(?int $userID = null):self
@@ -382,22 +373,13 @@
    {
        try {
            $fb = (array_key_exists('user', $data) && $data['user'] !== 0) ? $this->getInstance((int)$data['user']) : $this;
            switch ($operation->type) {
                case 'facebook_create_post':
                    return $fb->createFacebookPost($data);
                case 'facebook_update_post':
                    return $fb->updateFacebookPost($data);
                case 'facebook_delete_post':
                    return $fb->deleteFacebookPost($data);
                case 'facebook_create_event':
                    return $fb->createFacebookEvent($data);
                default:
                    return $result;
            }
            return match ($operation->type) {
                'facebook_create_post' => $fb->createFacebookPost($data),
                'facebook_update_post' => $fb->updateFacebookPost($data),
                'facebook_delete_post' => $fb->deleteFacebookPost($data),
                'facebook_create_event' => $fb->createFacebookEvent($data),
                default => $result,
            };
        } catch (Exception $e) {
            $this->logError('Operation failed', [
                'type' => $operation->type,
inc/integrations/services/GoogleMaps.php
@@ -9,7 +9,6 @@
use JVBase\Instance;
use JVBase\integrations\Integrations;
use JVBase\integrations\Requests;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
inc/integrations/services/GoogleMapsOld.php
@@ -7,6 +7,7 @@
namespace JVBase\integrations\services;
use JVBase\integrations\Integrations;
use JVBase\registrar\Registrar;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
@@ -17,7 +18,6 @@
    private string $public_key;
    private string $private_key;
    private string $map_id;
    private array $enabled_apis = [];
    // Edmonton default coordinates
    private const DEFAULT_LAT = 53.5461;
@@ -649,25 +649,20 @@
    {
        $theme = $this->credentials['theme'] ?? 'default';
        switch ($theme) {
            case 'dark':
                return [
                    ['elementType' => 'geometry', 'stylers' => [['color' => '#242f3e']]],
                    ['elementType' => 'labels.text.fill', 'stylers' => [['color' => '#746855']]],
                    ['elementType' => 'labels.text.stroke', 'stylers' => [['color' => '#242f3e']]],
                    ['featureType' => 'road', 'elementType' => 'geometry', 'stylers' => [['color' => '#38414e']]],
                    ['featureType' => 'water', 'elementType' => 'geometry', 'stylers' => [['color' => '#17263c']]]
                ];
            case 'minimal':
                return [
                    ['featureType' => 'poi', 'stylers' => [['visibility' => 'off']]],
                    ['featureType' => 'transit', 'stylers' => [['visibility' => 'off']]]
                ];
            default:
                return [];
        }
        return match ($theme) {
            'dark' => [
                ['elementType' => 'geometry', 'stylers' => [['color' => '#242f3e']]],
                ['elementType' => 'labels.text.fill', 'stylers' => [['color' => '#746855']]],
                ['elementType' => 'labels.text.stroke', 'stylers' => [['color' => '#242f3e']]],
                ['featureType' => 'road', 'elementType' => 'geometry', 'stylers' => [['color' => '#38414e']]],
                ['featureType' => 'water', 'elementType' => 'geometry', 'stylers' => [['color' => '#17263c']]]
            ],
            'minimal' => [
                ['featureType' => 'poi', 'stylers' => [['visibility' => 'off']]],
                ['featureType' => 'transit', 'stylers' => [['visibility' => 'off']]]
            ],
            default => [],
        };
    }
    /**
inc/integrations/services/GoogleMyBusiness.php
@@ -8,7 +8,6 @@
use JVBase\integrations\OAuthCredentials;
use JVBase\integrations\OAuthURLs;
use JVBase\integrations\Requests;
use JVBase\integrations\SyncTo;
use JVBase\integrations\SyncToPost;
use JVBase\integrations\UserConnection;
use WP_Error;
@@ -153,7 +152,7 @@
        ];
    }
    protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
    protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
    {
        if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
            return $this->response(false, 'Rate limit exceeded');
inc/integrations/services/GoogleMyBusinessOld.php
@@ -1,7 +1,10 @@
<?php
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Integrations;
use JVBase\meta\Meta;
use WP_Error;
use WP_Post;
if (!defined('ABSPATH')) {
    exit;
@@ -172,7 +175,7 @@
        try {
            $accounts = $this->getAccounts(true);
            return !empty($accounts);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            return false;
        }
    }
@@ -184,7 +187,7 @@
    {
        // Google APIs return errors in this format:
        // {"error": {"code": 401, "message": "...", "status": "UNAUTHENTICATED"}}
        return isset($response['error']) && isset($response['error']['code']);
        return isset($response['error']['code']);
    }
    protected function getRequestHeaders(): array
@@ -212,7 +215,7 @@
        return $credentials;
    }
    public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings):void
    public function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings):void
    {
        if (!$this->hasOAuthCredentials()) {
@@ -234,33 +237,18 @@
        try {
            $gmb = (array_key_exists('user', $data) && $data['user'] !== 0) ? $this->getInstance((int)$data['user']) : $this;
            $gmb->ensureInitialized();
            switch ($operation->type) {
                case 'gmb_create_menu_item':
                case 'gmb_update_menu_item':
                case 'gmb_menu_update':
                case 'gmb_menu_update_' . $this->userID:
                    return $gmb->processMenuUpdate($operation, $data);
                case 'gmb_create_event':
                case 'gmb_update_event':
                    return $gmb->syncEventPost($operation, $data);
                case 'gmb_create_offer':
                case 'gmb_update_offer':
                    return $gmb->syncOfferPost($operation, $data);
                case 'gmb_create_post':
                case 'gmb_update_post':
                    return $gmb->syncStandardPost($operation, $data);
                case 'gmb_specialHours':
                    return $gmb->setSpecialHours($data['date'], $data['data']);
                case 'gmb_business_hours':
                    return $gmb->updateBusinessHours($data);
                case 'gmb_business_info':
                    return $gmb->updateBusinessInfo($data);
                case 'gmb_update_attributes':
                    return $gmb->updateAttributes($data);
                default:
                    return $result;
            }
        } catch (\Exception $e) {
            return match ($operation->type) {
                'gmb_create_menu_item', 'gmb_update_menu_item', 'gmb_menu_update', 'gmb_menu_update_' . $this->userID => $gmb->processMenuUpdate($operation, $data),
                'gmb_create_event', 'gmb_update_event' => $gmb->syncEventPost($operation, $data),
                'gmb_create_offer', 'gmb_update_offer' => $gmb->syncOfferPost($operation, $data),
                'gmb_create_post', 'gmb_update_post' => $gmb->syncStandardPost($operation, $data),
                'gmb_specialHours' => $gmb->setSpecialHours($data['date'], $data['data']),
                'gmb_business_hours' => $gmb->updateBusinessHours($data),
                'gmb_business_info' => $gmb->updateBusinessInfo($data),
                'gmb_update_attributes' => $gmb->updateAttributes($data),
                default => $result,
            };
        } catch (Exception $e) {
            error_log('[GMB] Operation processing error: ' . $e->getMessage());
            return [
                'success' => false,
@@ -477,7 +465,7 @@
                'result' => (is_array($result)) ? array_merge($result, ['menu'=>$menu_data]) : ['menu'=>$menu_data],
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            error_log('GMB Menu Update Error: ' . $e->getMessage());
            return [
                'success'   => false,
@@ -514,7 +502,7 @@
                    }
                }
            }
        } catch (\Exception $e) {
        } catch (Exception $e) {
            error_log('[GMB] Error finding location resource name: ' . $e->getMessage());
        }
@@ -696,7 +684,7 @@
            $location_name = $this->getSelectedLocationResourceName();
            if (empty($patch_data)) {
                throw new \Exception('No valid fields to update');
                throw new Exception('No valid fields to update');
            }
            // Create updateMask from the fields being updated
@@ -714,7 +702,7 @@
                'result'    => $response
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'updateBusinessInfo'
            ]);
@@ -756,7 +744,7 @@
                'result'    => $response
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'updateAttributes'
            ]);
@@ -917,7 +905,7 @@
        try {
            // Validate required fields
            if (empty($post_data['summary'])) {
                throw new \Exception('Post summary is required');
                throw new Exception('Post summary is required');
            }
            // Build the post object according to API docs
@@ -958,7 +946,7 @@
            return $response;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'createPost'
            ]);
@@ -998,7 +986,7 @@
            return !empty($response);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'updatePost'
@@ -1015,7 +1003,7 @@
        try {
            $this->makeRequest('DELETE', "/v1/accounts/{$post_name}", [], 'posts');
            return true;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'deletePost'
@@ -1041,7 +1029,7 @@
        if (empty($location_name)) {
            error_log('[GMB] ERROR: No location selected or found');
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        try {
@@ -1050,7 +1038,7 @@
            if (empty($periods)) {
                error_log('[GMB] ERROR: No valid periods found after validation');
                throw new \Exception('No valid business hours provided');
                throw new Exception('No valid business hours provided');
            }
            $update_data = [
@@ -1086,7 +1074,7 @@
                'result'    => $response
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'updateBusinessHours'
            ]);
@@ -1176,7 +1164,7 @@
        $location_name = $this->getSelectedLocationResourceName();
        if (empty($location_name)) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        try {
@@ -1205,8 +1193,8 @@
                    }
                }
                if (!empty($options['close'])) {
                    $close_time = $this->formatTime($options['close']);
                if (!empty($options['closed'])) {
                    $close_time = $this->formatTime($options['closed']);
                    if ($close_time) {
                        $special_hour_period['closeTime'] = $close_time;
                    }
@@ -1251,7 +1239,7 @@
                'results'   => $response
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'setSpecialHours'
            ]);
@@ -1280,7 +1268,7 @@
            return $response !== null;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'clearSpecialHours'
@@ -1336,7 +1324,7 @@
            ];
            if (!in_array($category, $valid_categories)) {
                throw new \Exception('Invalid photo category: ' . $category);
                throw new Exception('Invalid photo category: ' . $category);
            }
            $mediaKey = $this->getImageMediaKey($imgID);
            if ($mediaKey) {
@@ -1364,7 +1352,7 @@
                return $mediaKey;
            }
            return null;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method' => 'uploadPhotoFromUrl'
            ]);
@@ -1423,10 +1411,10 @@
    {
        $this->ensureInitialized();
        if (!$this->location) {
            throw new \Exception('No location selected');
            throw new Exception('No location selected');
        }
        if (!$this->account_id) {
            throw new \Exception('No account configured');
            throw new Exception('No account configured');
        }
        $location = $this->getSelectedLocationResourceName();
@@ -1457,7 +1445,7 @@
            return $reviews;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method' => 'getReviews'
            ]);
@@ -1492,7 +1480,7 @@
            return null;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('Failed to get reviews view URL: ' . $e->getMessage(), [
                'method' => 'getReviewsViewUrl'
            ]);
@@ -1527,7 +1515,7 @@
            return null;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('Failed to get review URL: ' . $e->getMessage(), [
                'method' => 'getReviewUrl'
            ]);
@@ -1555,9 +1543,9 @@
        try {
            $response = $this->getRequest("/v1/{$account_name}/locations", $params,'accounts', $ttl, $force);
            return isset($response['locations']) ? $response['locations'] : [];
            return $response['locations'] ?? [];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            error_log('[GMB] Error getting locations for ' . $account_name . ': ' . $e->getMessage());
            return [];
        }
@@ -1610,7 +1598,7 @@
                'message' => 'No accounts found'
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            error_log('[GMB] Error refreshing stored data: ' . $e->getMessage());
            return [
                'success' => false,
@@ -1704,7 +1692,7 @@
                ?>
            </div>
            <?php
        } catch (\Exception $e) {
        } catch (Exception $e) {
            echo '<p>Error loading business locations.</p>';
        }
    }
@@ -1838,7 +1826,7 @@
            }
            if (!$selected_account) {
                throw new \Exception('Invalid account selected');
                throw new Exception('Invalid account selected');
            }
            // Update credentials with selected account
@@ -1853,7 +1841,7 @@
                'reload' => true
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('Failed to select account', [
                'error' => $e->getMessage(),
                'data' => $data
@@ -1882,12 +1870,12 @@
            }
            if (empty($location_name)) {
                throw new \Exception('No location selected');
                throw new Exception('No location selected');
            }
            // Verify this is a valid location for the selected account
            if (empty($this->credentials['account'])) {
                throw new \Exception('No account selected. Please select an account first.');
                throw new Exception('No account selected. Please select an account first.');
            }
            $locations = $this->getLocations($this->credentials['account']);
@@ -1902,7 +1890,7 @@
            }
            if (!$valid_location) {
                throw new \Exception('Invalid location selected');
                throw new Exception('Invalid location selected');
            }
            // Update credentials with selected location
@@ -1920,7 +1908,7 @@
                'reload' => false  // No need to reload for location selection
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('Failed to select location', [
                'error' => $e->getMessage(),
                'data' => $data
@@ -1948,7 +1936,7 @@
                'has_refresh_token' => !empty($this->credentials['refresh_token'])
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            return [
                'success' => false,
                'valid' => false,
@@ -2063,7 +2051,7 @@
    public function getFoodMenus(): ?array
    {
        if (!$this->credentials['location']) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        $response = $this->getRequest(
@@ -2080,7 +2068,7 @@
    public function updateFoodMenus(array $menus): bool
    {
        if (!$this->credentials['location']) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        try {
@@ -2102,7 +2090,7 @@
            ]);
            return !empty($response);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'updateFoodMenus'
            ]);
@@ -2164,7 +2152,7 @@
    {
        // Extract mediaKey from format: accounts/{accountId}/locations/{locationId}/media/{mediaKey}
        $parts = explode('/media/', $mediaResourceName);
        return isset($parts[1]) ? $parts[1] : '';
        return $parts[1] ?? '';
    }
    /**
@@ -2173,7 +2161,7 @@
    public function getLocationMedia(): array
    {
        if (!$this->credentials['location']) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        try {
@@ -2184,7 +2172,7 @@
            );
            return $response['mediaItems'] ?? [];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('Failed to get location media: ' . $e->getMessage());
            return [];
        }
@@ -2212,7 +2200,7 @@
                'cached' => !$force
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            return new WP_Error('failure', 'Something went wrong: '.$e->getMessage());
        }
    }
@@ -2233,7 +2221,7 @@
            }
            if (empty($selected_account)) {
                throw new \Exception('No account selected');
                throw new Exception('No account selected');
            }
            // Save the selected account
@@ -2257,7 +2245,7 @@
                'reload' => true  // Trigger page refresh to show location dropdown
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('Failed to update location', [
                'error' => $e->getMessage(),
                'data' => $data
@@ -2277,7 +2265,7 @@
            $this->cache->flush();
            return true;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            error_log('[GMB] Error clearing stored data: ' . $e->getMessage());
            return false;
        }
@@ -2286,13 +2274,13 @@
    public function getPriceLists(): ?array
    {
        if (!$this->credentials['location']) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        try {
            $location_data = $this->getLocation($this->credentials['location']);
            return $location_data['priceLists'] ?? [];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError($e->getMessage(), [
                'method'    => 'getPriceLists'
@@ -2387,7 +2375,7 @@
        $location_name = $this->getSelectedLocationResourceName();
        if (empty($location_name)) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        $post_data = [
@@ -2414,7 +2402,7 @@
        $location_name = $this->getSelectedLocationResourceName();
        if (empty($location_name)) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        return $this->updateBusinessInfo($location_name, $updates);
@@ -2430,7 +2418,7 @@
        $location_name = $this->getSelectedLocationResourceName();
        if (empty($location_name)) {
            throw new \Exception('No location selected. Please select a location first.');
            throw new Exception('No location selected. Please select a location first.');
        }
        return $this->clearSpecialHours($location_name);
@@ -2529,7 +2517,7 @@
        return [$gmb_menu]; // GMB expects an array of menus
    }
    protected function buildMenuItem(\WP_Post $item, array $defaults):array
    protected function buildMenuItem(WP_Post $item, array $defaults):array
    {
        $meta = Meta::forPost($item->ID);
        $fields = $this->mappedMenuFields($item->post_type);
inc/integrations/services/Helcim.php
@@ -4,10 +4,6 @@
use JVBase\integrations\Admin;
use JVBase\Instance;
use JVBase\integrations\Integrations;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\TypeConfig;
use JVBase\meta\Meta;
use JVBase\ui\Checkout;
if (!defined('ABSPATH')) {
    exit;
inc/integrations/services/HelcimOld.php
@@ -1,11 +1,17 @@
<?php
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Integrations;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\Queue;
use JVBase\managers\queue\TypeConfig;
use JVBase\meta\Meta;
use JVBase\ui\Checkout;
use WP_Error;
use WP_Post;
use WP_REST_Request;
use WP_REST_Response;
if (!defined('ABSPATH')) {
    exit;
@@ -999,7 +1005,7 @@
     * PRODUCT SYNC
     *****************************************************************/
    protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
    protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
    {
        error_log('==== [Helcim]::handleTheSavePost ====');
@@ -1265,7 +1271,7 @@
    /**
     * Override the webhook request handler to capture raw body for signature verification
     */
    public function handleWebhookRequest(\WP_REST_Request $request): \WP_REST_Response
    public function handleWebhookRequest(WP_REST_Request $request): WP_REST_Response
    {
        $payload = $request->get_params();
        $payload['_headers']  = $request->get_headers();
@@ -1273,7 +1279,7 @@
        $success = $this->handleWebhook($payload);
        return new \WP_REST_Response([
        return new WP_REST_Response([
            'success' => $success,
        ], $success ? 200 : 400);
    }
@@ -1284,11 +1290,11 @@
     * Sync a WordPress post to Helcim as a product.
     * Called by IntegrationExecutor::processSyncTo()
     */
    public function syncPostToService(int $postID): array|\WP_Error
    public function syncPostToService(int $postID): array|WP_Error
    {
        $post = get_post($postID);
        if (!$post) {
            return new \WP_Error('not_found', "Post {$postID} not found");
            return new WP_Error('not_found', "Post {$postID} not found");
        }
        $helcimProductId = get_post_meta($postID, BASE . '_helcim_item_id', true);
@@ -1324,7 +1330,7 @@
     * Delete a product from Helcim.
     * Called by IntegrationExecutor::processDeleteFrom()
     */
    public function deleteFromService(string $externalId): array|\WP_Error
    public function deleteFromService(string $externalId): array|WP_Error
    {
        $response = $this->deleteRequest("products/{$externalId}");
@@ -1338,14 +1344,14 @@
    /**
     * Build Helcim product payload from a WordPress post.
     */
    protected function buildProductPayload(int $postID): array|\WP_Error
    protected function buildProductPayload(int $postID): array|WP_Error
    {
        $meta = \JVBase\meta\Meta::forPost($postID);
        $meta = Meta::forPost($postID);
        $post = get_post($postID);
        $price = $meta->get('price');
        if (empty($price) || !is_numeric($price)) {
            return new \WP_Error('invalid_price', "Post {$postID} has no valid price");
            return new WP_Error('invalid_price', "Post {$postID} has no valid price");
        }
        return [
inc/integrations/services/Instagram.php
@@ -15,6 +15,7 @@
use JVBase\integrations\OAuthCredentials;
use JVBase\integrations\OAuthURLs;
use JVBase\integrations\Requests;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
@@ -69,7 +70,7 @@
        return [];
    }
    protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
    protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
    {
        // TODO: Implement handleResponse() method.
        return [];
inc/integrations/services/InstagramOld.php
@@ -5,9 +5,11 @@
 */
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Integrations;
use JVBase\meta\Meta;
use WP_Error;
use WP_Post;
if (!defined('ABSPATH')) {
    exit;
@@ -257,7 +259,7 @@
    /**
     * Handle post save - queue Instagram post creation
     */
    public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
    public function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
    {
        if (!$this->hasOAuthCredentials()) {
@@ -280,23 +282,16 @@
    /**
     * Process queued operations
     */
    public function processOperation(\WP_Error|array $result, object $operation, array $data): \WP_Error|array
    public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
    {
        $base = strtolower($this->service_name) . '_';
        $insta = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
        switch ($operation->type) {
            case $base . 'create_post':
                return $insta->processCreatePost($data);
            case $base . 'create_story':
                return $insta->processCreateStory($data);
            case $base . 'delete_post':
                return $insta->processDeletePost($data);
            default:
                return $result;
        }
        return match ($operation->type) {
            $base . 'create_post' => $insta->processCreatePost($data),
            $base . 'create_story' => $insta->processCreateStory($data),
            $base . 'delete_post' => $insta->processDeletePost($data),
            default => $result,
        };
    }
    private function processDeletePost(array $data): array
@@ -382,7 +377,7 @@
    /**
     * Build caption for Instagram post
     */
    private function buildCaption(\WP_Post $post): string
    private function buildCaption(WP_Post $post): string
    {
        $caption_parts = [];
@@ -410,7 +405,7 @@
                $default = preg_split('/[\s,]+/', $this->post_settings['default_hashtags']);
                foreach ($default as $tag) {
                    $tag = trim($tag);
                    if (strpos($tag, '#') !== 0) {
                    if (!str_starts_with($tag, '#')) {
                        $tag = '#' . $tag;
                    }
                    $hashtags[] = $tag;
inc/integrations/services/PostMark.php
@@ -212,7 +212,7 @@
        ];
    }
    protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
    protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
    {
        if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
            return $this->response(false, 'Rate limit exceeded');
inc/integrations/services/PostMarkOld.php
@@ -1,7 +1,10 @@
<?php
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Integrations;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
}
@@ -158,8 +161,8 @@
        // Determine content type
        $is_html = $parsed_headers['content-type'] === 'text/html' ||
            strpos($message, '<html') !== false ||
            strpos($message, '<body') !== false;
            str_contains($message, '<html') ||
            str_contains($message, '<body');
        // Build PostMark payload
        $payload = $this->buildEmailPayload(
@@ -293,7 +296,7 @@
            return true;
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('[POSTMARK]', ['method' => 'sendEmail', 'error' => $e->getMessage()]);
            return new WP_Error(
                'postmark_send_failed',
@@ -357,7 +360,7 @@
        switch ($name) {
            case 'content-type':
                if (strpos($value, 'text/html') !== false) {
                if (str_contains($value, 'text/html')) {
                    $parsed['content-type'] = 'text/html';
                }
                break;
@@ -386,7 +389,7 @@
            default:
                // Store custom headers
                if (strpos($name, 'x-') === 0) {
                if (str_starts_with($name, 'x-')) {
                    $parsed['x-headers'][] = [
                        'Name' => $parts[0], // Original case
                        'Value' => $value
@@ -513,7 +516,7 @@
                $this->cache->set($cache_key, $stats, 3600); // Cache for 1 hour
                return $stats;
            }
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('stats/outbound', ['error' => $e->getMessage()]);
        }
@@ -530,7 +533,7 @@
                'count' => $count,
                'offset' => $offset
            ]);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('[POSTMARK]', ['method' => 'getBounces', 'error' => $e->getMessage()]);
            return [];
        }
inc/integrations/services/Square.php
@@ -14,7 +14,9 @@
use JVBase\integrations\SyncFrom;
use JVBase\integrations\SyncTo;
use JVBase\integrations\Webhooks;
use JVBase\managers\Dashboard\Dashboard;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\Queue;
use JVBase\managers\queue\TypeConfig;
use JVBase\meta\Sanitizer;
use JVBase\registrar\Registrar;
@@ -298,7 +300,7 @@
        ];
    }
    protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
    protected function handleResponse(WP_Error|array $response, string $method, string $endpoint): array
    {
        if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
            return $this->response(false, 'Rate limit exceeded');
inc/integrations/services/SquareOld.php
@@ -3,7 +3,9 @@
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Integrations;
use JVBase\managers\Emails;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\Queue;
use JVBase\managers\queue\TypeConfig;
use JVBase\meta\Form;
use JVBase\meta\Meta;
@@ -51,7 +53,7 @@
    protected string $environment = 'sandbox';
    private const PASSWORD_RESET_INTERVAL = 3; // Reset password every 3 logins, for customers
    // Reset password every 3 logins, for customers
    /**
     * OAuth Configuration
inc/integrations/services/Umami.php
@@ -3,7 +3,6 @@
use JVBase\integrations\Admin;
use JVBase\Instance;
use JVBase\integrations\Integrations;
use JVBase\managers\Roles;
use JVBase\registrar\Registrar;
if (!defined('ABSPATH')) {
inc/integrations/services/UmamiOld.php
@@ -1,7 +1,8 @@
<?php
namespace JVBase\integrations\services;
use Exception;
use JVBase\integrations\Integrations;
use JVBase\registrar\Registrar;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
@@ -618,7 +619,7 @@
            $referer = $_SERVER['HTTP_REFERER'];
            $site_url = get_site_url();
            if (strpos($referer, $site_url) === false) {
            if (!str_contains($referer, $site_url)) {
                $domain = parse_url($referer, PHP_URL_HOST);
                return $domain ?: 'external';
            }
@@ -699,7 +700,7 @@
    /**
     * Process queued operations
     */
    public function processOperation(array|\WP_Error $result, object $operation, array $data): array|\WP_Error
    public function processOperation(array|WP_Error $result, object $operation, array $data): array|WP_Error
    {
        $prefix = strtolower($this->service_name) . '_';
inc/managers/AdminPages.php
@@ -2,8 +2,8 @@
namespace JVBase\managers;
use JVBase\Instance;
use JVBase\managers\queue\Queue;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use WP_REST_Response;
if (!defined('ABSPATH')) {
@@ -453,7 +453,7 @@
    public function enqueueAdminAssets(string $hook):void
    {
        // More robust check for JVB admin pages
        if (strpos($hook, BASE) === false) {
        if (!str_contains($hook, BASE)) {
            return;
        }
@@ -610,12 +610,7 @@
        $group = jvbNoBase($group);
        $registered = Registrar::getRegistered();
        foreach ($registered as $r) {
            if ($r === $group) {
                return true;
            }
        }
        return false;
        return in_array($group, $registered, true);
    }
    protected function formatConnections(array $data): string
@@ -740,7 +735,7 @@
                <?php if (empty($versions)): ?>
                    <tr><td colspan="5">No version history available</td></tr>
                <?php else: ?>
                    <?php foreach (array_reverse($versions) as $index => $version): ?>
                    <?php foreach (array_reverse($versions) as $version): ?>
                        <tr>
                            <th class="check-column">
                                <input type="checkbox"
@@ -804,7 +799,7 @@
    public function handleAdminPageSubmission(): void
    {
        // Only process on our admin pages
        if (!isset($_GET['page']) || strpos($_GET['page'], BASE) !== 0) {
        if (!isset($_GET['page']) || !str_starts_with($_GET['page'], BASE)) {
            return;
        }
inc/managers/Approvals.php
@@ -41,7 +41,7 @@
        $types = Registrar::withFeature('approve_new');
        foreach ($types as $type) {
            $requests = CustomTable::for("approval_{$type}_requests");
            $registrar = Registrar::getInstance($type);
//          $registrar = Registrar::getInstance($type);
            $requests->setColumns([
                'id'                => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
@@ -359,7 +359,7 @@
            }
        } elseif ($registrar->getType() === 'user') {
            $user = get_userdata($request['user_id']);
            $user->add_cap('skip_moderation', true);
            $user->add_cap('skip_moderation');
            update_user_meta($request['user_id'], BASE.'verification_date', current_time('mysql'));
        }
@@ -424,7 +424,7 @@
            }, $allRequests);
        } else {
            foreach ($this->tables as $type => $tables) {
                $requests = $tables['requests'];;
                $requests = $tables['requests'];
                $active = $requests->pluck('request_id', [
                    'status'    => 'pending',
                ]);
inc/managers/Cache.php
@@ -190,7 +190,7 @@
        $value = $callback();
        if ($value !== null && $value !== false) {
            $this->set($key, $value);
            $this->set($key, $value, $ttl);
        }
        return $value;
inc/managers/CacheBack.php
@@ -1,6 +1,10 @@
<?php
namespace JVBase\managers;
use Redis;
use WP_Object_Cache;
use WP_Post;
if (!defined('ABSPATH')) {
    exit;
}
@@ -135,7 +139,7 @@
        return $value;
    }
    public function get(int|string|array $id): mixed
    public function get(int|string|array $id): bool
    {
        return false;
        if (is_array($id)) {
@@ -361,11 +365,11 @@
    /****************************************************
     * REDIS
     ****************************************************/
    private static function redis(): ?\Redis
    private static function redis(): ?Redis
    {
        global $wp_object_cache;
        return $wp_object_cache instanceof \WP_Object_Cache
        return $wp_object_cache instanceof WP_Object_Cache
        && isset($wp_object_cache->redis)
            ? $wp_object_cache->redis
            : null;
@@ -490,7 +494,7 @@
    /****************************************************
     * HOOKS - Posts
     ****************************************************/
    public static function onPostChange(int $postId, \WP_Post $post): void
    public static function onPostChange(int $postId, WP_Post $post): void
    {
        if (wp_is_post_revision($postId) || wp_is_post_autosave($postId)) {
            return;
inc/managers/CustomTable.php
@@ -2,6 +2,7 @@
namespace JVBase\managers;
use Exception;
use wpdb;
if (!defined('ABSPATH')) {
    exit;
@@ -19,7 +20,9 @@
 */
class CustomTable
{
    protected \wpdb $wpdb;
    private static ?wpdb $wpdb = null;
    private static string $prefix;
    protected string $tableName;
    protected string $definition;
    protected string $fullTableName;
@@ -93,20 +96,20 @@
        protected function ensureUserTable():void
        {
            if (!isset(static::$userTable)) {
                static::$userTable = is_multisite() ? $this->getMultisiteUsersTable() : $this->wpdb->users;
                static::$userTable = is_multisite() ? $this->getMultisiteUsersTable() : self::$wpdb->users;
            }
        }
            protected function getMultisiteUsersTable():string
            {
                $siteUsersTable = $this->wpdb->prefix . 'users';
                $siteExists = $this->wpdb->get_var(
                    $this->wpdb->prepare("SHOW TABLES LIKE %s", $siteUsersTable)
                $siteUsersTable = self::$wpdb->prefix . 'users';
                $siteExists = self::$wpdb->get_var(
                    self::$wpdb->prepare("SHOW TABLES LIKE %s", $siteUsersTable)
                );
                if ($siteExists) {
                    return $siteUsersTable;
                }
                //fallback to main one
                return $this->wpdb->users;
                return self::$wpdb->users;
            }
        protected function ensureUserIDType():void
        {
@@ -119,27 +122,27 @@
        protected function ensureTermIDType():void
        {
            if (!isset(static::$termIDType)) {
                static::$termIDType = $this->getColumnType($this->wpdb->terms, 'term_id');
                static::$termIDType = $this->getColumnType(self::$wpdb->terms, 'term_id');
            }
        }
        protected function ensureTermTable():void
        {
            if (!isset(static::$termTable)) {
                static::$termTable = $this->wpdb->terms;
                static::$termTable = self::$wpdb->terms;
            }
        }
        protected function ensurePostIDType():void
        {
            if (!isset(static::$postIDType)) {
                static::$postIDType = $this->getColumnType($this->wpdb->posts, 'ID');
                static::$postIDType = $this->getColumnType(self::$wpdb->posts, 'ID');
            }
        }
        protected function ensurePostTable():void
        {
            if (!isset(static::$postTable)) {
                static::$postTable = $this->wpdb->posts;
                static::$postTable = self::$wpdb->posts;
            }
        }
@@ -238,29 +241,29 @@
            error_log('[CustomTable]No definition set for '.$this->tableName);
            return;
        }
        if ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) {
        if (self::$wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) {
            return;
        }
        require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
        error_log('Creating Database Table: '. $this->tableName);
        $charset = self::$charsetCollate;
        $schema = "CREATE TABLE IF NOT EXISTS {$this->fullTableName} {$this->definition} {$charset}";
        $this->wpdb->flush();
        self::$wpdb->flush();
        $hasForeignKey = stripos($schema, 'FOREIGN KEY') !== false;
        if ($hasForeignKey) {
            $result = $this->wpdb->query($schema);
            $success = ($result !== false && !$this->wpdb->last_error);
            $result = self::$wpdb->query($schema);
            $success = ($result !== false && !self::$wpdb->last_error);
        } else {
            dbDelta($schema.';');
            $success = !$this->wpdb->last_error;
            $success = !self::$wpdb->last_error;
        }
        if (!$success) {
            $error_msg = "SQL Error creating table {$this->tableName}: " . $this->wpdb->last_error;
            $error_msg = "SQL Error creating table {$this->tableName}: " . self::$wpdb->last_error;
            error_log($error_msg);
            error_log("Failed SQL Query: " . $schema);
        } elseif ($this->wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) {
        } elseif (self::$wpdb->get_var("SHOW TABLES LIKE '{$this->fullTableName}'")) {
            error_log("Successfully created table: {$this->fullTableName}");
        }
    }
@@ -273,16 +276,30 @@
        self::$instances = [];
    }
    protected function checkWPDB():void
    {
        if (is_null(self::$wpdb)) {
            global $wpdb;
            self::$wpdb = $wpdb;
            self::$prefix = $wpdb->prefix;
            $usersStatus = self::$wpdb->get_row("SHOW TABLE STATUS LIKE '{self::$wpdb->users}'");
            $parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci';
            self::$charsetCollate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}";
        }
    }
    /**
     * @param string $tableName Table name without prefix/BASE (e.g., 'favourites', 'notifications')
     * @param bool $useTransactions Whether to auto-wrap operations in transactions
     */
    public function __construct(string $tableName, bool $user = false, bool $auth = false, bool $useTransactions = false)
    private function __construct(string $tableName, bool $user = false, bool $auth = false, bool $useTransactions = false)
    {
        global $wpdb;
        $this->wpdb = $wpdb;
        $this->checkWPDB();
        $this->tableName = $tableName;
        $this->fullTableName = $wpdb->prefix . apply_filters('jvb_base', BASE) . $tableName;
        $this->fullTableName = self::$prefix . apply_filters('jvb_base', BASE) . $tableName;
        $this->useTransactions = $useTransactions;
        $this->cache = Cache::for($tableName);
@@ -293,9 +310,6 @@
            $this->cache->auth();
        }
        $usersStatus = $this->wpdb->get_row("SHOW TABLE STATUS LIKE '{$this->wpdb->users}'");
        $parentCollation = $usersStatus->Collation ?? 'utf8mb4_general_ci';
        self::$charsetCollate = "DEFAULT CHARACTER SET utf8mb4 COLLATE {$parentCollation}";
    }
@@ -479,7 +493,7 @@
            $data['created_at'] = current_time('mysql');
        }
        $result = $this->wpdb->insert(
        $result = self::$wpdb->insert(
            $this->fullTableName,
            $data,
            $format
@@ -492,7 +506,7 @@
        $this->cache->flush();
        return $this->wpdb->insert_id;
        return self::$wpdb->insert_id;
    }
    /**
@@ -586,7 +600,7 @@
                  (" . implode(',', $columns_escaped) . ")
                  VALUES " . implode(',', $placeholders);
        $result = $this->wpdb->query($this->wpdb->prepare($query, $values));
        $result = self::$wpdb->query(self::$wpdb->prepare($query, $values));
        if ($result === false) {
            $this->logError('bulkInsert', ['rows' => count($rows)]);
@@ -620,7 +634,7 @@
                $query .= " WHERE {$where['sql']}";
                $values = $where['values'];
                return $this->wpdb->get_row($this->wpdb->prepare($query, $values), $output);
                return self::$wpdb->get_row(self::$wpdb->prepare($query, $values), $output);
            }
        );
    }
@@ -680,10 +694,10 @@
                }
                if (empty($values)) {
                    return $this->wpdb->get_results($query, $output);
                    return self::$wpdb->get_results($query, $output);
                }
                return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output);
                return self::$wpdb->get_results(self::$wpdb->prepare($query, $values), $output);
            }
        );
@@ -776,10 +790,10 @@
                }
                if (empty($values)) {
                    return (int) $this->wpdb->get_var($query);
                    return (int) self::$wpdb->get_var($query);
                }
                return (int) $this->wpdb->get_var($this->wpdb->prepare($query, $values));
                return (int) self::$wpdb->get_var(self::$wpdb->prepare($query, $values));
            }
        );
@@ -822,7 +836,7 @@
            $data['updated_at'] = current_time('mysql');
        }
        $result = $this->wpdb->update(
        $result = self::$wpdb->update(
            $this->fullTableName,
            $data,
            $where,
@@ -854,7 +868,7 @@
     */
    public function delete(array $where, ?array $where_format = null): int|false
    {
        $result = $this->wpdb->delete(
        $result = self::$wpdb->delete(
            $this->fullTableName,
            $where,
            $where_format
@@ -877,7 +891,7 @@
     *
     * @param string $query SQL query (use {table} as placeholder)
     * @param array $values Values for prepare()
     * @return mixed Query result
     * @return int|bool|\mysqli_result|null Query result
     *
     * @example
     * $results = $table->query(
@@ -885,15 +899,15 @@
     *     [1, 'pending', 'active']
     * );
     */
    public function query(string $query, array $values = []): mixed
    public function query(string $query, array $values = []): int|bool|null|\mysqli_result
    {
        $query = str_replace('{table}', $this->fullTableName, $query);
        if (empty($values)) {
            return $this->wpdb->query($query);
            return self::$wpdb->query($query);
        }
        return $this->wpdb->query($this->wpdb->prepare($query, $values));
        return self::$wpdb->query(self::$wpdb->prepare($query, $values));
    }
    /**
@@ -909,10 +923,10 @@
        $query = str_replace('{table}', $this->fullTableName, $query);
        if (empty($values)) {
            return $this->wpdb->get_results($query, $output);
            return self::$wpdb->get_results($query, $output);
        }
        return $this->wpdb->get_results($this->wpdb->prepare($query, $values), $output);
        return self::$wpdb->get_results(self::$wpdb->prepare($query, $values), $output);
    }
    /**
@@ -920,17 +934,17 @@
     *
     * @param string $query SQL query (use {table} as placeholder)
     * @param array $values Values for prepare()
     * @return mixed
     * @return string|null
     */
    public function queryVar(string $query, array $values = []): mixed
    public function queryVar(string $query, array $values = []): ?string
    {
        $query = str_replace('{table}', $this->fullTableName, $query);
        if (empty($values)) {
            return $this->wpdb->get_var($query);
            return self::$wpdb->get_var($query);
        }
        return $this->wpdb->get_var($this->wpdb->prepare($query, $values));
        return self::$wpdb->get_var(self::$wpdb->prepare($query, $values));
    }
    // =========================================================================
@@ -942,7 +956,7 @@
     */
    public function startTransaction(): void
    {
        $this->wpdb->query('START TRANSACTION');
        self::$wpdb->query('START TRANSACTION');
    }
    /**
@@ -950,7 +964,7 @@
     */
    public function commit(): void
    {
        $this->wpdb->query('COMMIT');
        self::$wpdb->query('COMMIT');
    }
    /**
@@ -958,7 +972,7 @@
     */
    public function rollback(): void
    {
        $this->wpdb->query('ROLLBACK');
        self::$wpdb->query('ROLLBACK');
    }
    /**
@@ -1007,7 +1021,7 @@
     */
    public function getInsertId(): int
    {
        return $this->wpdb->insert_id;
        return self::$wpdb->insert_id;
    }
    /**
@@ -1015,7 +1029,7 @@
     */
    public function getLastError(): string
    {
        return $this->wpdb->last_error;
        return self::$wpdb->last_error;
    }
    /**
@@ -1023,7 +1037,7 @@
     */
    public function getAffectedRows(): int
    {
        return $this->wpdb->rows_affected;
        return self::$wpdb->rows_affected;
    }
    public function getUserIDType():string
@@ -1061,16 +1075,16 @@
    // PRIVATE HELPERS
    // =========================================================================
    private function getColumnType(string $table, string $column):string|false {
        $tableExists = $this->wpdb->get_var(
            $this->wpdb->prepare("SHOW TABLES LIKE %s", $table)
        $tableExists = self::$wpdb->get_var(
            self::$wpdb->prepare("SHOW TABLES LIKE %s", $table)
        );
        if (!$tableExists) {
            error_log("[CustomTable] Table {$table} does not exist for getColumnType");
            return 'bigint(20) unsigned'; // safe fallback
        }
        $result = $this->wpdb->get_row(
            $this->wpdb->prepare(
        $result = self::$wpdb->get_row(
            self::$wpdb->prepare(
                "SELECT COLUMN_TYPE
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE TABLE_SCHEMA = DATABASE()
@@ -1089,7 +1103,7 @@
    private function tableExists():bool
    {
        return !is_null($this->wpdb->get_var($this->wpdb->prepare("SHOW TABLES LIKE %s", $this->fullTableName)));
        return !is_null(self::$wpdb->get_var(self::$wpdb->prepare("SHOW TABLES LIKE %s", $this->fullTableName)));
    }
    /**
     * Build WHERE clause from associative array
@@ -1165,7 +1179,7 @@
        static $cache = [];
        if (!isset($cache[$this->tableName])) {
            $columns = $this->wpdb->get_col("DESCRIBE {$this->fullTableName}");
            $columns = self::$wpdb->get_col("DESCRIBE {$this->fullTableName}");
            $cache[$this->tableName] = array_flip($columns);
        }
@@ -1180,12 +1194,11 @@
        if (function_exists('JVB')) {
            Errors::getInstance()->log(
                $this->tableName,
                "CustomTable {$operation} failed: " . $this->wpdb->last_error,
                $context,
                'error'
                "CustomTable {$operation} failed: " . self::$wpdb->last_error,
                $context
            );
        } else {
            error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . $this->wpdb->last_error);
            error_log("[CustomTable:{$this->tableName}] {$operation} failed: " . self::$wpdb->last_error);
        }
    }
inc/managers/Dashboard/DashboardPage.php
@@ -2,7 +2,6 @@
namespace JVBase\managers\Dashboard;
use JVBase\managers\Cache;
use JVBase\registrar\Registrar;
use WP_Query;
if (!defined('ABSPATH')) {
inc/managers/Emails.php
@@ -559,16 +559,11 @@
        $confirm_url = $email_data['confirm_url'];
        $request_type = $email_data['action_name'];
        switch ($request_type) {
            case 'export_personal_data':
                $request_name = 'Export Personal Data';
                break;
            case 'remove_personal_data':
                $request_name = 'Erase Personal Data';
                break;
            default:
                $request_name = 'Data Request';
        }
        $request_name = match ($request_type) {
            'export_personal_data' => 'Export Personal Data',
            'remove_personal_data' => 'Erase Personal Data',
            default => 'Data Request',
        };
        $message = sprintf(
inc/managers/Errors.php
@@ -219,8 +219,7 @@
            $user_was_logged_in = $user_id > 0 || (!empty($context['isLoggedIn']));
            // Determine source from context
            $source = isset($context['source']) ? $context['source'] :
                (isset($context['url']) ? 'frontend' : 'backend');
            $source = $context['source'] ?? (isset($context['url']) ? 'frontend' : 'backend');
            $result = $this->table->insert(
                [
@@ -355,7 +354,7 @@
            );
        }
        $body .= Emails::getInstance()->spacer(20);
        $body .= Emails::getInstance()->spacer();
        // Frequent errors section
        if (!empty($summary['frequent'])) {
inc/managers/Favourites.php
@@ -6,6 +6,7 @@
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\rest\routes\FavouritesRoutes;
use WP_Query;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
@@ -845,7 +846,7 @@
    /**
     * Join favourite counts for WP_Query when orderby => 'favourite_count'
     */
    public function maybeJoinFavouriteOrderby(string $join, \WP_Query $query): string
    public function maybeJoinFavouriteOrderby(string $join, WP_Query $query): string
    {
        if ($query->get('orderby') !== 'favourites_count') {
            return $join;
@@ -861,7 +862,7 @@
        return $join;
    }
    public function maybeApplyFavouriteOrderby(string $orderby, \WP_Query $query): string
    public function maybeApplyFavouriteOrderby(string $orderby, WP_Query $query): string
    {
        if ($query->get('orderby') !== 'favourites_count') {
            return $orderby;
inc/managers/IconsManager.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers;
use Exception;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
@@ -531,7 +532,7 @@
     */
    public function __wakeup()
    {
        throw new \Exception("Cannot unserialize singleton");
        throw new Exception("Cannot unserialize singleton");
    }
    /**
inc/managers/ImageGenerator.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers;
use JVBase\managers\UploadManager;
use WP_Error;
use Imagick;
use ImagickPixel;
@@ -184,13 +183,10 @@
        $artist_name = $this->data['display_name'];
        $city = $this->data['city'];
        switch ($this->type) {
            case 'shop':
                $title = 'Featured Shop Image';
                break;
            default:
                $title = 'Featured Artist Image';
        }
        $title = match ($this->type) {
            'shop' => 'Featured Shop Image',
            default => 'Featured Artist Image',
        };
        $attachment = [
            'post_mime_type' => 'image/webp',
@@ -213,14 +209,10 @@
        require_once(ABSPATH . 'wp-admin/includes/media.php');
        switch ($this->type) {
            case 'shop':
                $type = 'Tattoo Studio';
                break;
            default:
                $type = $this->data['type'];
                break;
        }
        $type = match ($this->type) {
            'shop' => 'Tattoo Studio',
            default => $this->data['type'],
        };
        // Set alt text
        $alt_text = sprintf(
            'Featured image for %s - %sin %s',
@@ -240,14 +232,10 @@
    protected function generateFilename():void
    {
        if ($this->fileManager) {
            switch ($this->type) {
                case 'shop':
                    $title = 'tattoo-shop';
                    break;
                default:
                    $title = 'artist';
                    break;
            }
            $title = match ($this->type) {
                'shop' => 'tattoo-shop',
                default => 'artist',
            };
            // Use FileUploadManager to generate a filename
            $user_data = get_userdata($this->data['user_id']);
@@ -578,7 +566,7 @@
     * @param string $color Center color
     * @return void
     */
    protected function createGradientBackgroundImagick(Imagick &$canvas, string $color): void
    protected function createGradientBackgroundImagick(Imagick $canvas, string $color): void
    {
        $width = $canvas->getImageWidth();
        $height = $canvas->getImageHeight();
@@ -959,7 +947,7 @@
     * @param string $color Center color
     * @return void
     */
    protected function createGradientBackgroundGD(GdImage &$canvas, string $color):void
    protected function createGradientBackgroundGD(GdImage $canvas, string $color):void
    {
        $width   = imagesx($canvas);
        $height  = imagesy($canvas);
@@ -1017,22 +1005,13 @@
        $imageInfo = getimagesize($imagePath);
        // Create image from source based on file type
        switch ($imageInfo[2]) {
            case IMAGETYPE_JPEG:
                $sourceImage = imagecreatefromjpeg($imagePath);
                break;
            case IMAGETYPE_PNG:
                $sourceImage = imagecreatefrompng($imagePath);
                break;
            case IMAGETYPE_GIF:
                $sourceImage = imagecreatefromgif($imagePath);
                break;
            case IMAGETYPE_WEBP:
                $sourceImage = imagecreatefromwebp($imagePath);
                break;
            default:
                throw new Exception("Unsupported image type");
        }
        $sourceImage = match ($imageInfo[2]) {
            IMAGETYPE_JPEG => imagecreatefromjpeg($imagePath),
            IMAGETYPE_PNG => imagecreatefrompng($imagePath),
            IMAGETYPE_GIF => imagecreatefromgif($imagePath),
            IMAGETYPE_WEBP => imagecreatefromwebp($imagePath),
            default => throw new Exception("Unsupported image type"),
        };
        // Calculate dimensions while maintaining aspect ratio
        $sourceWidth = imagesx($sourceImage);
inc/managers/InvitationsManager.php
@@ -4,6 +4,7 @@
use Exception;
use JVBase\Instance;
use JVBase\managers\queue\executors\InvitationExecutor;
use JVBase\managers\queue\Queue;
use JVBase\managers\queue\TypeConfig;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
@@ -153,10 +154,12 @@
    /**
     * Build invite types from JVB constants
     * Combines JVB_MEMBERSHIP['can_invite'] and invitable taxonomies
     * TODO: Should be accessible via Site::getInstance()->membership()\
     *
     */
    protected function buildInviteTypes(): array
    {
        $invitable = $this->cache->remember(
        return $this->cache->remember(
            'invitableTypes',
            function () {
                $types = [];
@@ -206,7 +209,6 @@
                return $types;
            }
        );
        return $invitable;
    }
@@ -664,7 +666,7 @@
            // Grant capabilities
            $user = get_userdata($userID);
            $user->add_cap('skip_moderation', true);
            $user->add_cap('skip_moderation');
            return true;
        });
inc/managers/KarmaManager.php
@@ -4,12 +4,9 @@
use Exception;
use JVBase\managers\queue\Queue;
use JVBase\rest\Rest;
use JVBase\rest\routes\ApprovalRoutes;
use JVBase\rest\routes\VoteRoutes;
use WP_Error;
use WP_Post;
use WP_Term;
use WP_User;
use WP_Query;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
@@ -99,7 +96,7 @@
        };
    }
        public function maybeJoinKarmaOrderby(string $join, \WP_Query $query): string
        public function maybeJoinKarmaOrderby(string $join, WP_Query $query): string
        {
            if (!in_array($query->get('orderby'), ['upvotes', 'downvotes', 'karmic_score'])) {
                return $join;
@@ -120,7 +117,7 @@
            return $join;
        }
        public function maybeApplyKarmaOrderby(string $orderby, \WP_Query $query): string
        public function maybeApplyKarmaOrderby(string $orderby, WP_Query $query): string
        {
            $expr = $this->karmaOrderExpr($query->get('orderby'));
            if (!$expr) {
inc/managers/Login.php
@@ -1,7 +1,7 @@
<?php
namespace JVBase\managers;
use JVBase\base\Site;
use JetBrains\PhpStorm\NoReturn;use JVBase\base\Site;
use JVBase\forms\TaxonomySelector;
use JVBase\Instance;use JVBase\integrations\services\Cloudflare;use JVBase\meta\Form;
@@ -44,10 +44,6 @@
        $this->routes = new LoginRoutes();
//      $this->cache = Cache::for('login');
//      $this->cache->flush();
        // Initialize magic link support if enabled
        if (Site::has('magic_link')) {
            $this->initMagicLinkSupport();
        }
        // Create login page if it doesn't exist
        $this->ensureLoginPageExists();
@@ -148,7 +144,8 @@
                    $config = $registrar->getConfig('register');
                    $icon = $registrar->getIcon('user');
                    $icon = ($icon !== '') ? jvbIcon($icon) : '';
                    $select[$role] = '<span class="label">'.$icon.$registrar->getSingular().'</span><span class="text">'.$config['description']??Site::login()->getDescription('register')??''.'</span>';
                    $desc = $config['description']??Site::login()->getDescription('register')??'';
                    $select[$role] = '<span class="label">'.$icon.$registrar->getSingular().'</span><span class="text">'.$desc.'</span>';
                    if (!empty($config['fields'])){
                        foreach ($config['fields'] as $field) {
                            $field['condition'] = [
@@ -355,13 +352,6 @@
        return $self->isLoginPage();
    }
    protected function initMagicLinkSupport(): void
    {
        if (!Site::has('magic_link')) {
            return;
        }
    }
    /*********************************************************************
        RENDERING
    *********************************************************************/
@@ -396,18 +386,7 @@
    protected function getAction():string
    {
        if (array_key_exists('action', $_GET)) {
            switch ($_GET['action']){
                case 'lostpassword':
                case 'retrievepassword': // Alias
                    $action = 'lostpassword';
                    break;
                case 'rp':
                case 'resetpass':
                    $action = 'resetpass';
                    break;
                default:
                    $action = $_GET['action'];
            }
            $action=match($_GET['action']){'lostpassword','retrievepassword'=>'lostpassword','rp','resetpass'=>'resetpass',default=>$_GET['action'],};
        } else {
            $action = 'login';
        }
@@ -429,19 +408,7 @@
    protected function setupTitle():void
    {
        switch ($this->action) {
            case 'lostpassword':
                $title = 'Lost Your Password?';
                break;
            case 'resetpass':
                $title = 'Reset Your Password';
                break;
            case 'register':
                $title = 'Create Your Account';
                break;
            default:
                $title = 'Log In To Your Account';
        }
        $title=match($this->action){'lostpassword'=>'Lost Your Password?','resetpass'=>'Reset Your Password','register'=>'Create Your Account',default=>'Log In To Your Account',};
        $this->title = $title;
    }
@@ -835,22 +802,7 @@
    protected function getDefaultLabels(): array
    {
        switch ($this->action) {
            case 'register':
                return Site::login()->getLabels('register');
            case 'lostpassword':
                return Site::login()->getLabels('lostPassword');
            case 'resetpass':
            case 'rp':
                return Site::login()->getLabels('resetPassword');
            case 'logout':
                return Site::login()->getLabels('logout');
            case 'magic':
                return Site::login()->getLabels('magic');
            case 'login':
            default:
                return Site::login()->getLabels('login');
        }
        return match($this->action){'register'=>Site::login()->getLabels('register'),'lostpassword'=>Site::login()->getLabels('lostPassword'),'resetpass','rp'=>Site::login()->getLabels('resetPassword'),'logout'=>Site::login()->getLabels('logout'),'magic'=>Site::login()->getLabels('magic'),default=>Site::login()->getLabels('login'),};
    }
    protected function maybeMagicLink(): void
@@ -1003,7 +955,7 @@
    /**
     * Handle login errors
     */
    protected function handleLoginError(WP_Error $error): void
    #[NoReturn]protected function handleLoginError(WP_Error $error): void
    {
        $login_url = wp_login_url();
        $login_url = add_query_arg('login_error', urlencode($error->get_error_code()), $login_url);
inc/managers/MagicLinks.php
@@ -358,6 +358,7 @@
    /**
     * Process login via magic link
     */
    #[NoReturn]
    protected function processLogin(array $token_data): void
    {
        $user = get_user_by('ID', $token_data['user_id']);
@@ -466,6 +467,7 @@
    /**
     * Process password reset via magic link
     */
    #[NoReturn]
    protected function processPasswordReset(array $token_data): void
    {
        $user = get_user_by('ID', $token_data['user_id']);
inc/managers/NewsRelationships.php
@@ -1,7 +1,7 @@
<?php
namespace JVBase\managers;
use JVBase\JVB;
use JVBase\managers\queue\Queue;
use WP_Post;
use WP_Term;
use WP_Query;
@@ -15,7 +15,6 @@
 */
class NewsRelationships
{
    private CustomTable $relationships;
    protected Cache $cache;
    public function __construct()
inc/managers/Notifications/EmailDigests.php
@@ -1,13 +1,13 @@
<?php
namespace JVBase\managers\Notifications;
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use JVBase\managers\Emails;
use JVBase\managers\NotificationManager;
use JVBase\managers\queue\Queue;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use WP_Error;
use WP_Query;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
inc/managers/Notifications/Notifications.php
@@ -335,11 +335,9 @@
            'page'      => 1,
        ]);
        $args = array_merge([
        return array_merge([
            'where' => $where,
        ],$order);
        return $args;
    }
    public function getUserNotifications(int $user_id, array $args = []):array
inc/managers/OperationQueue.php
@@ -5,6 +5,7 @@
use WP_Error;
use WP_REST_Response;
use WP_REST_Request;
use wpdb;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
@@ -27,7 +28,7 @@
    protected string $table = BASE.'_operation_queue';
    protected string $metricsTable = BASE.'stats__operation_queue';
    protected int $max_attempts = 3;
    protected \wpdb $wpdb;
    protected wpdb $wpdb;
    // System load thresholds
    protected array $load_thresholds = [
inc/managers/Referrals.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers;
use Exception;
use JVBase\Instance;
use JVBase\integrations\services\Cloudflare;
use JVBase\meta\Form;
@@ -910,7 +911,7 @@
            'From ' . $yesterday
        );
        $content .= Emails::getInstance()->spacer(20);
        $content .= Emails::getInstance()->spacer();
        $content .= Emails::getInstance()->h2('New Referrals');
        // Build list of referrals
@@ -980,7 +981,7 @@
                Emails::getInstance()->stat($from_user->referral_count, 'Total Referrals'),
                Emails::getInstance()->stat($from_user->treated_count, 'Treated')
            ];
            $cardContent .= Emails::getInstance()->grid($stats, 2);
            $cardContent .= Emails::getInstance()->grid($stats);
            $content .= Emails::getInstance()->card($cardContent);
            $rank++;
@@ -1000,7 +1001,7 @@
     */
    protected function generateCSV(array $referrals): string
    {
        $cache = Cache::for('referralCSV', null, HOUR_IN_SECONDS);
        $cache = Cache::for('referralCSV');
        return $cache->remember(
            'csv',
            function () use ($referrals) {
@@ -1223,7 +1224,7 @@
        }
        // Check if code is unique
        if ($this->isCodeTaken($code, $user_id)) {
        if ($this->isCodeTaken($code)) {
            add_action('user_profile_update_errors', function($errors) {
                $errors->add('referral_code_taken',
                    'This referral code is already in use.');
@@ -1272,7 +1273,7 @@
        Cloudflare::getInstance()->renderTurnstile();
        $turnstile = ob_get_clean();
        $reward_text = $this->getRewardText(true);
        $reward_text = $this->getRewardText();
        // Pre-fill code if from referral link
        $prefill_code = $_GET['ref'] ?? '';
@@ -2781,7 +2782,7 @@
                'message' => 'Referral settings saved successfully!'
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            return [
                'success' => false,
                'message' => 'Failed to save settings: ' . $e->getMessage()
inc/managers/Responses.php
@@ -3,9 +3,7 @@
use JVBase\Instance;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\rest\routes\ResponseRoutes;
use WP_Error;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
inc/managers/Roles.php
@@ -5,7 +5,6 @@
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use WP_User;
use WP_Role;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
@@ -711,7 +710,7 @@
        $owned = [];
        foreach ($user->allcaps as $cap => $value) {
            if ($value && strpos($cap, BASE . 'can_own_') === 0) {
            if ($value && str_starts_with($cap, BASE . 'can_own_')) {
                $termID = (int) str_replace(BASE . 'can_own_', '', $cap);
                if ($termID) {
                    $owned[] = $termID;
@@ -751,8 +750,8 @@
        $managed = [];
        foreach ($user->allcaps as $cap => $value) {
            if ($value && (strpos($cap, BASE . 'can_manage_') === 0 ||
                    strpos($cap, BASE . 'can_own_') === 0)) {
            if ($value && (str_starts_with($cap, BASE . 'can_manage_') ||
                    str_starts_with($cap, BASE . 'can_own_'))) {
                $termID = (int) str_replace([BASE . 'can_manage_', BASE . 'can_own_'], '', $cap);
                if ($termID && !in_array($termID, $managed)) {
                    $managed[] = $termID;
inc/managers/SEO/SEOAdminPage.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\managers\SEO;
use JVBase\admin\SEOAdmin;
use JVBase\Instance;
use JVBase\managers\AdminPages;
use JVBase\meta\Form;
use JVBase\registrar\Registrar;
inc/managers/SEO/_edmonotonink.php
@@ -13,8 +13,6 @@
// SITE-WIDE SCHEMA CONFIGURATION
// ==================================================
    use JVBase\meta\Meta;
    add_filter('jvb_schema', function ($schema) {
    return array_merge($schema, [
        'site_type' => 'directory',
inc/managers/SEO/render/DataType/Date.php
@@ -1,6 +1,8 @@
<?php
namespace JVBase\managers\SEO\render\DataType;
use DateMalformedStringException;
if (!defined('ABSPATH')) {
    exit;
}
@@ -12,7 +14,7 @@
    protected string $date;
    /**
     * @throws \DateMalformedStringException
     * @throws DateMalformedStringException
     */
    public function __construct(string $date)
    {
inc/managers/SEO/render/SchemaOutput.php
@@ -30,7 +30,7 @@
        $registrar = false;
        if (is_front_page()) {
            $schema[] = $this->buildWebsiteSchema(true);
            $schema[] = $this->buildWebsiteSchema();
            $test = $this->buildOrganizationSchema();
            if (!empty($test)) {
                $schema[] = $test;
inc/managers/SEO/render/Thing/Action/Action.php
@@ -5,12 +5,10 @@
use JVBase\managers\SEO\render\Traits\_Properties\actionProcessTrait;
use JVBase\managers\SEO\render\Traits\_Properties\actionStatusTrait;
use JVBase\managers\SEO\render\Traits\_Properties\agentTrait;
use JVBase\managers\SEO\render\Traits\_Properties\descriptionTrait;
use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait;
use JVBase\managers\SEO\render\Traits\_Properties\errorTrait;
use JVBase\managers\SEO\render\Traits\_Properties\instrumentTrait;
use JVBase\managers\SEO\render\Traits\_Properties\locationTrait;
use JVBase\managers\SEO\render\Traits\_Properties\nameTrait;
use JVBase\managers\SEO\render\Traits\_Properties\objectTrait;
use JVBase\managers\SEO\render\Traits\_Properties\participantTrait;
use JVBase\managers\SEO\render\Traits\_Properties\providerTrait;
inc/managers/SEO/render/Thing/Action/InteractAction.php
@@ -1,20 +1,6 @@
<?php
namespace JVBase\inc\managers\SEO\render\Thing\Action;
use JVBase\managers\SEO\render\Traits\_Properties\actionProcessTrait;
use JVBase\managers\SEO\render\Traits\_Properties\actionStatusTrait;
use JVBase\managers\SEO\render\Traits\_Properties\agentTrait;
use JVBase\managers\SEO\render\Traits\_Properties\endTimeTrait;
use JVBase\managers\SEO\render\Traits\_Properties\errorTrait;
use JVBase\managers\SEO\render\Traits\_Properties\instrumentTrait;
use JVBase\managers\SEO\render\Traits\_Properties\locationTrait;
use JVBase\managers\SEO\render\Traits\_Properties\objectTrait;
use JVBase\managers\SEO\render\Traits\_Properties\participantTrait;
use JVBase\managers\SEO\render\Traits\_Properties\providerTrait;
use JVBase\managers\SEO\render\Traits\_Properties\resultTrait;
use JVBase\managers\SEO\render\Traits\_Properties\startTimeTrait;
use JVBase\managers\SEO\render\Traits\_Properties\targetTrait;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/DefinedTermSet.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\CreativeWork;
use JVBase\managers\SEO\render\Thing\Intangible\DefinedTerm;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\hasDefinedTermTrait;
inc/managers/SEO/render/Thing/CreativeWork/HowTo.php
@@ -1,15 +1,9 @@
<?php
namespace JVBase\managers\SEO\render\Thing\CreativeWork;
use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
use JVBase\managers\SEO\render\Thing\Intangible\HowToSupply;
use JVBase\managers\SEO\render\Thing\Intangible\HowToTool;
use JVBase\managers\SEO\render\Thing\Intangible\Quantity\Duration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\MonetaryAmount;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Organization\Organization;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Thing\Person\Person;
use JVBase\managers\SEO\render\Traits\_Properties\estimateCostTrait;
use JVBase\managers\SEO\render\Traits\_Properties\performTimeTrait;
use JVBase\managers\SEO\render\Traits\_Properties\prepTimeTrait;
inc/managers/SEO/render/Thing/CreativeWork/HowToSection.php
@@ -2,9 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork;
use JVBase\managers\SEO\render\Thing\Intangible\Quantity\Duration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\MonetaryAmount;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/CreativeWork/HowToStep.php
@@ -2,9 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork;
use JVBase\managers\SEO\render\Thing\Intangible\Quantity\Duration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\MonetaryAmount;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/CreativeWork/WebPage/AboutPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/CheckoutPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/ContactPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/FAQPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/ItemPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/MedicalWebPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/QAPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/RealEstateListing.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/CreativeWork/WebPage/SearchResultsPage.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\CreativeWork\WebPage;
use JVBase\managers\SEO\render\Thing\CreativeWork\CreativeWork;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Intangible/AggregateOffer.php
@@ -1,23 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\DataType\Date;
use JVBase\managers\SEO\render\DataType\DateTime;
use JVBase\managers\SEO\render\DataType\Time;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\AdultOrientedEnumeration;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\BusinessFunction;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\DeliveryMethod;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\ItemAvailability;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\OfferItemCondition;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\PhysicalActivityCategory;
use JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\GeoShape;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PriceSpecification;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Place\AdministrativeArea\AdministrativeArea;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\highPriceTrait;
use JVBase\managers\SEO\render\Traits\_Properties\lowPriceTrait;
use JVBase\managers\SEO\render\Traits\_Properties\offerCountTrait;
inc/managers/SEO/render/Thing/Intangible/CategoryCode.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\Thing\CreativeWork\CategoryCodeSet;
use JVBase\managers\SEO\render\Traits\_Properties\codeValueTrait;
use JVBase\managers\SEO\render\Traits\_Properties\inCodeSetTrait;
inc/managers/SEO/render/Thing/Intangible/DefinedTerm.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\Thing\CreativeWork\DefinedTermSet;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\aboutTrait;
use JVBase\managers\SEO\render\Traits\_Properties\inDefinedTermSetTrait;
inc/managers/SEO/render/Thing/Intangible/EntryPoint.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\Thing\CreativeWork\DefinedTermSet;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\DigitalPlatformEnumeration;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\actionPlatformTrait;
use JVBase\managers\SEO\render\Traits\_Properties\contentTypeTrait;
inc/managers/SEO/render/Thing/Intangible/Enumeration/ActionStatusType.php
@@ -1,10 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Intangible/Enumeration/AdultOrientedEnumeration.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Intangible/Enumeration/BusinessEntityType.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/BusinessFunction.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Intangible/Enumeration/CertificationStatusEnumeration.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/DayOfWeek.php
@@ -1,11 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\Schedule;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\OpeningHoursSpecification;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\ServicePeriod;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Intangible/Enumeration/DeliveryMethod.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/DigitalPlatformEnumeration.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/EventStatusType.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/OrderStatusType.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/SizeGroupEnumeration.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/SizeSystemEnumeration.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/StatusEnumeration.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/WarrantyScope.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/Enumeration/WearableSizeGroupEnumeration.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Enumeration;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Intangible/HowToItem.php
@@ -2,7 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\requiredQuantityTrait;
inc/managers/SEO/render/Thing/Intangible/HowToTool.php
@@ -2,9 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\MonetaryAmount;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\requiredQuantityTrait;
inc/managers/SEO/render/Thing/Intangible/MenuItem.php
@@ -2,9 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\Thing\CreativeWork\MenuSection;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\RestrictedDiet;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\NutritionalInformation;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\menuAddOnTrait;
use JVBase\managers\SEO\render\Traits\_Properties\nutritionTrait;
inc/managers/SEO/render/Thing/Intangible/PaymentMethod.php
@@ -2,11 +2,7 @@
namespace JVBase\managers\SEO\render\Thing\Intangible;
use JVBase\managers\SEO\render\Thing\CreativeWork\MenuSection;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\RestrictedDiet;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\NutritionalInformation;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\StructuredValue;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\ThingSchema;
if (!defined('ABSPATH')) {
inc/managers/SEO/render/Thing/Intangible/Quantity/DurationOfWarranty.php
New file
@@ -0,0 +1,22 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Quantity;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
class DurationOfWarranty extends Thing
{
    public function __construct(string $duration)
    {
        $this->duration = date('c', $duration);
    }
    /**
     * @var string Duration (use ISO 8601 duration format).
     */
    protected string $duration;
}
inc/managers/SEO/render/Thing/Intangible/Quantity/_setup.php
@@ -4,4 +4,5 @@
}
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Quantity/Distance.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Quantity/Duration.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Quantity/DurationOfWarranty.php');
require(JVB_DIR . '/inc/managers/SEO/render/Thing/Intangible/Quantity/Mass.php');
inc/managers/SEO/render/Thing/Intangible/Reservation/Reservation.php
@@ -1,9 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\Reservation;
use JVBase\managers\SEO\render\Thing\Organization\Organization;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Thing\Person\Person;
use JVBase\managers\SEO\render\Traits\_Properties\bookingTimeTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceCurrencyTrait;
use JVBase\managers\SEO\render\Traits\_Properties\providerTrait;
inc/managers/SEO/render/Thing/Intangible/StructuredValue/PropertyValue.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\StructuredValue;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\maxValueTrait;
use JVBase\managers\SEO\render\Traits\_Properties\minValueTrait;
use JVBase\managers\SEO\render\Traits\_Properties\propertyIDTrait;
inc/managers/SEO/render/Thing/Intangible/StructuredValue/ServicePeriod.php
@@ -1,10 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\StructuredValue;
use JVBase\managers\SEO\render\DataType\Time;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\DayOfWeek;
use JVBase\managers\SEO\render\Thing\Intangible\Quantity\Duration;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\businessDaysTrait;
use JVBase\managers\SEO\render\Traits\_Properties\cutoffTimeTrait;
use JVBase\managers\SEO\render\Traits\_Properties\durationTrait;
inc/managers/SEO/render/Thing/Intangible/StructuredValue/StructuredValue.php
@@ -2,7 +2,6 @@
namespace JVBase\managers\SEO\render\Thing\Intangible\StructuredValue;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\ThingSchema;
if (!defined('ABSPATH')) {
inc/managers/SEO/render/Thing/Intangible/StructuredValue/WarrantyPromise.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Intangible\StructuredValue;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\WarrantyScope;
use JVBase\managers\SEO\render\Traits\_Properties\durationOfWarrantyTrait;
use JVBase\managers\SEO\render\Traits\_Properties\warrantyScopeTrait;
inc/managers/SEO/render/Thing/Organization/LocalBusiness/AnimalShelter.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/AutomotiveBusiness.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/ChildCare.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/Dentist.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/DryCleaningOrLaundry.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/EmergencyService.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/EntertainmentBusiness.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FinancialService.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Bakery.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/BarOrPub.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Brewery.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/CafeOrCoffeeShop.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Distillery.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/FastFoodRestaurant.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/IceCreamShop.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Restaurant.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/FoodEstablishment/Winery.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\FoodEstablishment;
use JVBase\managers\SEO\render\Thing\Organization\LocalBusiness\LocalBusiness;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/GovernmentOffice.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/HealthAndBeautyBusiness.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/HomeAndConstructionBusiness.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/InternetCafe.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/LegalService.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/Library.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/LocalBusiness.php
@@ -2,12 +2,10 @@
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Organization\Organization;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
use JVBase\managers\SEO\render\Traits\Place\PlaceSchema;
if (!defined('ABSPATH')) {
inc/managers/SEO/render/Thing/Organization/LocalBusiness/LodgingBusiness.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/ProfessionalService.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/RadioStation.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/RealEstateAgent.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/RecyclingCenter.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/SelfStorage.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/ShoppingCenter.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/SportsActivityLocation.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/Store.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Organization/LocalBusiness/TelevisionStation.php
@@ -1,13 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Organization\LocalBusiness;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Properties\currenciesAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\openingHoursTrait;
use JVBase\managers\SEO\render\Traits\_Properties\paymentAcceptedTrait;
use JVBase\managers\SEO\render\Traits\_Properties\priceRangeTrait;
use JVBase\managers\SEO\render\Traits\Organization\OrganizationSchema;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Place/AdministrativeArea/AdministrativeArea.php
@@ -1,10 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Place\AdministrativeArea;
use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
use JVBase\managers\SEO\render\Thing\Intangible\Rating\AggregateRating;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\LocationFeatureSpecification;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue;
use JVBase\managers\SEO\render\Thing\Thing;
inc/managers/SEO/render/Thing/Place/AdministrativeArea/City.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Place\AdministrativeArea;
use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Place/AdministrativeArea/Country.php
@@ -1,9 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Place\AdministrativeArea;
use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Thing/Place/AdministrativeArea/State.php
@@ -1,10 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Thing\Place\AdministrativeArea;
use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
use JVBase\managers\SEO\render\Thing\Place\AdministrativeArea\AdministrativeArea;
use JVBase\managers\SEO\render\Thing\Thing;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Traits/ThingSchema.php
@@ -10,7 +10,6 @@
use JVBase\managers\SEO\render\DataType\DateTime;
use JVBase\managers\SEO\render\DataType\Time;
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\PropertyValue;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Properties\additionalTypeTrait;
use JVBase\managers\SEO\render\Traits\_Properties\alternateNameTrait;
@@ -24,6 +23,7 @@
use JVBase\managers\SEO\render\Traits\_Properties\sameAsTrait;
use JVBase\managers\SEO\render\Traits\_Properties\subjectOfTrait;
use JVBase\managers\SEO\render\Traits\_Properties\urlTrait;
use ReflectionClass;
trait ThingSchema {
    use additionalTypeTrait, alternateNameTrait, descriptionTrait,
@@ -86,7 +86,7 @@
    public function getTypeName():string
    {
        return (new \ReflectionClass($this))->getShortName();
        return (new ReflectionClass($this))->getShortName();
    }
    public function getProperties():array
inc/managers/SEO/render/Traits/_Properties/audioTrait.php
@@ -2,7 +2,7 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\AudioObject;
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\Clip;
use JVBase\managers\SEO\render\Thing\CreativeWork\Clip;
use JVBase\managers\SEO\render\Thing\CreativeWork\MusicRecording;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
inc/managers/SEO/render/Traits/_Properties/byMonthWeekTrait.php
@@ -27,7 +27,7 @@
        $byMonthWeek = array_filter($byMonthWeek,
            function($week) {
                if ($week === 0 || $week > 6) {
                    error_log('[SEO] Invalid month number: '.$month);
                    error_log('[SEO] Invalid month number: '.$week);
                    return false;
                }
                return true;
inc/managers/SEO/render/Traits/_Properties/certificationIdentificationTrait.php
@@ -2,7 +2,6 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\Intangible\DefinedTerm;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Traits/_Properties/creatorTrait.php
@@ -4,7 +4,6 @@
use JVBase\managers\SEO\render\Thing\Organization\Organization;
use JVBase\managers\SEO\render\Thing\Person\Person;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
use JVBase\registrar\config\seo\Resolver;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Traits/_Properties/dateCreatedTrait.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use DateMalformedStringException;
use JVBase\managers\SEO\render\DataType\Date;
use JVBase\managers\SEO\render\DataType\DateTime;
@@ -19,7 +20,7 @@
    }
    /**
     * @throws \DateMalformedStringException
     * @throws DateMalformedStringException
     */
    public function setDateCreated(Date|DateTime|string $dateCreated):void
    {
inc/managers/SEO/render/Traits/_Properties/deathDateTrait.php
@@ -1,8 +1,8 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use DateMalformedStringException;
use JVBase\managers\SEO\render\DataType\Date;
use JVBase\managers\SEO\render\DataType\DateTime;
if (!defined('ABSPATH')) {
    exit;
@@ -19,7 +19,7 @@
    }
    /**
     * @throws \DateMalformedStringException
     * @throws DateMalformedStringException
     */
    public function setDeathDate(Date|string $deathDate):void
    {
inc/managers/SEO/render/Traits/_Properties/durationOfWarrantyTrait.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\Intangible\Quantity\DurationOfWarranty;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
if (!defined('ABSPATH')) {
inc/managers/SEO/render/Traits/_Properties/foundingDateTrait.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use DateMalformedStringException;
use JVBase\managers\SEO\render\DataType\Date;
if (!defined('ABSPATH')) {
@@ -18,7 +19,7 @@
    }
    /**
     * @throws \DateMalformedStringException
     * @throws DateMalformedStringException
     */
    public function setFoundingDate(Date|string $foundingDate):void
    {
inc/managers/SEO/render/Traits/_Properties/hasMenuTrait.php
@@ -2,8 +2,6 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\CreativeWork\Menu;
use JVBase\managers\SEO\render\Thing\CreativeWork\MenuSection;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Traits/_Properties/homeLocationTrait.php
@@ -1,11 +1,8 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\Intangible\ContactPoint\PostalAddress;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\ContactPoint;
use JVBase\managers\SEO\render\Thing\Intangible\VirtualHomeLocation;
use JVBase\managers\SEO\render\Thing\Place\Place;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Traits/_Properties/isVariantOfTrait.php
@@ -1,11 +1,8 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\Intangible\Service;
use JVBase\managers\SEO\render\Thing\Product\Product;
use JVBase\managers\SEO\render\Thing\Product\ProductGroup;
use JVBase\managers\SEO\render\Thing\Product\ProductModel;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Traits/_Properties/occupationalCategoryTrait.php
@@ -2,7 +2,6 @@
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\Intangible\CategoryCode;
use JVBase\managers\SEO\render\Thing\Intangible\Enumeration\PhysicalActivityCategory;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
inc/managers/SEO/render/Traits/_Properties/offersTrait.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\inc\managers\SEO\render\Thing\Intangible\ItemList\OfferCatalog;
use JVBase\managers\SEO\render\Thing\Intangible\Demand;
use JVBase\managers\SEO\render\Thing\Intangible\Offer;
use JVBase\managers\SEO\render\Thing\Intangible\Service;
@@ -185,7 +184,7 @@
            if ($hasAny->have_posts()) {
                $offer = new Offer();
                $typeRegistrar = Registrar::getInstance($type);
            }
        }
    }
inc/managers/SEO/render/Traits/_Properties/skuTrait.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Traits\_Helpers\arrayHelper;
if (!defined('ABSPATH')) {
    exit;
}
inc/managers/SEO/render/Traits/_Properties/validThroughTrait.php
@@ -3,7 +3,6 @@
use JVBase\managers\SEO\render\DataType\Date;
use JVBase\managers\SEO\render\DataType\DateTime;
use JVBase\managers\SEO\render\DataType\Time;
if (!defined('ABSPATH')) {
    exit;
inc/managers/SEO/render/Traits/_Properties/videoTrait.php
@@ -1,7 +1,7 @@
<?php
namespace JVBase\managers\SEO\render\Traits\_Properties;
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\Clip;
use JVBase\managers\SEO\render\Thing\CreativeWork\Clip;
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\VideoObject;
if (!defined('ABSPATH')) {
inc/managers/TaxonomyRelationships.php
@@ -3,6 +3,7 @@
use Exception;
use JVBase\Instance;
use JVBase\managers\queue\Queue;
use WP_Error;
use WP_Post;
use WP_Query;
inc/managers/UserTermsManager.php
@@ -2,8 +2,8 @@
namespace JVBase\managers;
use JVBase\Instance;
use JVBase\managers\queue\Queue;
use JVBase\registrar\Registrar;
use WP_Post;
use WP_Error;
use Exception;
use WP_Query;
inc/managers/VerifyEntryManager.php
@@ -7,7 +7,6 @@
use JVBase\Instance;
use JVBase\registrar\Registrar;
use JVBase\managers\CustomTable;
use JVBase\rest\Rest;
use JVBase\rest\routes\ApprovalRoutes;
inc/managers/_setup.php
@@ -1,7 +1,5 @@
<?php
use JVBase\managers\Approvals;
use JVBase\managers\Cache;
use JVBase\managers\IconsManager;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
inc/managers/queue/Metrics.php
@@ -1,5 +1,8 @@
<?php
namespace JVBase\managers\queue;
use DateTimeInterface;
use wpdb;
if (!defined('ABSPATH')) {
    exit;
}
@@ -7,7 +10,7 @@
final class Metrics
{
    public function __construct(
        private \wpdb $db,
        private wpdb $db,
        private string $queueTable,
        private string $statsTable
    ) {}
@@ -147,8 +150,8 @@
    /* ---------- Historical metrics (stats table) ---------- */
    public function dailySummary(
        \DateTimeInterface $from,
        \DateTimeInterface $to
        DateTimeInterface $from,
        DateTimeInterface $to
    ): array {
        $sql = "
        SELECT
@@ -179,8 +182,8 @@
    public function dailyByType(
        \DateTimeInterface $from,
        \DateTimeInterface $to
        DateTimeInterface $from,
        DateTimeInterface $to
    ): array {
        $sql = "
        SELECT
inc/managers/queue/Processor.php
@@ -1,5 +1,7 @@
<?php
namespace JVBase\managers\queue;
use Throwable;
if (!defined('ABSPATH')) {
    exit;
}
@@ -62,7 +64,7 @@
            $op->result = $result->result;
            $op->completedAt = current_time('mysql');
        } catch (\Throwable $e) {
        } catch (Throwable $e) {
            error_log("[Processor] Exception caught: " . $e->getMessage());
            $this->handleFailure($op, $e);
        }
@@ -138,7 +140,7 @@
                    }
                }
            } catch (\Throwable $e) {
            } catch (Throwable $e) {
                error_log("[Processor] Chunk {$index} failed: " . $e->getMessage());
                // Record failed items from this chunk
@@ -199,7 +201,7 @@
        return $chunks;
    }
    private function handleFailure(Operation $op, \Throwable $e): void
    private function handleFailure(Operation $op, Throwable $e): void
    {
        $hash = md5($e->getMessage());
inc/managers/queue/Queue.php
@@ -4,9 +4,12 @@
    exit;
}
use Exception;
use JVBase\Instance;
use JVBase\managers\AdminPages;
use JVBase\managers\Errors;
use JVBase\rest\routes\QueueRoutes;
use Throwable;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
@@ -103,7 +106,7 @@
                'updated_existing' => false,
            ];
        } catch (\Exception $e) {
        } catch (Exception $e) {
            Errors::getInstance()->log('queue', $e->getMessage(), $data, 'high');
            return new WP_Error('queue_failed', $e->getMessage());
        }
@@ -112,7 +115,7 @@
    /**
     * Try to merge incoming operation into an existing pending/scheduled one.
     * Returns result array if merged, null if not.
     * @throws \Throwable
     * @throws Throwable
     */
    private function tryMerge(Operation $incoming): ?array
    {
@@ -181,7 +184,7 @@
    public function maintenance(): void
    {
        $this->locker->withLock(function () {
            $this->storage->resetStuckOperations(30);
            $this->storage->resetStuckOperations();
        });
        $this->runQueueOnShutdown();
    }
@@ -204,7 +207,7 @@
    /**
     * Get a specific value from an operation - backwards compatibility
     */
    public function getOperationValue(string $id, string $column, bool $decodeJson = true): mixed
    public function getOperationValue(string $id, string $column, bool $decodeJson = true): \static|string|int|array|null
    {
        $op = $this->get($id);
        if (!$op) {
inc/managers/queue/Storage.php
@@ -7,6 +7,7 @@
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use LogicException;
use Throwable;
class Storage
{
@@ -411,7 +412,7 @@
            $result = $callback();
            $this->table->commit();
            return $result;
        } catch (\Throwable $e) {
        } catch (Throwable $e) {
            $this->table->rollback();
            error_log('[Storage] Transaction rolled back: ' . $e->getMessage());
            throw $e;
@@ -440,7 +441,7 @@
    }
    /**
     * @throws \Throwable
     * @throws Throwable
     */
    public function replaceDependency(string $fromId, string $toId): int
    {
inc/managers/queue/TypeRegistry.php
@@ -3,6 +3,8 @@
if (!defined('ABSPATH')) {
    exit;
}
use InvalidArgumentException;
use JVBase\managers\queue\mergers\DefaultMerger;
final class TypeRegistry
@@ -72,7 +74,7 @@
        if ($config->executor && $config->chunkKey) {
            // Verify executor can handle chunked operations
            if (!method_exists($config->executor, 'execute')) {
                throw new \InvalidArgumentException(
                throw new InvalidArgumentException(
                    "Executor for '{$type}' must implement execute() method"
                );
            }
inc/managers/queue/executors/ContentExecutor.php
@@ -1,11 +1,14 @@
<?php
namespace JVBase\managers\queue\executors;
use JVBase\managers\queue\{Executor, Operation, Progress, Result, Storage};
use DateTime;
use JVBase\managers\queue\{Executor, Operation, Progress, Result};
use JVBase\managers\Errors;
use JVBase\managers\Roles;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use Exception;
use WP_Post;
if (!defined('ABSPATH')) {
    exit;
@@ -23,8 +26,6 @@
    ];
    private int $userId;
    private string $postType;
    private array $fields = [];
    public function execute(Operation $operation, Progress $progress): Result
    {
@@ -135,8 +136,8 @@
//      }
        $outcome = 'success';
        if (!empty($errors)) {
            $outcome = count($errors) === count($posts) ? 'failed' : 'partial';
        if (!empty($results['errors'])) {
            $outcome = count($results['success']) === count($posts) ? 'failed' : 'partial';
        }
        return new Result(
@@ -361,7 +362,7 @@
        }
    }
    private function calculateTimeline(\WP_POST $previous, \WP_POST $current): ?string
    private function calculateTimeline(WP_POST $previous, WP_POST $current): ?string
    {
        $previousDate = strtotime($previous->post_date);
        $currentDate = strtotime($current->post_date);
@@ -379,8 +380,8 @@
            return $weeks === 1 ? '1 Week' : $weeks . ' Weeks';
        }
        $previousDateTime = new \DateTime($previous->post_date);
        $currentDateTime = new \DateTime($current->post_date);
        $previousDateTime = new DateTime($previous->post_date);
        $currentDateTime = new DateTime($current->post_date);
        $interval = $previousDateTime->diff($currentDateTime);
        $months = ($interval->y * 12) + $interval->m;
@@ -392,7 +393,7 @@
        return ($months === 1) ? '1 Month' : $months . ' Months';
    }
    private function swapTimelineParent(\WP_Post $oldParent, \WP_Post $newParent, array $allPosts): void
    private function swapTimelineParent(WP_Post $oldParent, WP_Post $newParent, array $allPosts): void
    {
        // Swap titles and content
        $originalTitle = $oldParent->post_title;
@@ -416,7 +417,7 @@
        ]);
        // Clear timeline taxonomy from new parent
        wp_set_object_terms($newParent->ID, [], BASE . 'timeline', false);
        wp_set_object_terms($newParent->ID, [], BASE . 'timeline');
        // Update all other posts to new parent
        foreach ($allPosts as $index => $post) {
inc/managers/queue/executors/ContentTermExecutor.php
@@ -2,6 +2,7 @@
namespace JVBase\managers\queue\executors;
use JVBase\managers\CustomTable;
use JVBase\managers\NotificationManager;
use JVBase\managers\queue\Executor;
use JVBase\managers\queue\Operation;
use JVBase\managers\queue\Progress;
@@ -318,18 +319,16 @@
        $term = get_term($termID, jvbCheckBase($taxonomy));
        $user = get_userdata($userID);
        foreach ($managers as $managerID) {
            NotificationManager::getInstance()->addNotification(
                $managerID,
                $notificationType,
                [
                    'user_id' => $userID,
                    'user_name' => $user ? $user->display_name : 'Unknown',
                    'term_id' => $termID,
                    'term_name' => $term && !is_wp_error($term) ? $term->name : 'Unknown',
                    'taxonomy' => $taxonomy
                ]
            );
        }
        //TODO: Check this
        NotificationManager::getInstance()->notify(
            $managers,
            $notificationType,
            $userID,
            [
                'target_id' => $termID,
                'target_type' => $taxonomy
            ]
        );
    }
}
inc/managers/queue/executors/IntegrationExecutor.php
@@ -3,7 +3,6 @@
use JVBase\managers\queue\{Executor, Operation, Progress, Result};
use Exception;
use JVBase\integrations\Integrations;
use JVBase\integrations\ServiceMap;
use JVBase\managers\Errors;
inc/managers/queue/executors/InvitationExecutor.php
@@ -1,6 +1,7 @@
<?php
namespace JVBase\managers\queue\executors;
use Exception;
use JVBase\managers\CustomTable;
use JVBase\managers\queue\Executor;
use JVBase\managers\queue\Operation;
@@ -82,12 +83,9 @@
                $this->table->rollback();
            }
            return Result::success($results, [
                'sent' => count($results['success']),
                'failed' => count($results['failed'])
            ]);
            return Result::success($results);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->table->rollback();
            return Result::fail($e->getMessage());
        }
inc/managers/queue/executors/UploadExecutor.php
@@ -198,7 +198,7 @@
            }
        }
        $progress->advance(1);
        $progress->advance();
        return new Result(
            outcome: 'success',
@@ -269,7 +269,7 @@
                $this->applyMeta($attachmentId, $info);
                $updatedCount++;
                $progress->advance(1);
                $progress->advance();
            } catch (Exception $e) {
                $success = false;
@@ -308,7 +308,7 @@
                if (!empty($file['temp_path']) && file_exists($file['temp_path'])) {
                    @unlink($file['temp_path']);
                }
                $progress->advance(1);
                $progress->advance();
            }
        }
@@ -412,7 +412,7 @@
                    }
                    $usedUploads = array_merge($usedUploads, $created['usedUploads']);
                    $progress->advance(1);
                    $progress->advance();
                }
            } catch (Exception $e) {
                $errors[] = $e->getMessage();
@@ -557,7 +557,7 @@
                    if (array_key_exists($uploadId, $uploads)) {
                        $attachmentId = (int)$uploads[$uploadId]['attachment_id'];
                        if ($uploadId === $featured) {
                            $featuredID = $attachmentId;;
                            $featuredID = $attachmentId;
                        } else {
                            $childPosts[] = $attachmentId;
                        }
inc/meta/Form.php
@@ -850,15 +850,11 @@
            public static function renderExistingAttachment(int $attachmentId, string $subtype):string
            {
                switch ($subtype){
                    case 'video':
                        return static::renderVideoPreview($attachmentId);
                    case 'document':
                    case 'file':
                        return static::renderFilePreview($attachmentId);
                    default:
                        return static::renderImagePreview($attachmentId);
                }
                return match ($subtype) {
                    'video' => static::renderVideoPreview($attachmentId),
                    'document', 'file' => static::renderFilePreview($attachmentId),
                    default => static::renderImagePreview($attachmentId),
                };
            }
            protected static function renderUploadItemStart(?int $attachmentId = null):string
            {
@@ -1034,7 +1030,7 @@
                }
                public static function renderImagePreview(?int $ID = null, ?array $additionalFields = null, bool $isTemplate = false):string
                {
                    $out = static::renderUploadItemStart($ID, $isTemplate);
                    $out = static::renderUploadItemStart($ID);
                    //add image preview
                    if ($ID) {
                        $out .= jvbFormatImage($ID, 'tiny', 'medium');
@@ -1382,8 +1378,15 @@
                        $plural
                    );
                    break;
                default:
                    $attr = false;
                    $single = false;
                    $plural = false;
            }
            if (!$attr || !$single || !$plural) {
                return '';
            }
            $dataAttrs = [];
            if ($config['update']) {
                $dataAttrs[] = 'data-update="false"';
@@ -1634,7 +1637,7 @@
                        $values = array_filter(array_values($values));
                        return implode(', ', $values) ?: 'New Item';
                    default:
                        if (strpos($tagFormat, '{') !== false) {
                        if (str_contains($tagFormat, '{')) {
                            $text = $tagFormat;
                            foreach ($values as $key => $value) {
                                $text = str_replace('{'.$key.'}', $value, $text);
inc/meta/Group.php
@@ -83,7 +83,7 @@
            }
            $this->data[$field] = $value;
        }
        $this->sync(true);
        $this->sync();
        return $this;
    }
inc/meta/Item.php
@@ -193,10 +193,9 @@
     */
    public function toArray(): array
    {
        $data = [];
        foreach ($this->fields as $name => $field) {
            $data[$name] = $field->value;
        }
        $data = array_map(function ($field) {
            return $field->value;
        }, $this->fields);
        return $data;
    }
}
inc/meta/Meta.php
@@ -688,7 +688,7 @@
    /**
     * Get default value for a field type
     */
    protected function getDefaultValue(string $name): mixed
    protected function getDefaultValue(string $name): string|int|bool|array
    {
        $config = $this->item->getFieldConfig($name);
        $type = $config['type'] ?? 'text';
inc/meta/Registry.php
@@ -120,12 +120,11 @@
    protected function getFieldSchema(array $field): array
    {
        if ($field['type'] === 'repeater') {
            $properties = [];
            foreach ($field['fields'] as $key => $subfield) {
                $properties[$key] = [
                    'type' => MetaTypeManager::getMetaType($subfield['type'])
                ];
            }
            $properties = array_map(function ($subfield) {
                return [
                    'type' => MetaTypeManager::getMetaType($subfield['type'])
                ];
            }, $field['fields']);
            return [
                'type'  => 'object',
inc/meta/Storage.php
@@ -4,6 +4,7 @@
use Exception;
use JVBase\managers\Cache;
use JVBase\managers\Errors;
use wpdb;
if (!defined('ABSPATH')) {
    exit;
@@ -14,7 +15,7 @@
 */
class Storage
{
    protected \wpdb $wpdb;
    protected wpdb $wpdb;
    public function __construct()
    {
@@ -174,7 +175,7 @@
                'item_id' => $item->id,
                'object_type' => $item->objectType,
                'fields' => array_keys($dirty)
            ], 'error');
            ]);
            return false;
        }
    }
@@ -188,7 +189,7 @@
        $config = $item->getFieldConfig($name);
        if ($config && ($config['type'] ?? '') === 'taxonomy' && !isset($config['taxonomy_type'])) {
            $taxonomy = jvbCheckBase($config['taxonomy']);
            wp_set_object_terms($item->id, [], $taxonomy, false);
            wp_set_object_terms($item->id, [], $taxonomy);
            return true;
        }
@@ -326,7 +327,7 @@
                $results[$meta->item()->id ?? 'options'] = false;
            }
            Errors::getInstance()->log('meta_storage', 'Bulk save failed: ' . $e->getMessage(), [], 'error');
            Errors::getInstance()->log('meta_storage', 'Bulk save failed: ' . $e->getMessage());
            return $results;
        }
@@ -541,12 +542,12 @@
        $taxonomy = jvbCheckBase($field->config['taxonomy']);
        $value = $field->value;
        if (empty(trim((string)$value))) {
            wp_set_object_terms($item->id, [], $taxonomy, false);
            wp_set_object_terms($item->id, [], $taxonomy);
            return true;
        }
        $termIds = array_map('intval', array_filter(explode(',', $value)));
        $result = wp_set_object_terms($item->id, $termIds, $taxonomy, false);
        $result = wp_set_object_terms($item->id, $termIds, $taxonomy);
        return !is_wp_error($result);
    }
@@ -733,7 +734,7 @@
                ? []
                : array_map('intval', array_filter(explode(',', $update['value'])));
            wp_set_object_terms($update['object_id'], $termIds, $update['taxonomy'], false);
            wp_set_object_terms($update['object_id'], $termIds, $update['taxonomy']);
        }
    }
inc/registrar/Fields.php
@@ -19,7 +19,7 @@
class Fields {
    protected array $fields;
    private ?Registrar $registrar = null;
    private ?Registrar $registrar;
    protected ?string $type;
    public function __construct(?string $type = null, ?Registrar $registrar = null) {
inc/registrar/Posts.php
@@ -2,6 +2,8 @@
namespace JVBase\registrar;
use JVBase\forms\TaxonomySelector;
use JVBase\managers\Errors;
use WP_Post;
if (!defined('ABSPATH')) {
    exit;
@@ -426,18 +428,17 @@
        // Use 'bottom' priority so taxonomy rules match first
        add_rewrite_rule(
            "{$this->unbased}/([^/]+)/?$",
            "index.php?post_type={$this->postType}&name=\$matches[1]",
            'bottom'
            "index.php?post_type={$this->postType}&name=\$matches[1]"
        );
    }
    /**
     * Set $this->rewrite_taxonomy to a valid taxonomy
     * @param string $url
     * @param \WP_Post $post
     * @param WP_Post $post
     * @return string
     */
    public function rewriteTaxonomySingle(string $url, \WP_Post $post): string
    public function rewriteTaxonomySingle(string $url, WP_Post $post): string
    {
        if ($post->post_type === $this->postType && !is_null($this->taxonomyRewrite)) {
            $type = $this->taxonomyRewrite;
@@ -472,7 +473,7 @@
    public function modifyWooCommerce() {
        if ($this->modify) {
            $args = $this->buildArgs();
            unset($args['labels']);;
            unset($args['labels']);
            $registered = get_post_type_object($this->postType);
            foreach ($args as $key => $value) {
inc/registrar/Registrar.php
@@ -267,14 +267,11 @@
                $exclude = $cache->remember(
                    $feature,
                    function () use ($instance, $feature) {
                        switch ($feature) {
                            case 'hide_single':
                                return $instance->excludeSingle();
                            case 'is_timeline':
                                return $instance->excludeTimeline();
                            default:
                                return [];
                        }
                        return match ($feature) {
                            'hide_single' => $instance->excludeSingle(),
                            'is_timeline' => $instance->excludeTimeline(),
                            default => [],
                        };
                    }
                );
@@ -483,7 +480,7 @@
            error_log('Integration not available for '.$this->slug.': '.$integration);
            return $this;
        }
        $this->integrationConfigs[$integration] = new Integration($integration, $this);
        $this->integrationConfigs[$integration] = new Integration($integration);
        $this->integrationFields[$integration] = new AddIntegrationFields($integration, $this);
        return $this;
    }
inc/registrar/config/Dashboard.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\registrar\config;
use JVBase\registrar\Registrar;
if (!defined('ABSPATH')) {
    exit;
}
inc/registrar/config/Directory.php
@@ -1,8 +1,6 @@
<?php
namespace JVBase\registrar\config;
use JVBase\registrar\Registrar;
if (!defined('ABSPATH')) {
    exit;
}
inc/registrar/config/seo/Helpers.php
@@ -8,7 +8,7 @@
function jvbTSFDoIt(string $slug, ?array $args):bool
{
    if (!class_exists('THE_SEO_Framework')) {
    if (!class_exists('THE_SEO_Framework') || !function_exists('tsf')) {
        return false;
    }
    $tsf = tsf();
inc/registrar/config/seo/Meta.php
@@ -127,8 +127,7 @@
        $meta = new \JVBase\meta\Meta($ID, Registrar::getInstance($this->slug)->getType());
        $resolved = Resolver::resolve($this->title, $meta);
        $resolved = $this->testLength($ID, $resolved, 'title');
        return $resolved;
        return $this->testLength($ID, $resolved, 'title');
    }
    public function filterDescription(string $description, ?array $args): string
inc/registrar/config/seo/Resolver.php
@@ -2,9 +2,10 @@
namespace JVBase\registrar\config\seo;
use JVBase\managers\Cache;
use JVBase\managers\SEO\render\SchemaOutput;
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\ImageObject;
use JVBase\managers\SEO\render\Thing\Intangible\Quantity\Distance;
use JVBase\managers\SEO\render\Thing\Intangible\StructuredValue\QuantitativeValue;
use JVBase\managers\SEO\render\Thing\Thing;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
@@ -15,7 +16,7 @@
class Resolver {
    protected static ?Meta $meta;
    public static function resolve(string $template, ?Meta $meta): string|array|\JVBase\managers\SEO\render\Thing\Thing
    public static function resolve(string $template, ?Meta $meta): string|array|Thing
    {
        if (!$meta) {
            if (is_singular()){
inc/registrar/config/seo/Schema.php
@@ -356,7 +356,7 @@
    public function define(string $property, string $value):void
    {
        $class = $this->getConfig('schema')['type'];
        $class = $this->getConfig()['type'];
        if (!class_exists($class)) {
            error_log('[SEO]Schema::defineReference Class not found: '.$class);
            return;
inc/registrar/fields/Field.php
@@ -59,7 +59,7 @@
        foreach ($config as $key => $value) {
            if (property_exists($class, $key)) {
                $method = 'set'.implode('',array_map('ucfirst',explode('_', $key)));;
                $method = 'set'.implode('',array_map('ucfirst',explode('_', $key)));
                $class->$method($value);
            } else {
//              error_log('Instance: '.print_r($class, true));
inc/registrar/fields/RepeaterField.php
@@ -2,6 +2,8 @@
namespace JVBase\registrar\fields;
use ReflectionClass;
if (!defined('ABSPATH')) {
    exit;
}
@@ -27,7 +29,7 @@
        $fields = array_filter(array_map(function($field) { return str_replace('}}','',$field);}, explode('{{',$variables)));
        foreach ($fields as $field) {
            if (!array_key_exists($field, $this->fields)) {
                error_log('['.(new \ReflectionClass($this))->getShortName().']Invalid field attempted: '.$field);
                error_log('['.(new ReflectionClass($this))->getShortName().']Invalid field attempted: '.$field);
                return false;
            }
        }
inc/registrar/helpers/HideSingle.php
@@ -1,9 +1,7 @@
<?php
namespace JVBase\inc\registrar\helpers;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use WP_Post;
if (!defined('ABSPATH')) {
    exit;
inc/registrar/helpers/MakeTimelineType.php
@@ -1,10 +1,6 @@
<?php
namespace JVBase\registrar\helpers;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use WP_Post;
if (!defined('ABSPATH')) {
    exit;
}
inc/rest/Response.php
@@ -26,9 +26,8 @@
    private array $data = [];
    private int $status = 200;
    private array $headers = [];
    private ?WP_REST_Response $response = null;
    /**
    /**
     * Create a success response
     */
    public static function success(array $data = [], int $status = 200): WP_REST_Response
inc/rest/Rest.php
@@ -1,7 +1,6 @@
<?php
namespace JVBase\rest;
use JVBase\Instance;
use JVBase\integrations\services\Cloudflare;
use JVBase\managers\Cache;
use JVBase\managers\Errors;
@@ -897,7 +896,7 @@
        if (!array_key_exists($taxonomy, $terms)) {
            $terms[$taxonomy] = [];
            $registrar = Registrar::getInstance($taxonomy);
            $terms[$taxonomy]['icon'] = $registrar ? $registrar->getIcon() : jvbDefaultIcon();;
            $terms[$taxonomy]['icon'] = $registrar ? $registrar->getIcon() : jvbDefaultIcon();
        }
        foreach ($ids as $id) {
            $data = $cache->remember(
inc/rest/RestRouteManager.php
@@ -71,10 +71,10 @@
     * @param string $nonceName nonce name to check
     * @param string|null $nonce optional additional nonce to check
     *
     * @return void|WP_Error
     */
    public function verifyNonce(WP_REST_Request $request, string $nonceName = 'wp_rest', string|null $nonce = null):mixed
    {
     * @return bool|WP_Error
     */
    public function verifyNonce(WP_REST_Request $request, string $nonceName = 'wp_rest', string|null $nonce = null): bool|WP_Error
    {
        $nonce = (!is_null($nonce)) ? $nonce :  $request->get_header('X-WP-Nonce');
        if (!wp_verify_nonce($nonce, $nonceName)) {
@@ -112,7 +112,7 @@
            return false;
        }
        // Verify nonces
        $this->verifyNonce($request, 'wp_rest');
        $this->verifyNonce($request);
        if ($this->action!=='') {
            $this->verifyNonce($request, $this->action . $user_id, $request->get_header('action_nonce'));
        }
@@ -925,7 +925,7 @@
                $context,
                'info'
            );
        } catch (\Exception $e) {
        } catch (Exception $e) {
            // Fallback to error_log if handler fails
            error_log("Security Audit: {$event} - " . json_encode($context));
        }
inc/rest/_setup.php
@@ -3,8 +3,6 @@
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
use JVBase\registrar\Registrar;
use JVBase\base\Site;
//NEW METHOD
require(JVB_DIR . '/inc/rest/Route.php');
inc/rest/routes/AdminRoutes.php
@@ -2,14 +2,10 @@
namespace JVBase\rest\routes;
use JVBase\Instance;
use JVBase\JVB;
use JVBase\managers\Cache;
use JVBase\managers\IconsManager;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\rest\PermissionHandler;
use WP_Query;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
@@ -72,14 +68,14 @@
    /**
     * Handle cache-related actions
     */
    public function handleCacheAction(\WP_REST_Request $request): \WP_REST_Response
    public function handleCacheAction(WP_REST_Request $request): WP_REST_Response
    {
        $action = sanitize_text_field($request->get_param('action'));
        switch ($action) {
            case 'flush-all':
                $total = Cache::flushAll();
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => true,
                    'message' => $total.' caches flushed successfully'
                ]);
@@ -87,7 +83,7 @@
            case 'flush-cache':
                $group = sanitize_text_field($request->get_param('group'));
                if (empty($group)) {
                    return new \WP_REST_Response([
                    return new WP_REST_Response([
                        'success' => false,
                        'message' => 'No cache group specified'
                    ], 400);
@@ -95,13 +91,13 @@
                Cache::for($group)?->flush();
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => true,
                    'message' => "Cache group '{$group}' flushed successfully"
                ]);
            default:
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => false,
                    'message' => 'Invalid action'
                ], 400);
@@ -111,7 +107,7 @@
    /**
     * Handle icon-related actions
     */
    public function handleIconAction(\WP_REST_Request $request): \WP_REST_Response
    public function handleIconAction(WP_REST_Request $request): WP_REST_Response
    {
        $action = sanitize_text_field($request->get_param('action'));
        $source = sanitize_text_field($request->get_param('source') ?? 'icons');
@@ -123,7 +119,7 @@
                $icons->forceRefresh();
                IconsManager::regenerateAllCSS([$source => true]);
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => true,
                    'message' => "Icon CSS regenerated successfully for '{$source}'"
                ]);
@@ -135,7 +131,7 @@
                }
                IconsManager::regenerateAllCSS();
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => true,
                    'message' => 'All icon CSS files regenerated successfully'
                ]);
@@ -143,20 +139,20 @@
            case 'restore-icon-version':
                $timestamp = (int)$request->get_param('timestamp');
                if (empty($timestamp)) {
                    return new \WP_REST_Response([
                    return new WP_REST_Response([
                        'success' => false,
                        'message' => 'No timestamp provided'
                    ], 400);
                }
                if ($icons->restoreVersion($timestamp)) {
                    return new \WP_REST_Response([
                    return new WP_REST_Response([
                        'success' => true,
                        'message' => 'Icon version restored successfully'
                    ]);
                }
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => false,
                    'message' => 'Failed to restore icon version'
                ], 500);
@@ -165,7 +161,7 @@
                $timestamps = $request->get_param('timestamps');
                if (empty($timestamps) || !is_array($timestamps)) {
                    return new \WP_REST_Response([
                    return new WP_REST_Response([
                        'success' => false,
                        'message' => 'No versions selected for merging'
                    ], 400);
@@ -174,7 +170,7 @@
                $timestamps = array_map('intval', $timestamps);
                if (count($timestamps) < 2) {
                    return new \WP_REST_Response([
                    return new WP_REST_Response([
                        'success' => false,
                        'message' => 'Please select at least 2 versions to merge'
                    ], 400);
@@ -184,19 +180,19 @@
                    // Regenerate CSS after merge
                    IconsManager::regenerateAllCSS([$source => true]);
                    return new \WP_REST_Response([
                    return new WP_REST_Response([
                        'success' => true,
                        'message' => 'Icon versions merged successfully'
                    ]);
                }
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => false,
                    'message' => 'Failed to merge icon versions'
                ], 500);
            default:
                return new \WP_REST_Response([
                return new WP_REST_Response([
                    'success' => false,
                    'message' => 'Invalid action'
                ], 400);
inc/rest/routes/ApprovalRoutes.php
@@ -179,16 +179,14 @@
     * @param string $taxonomy Taxonomy
     * @param string $name New Term Name
     * @param int $parent Parent Term ID
     * @param int $required_approvals Number of approvals required
     *
     *
     * @return int|false Request ID or false on failure
     */
    public function createTermApprovalRequest(
        int $user_id,
        string $taxonomy,
        string $name,
        int $parent = 0,
        int $required_approvals = 3
        int $parent = 0
    ): int|false {
        $result = Approvals::getInstance()->createApproval(
@@ -204,7 +202,7 @@
    {
        $user_id = get_current_user_id();
        $type = sanitize_text_field($request->get_param('type') ?? 'all');
        $status = sanitize_text_field($request->get_param('status') ?? 'pending');
//      $status = sanitize_text_field($request->get_param('status') ?? 'pending');
        $result = Approvals::getInstance()->getApprovalRequests($user_id, $type === 'all' ? null : $type);
inc/rest/routes/ContentRoutes.php
@@ -399,7 +399,7 @@
        // Search term is already in $args['s'] from earlier
        // Get all taxonomies registered to this post type
        $taxonomies = get_object_taxonomies($this->post_type, 'names');
        $taxonomies = get_object_taxonomies($this->post_type);
        if (empty($taxonomies)) {
            return $args;
inc/rest/routes/ContentTermsRoutes.php
@@ -11,7 +11,6 @@
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\rest\PermissionHandler;
use JVBase\base\Site;
use JVBase\managers\CustomTable;
use WP_REST_Request;
use WP_REST_Response;
@@ -269,7 +268,7 @@
            $total = $query->countResults();
            $members = $query
                ->orderBy('is_primary', 'DESC')
                ->orderBy('is_primary')
                ->orderBy('created_at', 'ASC')
                ->limit($perPage, $offset)
                ->getResults(ARRAY_A);
@@ -338,7 +337,7 @@
            $total = $query->countResults();
            $requests = $query
                ->orderBy('created_date', 'DESC')
                ->orderBy('created_date')
                ->limit($perPage, $offset)
                ->getResults(ARRAY_A);
@@ -463,16 +462,16 @@
                }
            });
            // Notify user
            NotificationManager::getInstance()->addNotification(
                $requestData['user_id'],
                'request_' . $status,
                [
                    'term_id' => $requestData['term_id'],
                    'taxonomy' => $this->taxonomy,
                    'notes' => $notes
                ]
            );
            // Notify user TODO
//          NotificationManager::getInstance()->notify(
//              $requestData['user_id'],
//              'request_' . $status,
//              [
//                  'term_id' => $requestData['term_id'],
//                  'taxonomy' => $this->taxonomy,
//                  'notes' => $notes
//              ]
//          );
            $this->cache->flush();
@@ -548,17 +547,18 @@
        $term = get_term($termID, jvbCheckBase($this->taxonomy));
        foreach ($managers as $managerID) {
            NotificationManager::getInstance()->addNotification(
                $managerID,
                $notificationType,
                [
                    'user_id' => $actingUserID,
                    'term_id' => $termID,
                    'term_name' => $term->name ?? 'Unknown',
                    'taxonomy' => $this->taxonomy
                ]
            );
        }
        //TODO
//      foreach ($managers as $managerID) {
//          NotificationManager::getInstance()->addNotification(
//              $managerID,
//              $notificationType,
//              [
//                  'user_id' => $actingUserID,
//                  'term_id' => $termID,
//                  'term_name' => $term->name ?? 'Unknown',
//                  'taxonomy' => $this->taxonomy
//              ]
//          );
//      }
    }
}
inc/rest/routes/FavouritesRoutes.php
@@ -4,7 +4,10 @@
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use JVBase\managers\Emails;
use JVBase\managers\Favourites;
use JVBase\managers\NotificationManager;
use JVBase\managers\queue\Queue;
use JVBase\registrar\Registrar;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Response;
@@ -553,7 +556,7 @@
                // Get list items
                $items = $this->listItems
                    ->where(['list_id' => $list_id])
                    ->orderBy('added_at', 'DESC')
                    ->orderBy('added_at')
                    ->getResults();
                // Format items - convert to favourite-like objects
@@ -578,10 +581,11 @@
                // Get shared users if owner
                $shared_users = [];
                $is_owner = false;//TODO
                if ($is_owner) {
                    $shares = $this->listShares
                        ->where(['list_id' => $list_id])
                        ->orderBy('created_at', 'DESC')
                        ->orderBy('created_at')
                        ->getResults();
                    foreach ($shares as $share_item) {
@@ -600,7 +604,7 @@
                        $shared_users[] = $shared_user;
                    }
                }
                $list = []; //TODO: check this
                return [
                    'success' => true,
                    'list' => [
@@ -1027,19 +1031,20 @@
                ]);
                // Send notification
                NotificationManager::getInstance()->addNotification(
                    $share_user->ID,
                    'list_shared',
                    $user_id,
                    sprintf('%s shared a favorites list with you: "%s"', $owner_name, $list->name),
                    $list_id,
                    'favourites_list',
                    [
                        'list_id' => $list_id,
                        'list_name' => $list->name,
                        'permission_type' => $permission_type
                    ]
                );
                // TODO This should be handled by the Favourites:: manager
//              NotificationManager::getInstance()->addNotification(
//                  $share_user->ID,
//                  'list_shared',
//                  $user_id,
//                  sprintf('%s shared a favorites list with you: "%s"', $owner_name, $list->name),
//                  $list_id,
//                  'favourites_list',
//                  [
//                      'list_id' => $list_id,
//                      'list_name' => $list->name,
//                      'permission_type' => $permission_type
//                  ]
//              );
                return [
                    'success' => true,
@@ -1338,7 +1343,7 @@
        // For posts
        if (str_contains($type, 'post') || in_array($type, array_keys($this->valid_types))) {
            $post = get_post($target_id);
            return $post ? $post->post_author : null;
            return $post?->post_author;
        }
        // For terms (shops, etc.)
@@ -1579,7 +1584,7 @@
        };
        if (!$operation_type) {
            return $this->error('Invalid action', 'invalid_action', 400);
            return $this->error('Invalid action', 'invalid_action');
        }
        Queue::getInstance()->queueOperation(
inc/rest/routes/FeedRoutes.php
@@ -71,7 +71,7 @@
                'highlight' => 'string',
            ])
            ->auth('public')
            ->rateLimit(30, 60)
            ->rateLimit(30)
            ->post([$this, 'handleFeedRequest'])
            ->args([
                'content' => 'string',
inc/rest/routes/FormRoutes.php
@@ -8,7 +8,6 @@
use JVBase\meta\Validator;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Rest;
use JVBase\managers\Cache;
use JVBase\blocks\FormBlock;
use JVBase\rest\Route;
use JVBase\base\Site;
@@ -99,8 +98,7 @@
            if (is_wp_error($result)) {
                return $this->error(
                    $result->get_error_message(),
                    $result->get_error_code(),
                    400
                    $result->get_error_code()
                );
            }
            if (array_key_exists('success', $result)){
@@ -161,7 +159,7 @@
        if (!empty($files)) {
            try {
                $files = $this->validateFileUploads($files, $form_config);
            } catch (\Exception $e) {
            } catch (Exception $e) {
                return new WP_Error('file_validation_failed', 'File validation error: ' . $e->getMessage());
            }
        }
@@ -169,7 +167,7 @@
        // Validate and sanitize form data
        try {
            $processed_data = $this->validateAndSanitizeData($form_config, $form_data);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            return new WP_Error('validation_failed', 'Data validation error: ' . $e->getMessage());
        }
        if (array_key_exists('success', $processed_data) && $processed_data['success'] === false) {
@@ -179,7 +177,7 @@
        // Send email notification with attachments
        try {
            $email_sent = $this->sendEmailNotification($form_type, $form_config, $processed_data, $files);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            return new WP_Error('email_failed', 'Email error: ' . $e->getMessage());
        }
@@ -367,7 +365,7 @@
            ];
        }
        $body .= Emails::getInstance()->spacer(20);
        $body .= Emails::getInstance()->spacer();
        $body .= Emails::getInstance()->table($form);
        // Show attachment info
@@ -375,8 +373,7 @@
        if (!empty($attachments)) {
            $body .= Emails::getInstance()->spacer(10);
            $body .= Emails::getInstance()->alert(
                sprintf('%d file(s) attached to this email', count($attachments)),
                'info'
                sprintf('%d file(s) attached to this email', count($attachments))
            );
        }
@@ -707,18 +704,18 @@
        $forms = FormBlock::getForms();
        // Remove sensitive data
        $public_forms = [];
        foreach ($forms as $key => $config) {
            $public_forms[$key] = [
        // Remove sensitive configuration
        $public_forms = array_map(function ($config) use ($field) {
            return [
                'title' => $config['title'],
                'description' => $config['description'],
                'fields' => array_map(function($field) {
                'fields' => array_map(function ($field) {
                    // Remove sensitive configuration
                    unset($field['email_to']);
                    return $field;
                }, $config['fields'])
            ];
        }
        }, $forms);
        return $this->success($public_forms);
    }
inc/rest/routes/ImporterRoutes.php
@@ -9,7 +9,6 @@
use JVBase\rest\Route;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
@@ -76,14 +75,14 @@
        // Get uploaded file
        $files = $request->get_file_params();
        if (empty($files['file'])) {
            return $this->error('No file uploaded', 'no_file', 400);
            return $this->error('No file uploaded', 'no_file');
        }
        $file = $files['file'];
        // Validate file type
        if (!$this->isValidCSV($file)) {
            return $this->error('Invalid file type. Please upload a CSV file.', 'invalid_file', 400);
            return $this->error('Invalid file type. Please upload a CSV file.', 'invalid_file');
        }
        // Get and parse options
@@ -140,14 +139,14 @@
        // Get uploaded file
        $files = $request->get_file_params();
        if (empty($files['file'])) {
            return $this->error('No file uploaded', 'no_file', 400);
            return $this->error('No file uploaded', 'no_file');
        }
        $file = $files['file'];
        // Validate file type
        if (!$this->isValidCSV($file)) {
            return $this->error('Invalid file type. Please upload a CSV file.', 'invalid_file', 400);
            return $this->error('Invalid file type. Please upload a CSV file.', 'invalid_file');
        }
        // Get and parse options
inc/rest/routes/Invitations.php
@@ -4,6 +4,7 @@
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\managers\InvitationsManager;
use JVBase\managers\queue\Queue;
use JVBase\managers\Roles;
use JVBase\rest\Rest;
use JVBase\managers\CustomTable;
inc/rest/routes/LoginRoutes.php
@@ -1,9 +1,11 @@
<?php
namespace JVBase\rest\routes;
use Exception;
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\managers\Emails;
use JVBase\managers\MagicLinks;
use JVBase\managers\Referrals;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\rest\Route;
@@ -414,8 +416,7 @@
        if (is_wp_error($user)) {
            return $this->error(
                'Invalid or expired reset link.',
                'invalid_key',
                400
                'invalid_key'
            );
        }
@@ -441,8 +442,7 @@
        if (!Site::has('magic_link')) {
            return $this->error(
                'Magic link authentication is not enabled.',
                'feature_disabled',
                400
                'feature_disabled'
            );
        }
@@ -476,8 +476,7 @@
            return $this->error(
                $result->get_error_message(),
                $result->get_error_code(),
                400
                $result->get_error_code()
            );
        }
@@ -597,7 +596,7 @@
        try {
            Referrals::getInstance()->processReferralCode($user_id, $referral_code);
        } catch (\Exception $e) {
        } catch (Exception $e) {
            $this->logError('Referral processing failed', [
                'user_id' => $user_id,
                'code' => $referral_code,
@@ -639,18 +638,13 @@
     */
    protected function sanitizeFieldValue(mixed $value, string $type):string|int
    {
        switch ($type) {
            case 'email':
                return sanitize_email($value);
            case 'url':
                return esc_url_raw($value);
            case 'textarea':
                return sanitize_textarea_field($value);
            case 'number':
                return absint($value);
            default:
                return sanitize_text_field($value);
        }
        return match ($type) {
            'email' => sanitize_email($value),
            'url' => esc_url_raw($value),
            'textarea' => sanitize_textarea_field($value),
            'number' => absint($value),
            default => sanitize_text_field($value),
        };
    }
    public static function auth():array
inc/rest/routes/MagicLinkRoutes.php
@@ -5,7 +5,6 @@
use JVBase\managers\MagicLinks;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
if (!defined('ABSPATH')) {
    exit;
inc/rest/routes/NewsRoutes.php
@@ -2,6 +2,7 @@
namespace JVBase\rest\routes;
use JVBase\Instance;
use JVBase\managers\queue\Queue;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Rest;
use JVBase\meta\Meta;
inc/rest/routes/NotificationsRoutes.php
@@ -1,13 +1,11 @@
<?php
namespace JVBase\rest\routes;
use JVBase\base\Site;
use JVBase\Instance;
use JVBase\managers\Approvals;
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use JVBase\managers\NotificationManager;
use JVBase\registrar\Registrar;
use JVBase\managers\queue\Queue;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use WP_REST_Request;
@@ -337,7 +335,7 @@
                            'action_user_id' => $group->action_user_id,
                            'type' => $group->type
                        ])
                        ->orderBy('created_at', 'DESC')
                        ->orderBy('created_at')
                        ->first();
                    if ($notification) {
@@ -1042,19 +1040,10 @@
     */
    protected function getItemLink(int $ID, string $content): string
    {
        switch ($content) {
            case BASE.'artist':
            case BASE.'artwork':
            case BASE.'event':
            case BASE.'news':
            case BASE.'offer':
            case BASE.'partner':
            case BASE.'piercing':
            case BASE.'tattoo':
                return get_permalink($ID);
            default:
                return get_term_link($ID, BASE.$content);
        }
        return match ($content) {
            BASE . 'artist', BASE . 'artwork', BASE . 'event', BASE . 'news', BASE . 'offer', BASE . 'partner', BASE . 'piercing', BASE . 'tattoo' => get_permalink($ID),
            default => get_term_link($ID, BASE . $content),
        };
    }
    /**
@@ -1143,7 +1132,7 @@
            return $irregular[$word];
        }
        if (substr($word, -1) === 'y') {
        if (str_ends_with($word, 'y')) {
            return substr($word, 0, -1) . 'ies';
        }
inc/rest/routes/OptionsRoutes.php
@@ -3,11 +3,9 @@
use JVBase\base\Options;
use JVBase\Instance;
use JVBase\registrar\Registrar;
use JVBase\managers\queue\Queue;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Rest;
use JVBase\managers\Cache;
use JVBase\meta\Meta;
use JVBase\rest\Route;
use WP_REST_Request;
use WP_REST_Response;
inc/rest/routes/QueueRoutes.php
@@ -257,7 +257,7 @@
            return Response::notFound('Operation not found');
        }
        return Response::item($this->formatOperation($op, true), 'operation');
        return Response::item($this->formatOperation($op), 'operation');
    }
inc/rest/routes/ReferralRoutes.php
@@ -134,7 +134,7 @@
            'treated' => $this->actionUpdateStatus($request, 'treated'),
            'remove' => $this->actionRemove($request),
            'resend' => $this->actionResend($request),
            default => $this->error('Invalid action', 'invalid_action', 400)
            default => $this->error('Invalid action', 'invalid_action')
        };
    }
@@ -164,7 +164,7 @@
        }
        if (empty($sanitized_invitations)) {
            return $this->error('No valid invitations provided', 'no_invitations', 400);
            return $this->error('No valid invitations provided', 'no_invitations',);
        }
        $operationID = sanitize_text_field($request->get_param('id'));
@@ -193,7 +193,7 @@
        $referral_id = $request->get_param('referral_id');
        if (!$referral_id) {
            return $this->error('referral_id required', 'missing_id', 400);
            return $this->error('referral_id required', 'missing_id');
        }
        [$success, $message] = Referrals::getInstance()->updateStatus($referral_id, $status);
@@ -215,7 +215,7 @@
    {
        $referral_id = $request->get_param('referral_id');
        if (!$referral_id) {
            return $this->error('referral_id required', 'missing_id', 400);
            return $this->error('referral_id required', 'missing_id');
        }
        $result = Referrals::getInstance()->removeReferral($referral_id, get_current_user_id());
@@ -234,7 +234,7 @@
    {
        $referral_id = $request->get_param('referral_id');
        if (!$referral_id) {
            return $this->error('referral_id required', 'missing_id', 400);
            return $this->error('referral_id required', 'missing_id');
        }
        $result = Referrals::getInstance()->resendInvitation($referral_id, get_current_user_id());
@@ -261,7 +261,7 @@
        $code = Referrals::getInstance()->getUserReferralCode($user_id);
        if (is_wp_error($code)) {
            return $this->error($code->get_error_message(), 'code_error', 400);
            return $this->error($code->get_error_message(), 'code_error');
        }
        return $this->success([
@@ -279,7 +279,7 @@
        $code = strtoupper(sanitize_text_field($request->get_param('code')??''));
        if (empty($code)) {
            return $this->error('Code required', 'missing_code', 400);
            return $this->error('Code required', 'missing_code');
        }
        $referrer = Referrals::getInstance()->getUserByReferralCode($code);
@@ -384,13 +384,13 @@
    public function handleClientUpload(WP_REST_Request $request): WP_REST_Response
    {
        if (empty($_FILES['file'])) {
            return $this->error('No file uploaded', 'no_file', 400);
            return $this->error('No file uploaded', 'no_file');
        }
        $file = $_FILES['file'];
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return $this->error('File upload error: ' . $file['error'], 'upload_error', 400);
            return $this->error('File upload error: ' . $file['error'], 'upload_error');
        }
        // Validate file type
@@ -400,12 +400,12 @@
        finfo_close($finfo);
        if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
            return $this->error('File must be a CSV', 'invalid_file_type', 400);
            return $this->error('File must be a CSV', 'invalid_file_type');
        }
        // Validate file size (10MB max)
        if ($file['size'] > 10 * 1024 * 1024) {
            return $this->error('File size exceeds 10MB limit', 'file_too_large', 400);
            return $this->error('File size exceeds 10MB limit', 'file_too_large');
        }
        // Import using JaneAppClientImporter
@@ -445,13 +445,13 @@
    public function handleSalesUpload(WP_REST_Request $request): WP_REST_Response
    {
        if (empty($_FILES['file'])) {
            return $this->error('No file uploaded', 'no_file', 400);
            return $this->error('No file uploaded', 'no_file');
        }
        $file = $_FILES['file'];
        if ($file['error'] !== UPLOAD_ERR_OK) {
            return $this->error('File upload error: ' . $file['error'], 'upload_error', 400);
            return $this->error('File upload error: ' . $file['error'], 'upload_error');
        }
        // Validate file type
@@ -461,12 +461,12 @@
        finfo_close($finfo);
        if (!in_array($mime_type, $allowed_types) && !in_array($file['type'], $allowed_types)) {
            return $this->error('File must be a CSV', 'invalid_file_type', 400);
            return $this->error('File must be a CSV', 'invalid_file_type');
        }
        // Validate file size (10MB max)
        if ($file['size'] > 10 * 1024 * 1024) {
            return $this->error('File size exceeds 10MB limit', 'file_too_large', 400);
            return $this->error('File size exceeds 10MB limit', 'file_too_large');
        }
        // Import using JaneAppSalesImporter
inc/rest/routes/ResponseRoutes.php
@@ -2,10 +2,7 @@
namespace JVBase\rest\routes;
use JVBase\Instance;
use JVBase\JVB;
use JVBase\managers\Cache;
use JVBase\managers\NotificationManager;
use JVBase\managers\Notifications\Notifications;
use JVBase\managers\Responses;
use JVBase\rest\Rest;
use JVBase\managers\CustomTable;
inc/rest/routes/SEORoutes.php
@@ -4,7 +4,6 @@
use JVBase\base\SchemaHelper;
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\managers\SEO\render\SchemaOutput;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\rest\Route;
inc/rest/routes/SettingsRoutes.php
@@ -2,7 +2,7 @@
namespace JVBase\rest\routes;
use JVBase\Instance;
use JVBase\JVB;
use JVBase\managers\queue\Queue;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Rest;
use JVBase\managers\Cache;
inc/rest/routes/ShopRoutes.php
@@ -1,10 +1,11 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\managers\Emails;
use JVBase\managers\Errors;
use JVBase\managers\ImageGenerator;
use JVBase\managers\NotificationManager;
use JVBase\managers\queue\Queue;
use JVBase\managers\UploadManager;
use JVBase\registrar\Registrar;
use JVBase\rest\RestRouteManager;
@@ -111,7 +112,6 @@
        $operationID = $data['id'];
        unset($data['id']);
        $data = json_encode($data);
        $queue->queueOperation(
            'shop_update',
@@ -545,19 +545,20 @@
        $shop_name = get_term($shopID, BASE . 'shop')->name;
        // Notify each manager
        foreach ($owners as $owner) {
            NotificationManager::getInstance()->addNotification(
                $owner,
                'artist_request',
                [
                    'artist_id' => $artistID,
                    'artist_name' => $artist_name,
                    'user_id' => $userID,
                    'shop_id' => $shopID,
                    'shop_name' => $shop_name
                ]
            );
        }
        //TODO
//        foreach ($owners as $owner) {
//            NotificationManager::getInstance()->addNotification(
//                $owner,
//                'artist_request',
//                [
//                    'artist_id' => $artistID,
//                    'artist_name' => $artist_name,
//                    'user_id' => $userID,
//                    'shop_id' => $shopID,
//                    'shop_name' => $shop_name
//                ]
//            );
//        }
    }
    /**
@@ -681,16 +682,17 @@
        $invitation_id = $wpdb->insert_id;
        // If user exists, send notification through the system
        if ($existing_user_id) {
            NotificationManager::getInstance()->addNotification(
                $existing_user_id,
                'artist_invitation',
                $manager_id,
                sprintf('You have been invited to join %s', get_term($shop_id, BASE . 'shop')->name),
                $invitation_id,
                'invitation'
            );
        }
        //TODO
//        if ($existing_user_id) {
//            NotificationManager::getInstance()->addNotification(
//                $existing_user_id,
//                'artist_invitation',
//                $manager_id,
//                sprintf('You have been invited to join %s', get_term($shop_id, BASE . 'shop')->name),
//                $invitation_id,
//                'invitation'
//            );
//        }
        // Send email invitation
        $this->sendInvitationEmail($artist_email, $artist_name, $manager_id, $shop_id, $token);
@@ -802,16 +804,17 @@
        // If invitation was for an existing user, notify them
        $user_id = email_exists($invitation->email);
        if ($user_id) {
            NotificationManager::getInstance()->addNotification(
                $user_id,
                'invitation_revoked',
                $manager_id,
                sprintf('Your invitation to %s has been revoked', get_term($shop_id, BASE . 'shop')->name),
                $shop_id,
                BASE . 'shop'
            );
        }
        //TODO
//        if ($user_id) {
//            NotificationManager::getInstance()->addNotification(
//                $user_id,
//                'invitation_revoked',
//                $manager_id,
//                sprintf('Your invitation to %s has been revoked', get_term($shop_id, BASE . 'shop')->name),
//                $shop_id,
//                BASE . 'shop'
//            );
//        }
        return new WP_REST_Response([
            'success' => true,
@@ -871,15 +874,16 @@
        // Notify the artist
        $notification_type = $status === 'accepted' ? 'shop_accepted' : 'shop_rejected';
        NotificationManager::getInstance()->addNotification(
            $request->user_id,
            $notification_type,
            [
                'shop_id' => $request->shop_id,
                'shop_name' => get_term($request->shop_id, BASE . 'shop')->name,
                'notes' => $notes
            ]
        );
        //TODO
//        NotificationManager::getInstance()->addNotification(
//            $request->user_id,
//            $notification_type,
//            [
//                'shop_id' => $request->shop_id,
//                'shop_name' => get_term($request->shop_id, BASE . 'shop')->name,
//                'notes' => $notes
//            ]
//        );
        return new WP_REST_Response([
            'success' => true,
@@ -989,20 +993,21 @@
            }
            // Notify inviters
            $inviters = json_decode($invitation['inviters'], true);
            if (!empty($inviters)) {
                foreach ($inviters as $inviter_id) {
                    NotificationManager::getInstance()->addNotification(
                        $inviter_id,
                        'invitation_accepted',
                        [
                            'artist_id' => $user_id,
                            'artist_name' => jvbGetUsername($user_id),
                            'shop_id' => $invitation['to_shop']
                        ]
                    );
                }
            }
            //TODO
//            $inviters = json_decode($invitation['inviters'], true);
//            if (!empty($inviters)) {
//                foreach ($inviters as $inviter_id) {
//                    NotificationManager::getInstance()->addNotification(
//                        $inviter_id,
//                        'invitation_accepted',
//                        [
//                            'artist_id' => $user_id,
//                            'artist_name' => jvbGetUsername($user_id),
//                            'shop_id' => $invitation['to_shop']
//                        ]
//                    );
//                }
//            }
            $wpdb->query('COMMIT');
            return true;
@@ -1014,9 +1019,8 @@
                [
                    'user_id' => $user_id,
                    'invitation' => $invitation
                ],
                'error'
            );
                ]
            );
            return false;
        }
@@ -1059,20 +1063,21 @@
            );
            // Notify inviters
            $inviters = json_decode($invitation['inviters'], true);
            if (!empty($inviters)) {
                foreach ($inviters as $inviter_id) {
                    NotificationManager::getInstance()->addNotification(
                        $inviter_id,
                        'invitation_declined',
                        [
                            'artist_id' => $user_id,
                            'artist_name' => jvbGetUsername($user_id),
                            'shop_id' => $invitation['to_shop']
                        ]
                    );
                }
            }
            //TODO
//            $inviters = json_decode($invitation['inviters'], true);
//            if (!empty($inviters)) {
//                foreach ($inviters as $inviter_id) {
//                    NotificationManager::getInstance()->addNotification(
//                        $inviter_id,
//                        'invitation_declined',
//                        [
//                            'artist_id' => $user_id,
//                            'artist_name' => jvbGetUsername($user_id),
//                            'shop_id' => $invitation['to_shop']
//                        ]
//                    );
//                }
//            }
            return true;
        } catch (Exception $e) {
@@ -1082,9 +1087,8 @@
                [
                    'user_id' => $user_id,
                    'invitation' => $invitation
                ],
                'error'
            );
                ]
            );
            return false;
        }
@@ -1225,19 +1229,19 @@
            $shop_meta = Meta::forTerm($shop_id);
            $owners = $shop_meta->getAll(['owner', 'managers']);
            $owners = array_unique(array_merge(explode(',', $owners['owner']),explode(',', $owners['managers'])));
            foreach ($owners as $owner_id) {
                if (!empty($owner_id)) {
                    NotificationManager::getInstance()->addNotification(
                        $owner_id,
                        'artist_left',
                        $user_id,
                        sprintf('%s has left %s', jvbGetUsername($user_id), $shop_name),
                        $shop_id,
                        BASE . 'shop'
                    );
                }
            }
// TODO
//            foreach ($owners as $owner_id) {
//                if (!empty($owner_id)) {
//                    NotificationManager::getInstance()->addNotification(
//                        $owner_id,
//                        'artist_left',
//                        $user_id,
//                        sprintf('%s has left %s', jvbGetUsername($user_id), $shop_name),
//                        $shop_id,
//                        BASE . 'shop'
//                    );
//                }
//            }
            return true;
        } catch (Exception $e) {
@@ -1248,9 +1252,8 @@
                [
                    'user_id' => $user_id,
                    'shop_id' => $shop_id
                ],
                'error'
            );
                ]
            );
            return false;
        }
inc/rest/routes/TermRoutes.php
@@ -560,10 +560,10 @@
            if ($membership && $membership->has('term_approval')) {
                $approval_routes = ApprovalRoutes::getInstance();
                $request_id = $approval_routes->createTermApprovalRequest(
                    $user_id,
                    $taxonomy,
                    sanitize_title($name),
                    absint($parent)
                    $user_id,
                    $taxonomy,
                    sanitize_title($name),
                    absint($parent)
                );
                if (!$request_id) {
inc/rest/routes/UserRoutes.php
@@ -3,11 +3,9 @@
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\meta\Meta;
use JVBase\registrar\Registrar;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\base\Site;
if (!defined('ABSPATH')) {
    exit;
inc/ui/CRUDSkeleton.php
@@ -936,7 +936,7 @@
    protected function getCommonTerms(string $taxonomy, ?string $limit = null):array {
        if ($limit) {
            if (Site::has('membership') && $limit === 'user') {
                $manager = new UserTermsManager();
                $manager = UserTermsManager::getInstance();
                return $manager->fetchUserTerms($this->user_id, $taxonomy);
            } else {
                $limit = (int)$limit;
inc/ui/Checkout.php
@@ -4,6 +4,7 @@
use JVBase\base\Site;
use JVBase\integrations\Integrations;
use JVBase\integrations\ServiceMap;
use JVBase\managers\MagicLinks;
use JVBase\meta\Form;
use JVBase\meta\Meta;
inc/ui/Navigation.php
@@ -266,8 +266,7 @@
    private ?string $icon = null;
    private ?Navigation $submenu = null;
    private array $classes = [];
    private array $menuClasses = [];
    private array $attributes = [];
    private array $attributes = [];
    private bool $current = false;
    public function __construct(int $id) {
inc/users/UserSettings.php
@@ -1,635 +1,635 @@
<?php
namespace JVBase\users;
use JVBase\JVB;
use JVBase\registry\ContentRegistry;
//use JVBase\registry\ContentRegistry;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
class UserSettings extends ContentRegistry
{
    protected array $fields;
    protected array $sections = [
        'account'   => [
            'title'         => 'Account',
            'icon'          => 'settings',
        ],
        'notifications' => [
            'title'         => 'Notifications',
            'icon'          => 'email'
        ],
        'newsletter' => [
            'title'         => 'Newsletter',
            'icon'          => 'logo'
        ],
        'invitations'    => [
            'title'        => 'Invite Artists',
            'icon'        => 'new-user',
        ]
    ];
    protected array $frequency = [
        'never'     => 'Never',
        'daily'     => 'Daily',
        'weekly'    => 'Weekly',
        'monthly'   => 'Monthly',
    ];
    public function initType():void
    {
        $this->object_type = 'user';
        $this->userFields();
    }
    public function getFields():array
    {
        return $this->fields;
    }
    public function getSections():array
    {
        return $this->sections;
    }
    public function userFields():void
    {
        $this->fields = [
            'display_name'  => [
                'type'  => 'text',
                'label' => 'Name',
                'description'   => 'Optional. Only used if you allow artists to see your info when you favourite them.',
                'default'   => '',
                'section'   => 'account',
                'role'      => ['ei_enthusiast']
            ],
            'user_email'    => [
                'type'  => 'email',
                'label' => 'Email',
                'description'   => 'Required for any notifications you subscribe to.',
                'default'   => '',
                'section'   => 'account',
                'role'      => ['ei_enthusiast']
            ],
            'city'      => [
                'type'  => 'text',
                'label' => 'City',
                'description'   => 'Optional. Just helps us know where people who use this site are located.',
                'default'   => '',
                'section'   => 'account',
                'role'      => ['ei_enthusiast']
            ],
            'notify'    => [
                'type'  => 'true_false',
                'label' => 'Notify artists when you favourite their work',
                'description'   => 'If you select this, artists will see your name when you favourite their work, else they will just get an anonymous notification.',
                'default'   => false,
                'section'   => 'account',
            ],
            'dark_mode'    => [
                'type'  => 'true_false',
                'label' => 'Dark Mode',
                'default'   => false,
            ],
            'email_digest'  => [
                'type'      => 'radio',
                'label'     => 'Email Summary Frequency',
                'options'   => $this->frequency,
                'default'   => 'never',
                'section'       => 'notifications'
            ],
            'digest_override'   => [
                'type'  => 'true_false',
                'label' => 'Apply to all notifications',
                'description'   => 'Override individual notification settings and apply this frequency to all your notifications',
                'default'     => false,
                'section'       => 'notifications'
            ],
            'notifications' => [
                'type'      => 'callback',
                'callback'  => 'renderNotifications',
                'label'     => 'Notifications',
                'section'   => 'notifications',
                'default'   => '',
            ],
            'site_updates'  => [
                'type'  => 'true_false',
                'label' => 'Site Updates',
                'description' => 'Get notified when new features are added to edmonton.ink',
                'default'   => false,
                'section'   => 'account',
            ],
            'newsletter'    => [
                'type'  => 'true_false',
                'label' => 'Newsletter',
                'description'   => 'Sign up for our monthly-ish newsletter, featuring upcoming events, featured artists, new artists, and their work.',
                'default'   => false,
                'section'   => 'newsletter',
            ],
            'owner_of'    => [
                'type' => 'selector',
                'subtype'=> 'taxonomy',
                'label'    => __('Owner of', 'jvb'),
                'isReference' => true,
                'taxonomy' => BASE. 'shop',
                'quickEdit'    => true,
                'default'    => '',
            ],
            'manager_of'    => [
                'type' => 'selector',
                'subtype' => 'taxonomy',
                'label'    => __('Manager of', 'jvb'),
                'isReference' => true,
                'taxonomy' => BASE. 'shop',
                'hidden'    => true,
                'default'    => '',
            ],
            'invitations'    => [
                'type'    => 'callback',
                'callback'    => 'handleInvitationsManagement',
                'section'    => 'invitations',
                'label'        => __('Invite Artists', 'jvb'),
                'role'      => ['ei_artist']
            ],
        ];
    }
    /**
     * Render custom notification preferences UI
     * @param int $user_id
     *
     * @return string
     */
    public function renderNotifications(int $user_id):string
    {
        $favourites = $this->getUserFavourites($user_id);
        $preferences = $this->getUserNotificationPreferences($user_id);
        global $notify;
        ob_start();
        ?>
        <div class="notification-preferences">
            <?php foreach ($favourites as $type => $items) : ?>
                <details class="notification-group">
                    <summary class="notification-group-header row x-btw">
                        <span class="type-label"><?=jvbIcon('heart', ['style' => 'fill'])?> <?= $notify[$type] ?></span>
                        <span class="type-count">( <?= count($items); ?> )</span>
                    </summary>
                    <?php if (!empty($items)) : ?>
                        <table class="notification-table">
                            <thead>
                            <tr>
                                <th>Name</th>
                                <th>Frequency</th>
                            </tr>
                            </thead>
                            <tbody>
                            <?php foreach ($items as $item) : ?>
                                <?php
                                $current = isset($preferences['preferences'][$type][$item['id']])
                                    ? $preferences['preferences'][$type][$item['id']]
                                    : $preferences['default_frequency'];
                                ?>
                                <tr>
                                    <td><?= esc_html($item['name']); ?></td>
                                    <td>
                                        <div class="radio-options row">
                                            <?php foreach (['never', 'daily', 'weekly', 'monthly'] as $freq) : ?>
                                                <input type="radio"
                                                       name="notification_preferences[<?= esc_attr($type); ?>][<?= esc_attr($item['id']); ?>]"
                                                       id="<?= esc_attr("{$type}_{$item['id']}_$freq"); ?>"
                                                       value="<?= esc_attr($freq); ?>"
                                                    <?php checked($current, $freq); ?>>
                                                <label for="<?= esc_attr("{$type}_{$item['id']}_$freq"); ?>">
                                                    <?= ucfirst($freq); ?>
                                                </label>
                                            <?php endforeach; ?>
                                        </div>
                                    </td>
                                </tr>
                            <?php endforeach; ?>
                            </tbody>
                            <tfoot>
                            <tr>
                                <th>Set all <?= $type ?>s to: </th>
                                <th>
                                    <div class="radio-options row">
                                        <?php foreach (['never', 'daily', 'weekly', 'monthly'] as $freq) : ?>
                                            <input type="radio"
                                                   name="toggle-all-<?=$type?>"
                                                   id="toggle-all-<?=$type?>-<?=$freq?>"
                                                   value="<?= esc_attr($freq); ?>"
                                                class="toggle-all">
                                            <label for="toggle-all-<?=$type?>-<?=$freq?>">
                                                <?= ucfirst($freq); ?>
                                            </label>
                                        <?php endforeach; ?>
                                    </div>
                                </th>
                            </tr>
                            </tfoot>
                        </table>
                    <?php else : ?>
                        <p class="no-items">No favourites yet</p>
                    <?php endif; ?>
                </details>
            <?php endforeach; ?>
        </div>
        <script>
            function initGroupOverrides() {
                document.querySelectorAll('.toggle-all').forEach(toggle => {
                    toggle.addEventListener('change', e => {
                        e.preventDefault();
                        if(toggle.checked){
                            const table = e.target.closest('table');
                            console.log(table);
                            const value = toggle.value;
                            // Enable/disable individual controls
                            table.querySelectorAll('input[value="'+value+'"]').forEach(radio => {
                                radio.checked = true;
                            });
                        }
                    });
                });
                // Listen for changes to default frequency
                document.querySelector('input[name="digest_override"]').addEventListener('change', e=>{
                    let value = document.querySelector('input[name="email_digest"]:checked').value;
                    console.log(value);
                    document.querySelectorAll('input[value="'+value+'"]').forEach(toggle => {
                        toggle.checked = true;
                    });
                    setTimeout(function () {
                        e.target.checked = false;
                    }, 1000)
                });
            }
            initGroupOverrides();
        </script>
        <?php
        return ob_get_clean();
    }
    /**
     * Get user's favourites grouped by type
     */
    protected function getUserFavourites(int $user_id):array
    {
        global $wpdb;
        $table = $wpdb->prefix . BASE.'favourites';
        global $notify;
        global $types;
        $favourites = [];
        foreach (array_keys($notify) as $type) {
            $favourites[$type] = [];
        }
        // Fetch favourites from database
        $results = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM $table WHERE user_id = %d",
            $user_id
        ));
        foreach ($results as $result) {
            $type = str_replace(BASE, '', $result->type);
            if (!array_key_exists($type, $favourites)) {
                continue;
            }
            // Get proper name based on type
            switch ($types[BASE.$type]['table']) {
                case 'post':
                    $name = get_the_title($result->target_id);
                    break;
                case 'term':
                    $term = get_term($result->target_id, BASE.$type);
                    $name = $term ? html_entity_decode($term->name) : '';
                    break;
                default:
                    $name = '';
            }
            if ($name) {
                $favourites[$type][] = [
                    'id' => $result->target_id,
                    'name' => $name
                ];
            }
        }
        return $favourites;
    }
    /**
     * Get current notification preferences
     */
    protected function getNotificationPreferences(int $user_id):array
    {
        return get_user_meta($user_id, 'notification_preferences', true) ?: [];
    }
    protected function getUserNotificationPreferences(int $user_id):array
    {
        global $wpdb;
        $table = $wpdb->prefix . BASE.'notification_preferences';
//        error_log('Getting notification preferences for user: ' . $user_id);
        // Get user's preferences from the table
        $raw_preferences = $wpdb->get_results($wpdb->prepare(
            "SELECT notification_type, item_id, frequency
         FROM {$table}
         WHERE user_id = %d",
            $user_id
        ), ARRAY_A);
//        error_log('Raw preferences from database: ' . print_r($raw_preferences, true));
        // Structure the preferences
        $preferences = [];
        if (!empty($raw_preferences)) {
            foreach ($raw_preferences as $pref) {
                $type = $pref['notification_type'];
                $item_id = intval($pref['item_id']);
                $frequency = $pref['frequency'];
                if (!isset($preferences[$type])) {
                    $preferences[$type] = [];
                }
                $preferences[$type][$item_id] = $frequency;
            }
        }
        // Get default frequency
        $default_frequency = get_user_meta($user_id, BASE.'email_digest', true) ?: 'never';
//        error_log('Structured preferences: ' . print_r($preferences, true));
//        error_log('Default frequency: ' . $default_frequency);
        return [
            'preferences' => $preferences,
            'default_frequency' => $default_frequency
        ];
    }
    public function handleInvitationsManagement(int $userID):void
    {
        $name = 'artist_invite';
        ?>
        <div class="field">
            <script type="text/javascript">
                function addArtist(button){
                    let template = window.getTemplate('artistInvite');
                    let container = button.closest('.field').querySelector('.invite-artists');
                    let nameLabel = template.querySelector('label[for="invited-artist-name"]'),
                        nameInput = template.querySelector('input#invited-artist-name'),
                        emailLabel = template.querySelector('label[for="invited-artist-email"]'),
                        emailInput = template.querySelector('input#invited-artist-email'),
                        shopLabel = template.querySelector('label[for="invited-add-to-shop"]'),
                        shopInput = template.querySelector('input#invited-add-to-shop');
                    let count = container.childNodes.length;
                    console.log(container.childNodes);
                    [
                        nameLabel.htmlFor,
                        nameInput.id,
                        nameInput.name,
                        emailLabel.htmlFor,
                        emailInput.id,
                        emailInput.name,
                        shopLabel.htmlFor,
                        shopInput.id,
                        shopInput.name
                    ] =
                    [
                        `${nameLabel.getAttribute('for')}-${count}`,
                        `${nameInput.id}-${count}`,
                        `${nameInput.name}-${count}`,
                        `${emailLabel.htmlFor}-${count}`,
                        `${emailInput.id}-${count}`,
                        `${emailInput.name}-${count}`,
                        `${shopLabel.htmlFor}-${count}`,
                        `${shopInput.id}-${count}`,
                        `${shopInput.name}-${count}`
                    ];
                    container.append(template);
                }
                function removeArtist(button){
                    button.closest('.invited-artist').remove();
                }
                async function revokeInvite(button){
                    try {
                        let artist = button.closest('tr');
                        let id = artist.id;
                        const headers = {
                            'X-WP-Nonce': eiSettings.nonce,
                            'action_nonce': eiSettings.dash,
                            'Content-Type': 'application/json'
                        };
                        let body = {
                            user: eiSettings.currentUser,
                            revoke: id
                        };
                        let request = {
                            method: 'POST',
                            headers: headers,
                            body: JSON.stringify(body)
                        };
                        console.log(request);
                        const response = await fetch(`${eiSettings.api}invitations`, request);
                        console.log(response);
                        if (!response.ok) {
                            const errorData = await response.json().catch(() => ({}));
                        }
                        const data =  await response.json();
                        if(data.success){
                            artist.remove();
                        }
                    }catch (error) {
                    }
                }
                async function resendInvite(button){
                    try {
                        let artist = button.closest('tr');
                        let id = artist.id;
                        const headers = {
                            'X-WP-Nonce': eiSettings.nonce,
                            'action_nonce': eiSettings.dash,
                            'Content-Type': 'application/json'
                        };
                        let body = {
                            user: eiSettings.currentUser,
                            refresh: id
                        };
                        let request = {
                            method: 'POST',
                            headers: headers,
                            body: JSON.stringify(body)
                        };
                        console.log(request);
                        const response = await fetch(`${eiSettings.api}invitations`, request);
                        console.log(response);
                        if (!response.ok) {
                            const errorData = await response.json().catch(() => ({}));
                        }
                        const data =  await response.json();
                    }catch (error) {
                    }
                }
                function sendInvites(button){
                    if(document.querySelector('.invited-artist')){
                        let newArtists = [];
                        document.querySelectorAll('.invited-artist').forEach(invited => {
                            let name = invited.querySelector('input[type="text"]'),
                                email = invited.querySelector('input[type="email"]'),
                                toShop = invited.querySelector('input[type="checkbox"]');
                            if(name.value === '' || email.value === ''){
                                invited.classList.add('required');
                                return;
                            }
                            newArtists.push({
                                name: name.value,
                                email: email.value,
                                toShop: toShop.checked
                            });
                            invited.remove();
                        });
                        if(newArtists.length > 0){
                            window.queueManager.addToQueue({
                                type: 'invite_artist',
                                data: newArtists
                            });
                        }
                    }
                }
            </script>
            <section class="invite-artists">
                <div class="invited-artist">
                    <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('trash')?></button>
                    <div>
                        <label for="invited-artist-name-1">First Name</label>
                        <input type="text" id="invited-artist-name-1" name="invited-artist-name-1">
                    </div>
                    <div>
                        <label for="invited-artist-email-1">Email</label>
                        <input type="email" id="invited-artist-email-1" name="invited-artist-email-1">
                    </div>
                    <div>
                        <input type="checkbox" name="invited-add-to-shop-1" id="invited-add-to-shop-1">
                        <label for="invited-add-to-shop-1">Add to shop?</label>
                    </div>
                </div>
            </section>
            <div class="actions">
                <button type="button" onclick="addArtist(this)" class="add-artist"><?= jvbIcon('plus-square')?>Add Artist</button>
                <button type="button" onclick="sendInvites(this)" class="send-invites"><?=jvbIcon('envelope')?>Send Invites</button>
            </div>
            <template class="artistInvite">
                <div class="invited-artist">
                    <button type="button" onclick="removeArtist(this)" title="Remove Invite"><?=jvbIcon('trash')?></button>
                    <div>
                        <label for="invited-artist-name">First Name</label>
                        <input type="text" id="invited-artist-name" name="invited-artist-name">
                    </div>
                    <div>
                        <label for="invited-artist-email">Email</label>
                        <input type="email" id="invited-artist-email" name="invited-artist-email">
                    </div>
                    <div>
                        <input type="checkbox" name="invited-add-to-shop" id="invited-add-to-shop">
                        <label for="invited-add-to-shop">Add to shop?</label>
                    </div>
                </div>
            </template>
        </div>
        <?php
        $invites = JVB()->routes('invites')->getUserInvitations($userID);
        ?>
        <table class="artist-invite">
            <thead>
                <tr>
                    <th scope="col" style="width:100%;">Name</th>
                    <th scope="col" style="padding:0 .5rem">Email</th>
                    <th scope="col" style="padding:0 .5rem">Status</th>
                    <th scope="col" style="padding:0 .5rem">Expires</th>
                    <th scope="col" style="padding:0 .5rem">Actions</th>
                </tr>
            </thead>
            <tbody>
            <?php
            if (!empty($invites->invitations)) {
                foreach ($invites->invitations as $invite) {
                    ?>
                    <tr id="<?=$invite['id']?>">
                        <td>
                            <?=$invite['name']?>
                        </td>
                        <td>
                            <?=$invite['email']?>
                        </td>
                        <td>
                            <?=$invite['status']?>
                        </td>
                        <td>
                            <?=$invite['expires_at']?>
                        </td>
                        <td class="actions">
                            <button type="button" data-action="revoke" title="Revoke Invitation" onclick="revokeInvite(this)">
                                <?=jvbIcon('trash')?>
                                <span class="screen-reader-text">Revoke invite</span>
                            </button>
                            <button type="button" data-action="resend"<?=($invite['status'] === 'exipired') ? '' : ' disabled'?> title="Resend Invitation" onclick="resendInvite(this)">
                                <?=jvbIcon('arrows-clockwise')?>
                                <span class="screen-reader-text">Try Again</span>
                            </button>
                        </td>
                    </tr>
                    <?php
                }
            } else {
                ?>
                <tr><td><i>No invites yet</i></td><td></td><td></td><td></td><td></td></tr>
                <?php
            }
            ?>
            </tbody>
        </table>
        <?php
    }
}
//TODO: Likely replaced with Registrar?
//
//class UserSettings extends ContentRegistry
//{
//    protected array $fields;
//    protected array $sections = [
//        'account'   => [
//            'title'         => 'Account',
//            'icon'          => 'settings',
//        ],
//        'notifications' => [
//            'title'         => 'Notifications',
//            'icon'          => 'email'
//        ],
//        'newsletter' => [
//            'title'         => 'Newsletter',
//            'icon'          => 'logo'
//        ],
//        'invitations'    => [
//            'title'        => 'Invite Artists',
//            'icon'        => 'new-user',
//        ]
//    ];
//    protected array $frequency = [
//        'never'     => 'Never',
//        'daily'     => 'Daily',
//        'weekly'    => 'Weekly',
//        'monthly'   => 'Monthly',
//    ];
//
//    public function initType():void
//    {
//        $this->object_type = 'user';
//        $this->userFields();
//    }
//
//    public function getFields():array
//    {
//        return $this->fields;
//    }
//    public function getSections():array
//    {
//        return $this->sections;
//    }
//    public function userFields():void
//    {
//
//        $this->fields = [
//            'display_name'  => [
//                'type'  => 'text',
//                'label' => 'Name',
//                'description'   => 'Optional. Only used if you allow artists to see your info when you favourite them.',
//                'default'   => '',
//                'section'   => 'account',
//                'role'      => ['ei_enthusiast']
//            ],
//            'user_email'    => [
//                'type'  => 'email',
//                'label' => 'Email',
//                'description'   => 'Required for any notifications you subscribe to.',
//                'default'   => '',
//                'section'   => 'account',
//                'role'      => ['ei_enthusiast']
//            ],
//            'city'      => [
//                'type'  => 'text',
//                'label' => 'City',
//                'description'   => 'Optional. Just helps us know where people who use this site are located.',
//                'default'   => '',
//                'section'   => 'account',
//                'role'      => ['ei_enthusiast']
//            ],
//            'notify'    => [
//                'type'  => 'true_false',
//                'label' => 'Notify artists when you favourite their work',
//                'description'   => 'If you select this, artists will see your name when you favourite their work, else they will just get an anonymous notification.',
//                'default'   => false,
//                'section'   => 'account',
//            ],
//            'dark_mode'    => [
//                'type'  => 'true_false',
//                'label' => 'Dark Mode',
//                'default'   => false,
//            ],
//            'email_digest'  => [
//                'type'      => 'radio',
//                'label'     => 'Email Summary Frequency',
//                'options'   => $this->frequency,
//                'default'   => 'never',
//                'section'       => 'notifications'
//            ],
//            'digest_override'   => [
//                'type'  => 'true_false',
//                'label' => 'Apply to all notifications',
//                'description'   => 'Override individual notification settings and apply this frequency to all your notifications',
//                'default'     => false,
//                'section'       => 'notifications'
//            ],
//            'notifications' => [
//                'type'      => 'callback',
//                'callback'  => 'renderNotifications',
//                'label'     => 'Notifications',
//                'section'   => 'notifications',
//                'default'   => '',
//            ],
//            'site_updates'  => [
//                'type'  => 'true_false',
//                'label' => 'Site Updates',
//                'description' => 'Get notified when new features are added to edmonton.ink',
//                'default'   => false,
//                'section'   => 'account',
//            ],
//            'newsletter'    => [
//                'type'  => 'true_false',
//                'label' => 'Newsletter',
//                'description'   => 'Sign up for our monthly-ish newsletter, featuring upcoming events, featured artists, new artists, and their work.',
//                'default'   => false,
//                'section'   => 'newsletter',
//            ],
//            'owner_of'    => [
//                'type' => 'selector',
//              'subtype'=> 'taxonomy',
//                'label'    => __('Owner of', 'jvb'),
//                'isReference' => true,
//                'taxonomy' => BASE. 'shop',
//                'quickEdit'    => true,
//                'default'    => '',
//            ],
//            'manager_of'    => [
//                'type' => 'selector',
//                'subtype' => 'taxonomy',
//                'label'    => __('Manager of', 'jvb'),
//                'isReference' => true,
//                'taxonomy' => BASE. 'shop',
//                'hidden'    => true,
//                'default'    => '',
//            ],
//
//            'invitations'    => [
//                'type'    => 'callback',
//                'callback'    => 'handleInvitationsManagement',
//                'section'    => 'invitations',
//                'label'        => __('Invite Artists', 'jvb'),
//                'role'      => ['ei_artist']
//            ],
//        ];
//    }
//
//    /**
//     * Render custom notification preferences UI
//     * @param int $user_id
//     *
//     * @return string
//     */
//    public function renderNotifications(int $user_id):string
//    {
//        $favourites = $this->getUserFavourites($user_id);
//        $preferences = $this->getUserNotificationPreferences($user_id);
//        global $notify;
//
//        ob_start();
//        ?>
<!--        <div class="notification-preferences">-->
<!--            --><?php //foreach ($favourites as $type => $items) : ?>
<!--                <details class="notification-group">-->
<!--                    <summary class="notification-group-header row x-btw">-->
<!--                        <span class="type-label">--><?php //=jvbIcon('heart', ['style' => 'fill'])?><!-- --><?php //= $notify[$type] ?><!--</span>-->
<!--                        <span class="type-count">( --><?php //= count($items); ?><!-- )</span>-->
<!--                    </summary>-->
<!---->
<!--                    --><?php //if (!empty($items)) : ?>
<!--                        <table class="notification-table">-->
<!--                            <thead>-->
<!--                            <tr>-->
<!--                                <th>Name</th>-->
<!--                                <th>Frequency</th>-->
<!--                            </tr>-->
<!--                            </thead>-->
<!--                            <tbody>-->
<!--                            --><?php //foreach ($items as $item) : ?>
<!--                                --><?php
//                                $current = isset($preferences['preferences'][$type][$item['id']])
//                                    ? $preferences['preferences'][$type][$item['id']]
//                                    : $preferences['default_frequency'];
//                                ?>
<!--                                <tr>-->
<!--                                    <td>--><?php //= esc_html($item['name']); ?><!--</td>-->
<!--                                    <td>-->
<!--                                        <div class="radio-options row">-->
<!--                                            --><?php //foreach (['never', 'daily', 'weekly', 'monthly'] as $freq) : ?>
<!--                                                <input type="radio"-->
<!--                                                       name="notification_preferences[--><?php //= esc_attr($type); ?><!--][--><?php //= esc_attr($item['id']); ?><!--]"-->
<!--                                                       id="--><?php //= esc_attr("{$type}_{$item['id']}_$freq"); ?><!--"-->
<!--                                                       value="--><?php //= esc_attr($freq); ?><!--"-->
<!--                                                    --><?php //checked($current, $freq); ?><!-->-->
<!--                                                <label for="--><?php //= esc_attr("{$type}_{$item['id']}_$freq"); ?><!--">-->
<!--                                                    --><?php //= ucfirst($freq); ?>
<!--                                                </label>-->
<!--                                            --><?php //endforeach; ?>
<!--                                        </div>-->
<!--                                    </td>-->
<!--                                </tr>-->
<!--                            --><?php //endforeach; ?>
<!--                            </tbody>-->
<!--                            <tfoot>-->
<!--                            <tr>-->
<!---->
<!--                                <th>Set all --><?php //= $type ?><!--s to: </th>-->
<!--                                <th>-->
<!--                                    <div class="radio-options row">-->
<!--                                        --><?php //foreach (['never', 'daily', 'weekly', 'monthly'] as $freq) : ?>
<!--                                            <input type="radio"-->
<!--                                                   name="toggle-all---><?php //=$type?><!--"-->
<!--                                                   id="toggle-all---><?php //=$type?><!-----><?php //=$freq?><!--"-->
<!--                                                   value="--><?php //= esc_attr($freq); ?><!--"-->
<!--                                                class="toggle-all">-->
<!--                                            <label for="toggle-all---><?php //=$type?><!-----><?php //=$freq?><!--">-->
<!--                                                --><?php //= ucfirst($freq); ?>
<!--                                            </label>-->
<!--                                        --><?php //endforeach; ?>
<!--                                    </div>-->
<!--                                </th>-->
<!---->
<!--                            </tr>-->
<!--                            </tfoot>-->
<!--                        </table>-->
<!--                    --><?php //else : ?>
<!--                        <p class="no-items">No favourites yet</p>-->
<!--                    --><?php //endif; ?>
<!--                </details>-->
<!--            --><?php //endforeach; ?>
<!--        </div>-->
<!--        <script>-->
<!--            function initGroupOverrides() {-->
<!--                document.querySelectorAll('.toggle-all').forEach(toggle => {-->
<!--                    toggle.addEventListener('change', e => {-->
<!--                        e.preventDefault();-->
<!--                        if(toggle.checked){-->
<!--                            const table = e.target.closest('table');-->
<!--                            console.log(table);-->
<!--                            const value = toggle.value;-->
<!---->
<!--                            // Enable/disable individual controls-->
<!--                            table.querySelectorAll('input[value="'+value+'"]').forEach(radio => {-->
<!--                                radio.checked = true;-->
<!--                            });-->
<!--                        }-->
<!---->
<!--                    });-->
<!--                });-->
<!---->
<!--                // Listen for changes to default frequency-->
<!--                document.querySelector('input[name="digest_override"]').addEventListener('change', e=>{-->
<!--                    let value = document.querySelector('input[name="email_digest"]:checked').value;-->
<!---->
<!--                    console.log(value);-->
<!--                    document.querySelectorAll('input[value="'+value+'"]').forEach(toggle => {-->
<!--                        toggle.checked = true;-->
<!--                    });-->
<!---->
<!--                    setTimeout(function () {-->
<!--                        e.target.checked = false;-->
<!--                    }, 1000)-->
<!---->
<!--                });-->
<!---->
<!--            }-->
<!--            initGroupOverrides();-->
<!--        </script>-->
<!--        --><?php
//        return ob_get_clean();
//    }
//
//    /**
//     * Get user's favourites grouped by type
//     */
//    protected function getUserFavourites(int $user_id):array
//    {
//        global $wpdb;
//        $table = $wpdb->prefix . BASE.'favourites';
//        global $notify;
//        global $types;
//
//
//        $favourites = [];
//        foreach (array_keys($notify) as $type) {
//            $favourites[$type] = [];
//        }
//
//        // Fetch favourites from database
//        $results = $wpdb->get_results($wpdb->prepare(
//            "SELECT * FROM $table WHERE user_id = %d",
//            $user_id
//        ));
//
//        foreach ($results as $result) {
//            $type = str_replace(BASE, '', $result->type);
//
//
//            if (!array_key_exists($type, $favourites)) {
//                continue;
//            }
//            // Get proper name based on type
//            switch ($types[BASE.$type]['table']) {
//                case 'post':
//                    $name = get_the_title($result->target_id);
//                    break;
//                case 'term':
//                    $term = get_term($result->target_id, BASE.$type);
//                    $name = $term ? html_entity_decode($term->name) : '';
//                    break;
//                default:
//                    $name = '';
//            }
//
//            if ($name) {
//                $favourites[$type][] = [
//                    'id' => $result->target_id,
//                    'name' => $name
//                ];
//            }
//        }
//
//        return $favourites;
//    }
//
//    /**
//     * Get current notification preferences
//     */
//    protected function getNotificationPreferences(int $user_id):array
//    {
//        return get_user_meta($user_id, 'notification_preferences', true) ?: [];
//    }
//
//    protected function getUserNotificationPreferences(int $user_id):array
//    {
//        global $wpdb;
//        $table = $wpdb->prefix . BASE.'notification_preferences';
//
////        error_log('Getting notification preferences for user: ' . $user_id);
//
//        // Get user's preferences from the table
//        $raw_preferences = $wpdb->get_results($wpdb->prepare(
//            "SELECT notification_type, item_id, frequency
//         FROM {$table}
//         WHERE user_id = %d",
//            $user_id
//        ), ARRAY_A);
//
////        error_log('Raw preferences from database: ' . print_r($raw_preferences, true));
//
//        // Structure the preferences
//        $preferences = [];
//        if (!empty($raw_preferences)) {
//            foreach ($raw_preferences as $pref) {
//                $type = $pref['notification_type'];
//                $item_id = intval($pref['item_id']);
//                $frequency = $pref['frequency'];
//
//                if (!isset($preferences[$type])) {
//                    $preferences[$type] = [];
//                }
//                $preferences[$type][$item_id] = $frequency;
//            }
//        }
//
//        // Get default frequency
//        $default_frequency = get_user_meta($user_id, BASE.'email_digest', true) ?: 'never';
//
////        error_log('Structured preferences: ' . print_r($preferences, true));
////        error_log('Default frequency: ' . $default_frequency);
//
//        return [
//            'preferences' => $preferences,
//            'default_frequency' => $default_frequency
//        ];
//    }
//
//    public function handleInvitationsManagement(int $userID):void
//    {
//        $name = 'artist_invite';
//        ?>
<!--        <div class="field">-->
<!--            <script type="text/javascript">-->
<!--                function addArtist(button){-->
<!--                    let template = window.getTemplate('artistInvite');-->
<!--                    let container = button.closest('.field').querySelector('.invite-artists');-->
<!---->
<!--                    let nameLabel = template.querySelector('label[for="invited-artist-name"]'),-->
<!--                        nameInput = template.querySelector('input#invited-artist-name'),-->
<!--                        emailLabel = template.querySelector('label[for="invited-artist-email"]'),-->
<!--                        emailInput = template.querySelector('input#invited-artist-email'),-->
<!--                        shopLabel = template.querySelector('label[for="invited-add-to-shop"]'),-->
<!--                        shopInput = template.querySelector('input#invited-add-to-shop');-->
<!---->
<!---->
<!--                    let count = container.childNodes.length;-->
<!--                    console.log(container.childNodes);-->
<!--                    [-->
<!--                        nameLabel.htmlFor,-->
<!--                        nameInput.id,-->
<!--                        nameInput.name,-->
<!--                        emailLabel.htmlFor,-->
<!--                        emailInput.id,-->
<!--                        emailInput.name,-->
<!--                        shopLabel.htmlFor,-->
<!--                        shopInput.id,-->
<!--                        shopInput.name-->
<!--                    ] =-->
<!--                    [-->
<!--                        `${nameLabel.getAttribute('for')}-${count}`,-->
<!--                        `${nameInput.id}-${count}`,-->
<!--                        `${nameInput.name}-${count}`,-->
<!--                        `${emailLabel.htmlFor}-${count}`,-->
<!--                        `${emailInput.id}-${count}`,-->
<!--                        `${emailInput.name}-${count}`,-->
<!--                        `${shopLabel.htmlFor}-${count}`,-->
<!--                        `${shopInput.id}-${count}`,-->
<!--                        `${shopInput.name}-${count}`-->
<!--                    ];-->
<!--                    container.append(template);-->
<!--                }-->
<!--                function removeArtist(button){-->
<!--                    button.closest('.invited-artist').remove();-->
<!--                }-->
<!---->
<!--                async function revokeInvite(button){-->
<!--                    try {-->
<!--                        let artist = button.closest('tr');-->
<!--                        let id = artist.id;-->
<!---->
<!--                        const headers = {-->
<!--                            'X-WP-Nonce': eiSettings.nonce,-->
<!--                            'action_nonce': eiSettings.dash,-->
<!--                            'Content-Type': 'application/json'-->
<!--                        };-->
<!---->
<!--                        let body = {-->
<!--                            user: eiSettings.currentUser,-->
<!--                            revoke: id-->
<!--                        };-->
<!--                        let request = {-->
<!--                            method: 'POST',-->
<!--                            headers: headers,-->
<!--                            body: JSON.stringify(body)-->
<!--                        };-->
<!--                        console.log(request);-->
<!---->
<!---->
<!--                        const response = await fetch(`${eiSettings.api}invitations`, request);-->
<!---->
<!--                        console.log(response);-->
<!--                        if (!response.ok) {-->
<!--                            const errorData = await response.json().catch(() => ({}));-->
<!--                        }-->
<!---->
<!--                        const data =  await response.json();-->
<!--                        if(data.success){-->
<!--                            artist.remove();-->
<!--                        }-->
<!--                    }catch (error) {-->
<!--                    }-->
<!--                }-->
<!---->
<!--                async function resendInvite(button){-->
<!--                    try {-->
<!--                        let artist = button.closest('tr');-->
<!--                        let id = artist.id;-->
<!---->
<!--                        const headers = {-->
<!--                            'X-WP-Nonce': eiSettings.nonce,-->
<!--                            'action_nonce': eiSettings.dash,-->
<!--                            'Content-Type': 'application/json'-->
<!--                        };-->
<!---->
<!--                        let body = {-->
<!--                            user: eiSettings.currentUser,-->
<!--                            refresh: id-->
<!--                        };-->
<!--                        let request = {-->
<!--                            method: 'POST',-->
<!--                            headers: headers,-->
<!--                            body: JSON.stringify(body)-->
<!--                        };-->
<!--                        console.log(request);-->
<!---->
<!---->
<!--                        const response = await fetch(`${eiSettings.api}invitations`, request);-->
<!---->
<!--                        console.log(response);-->
<!--                        if (!response.ok) {-->
<!--                            const errorData = await response.json().catch(() => ({}));-->
<!--                        }-->
<!---->
<!--                        const data =  await response.json();-->
<!---->
<!--                    }catch (error) {-->
<!--                    }-->
<!--                }-->
<!---->
<!--                function sendInvites(button){-->
<!--                    if(document.querySelector('.invited-artist')){-->
<!--                        let newArtists = [];-->
<!--                        document.querySelectorAll('.invited-artist').forEach(invited => {-->
<!--                            let name = invited.querySelector('input[type="text"]'),-->
<!--                                email = invited.querySelector('input[type="email"]'),-->
<!--                                toShop = invited.querySelector('input[type="checkbox"]');-->
<!--                            if(name.value === '' || email.value === ''){-->
<!--                                invited.classList.add('required');-->
<!--                                return;-->
<!--                            }-->
<!---->
<!--                            newArtists.push({-->
<!--                                name: name.value,-->
<!--                                email: email.value,-->
<!--                                toShop: toShop.checked-->
<!--                            });-->
<!--                            invited.remove();-->
<!--                        });-->
<!--                        if(newArtists.length > 0){-->
<!--                            window.queueManager.addToQueue({-->
<!--                                type: 'invite_artist',-->
<!--                                data: newArtists-->
<!--                            });-->
<!--                        }-->
<!--                    }-->
<!--                }-->
<!--            </script>-->
<!---->
<!--            <section class="invite-artists">-->
<!--                <div class="invited-artist">-->
<!--                    <button type="button" onclick="removeArtist(this)" title="Remove Invite">--><?php //=jvbIcon('trash')?><!--</button>-->
<!--                    <div>-->
<!--                        <label for="invited-artist-name-1">First Name</label>-->
<!--                        <input type="text" id="invited-artist-name-1" name="invited-artist-name-1">-->
<!--                    </div>-->
<!--                    <div>-->
<!--                        <label for="invited-artist-email-1">Email</label>-->
<!--                        <input type="email" id="invited-artist-email-1" name="invited-artist-email-1">-->
<!--                    </div>-->
<!--                    <div>-->
<!--                        <input type="checkbox" name="invited-add-to-shop-1" id="invited-add-to-shop-1">-->
<!--                        <label for="invited-add-to-shop-1">Add to shop?</label>-->
<!--                    </div>-->
<!--                </div>-->
<!--            </section>-->
<!--            <div class="actions">-->
<!--                <button type="button" onclick="addArtist(this)" class="add-artist">--><?php //= jvbIcon('plus-square')?><!--Add Artist</button>-->
<!--                <button type="button" onclick="sendInvites(this)" class="send-invites">--><?php //=jvbIcon('envelope')?><!--Send Invites</button>-->
<!--            </div>-->
<!---->
<!--            <template class="artistInvite">-->
<!--                <div class="invited-artist">-->
<!--                    <button type="button" onclick="removeArtist(this)" title="Remove Invite">--><?php //=jvbIcon('trash')?><!--</button>-->
<!--                    <div>-->
<!--                        <label for="invited-artist-name">First Name</label>-->
<!--                        <input type="text" id="invited-artist-name" name="invited-artist-name">-->
<!--                    </div>-->
<!--                    <div>-->
<!--                        <label for="invited-artist-email">Email</label>-->
<!--                        <input type="email" id="invited-artist-email" name="invited-artist-email">-->
<!--                    </div>-->
<!--                    <div>-->
<!--                        <input type="checkbox" name="invited-add-to-shop" id="invited-add-to-shop">-->
<!--                        <label for="invited-add-to-shop">Add to shop?</label>-->
<!--                    </div>-->
<!--                </div>-->
<!--            </template>-->
<!--        </div>-->
<!--        --><?php
//
//        $invites = JVB()->routes('invites')->getUserInvitations($userID);
//
//        ?>
<!--        <table class="artist-invite">-->
<!--            <thead>-->
<!--                <tr>-->
<!--                    <th scope="col" style="width:100%;">Name</th>-->
<!--                    <th scope="col" style="padding:0 .5rem">Email</th>-->
<!--                    <th scope="col" style="padding:0 .5rem">Status</th>-->
<!--                    <th scope="col" style="padding:0 .5rem">Expires</th>-->
<!--                    <th scope="col" style="padding:0 .5rem">Actions</th>-->
<!--                </tr>-->
<!--            </thead>-->
<!--            <tbody>-->
<!--            --><?php
//            if (!empty($invites->invitations)) {
//                foreach ($invites->invitations as $invite) {
//                    ?>
<!--                    <tr id="--><?php //=$invite['id']?><!--">-->
<!--                        <td>-->
<!--                            --><?php //=$invite['name']?>
<!--                        </td>-->
<!--                        <td>-->
<!--                            --><?php //=$invite['email']?>
<!--                        </td>-->
<!--                        <td>-->
<!--                            --><?php //=$invite['status']?>
<!--                        </td>-->
<!--                        <td>-->
<!--                            --><?php //=$invite['expires_at']?>
<!--                        </td>-->
<!--                        <td class="actions">-->
<!--                            <button type="button" data-action="revoke" title="Revoke Invitation" onclick="revokeInvite(this)">-->
<!--                                --><?php //=jvbIcon('trash')?>
<!--                                <span class="screen-reader-text">Revoke invite</span>-->
<!--                            </button>-->
<!--                            <button type="button" data-action="resend"--><?php //=($invite['status'] === 'exipired') ? '' : ' disabled'?><!-- title="Resend Invitation" onclick="resendInvite(this)">-->
<!--                                --><?php //=jvbIcon('arrows-clockwise')?>
<!--                                <span class="screen-reader-text">Try Again</span>-->
<!--                            </button>-->
<!--                        </td>-->
<!---->
<!--                    </tr>-->
<!--                    --><?php
//                }
//            } else {
//                ?>
<!--                <tr><td><i>No invites yet</i></td><td></td><td></td><td></td><td></td></tr>-->
<!--                --><?php
//            }
//            ?>
<!--            </tbody>-->
<!--        </table>-->
<!--        --><?php
//    }
//}
inc/utility/Video.php
@@ -115,7 +115,7 @@
    public function getImageData(int $imgID):array
    {
        return $this->imgData->remember(
        return $this->vidData->remember(
            $imgID,
            function() use ($imgID) {
                if (!wp_get_attachment_image($imgID, 'tiny')) {
jvb.php
@@ -27,7 +27,6 @@
use JVBase\managers\Login;
use JVBase\managers\MagicLinks;
use JVBase\managers\NotificationManager;
use JVBase\managers\Notifications\Notifications;
use JVBase\managers\queue\Queue;
use JVBase\managers\Referrals;
use JVBase\managers\Responses;
@@ -127,11 +126,11 @@
define('JVB_URL', plugin_dir_url(__FILE__));
// Session Security
define('JVB_SESSION_FINGERPRINT', true);
const JVB_SESSION_FINGERPRINT = true;
// Login Security
define('JVB_MAX_LOGIN_ATTEMPTS', 5);
define('JVB_LOCKOUT_DURATION', 15 * MINUTE_IN_SECONDS);
const JVB_MAX_LOGIN_ATTEMPTS = 5;
const JVB_LOCKOUT_DURATION = 15 * MINUTE_IN_SECONDS;
require(JVB_DIR.'/inc/Instance.php');
@@ -525,8 +524,8 @@
function jvbImageSize():void
{
    add_theme_support('post-thumbnails');
    add_image_size( 'tiny', 50, 50, false );
    add_image_size( 'directory-preview', 100, 100, false );
    add_image_size( 'tiny', 50, 50 );
    add_image_size( 'directory-preview', 100, 100 );
}
src/forms/render.php
@@ -1,7 +1,5 @@
<?php
use JVBase\meta\Form;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
src/summary/render.php
@@ -19,12 +19,10 @@
    // Buffer output
    if (is_tax()) {
        switch (get_queried_object()->taxonomy) {
            case BASE.'shop':
                return jvbRenderShopSummary();
            default:
                return jvbRenderTermSummary();
        }
        return match (get_queried_object()->taxonomy) {
            BASE . 'shop' => jvbRenderShopSummary(),
            default => jvbRenderTermSummary(),
        };
    } elseif (is_singular()) {
        return jvbRenderArtistSummary();
    }
@@ -152,7 +150,7 @@
        <h2>Contact <?=$artist['name']?></h2>
        <?php
        echo jvbRenderContactInfo($current->ID, 'post');
        echo jvbRenderLinks($current->ID, 'post');
        echo jvbRenderLinks($current->ID);
        ?>
    </section>
    <?php
@@ -294,16 +292,11 @@
    ob_start();
    $tax = jvbNoBase($current->taxonomy);
    switch ($tax) {
        case 'style':
            $title = 'Tattoo Artists';
            break;
        case 'theme':
            $title = 'Tattoos';
            break;
        default:
            $title = '';
    }
    $title = match ($tax) {
        'style' => 'Tattoo Artists',
        'theme' => 'Tattoos',
        default => '',
    };
    $meta = Meta::forTerm($current->ID);
    $fields = $meta->getAll();
templates/dashboard/sections/shop.php
@@ -50,7 +50,7 @@
foreach ($shops as $shop) {
    $active = ($i=== 0) ? ' active' : '';
    echo '<section class="tab-content'.$active.'" data-tab="shop-'.$shop->term_id.'" role="tabpanel">';
        jvbRenderSections($handler, $shop->term_id, 'term', BASE.'shop', true);
        jvbRenderSections($handler, $shop->term_id, 'term', BASE.'shop');
    echo '</section>';
    $i++;
}
templates/email/notification-digest.php
@@ -141,19 +141,12 @@
        <div class="section">
            <h2 class="section-title">
                <?php
                switch($type) {
                    case 'artist_new_tattoo':
                        echo "FRESH INK ↯";
                        break;
                    case 'artist_new_artwork':
                        echo "NEW ART DROPS ⚡";
                        break;
                    case 'shop_update':
                        echo "SHOP TALK";
                        break;
                    default:
                        echo "SCENE NEWS";
                }
                echo match ($type) {
                    'artist_new_tattoo' => "FRESH INK ↯",
                    'artist_new_artwork' => "NEW ART DROPS ⚡",
                    'shop_update' => "SHOP TALK",
                    default => "SCENE NEWS",
                };
                ?>
            </h2>