=Switched the /base/options.php to the same pattern as Site.php: a class based approached rather than a filter. Updated Meta.php to play along with the defined fields from there in Meta::forOptions. Had to change openingHoursSpecificationsTrait.php to not use the translater functions __('text','textdomain') for now, as we load before init.
| | |
| | | } |
| | | }); |
| | | |
| | | this.queue = { |
| | | add: new Map(), |
| | | remove: new Map(), |
| | | }; |
| | | |
| | | this.store = store.favourites; |
| | | |
| | | // this.listStore = window.jvbStore.register( |
| | |
| | | // Update button icon |
| | | button.innerHTML = jvbSettings.icons[button.classList.contains('favourited') ? 'heart-filled' : 'heart']; |
| | | |
| | | window.debouncer.schedule( |
| | | `favourite-${button.dataset.id}`, |
| | | this.postFavourite({ |
| | | user: window.auth.getUser(), |
| | | target_id: button.dataset.id, |
| | | action: action, |
| | | type: button.dataset.type |
| | | }), |
| | | 100 |
| | | ); |
| | | this.checkQueue(action, { |
| | | target_id: button.dataset.id, |
| | | action: action, |
| | | type: button.dataset.type |
| | | }); |
| | | window.debouncer.schedule('favourites', this.postFavourites.bind(this), 200); |
| | | |
| | | |
| | | } |
| | | |
| | | async postFavourite(args) { |
| | | await window.auth.fetch( |
| | | checkQueue(action, item) { |
| | | switch (action) { |
| | | case 'add': |
| | | if (this.queue.remove.has(item.target_id)) { |
| | | this.queue.remove.delete(item.target_id); |
| | | } |
| | | this.queue.add.set(item.target_id, item); |
| | | break; |
| | | case 'remove': |
| | | if (this.queue.add.has(item.target_id)) { |
| | | this.queue.add.delete(item.target_id); |
| | | } |
| | | this.queue.remove.set(item.target_id, item); |
| | | break; |
| | | default: |
| | | return; |
| | | } |
| | | } |
| | | async postFavourites() { |
| | | console.log(this.queue,'Posting favourites'); |
| | | const response = await window.auth.fetch( |
| | | `${jvbSettings.api}favourites`, |
| | | { |
| | | method: 'POST', |
| | | headers: { |
| | | 'X-Action-Nonce': window.auth.getNonce('favourites') |
| | | }, |
| | | body: args |
| | | body: { |
| | | user: window.auth.getUser(), |
| | | ... this.queue |
| | | } |
| | | } |
| | | ); |
| | | |
| | | if (args.action === 'add') { |
| | | await this.store.setItem(button.dataset.id, { |
| | | target_id: button.dataset.id, |
| | | action: action, |
| | | type: button.dataset.type, |
| | | }).then(()=>{}); |
| | | if (response.ok) { |
| | | console.log('Posted favourites - clearing queue'); |
| | | //Add or remove from store |
| | | for (let item of this.queue.add.entries()) { |
| | | await this.store.setItem(item.target_id, |
| | | item); |
| | | } |
| | | for (let item of this.queue.remove.entries()) { |
| | | await this.store.delete(item.target_id); |
| | | } |
| | | //Clear the favourite queue |
| | | this.queue.add.clear(); |
| | | this.queue.remove.clear(); |
| | | } else { |
| | | await this.store.delete(button.dataset.id).then(()=>{}); |
| | | console.log(await response.json(), 'Something went wrong'); |
| | | window.debouncer.schedule('favourites', this.postFavourites.bind(this), 200); |
| | | } |
| | | } |
| | | |
| | |
| | | */ |
| | | protected static bool $limit_hours = false; |
| | | /** |
| | | * @var bool $has_hours Whether this site has business hours |
| | | */ |
| | | protected static bool $has_hours = false; |
| | | /** |
| | | * @var bool $enthusiast Whether to scaffold enthusiasts (users that can interact with and save favourites) |
| | | */ |
| | | protected static bool $enthusiast = false; |
| | |
| | | //require(JVB_DIR.'/base/users.php'); |
| | | require(JVB_DIR.'/base/Login.php'); |
| | | require(JVB_DIR.'/base/Membership.php'); |
| | | require(JVB_DIR.'/base/options.php'); |
| | | require(JVB_DIR.'/base/Options.php'); |
| | | require(JVB_DIR.'/base/SchemaHelper.php'); |
| | | |
| | | $base = apply_filters('jvb_base', 'jvb_'); |
| | |
| | | <?php |
| | | namespace JVBase\base; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\meta\Form; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Fields; |
| | | |
| | | /** |
| | | * Custom options for the site can be set here. They will need to be handled by the child plugin, but you can make use of the Meta.php this way |
| | | */ |
| | | |
| | | $options = apply_filters('jvb_options', []); |
| | | define('JVB_OPTIONS', $options); |
| | | |
| | | |
| | | class Options { |
| | | protected static Options $instance; |
| | | protected array $resetFields = []; |
| | | protected string $resetInterval = 'daily'; |
| | | protected Fields $fields; |
| | | protected array $sections = []; |
| | | protected static array $values = []; |
| | | |
| | | private function __construct(){ |
| | | $this->setFields(); |
| | | if (!empty($this->resetFields)) { |
| | | add_action('jvbDailyReset', [$this, 'resetOptions']); |
| | | } |
| | | } |
| | | |
| | | public static function getInstance():Options { |
| | | if (!isset(self::$instance)) { |
| | | self::$instance = new self(); |
| | | do_action('jvb_define_options'); |
| | | } |
| | | return self::$instance; |
| | | } |
| | | |
| | | public function setFields():void |
| | | { |
| | | $this->fields = new Fields('options'); |
| | | if (Site::has('limit_hours')) { |
| | | $this->fields->addField( |
| | | 'today_hours', |
| | | [ |
| | | 'type' => 'group', |
| | | 'label' => 'Today\'s Hours', |
| | | 'fields' => [ |
| | | 'time_start' => [ |
| | | 'type' => 'time', |
| | | 'label' => 'Open', |
| | | ], |
| | | 'time_end' => [ |
| | | 'type' => 'time', |
| | | 'label' => 'Closed', |
| | | ] |
| | | ] |
| | | ] |
| | | ); |
| | | $this->fields->addField( |
| | | 'open_to_public', |
| | | [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Open to Public?' |
| | | ] |
| | | ); |
| | | |
| | | if (Site::hasAnyIntegration(['facebook','instagram'])) { |
| | | $this->fields->addField( |
| | | 'post_to_social', |
| | | [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Post to Socials?', |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | | |
| | | if (Site::hasIntegration('gmb') || Site::has('hours')) { |
| | | $this->fields->addCommon('hours'); |
| | | } |
| | | } |
| | | public function fields():Fields |
| | | { |
| | | return $this->fields; |
| | | } |
| | | |
| | | public function resetOptions():void |
| | | { |
| | | if (empty($this->resetFields)) { |
| | | return; |
| | | } |
| | | foreach ($this->resetFields as $field) { |
| | | self::delete($field); |
| | | } |
| | | Cache::for('options')->flush(); |
| | | } |
| | | |
| | | public static function get(string $fieldName):mixed |
| | | { |
| | | if (!array_key_exists($fieldName, self::$values)) { |
| | | $meta = Meta::forOptions(); |
| | | self::$values[$fieldName] = $meta->get($fieldName); |
| | | } |
| | | return self::$values[$fieldName]; |
| | | } |
| | | public static function delete(string $fieldName):void |
| | | { |
| | | if (array_key_exists($fieldName, self::$values)) { |
| | | unset(self::$values[$fieldName]); |
| | | } |
| | | $meta = Meta::forOptions(); |
| | | $meta->delete($fieldName); |
| | | } |
| | | public static function set(string $fieldName, mixed $value):void |
| | | { |
| | | $meta = Meta::forOptions(); |
| | | $meta->set($fieldName, $value); |
| | | self::$values = $value; |
| | | } |
| | | |
| | | public static function render(array $fieldNames, array $options = [], bool $output = false):string |
| | | { |
| | | $meta = Meta::forOptions(); |
| | | $result = Form::renderFormFrom( |
| | | $meta, |
| | | 'options', |
| | | $options, |
| | | $fieldNames |
| | | ); |
| | | if ($output) { |
| | | echo $result; |
| | | } |
| | | return $result; |
| | | } |
| | | |
| | | public function getFields():array |
| | | { |
| | | return array_map(function ($field) { |
| | | return $field->getConfig(); |
| | | }, |
| | | $this->fields->getFields() |
| | | ); |
| | | } |
| | | } |
| | |
| | | <?php |
| | | |
| | | use JVBase\base\Options; |
| | | use JVBase\base\Site; |
| | | use JVBase\managers\Cache; |
| | | use JVBase\registrar\Registrar; |
| | |
| | | } |
| | | |
| | | |
| | | //function jvbIsOpen():bool |
| | | //{ |
| | | // |
| | | // if (!jvbCheck('limit_hours', JVB_SITE)) { |
| | | // return true; |
| | | // } |
| | | // if (get_option(BASE.'open_to_public') !== '1') { |
| | | // return false; |
| | | // } |
| | | // //Check if today_hours is set |
| | | // if (get_option(BASE.'today_hours')) { |
| | | // return jvbIsTimeBetween(); |
| | | // } |
| | | // //Default to the stored settings |
| | | // return jvbIsCurrentlyOpen(); |
| | | //} |
| | | |
| | | function jvbIsOpen():bool |
| | | { |
| | | if (!Site::has('limit_hours')) { |
| | | return true; |
| | | } |
| | | if (Options::get('open_to_public') !== 1) { |
| | | error_log('Not open to public'); |
| | | return false; |
| | | } |
| | | if (Options::get('today_hours')) { |
| | | return jvbIsTimeBetween(); |
| | | } |
| | | return jvbIsCurrentlyOpen(); |
| | | } |
| | | |
| | | |
| | | function jvbTermHasPosts(int $termID, string $taxonomy):bool |
| | |
| | | <?php |
| | | |
| | | use JVBase\base\Site; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | |
| | | 'post_type' => $type |
| | | ]); |
| | | } |
| | | |
| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use Exception; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | public function __construct() |
| | | { |
| | | $this->defineTables(); |
| | | $this->registerHooks(); |
| | | } |
| | | |
| | | protected function registerHooks():void |
| | | { |
| | | add_action('before_delete_post', [$this, 'cleanupPostFavourites']); |
| | | add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3); |
| | | add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3); |
| | | add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']); |
| | | } |
| | | protected function defineTables():void |
| | | { |
| | | $this->defineFavouriteTable(); |
| | |
| | | 'list_id' => 'bigint(20) unsigned NOT NULL', |
| | | 'user_id' => "{$table->getUserIDType()} NOT NULL", |
| | | 'email' => 'varchar(255) NOT NULL', |
| | | 'invite_token' => 'varchar(255) NOT NULL', |
| | | 'permission' => "ENUM('view', 'edit') NOT NULL DEFAULT 'view'", |
| | | 'status' => "ENUM('pending', 'accepted', 'rejected', 'revoked') NOT NULL DEFAULT 'pending'", |
| | | 'created_at' => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP', |
| | |
| | | } elseif ($newUser) { |
| | | $email = sanitize_email($newUser['email']); |
| | | $name = sanitize_text_field($newUser['name']); |
| | | $args['invite_token'] = wp_generate_password(32, false); |
| | | $args['email'] = $email; |
| | | //TODO: Magic Link setup |
| | | } |
| | |
| | | 'shared_users' => $sharedWith |
| | | ]; |
| | | } |
| | | |
| | | public function getFavouriteCounts(int $user_id): array |
| | | { |
| | | $results = $this->favourites->queryResults( |
| | | "SELECT type, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY type", |
| | | [$user_id] |
| | | ); |
| | | |
| | | $counts = []; |
| | | foreach ($results as $row) { |
| | | $type = str_replace(BASE, '', $row->type); |
| | | $counts[$type] = (int) $row->count; |
| | | } |
| | | $defaults = array_fill_keys( |
| | | array_map(fn($t) => str_replace(BASE, '', $t), Registrar::withFeature('favouritable')), |
| | | 0 |
| | | ); |
| | | return array_merge($defaults, $counts); |
| | | } |
| | | |
| | | public function cleanupOrphanedFavourites(): bool |
| | | { |
| | | global $wpdb; |
| | | |
| | | // Posts - no FK possible since target_id is generic |
| | | $this->favourites->query( |
| | | "DELETE f FROM {table} f |
| | | LEFT JOIN {$wpdb->posts} p ON f.target_id = p.ID |
| | | WHERE f.type IN ( |
| | | SELECT CONCAT('" . BASE . "', post_type) |
| | | FROM {$wpdb->posts} GROUP BY post_type |
| | | ) |
| | | AND p.ID IS NULL" |
| | | ); |
| | | |
| | | // Terms - same reason |
| | | $this->favourites->query( |
| | | "DELETE f FROM {table} f |
| | | LEFT JOIN {$wpdb->term_taxonomy} tt ON f.target_id = tt.term_id |
| | | AND f.type = CONCAT('" . BASE . "', tt.taxonomy) |
| | | WHERE tt.term_id IS NULL |
| | | AND f.type != CONCAT('" . BASE . "', 'user')" |
| | | ); |
| | | |
| | | $this->listItems->query( |
| | | "DELETE li FROM {table} li |
| | | LEFT JOIN {$wpdb->posts} p ON li.target_id = p.ID |
| | | LEFT JOIN {$wpdb->term_taxonomy} tt ON li.target_id = tt.term_id |
| | | WHERE p.ID IS NULL AND tt.term_id IS NULL" |
| | | ); |
| | | |
| | | return true; |
| | | } |
| | | /*************************************************************** |
| | | * UTILITY METHODS |
| | | **************************************************************/ |
| | |
| | | unset($notes[$index]); |
| | | return array_values($notes); //reindexed array |
| | | } |
| | | |
| | | public function cleanupPostFavourites(int $post_id):void |
| | | { |
| | | try { |
| | | $type = get_post_type($post_id); |
| | | if (!$type) return; |
| | | |
| | | $this->favourites->where([ |
| | | 'type' => $type, |
| | | 'target_id' => $post_id |
| | | ])->deleteResults(); |
| | | |
| | | $this->listItems->where([ |
| | | 'item_type' => $type, |
| | | 'item_id' => $post_id |
| | | ])->deleteResults(); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('cleanupPostFavourites', $e->getMessage(), [ |
| | | 'post_id' => $post_id |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy):void |
| | | { |
| | | try { |
| | | $registrar = Registrar::getInstance($taxonomy); |
| | | if (!$registrar || !$registrar->hasFeature('favouritable')) { |
| | | return; |
| | | } |
| | | $this->favourites->where([ |
| | | 'type' => $taxonomy, |
| | | 'target_id' => $term_id, |
| | | ])->deleteResults(); |
| | | |
| | | $this->listItems->where([ |
| | | 'item_type' => $taxonomy, |
| | | 'item_id' => $term_id |
| | | ])->deleteResults(); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('cleanupTermFavourites', $e->getMessage(), [ |
| | | 'term_id' => $term_id, |
| | | 'taxonomy'=> $taxonomy |
| | | ]); |
| | | } |
| | | } |
| | | } |
| | |
| | | { |
| | | $fields->addField('openingHours', [ |
| | | 'type' => 'repeater', |
| | | 'label' => __('Opening Hours', 'jvb'), |
| | | 'label' => 'Opening Hours', |
| | | 'fields' => [ |
| | | 'dayOfWeek' => [ |
| | | 'type' => 'set', |
| | | 'label' => __('Day(s) of Week', 'jvb'), |
| | | 'label' => 'Day(s) of Week', |
| | | 'options' => [ |
| | | 'Mo' => 'Monday', |
| | | 'Tu' => 'Tuesday', |
| | |
| | | ], |
| | | 'opens' => [ |
| | | 'type' => 'time', |
| | | 'label' => __('Opens at', 'jvb'), |
| | | 'label' => 'Opens at', |
| | | 'required' => true |
| | | ], |
| | | 'closes' => [ |
| | | 'type' => 'time', |
| | | 'label' => __('Closes at', 'jvb'), |
| | | 'label' => 'Closes at', |
| | | 'required' => true |
| | | ] |
| | | ] |
| | | ]); |
| | | $fields->addField('by_appointment', [ |
| | | 'type' => 'true_false', |
| | | 'label' => __('By Appointment Only', 'jvb'), |
| | | 'label' => 'By Appointment Only', |
| | | ]); |
| | | $fields->addField('allow_walkins', [ |
| | | 'type' => 'true_false', |
| | | 'label' => __('Walk Ins Welcome', 'jvb') |
| | | 'label' => 'Walk Ins Welcome' |
| | | ]); |
| | | } |
| | | public function formatOpeningHoursSpecificationField(Meta $meta):void |
| | |
| | | /** |
| | | * Render complete form from Meta instance |
| | | */ |
| | | public static function renderFormFrom(Meta $meta, string $endpoint, array $options = []): string |
| | | public static function renderFormFrom(Meta $meta, string $endpoint, array $options = [], array $fields = []): string |
| | | { |
| | | $id = $options['form-id'] ?? $endpoint; |
| | | $classes = isset($options['classes']) ? ' class="' . implode(' ', $options['classes']) . '"' : ''; |
| | |
| | | $output .= '<p>' . esc_html($d) . '</p>'; |
| | | } |
| | | } |
| | | |
| | | foreach ($meta->configs() as $name => $config) { |
| | | $output .= static::render($name, $meta->get($name), $config); |
| | | $allFields = $meta->getAll($fields); |
| | | foreach ($allFields as $name => $value) { |
| | | $config = $meta->config($name); |
| | | $output .= static::render($name, $value, $config); |
| | | } |
| | | |
| | | if (!empty($options['submit'])) { |
| | |
| | | <?php |
| | | namespace JVBase\meta; |
| | | |
| | | use JVBase\base\Options; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | use WP_Term; |
| | |
| | | /** |
| | | * Create Meta instance for options |
| | | */ |
| | | public static function forOptions(?string $baseKey = 'ajv'): self |
| | | public static function forOptions(?string $baseKey = BASE): self |
| | | { |
| | | if (array_key_exists($baseKey, self::$instances['options'])) { |
| | | return self::$instances['options'][$baseKey]; |
| | |
| | | |
| | | $registrar = !is_null($this->slug) ? Registrar::getInstance($this->slug) : false; |
| | | $fields = $registrar ? $registrar->getFields() : []; |
| | | if ($this->type == 'options') { |
| | | $options = Options::getInstance(); |
| | | $fields = $options->getFields(); |
| | | } |
| | | $meta = match($type) { |
| | | 'post' => get_post_meta($id), |
| | | 'term' => get_term_meta($id), |
| | |
| | | |
| | | class Fields { |
| | | protected array $fields; |
| | | private Registrar $registrar; |
| | | private ?Registrar $registrar = null; |
| | | |
| | | public function __construct(?string $type = null, ?Registrar $registrar = null) { |
| | | $this->registrar = $registrar; |
| | |
| | | use JVBase\registrar\config\Directory; |
| | | use JVBase\registrar\config\Feed; |
| | | use JVBase\registrar\config\Integration; |
| | | use JVBase\registrar\config\Register; |
| | | use JVBase\registrar\config\Section; |
| | | use JVBase\registrar\config\SEO; |
| | | use JVBase\registrar\helpers\AddIntegrationFields; |
| | |
| | | protected Dashboard $dashboard; |
| | | protected Directory|false $directory; |
| | | protected Feed|false $feed; |
| | | protected Register|false $login; |
| | | // protected Management $management; |
| | | // protected Responses $responses; |
| | | protected ?SEO $seo = null; |
| | |
| | | |
| | | public function config(string $config):mixed |
| | | { |
| | | $allowed = ['breadcrumbs','calendar','dashboard','directory','feed','management','has_responses','seo','trackchanges','verification']; |
| | | $allowed = ['breadcrumbs','calendar','register','login','dashboard','directory','feed','management','has_responses','seo','trackchanges','verification']; |
| | | if (!in_array(strtolower($config), $allowed)) { |
| | | error_log('Invalid config requested from Registrar: '.$config); |
| | | return []; |
| | |
| | | 'breadcrumbs' => $this->getBreadcrumbs(), |
| | | 'dashboard' => $this->getDashboard(), |
| | | 'directory' => $this->getDirectory(), |
| | | 'register','login' => $this->getLogin(), |
| | | 'feed' => $this->getFeed(), |
| | | 'management' => $this->getManagement(), |
| | | 'has_responses' => $this->getResponses(), |
| | |
| | | 'verification' => $this->getVerification() |
| | | }; |
| | | } |
| | | protected function getLogin():Register|false |
| | | { |
| | | if (!isset($this->login)) { |
| | | $this->login = new Register(); |
| | | } |
| | | return $this->login; |
| | | } |
| | | protected function getBreadcrumbs():Breadcrumbs |
| | | { |
| | | if (!isset($this->breadcrumbs)) { |
| | |
| | | * @var string Must match as defined in the JVBase\integrations namespace |
| | | */ |
| | | protected string $service_name; |
| | | protected string $content_type; |
| | | protected ?string $content_type = null; |
| | | /** |
| | | * @var bool Whether to send to the integration on publish |
| | | */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * @param string $content must match what integration expects |
| | | * @param ?string $content must match what integration expects |
| | | * @return self |
| | | */ |
| | | public function setContentType(string $content):self |
| | | public function setContentType(?string $content):self |
| | | { |
| | | if (is_null($content)) { |
| | | return $this; |
| | | } |
| | | $connection = JVB()->connect($this->service_name); |
| | | if (!$connection){ |
| | | error_log('[Integration]::setContentType Service is not setup. '.$this->service_name); |
| | |
| | | return $this; |
| | | } |
| | | |
| | | public function getContentType():string |
| | | public function getContentType():?string |
| | | { |
| | | return $this->content_type; |
| | | } |
| | |
| | | $this->lists = CustomTable::for('favourites_lists'); |
| | | $this->listItems = CustomTable::for('favourites_list_items'); |
| | | $this->listShares = CustomTable::for('favourites_list_shares'); |
| | | |
| | | // Register hooks |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | add_action('before_delete_post', [$this, 'cleanupPostFavourites']); |
| | | add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3); |
| | | add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3); |
| | | add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']); |
| | | } |
| | | |
| | | public function registerRoutes(): void |
| | |
| | | */ |
| | | public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | |
| | | $user_id = absint($request->get_param('user')); |
| | | |
| | | if (!$this->userCheck($user_id)) { |
| | | return $this->unauthorized(); |
| | | } |
| | | |
| | | $key = "counts_{$user_id}"; |
| | | |
| | | $counts = $this->cache->remember($key, function() use ($user_id) { |
| | | try { |
| | | // Get counts grouped by type using raw query |
| | | $results = $this->favourites->queryResults( |
| | | "SELECT type, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY type", |
| | | [$user_id], |
| | | OBJECT_K |
| | | ); |
| | | |
| | | $all_counts = array_fill_keys( |
| | | array_map(fn($type) => str_replace(BASE, '', $type), array_keys($this->valid_types)), |
| | | 0 |
| | | ); |
| | | |
| | | foreach ($results as $type => $data) { |
| | | $type_key = str_replace(BASE, '', $type); |
| | | $all_counts[$type_key] = (int)$data->count; |
| | | } |
| | | |
| | | return $all_counts; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('getFavouriteCounts', [ |
| | | 'error' => $e->getMessage(), |
| | | 'user_id' => $user_id |
| | | ]); |
| | | |
| | | return array_fill_keys(array_keys($this->valid_types), 0); |
| | | } |
| | | }); |
| | | $counts = JVB()->favourites()->getFavouriteCounts($user_id); |
| | | |
| | | return Response::success(['counts' => $counts]); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * Clean up favourites when a post is deleted |
| | | */ |
| | | public function cleanupPostFavourites(int $post_id): void |
| | | { |
| | | try { |
| | | $type = get_post_type($post_id); |
| | | if (!$type) return; |
| | | |
| | | $type = BASE . $type; |
| | | |
| | | // Delete using fluent interface |
| | | $this->favourites->where([ |
| | | 'type' => $type, |
| | | 'target_id' => $post_id |
| | | ])->deleteResults(); |
| | | |
| | | $this->listItems->where([ |
| | | 'item_type' => $type, |
| | | 'item_id' => $post_id |
| | | ])->deleteResults(); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('cleanupPostFavourites', [ |
| | | 'error' => $e->getMessage(), |
| | | 'post_id' => $post_id |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Clean up favourites when a term is deleted |
| | | */ |
| | | public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy): void |
| | | { |
| | | try { |
| | | if (!isset($this->valid_types[$taxonomy])) { |
| | | return; |
| | | } |
| | | |
| | | // Delete using fluent interface |
| | | $this->favourites->where([ |
| | | 'type' => $taxonomy, |
| | | 'target_id' => $term_id |
| | | ])->deleteResults(); |
| | | |
| | | $this->listItems->where([ |
| | | 'item_type' => $taxonomy, |
| | | 'item_id' => $term_id |
| | | ])->deleteResults(); |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('cleanupTermFavourites', [ |
| | | 'error' => $e->getMessage(), |
| | | 'term_id' => $term_id, |
| | | 'taxonomy' => $taxonomy |
| | | ]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Cleanup orphaned favourites using CustomTable query method |
| | | */ |
| | | public function cleanupOrphanedFavourites(): bool |
| | | { |
| | | try { |
| | | // Delete favourites for non-existent users |
| | | $this->favourites->query( |
| | | "DELETE f FROM {table} f |
| | | LEFT JOIN {$GLOBALS['wpdb']->users} u ON f.user_id = u.ID |
| | | WHERE u.ID IS NULL" |
| | | ); |
| | | |
| | | // Delete favourites for non-existent posts |
| | | $post_types = array_filter( |
| | | array_keys($this->valid_types), |
| | | fn($type) => $this->valid_types[$type]['table'] === 'post' |
| | | ); |
| | | |
| | | foreach ($post_types as $type) { |
| | | $this->favourites->query( |
| | | "DELETE f FROM {table} f |
| | | LEFT JOIN {$GLOBALS['wpdb']->posts} p ON f.target_id = p.ID |
| | | WHERE f.type = %s AND p.ID IS NULL", |
| | | [$type] |
| | | ); |
| | | } |
| | | |
| | | return true; |
| | | |
| | | } catch (Exception $e) { |
| | | $this->logError('cleanupOrphanedFavourites', [ |
| | | 'error' => $e->getMessage() |
| | | ]); |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Helper methods |
| | | */ |
| | | protected function buildParams(WP_REST_Request $request): array |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Notify content owner of new favourite if configured |
| | | * |
| | | * @param string $type Content type |
| | | * @param int $target_id Content ID |
| | | * @param int $user_id User who favourited |
| | | * @return void |
| | | */ |
| | | protected function maybeNotifyOwner(string $type, int $target_id, int $user_id): void |
| | | { |
| | | try { |
| | | $owner_id = $this->getContentOwner($type, $target_id); |
| | | |
| | | if ($owner_id && $owner_id !== $user_id) { |
| | | JVB()->notification()->addNotification( |
| | | $owner_id, |
| | | 'new_favourite', |
| | | [ |
| | | 'user_id' => $user_id, |
| | | 'type' => $type, |
| | | 'target_id' => $target_id |
| | | ] |
| | | ); |
| | | } |
| | | } catch (Exception $e) { |
| | | // Silent fail - notifications are non-critical |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Remove any existing notifications about a favorite action |
| | | * |
| | | * @param int $user_id User who removed the favorite |
| | | * @param string $type Content type |
| | | * @param int $target_id Content ID |
| | | * @return void |
| | | */ |
| | | protected function removeRelatedNotifications(int $user_id, string $type, int $target_id):void |
| | | { |
| | | try { |
| | | // Get the content owner(s) |
| | | $owner_ids = $this->getContentOwner($type, $target_id); |
| | | if (!$owner_ids) { |
| | | return; |
| | | } |
| | | |
| | | $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids]; |
| | | |
| | | foreach ($owner_ids as $owner_id) { |
| | | // Skip if owner is the same as the user who unfavorited |
| | | if ($owner_id === $user_id) { |
| | | continue; |
| | | } |
| | | |
| | | global $wpdb; |
| | | $notifications_table = $wpdb->prefix . BASE . 'notifications'; |
| | | |
| | | // Find recent (within last 30 days) new_favourite notifications from this user for this content |
| | | $notifications = $wpdb->get_results($wpdb->prepare( |
| | | "SELECT id FROM {$notifications_table} |
| | | WHERE owner_id = %d |
| | | AND action_user_id = %d |
| | | AND type = 'new_favourite' |
| | | AND target_id = %d |
| | | AND target_type = %s |
| | | AND created_at > DATE_SUB(%s, INTERVAL 30 DAY)", |
| | | $owner_id, |
| | | $user_id, |
| | | $target_id, |
| | | $type, |
| | | current_time('mysql') |
| | | )); |
| | | |
| | | if (empty($notifications)) { |
| | | continue; |
| | | } |
| | | |
| | | // Delete the notifications |
| | | foreach ($notifications as $notification) { |
| | | $wpdb->delete( |
| | | $notifications_table, |
| | | ['id' => $notification->id], |
| | | ['%d'] |
| | | ); |
| | | } |
| | | |
| | | // Invalidate notification cache for this user |
| | | // if (method_exists(JVB()->notification(), 'clearNotificationCache')) { |
| | | // JVB()->notification()->clearNotificationCache($owner_id); |
| | | // } |
| | | } |
| | | } catch (Exception $e) { |
| | | // Log but continue |
| | | JVB()->error()->log( |
| | | 'favourites', |
| | | 'Error removing related notifications: ' . $e->getMessage(), |
| | | ['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id], |
| | | 'warning' |
| | | ); |
| | | } |
| | | } |
| | | |
| | | public function maybeAcceptListInvite(int $user_id, string $email, array $data):void |
| | | { |
| | | if (array_key_exists('list_token', $data) && !empty($data['list_token'])) { |
| | | $this->acceptListInvitation($data['list_token'], $email); |
| | | JVB()->favourites()->acceptListShare($data['list_token'], $user_id); |
| | | } |
| | | } |
| | | |
| | |
| | | add_action('plugins_loaded', 'jvb_site_definitions',1); |
| | | add_action('plugins_loaded', 'jvb_registrar_definitions',2); |
| | | add_action('plugins_loaded', 'jvb_field_definitions', 3); |
| | | add_action('plugins_loaded', 'jvb_options_definitions',3); |
| | | add_action('init', 'jvbLoadBase', 1); |
| | | add_action('init', 'jvb_integration_definitions',3); |
| | | add_action('init', 'jvb_field_section_definitions', 5); |
| | |
| | | do_action('jvb_define_fields'); |
| | | } |
| | | |
| | | function jvb_options_definitions():void |
| | | { |
| | | do_action('jvb_define_options'); |
| | | } |
| | | |
| | | function jvbLoadBase():void |
| | | { |