Changeset 3470118
- Timestamp:
- 02/26/2026 10:04:37 AM (5 weeks ago)
- Location:
- understory
- Files:
-
- 12 edited
- 1 copied
-
tags/1.8.2 (copied) (copied from understory/trunk)
-
tags/1.8.2/CLAUDE.md (modified) (1 diff)
-
tags/1.8.2/includes/utils/class-company-data-updater.php (modified) (1 diff)
-
tags/1.8.2/includes/utils/class-experiences.php (modified) (1 diff)
-
tags/1.8.2/package-lock.json (modified) (2 diffs)
-
tags/1.8.2/readme.txt (modified) (2 diffs)
-
tags/1.8.2/understory.php (modified) (2 diffs)
-
trunk/CLAUDE.md (modified) (1 diff)
-
trunk/includes/utils/class-company-data-updater.php (modified) (1 diff)
-
trunk/includes/utils/class-experiences.php (modified) (1 diff)
-
trunk/package-lock.json (modified) (2 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/understory.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
understory/tags/1.8.2/CLAUDE.md
r3465724 r3470118 204 204 ### Version Management 205 205 206 Current version: 1.8. 1(defined in both `package.json` and `understory.php`)206 Current version: 1.8.2 (defined in both `package.json` and `understory.php`) -
understory/tags/1.8.2/includes/utils/class-company-data-updater.php
r3351241 r3470118 3 3 namespace Understory\Utils; 4 4 5 if ( !defined('ABSPATH')) {6 exit; // Exit if accessed directly.5 if ( ! defined( 'ABSPATH' ) ) { 6 exit; // Exit if accessed directly. 7 7 } 8 8 9 class CompanyDataUpdater 10 { 11 public static function update($company_id) 12 { 13 $options = get_option(UNDERSTORY_OPTION_KEY); 9 class CompanyDataUpdater { 14 10 15 $company_home_view_api_url = UNDERSTORY_API_BASE_URL . "/companies/{$company_id}/home-view"; 16 $home_view_data = DataFetcher::get($company_home_view_api_url); 17 if (empty($home_view_data)) { 18 throw new \Exception('Failed to fetch company details from Understory API. Please check your connection and try again. If the problem persists, contact support.'); 19 } 11 public static function update( $company_id ) { 12 $options = get_option( UNDERSTORY_OPTION_KEY ); 20 13 21 $options['company'] = [ 22 'id' => $company_id, 23 'name' => $home_view_data['company']['name'] ?? null, 24 'languages' => $home_view_data['company']['languages'] ?? null, 25 'customization' => $home_view_data['company']['customization'] ?? null, 26 'defaultLanguage' => $home_view_data['company']['defaultLanguage'] ?? null 27 ]; 14 $company_home_view_api_url = UNDERSTORY_API_BASE_URL . "/companies/{$company_id}/home-view"; 15 $home_view_data = DataFetcher::get( $company_home_view_api_url ); 16 if ( empty( $home_view_data ) ) { 17 throw new \Exception( 'Failed to fetch company details from Understory API. Please check your connection and try again. If the problem persists, contact support.' ); 18 } 28 19 29 // Save the updated options back to the database 30 update_option(UNDERSTORY_OPTION_KEY, $options); 31 } 20 $options['company'] = array( 21 'id' => $company_id, 22 'name' => $home_view_data['company']['name'] ?? null, 23 'languages' => $home_view_data['company']['languages'] ?? null, 24 'customization' => $home_view_data['company']['customization'] ?? null, 25 'defaultLanguage' => $home_view_data['company']['defaultLanguage'] ?? null, 26 ); 27 28 // Save the updated options back to the database 29 update_option( UNDERSTORY_OPTION_KEY, $options ); 30 } 32 31 } -
understory/tags/1.8.2/includes/utils/class-experiences.php
r3369591 r3470118 3 3 namespace Understory\Utils; 4 4 5 if ( !defined('ABSPATH')) {6 exit; // Exit if accessed directly.5 if ( ! defined( 'ABSPATH' ) ) { 6 exit; // Exit if accessed directly. 7 7 } 8 class Experiences 9 { 10 public static function render($company_id, $language = null, $tag_ids = null, $storefront_id = null) 11 { 12 if (empty($language)) { 13 $language = \Understory_Settings::get_default_language(); 14 } 8 class Experiences { 15 9 16 if (empty($storefront_id)) { 17 $storefront_id = \Understory_Settings::get_default_storefront($company_id); 18 } 10 private static $translations = array( 11 'da' => array( 12 'from' => 'Fra', 13 'person' => 'person', 14 ), 15 'en' => array( 16 'from' => 'From', 17 'person' => 'guest', 18 ), 19 'de' => array( 20 'from' => 'von', 21 'person' => 'Person', 22 ), 23 'sv' => array( 24 'from' => 'Från', 25 'person' => 'person', 26 ), 27 'nb' => array( 28 'from' => 'Fra', 29 'person' => 'menneske', 30 ), 31 'nl' => array( 32 'from' => 'Vanaf', 33 'person' => 'persoon', 34 ), 35 'it' => array( 36 'from' => 'Da', 37 'person' => 'persona', 38 ), 39 'fr' => array( 40 'from' => 'À partir de', 41 'person' => 'personne', 42 ), 43 'es' => array( 44 'from' => 'Desde', 45 'person' => 'persona', 46 ), 47 'pt' => array( 48 'from' => 'A partir de', 49 'person' => 'pessoa', 50 ), 51 ); 19 52 20 $utils_translations = [ 21 "da" => [ 22 "from" => "Fra", 23 "person" => "person", 24 "currencySymbol" => "kr" 25 ], 26 "en" => [ 27 "from" => "From", 28 "person" => "guest", 29 "currencySymbol" => "kr" 30 ], 31 "de" => [ 32 "from" => "von", 33 "person" => "Person", 34 "currencySymbol" => "kr" 35 ], 36 "sv" => [ 37 "from" => "Från", 38 "person" => "person", 39 "currencySymbol" => "kr" 40 ], 41 "nb" => [ 42 "from" => "Fra", 43 "person" => "menneske", 44 "currencySymbol" => "kr" 45 ], 46 ]; 47 $card_url_base_prefix = !empty($language) ? '/' . $language . '/experience/' : '/en/experience/'; 48 $currency_symbol = !empty($language) ? $utils_translations[$language]['currencySymbol'] : $utils_translations['en']['currencySymbol']; 49 $price_prefix = !empty($language) ? $utils_translations[$language]['from'] : $utils_translations['en']['from']; 50 $fallback_price_suffix = (!empty($language) ? $utils_translations[$language]['person'] : $utils_translations['en']['person']); 53 public static function render( $company_id, $language = null, $tag_ids = null, $storefront_id = null ) { 54 if ( empty( $language ) ) { 55 $language = \Understory_Settings::get_default_language(); 56 } 51 57 52 $storefront = \Understory_Settings::get_storefront($company_id, $storefront_id); 53 if (empty($storefront)) { 54 return '<p>' . esc_html__('The selected storefront could not be found. Please, check your storefront ID and try again.', 'understory') . '</p>'; 55 } 56 $storefront_fqdn = $storefront['fqdn']; 57 $experience_ids = $storefront['experienceIds']; 58 if ( empty( $storefront_id ) ) { 59 $storefront_id = \Understory_Settings::get_default_storefront( $company_id ); 60 } 58 61 59 // Fetch data from API with storefront filtering 60 $data = ExperienceFetcher::fetch_experiences($company_id, $language, $tag_ids, $experience_ids); 62 $translation = self::$translations[ $language ] ?? self::$translations['en']; 61 63 62 if (empty($data)) { 63 return '';64 } 64 $card_url_base_prefix = ! empty( $language ) ? '/' . $language . '/experience/' : '/en/experience/'; 65 $price_prefix = $translation['from']; 66 $fallback_price_suffix = $translation['person']; 65 67 66 ob_start(); 68 $storefront = \Understory_Settings::get_storefront( $company_id, $storefront_id ); 69 if ( empty( $storefront ) ) { 70 return '<p>' . esc_html__( 'The selected storefront could not be found. Please, check your storefront ID and try again.', 'understory' ) . '</p>'; 71 } 72 $storefront_fqdn = $storefront['fqdn']; 73 $experience_ids = $storefront['experienceIds']; 67 74 68 $root_classnames = ['understory-experiences-widget']; 69 if (count($data) > 2) { 70 $root_classnames[] = 'has-max-three-columns'; 71 } 72 ?> 73 <div class="<?php echo esc_attr(implode(' ', $root_classnames)); ?>" 74 data-company-id="<?php echo esc_attr($company_id); ?>" data-storefront-id="<?php echo esc_attr($storefront_id); ?>" 75 <?php if (!empty($language)): ?> data-language="<?php echo esc_attr($language); ?>" <?php endif; ?> <?php if (!empty($tag_ids)): ?> data-tag-ids="<?php echo esc_attr($tag_ids); ?>" <?php endif; ?>> 76 <?php foreach ($data as $experience): ?> 77 <?php 78 // Sanitize output 79 $href = esc_url($card_url_base_prefix . $experience['id']); 80 $image_url = esc_url($experience['image']); 81 $name = esc_html($experience['name']); 82 $description = self::markdownToText($experience['description']); 83 $price_item = esc_html($experience['price'] . ' ' . $currency_symbol); 84 $price_suffix = '/ ' . (!empty($experience['priceName']) ? esc_html($experience['priceName']) : $fallback_price_suffix); 85 ExperienceCard::render($href, $image_url, $name, $description, $price_prefix, $price_item, strtolower($price_suffix), $storefront_fqdn); 86 ?> 87 <?php endforeach; ?> 88 </div> 89 <?php 90 return ob_get_clean(); 91 } 75 // Fetch data from API with storefront filtering 76 $data = ExperienceFetcher::fetch_experiences( $company_id, $language, $tag_ids, $experience_ids ); 92 77 93 private static function markdownToText($markdown) 94 { 95 $text = preg_replace('/[\r\n]+/', ' ', $markdown); 78 if ( empty( $data ) ) { 79 return ''; 80 } 96 81 97 // Remove markdown characters and backslashes 98 $text = str_replace(['#', '*', '\\'], '', $text); 99 $text = esc_html(trim($text)); 82 ob_start(); 100 83 101 return trim($text); 102 } 84 $root_classnames = array( 'understory-experiences-widget' ); 85 if ( count( $data ) > 2 ) { 86 $root_classnames[] = 'has-max-three-columns'; 87 } 88 ?> 89 <div class="<?php echo esc_attr( implode( ' ', $root_classnames ) ); ?>" 90 data-company-id="<?php echo esc_attr( $company_id ); ?>" data-storefront-id="<?php echo esc_attr( $storefront_id ); ?>" 91 <?php 92 if ( ! empty( $language ) ) : 93 ?> 94 data-language="<?php echo esc_attr( $language ); ?>" <?php endif; ?> 95 <?php 96 if ( ! empty( $tag_ids ) ) : 97 ?> 98 data-tag-ids="<?php echo esc_attr( $tag_ids ); ?>" <?php endif; ?>> 99 <?php foreach ( $data as $experience ) : ?> 100 <?php 101 // Skip experiences without currency to avoid showing wrong prices 102 if ( empty( $experience['currency'] ) ) { 103 continue; 104 } 105 // Sanitize output 106 $href = esc_url( $card_url_base_prefix . $experience['id'] ); 107 $image_url = esc_url( $experience['image'] ); 108 $name = esc_html( $experience['name'] ); 109 $description = self::markdown_to_text( $experience['description'] ); 110 $price_item = esc_html( self::format_price( $experience['price'], $experience['currency'], $language ) ); 111 $price_suffix = '/ ' . ( ! empty( $experience['priceName'] ) ? esc_html( $experience['priceName'] ) : $fallback_price_suffix ); 112 ExperienceCard::render( $href, $image_url, $name, $description, $price_prefix, $price_item, strtolower( $price_suffix ), $storefront_fqdn ); 113 ?> 114 <?php endforeach; ?> 115 </div> 116 <?php 117 return ob_get_clean(); 118 } 119 120 /** 121 * Format a price with the correct currency symbol using Intl NumberFormatter. 122 * 123 * @param int|float $amount The price amount. 124 * @param string $currency The ISO 4217 currency code (e.g. "eur", "dkk"). 125 * @param string $language The language code for locale formatting. 126 * @return string Formatted price string (e.g. "€125", "125 kr"). 127 */ 128 private static function format_price( $amount, $currency, $language ) { 129 $locale = self::language_to_locale( $language ); 130 $currency = strtoupper( $currency ); 131 $formatter = new \NumberFormatter( $locale, \NumberFormatter::CURRENCY ); 132 $formatter->setAttribute( \NumberFormatter::FRACTION_DIGITS, 0 ); 133 $formatted = $formatter->formatCurrency( (float) $amount, $currency ); 134 if ( false !== $formatted ) { 135 return $formatted; 136 } 137 138 return $currency . $amount; 139 } 140 141 /** 142 * Map a language code to a full locale for Intl formatting. 143 */ 144 private static function language_to_locale( $language ) { 145 $locales = array( 146 'da' => 'da_DK', 147 'en' => 'en_US', 148 'de' => 'de_DE', 149 'sv' => 'sv_SE', 150 'nb' => 'nb_NO', 151 'nl' => 'nl_NL', 152 'it' => 'it_IT', 153 'fr' => 'fr_FR', 154 'es' => 'es_ES', 155 'pt' => 'pt_PT', 156 ); 157 return $locales[ $language ] ?? 'en_US'; 158 } 159 160 private static function markdown_to_text( $markdown ) { 161 $text = preg_replace( '/[\r\n]+/', ' ', $markdown ); 162 163 // Remove markdown characters and backslashes 164 $text = str_replace( array( '#', '*', '\\' ), '', $text ); 165 $text = esc_html( trim( $text ) ); 166 167 return trim( $text ); 168 } 103 169 } -
understory/tags/1.8.2/package-lock.json
r3465724 r3470118 1 1 { 2 2 "name": "understory", 3 "version": "1.8. 1",3 "version": "1.8.2", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "understory", 9 "version": "1.8. 1",9 "version": "1.8.2", 10 10 "dependencies": { 11 11 "@mui/material": "6.4.2", -
understory/tags/1.8.2/readme.txt
r3465724 r3470118 4 4 Requires at least: 5.0 5 5 Tested up to: 6.8 6 Stable tag: 1.8. 16 Stable tag: 1.8.2 7 7 Requires PHP: 7.0 8 8 License: GPLv2 or later … … 77 77 78 78 == Changelog == 79 80 = 1.8.2 = 81 * Experiences widget: Fix incorrect or missing currency symbols by using the currency from each experience directly. 79 82 80 83 = 1.8.1 = -
understory/tags/1.8.2/understory.php
r3465724 r3470118 3 3 Plugin Name: Understory 4 4 Description: Connect your WordPress site with Understory, to easily add your booking widget to posts and pages. 5 Version: 1.8. 15 Version: 1.8.2 6 6 Author: Understory 7 7 Text Domain: understory … … 18 18 define('UNDERSTORY_PLUGIN_URL', plugin_dir_url(__FILE__)); 19 19 define('UNDERSTORY_PLUGIN_SLUG', 'understory'); 20 define('UNDERSTORY_PLUGIN_VERSION', '1.8. 1');20 define('UNDERSTORY_PLUGIN_VERSION', '1.8.2'); 21 21 define('UNDERSTORY_OPTION_KEY', 'understory_options'); 22 22 define('UNDERSTORY_NONCE_KEY', 'understory_nonce'); -
understory/trunk/CLAUDE.md
r3465724 r3470118 204 204 ### Version Management 205 205 206 Current version: 1.8. 1(defined in both `package.json` and `understory.php`)206 Current version: 1.8.2 (defined in both `package.json` and `understory.php`) -
understory/trunk/includes/utils/class-company-data-updater.php
r3351241 r3470118 3 3 namespace Understory\Utils; 4 4 5 if ( !defined('ABSPATH')) {6 exit; // Exit if accessed directly.5 if ( ! defined( 'ABSPATH' ) ) { 6 exit; // Exit if accessed directly. 7 7 } 8 8 9 class CompanyDataUpdater 10 { 11 public static function update($company_id) 12 { 13 $options = get_option(UNDERSTORY_OPTION_KEY); 9 class CompanyDataUpdater { 14 10 15 $company_home_view_api_url = UNDERSTORY_API_BASE_URL . "/companies/{$company_id}/home-view"; 16 $home_view_data = DataFetcher::get($company_home_view_api_url); 17 if (empty($home_view_data)) { 18 throw new \Exception('Failed to fetch company details from Understory API. Please check your connection and try again. If the problem persists, contact support.'); 19 } 11 public static function update( $company_id ) { 12 $options = get_option( UNDERSTORY_OPTION_KEY ); 20 13 21 $options['company'] = [ 22 'id' => $company_id, 23 'name' => $home_view_data['company']['name'] ?? null, 24 'languages' => $home_view_data['company']['languages'] ?? null, 25 'customization' => $home_view_data['company']['customization'] ?? null, 26 'defaultLanguage' => $home_view_data['company']['defaultLanguage'] ?? null 27 ]; 14 $company_home_view_api_url = UNDERSTORY_API_BASE_URL . "/companies/{$company_id}/home-view"; 15 $home_view_data = DataFetcher::get( $company_home_view_api_url ); 16 if ( empty( $home_view_data ) ) { 17 throw new \Exception( 'Failed to fetch company details from Understory API. Please check your connection and try again. If the problem persists, contact support.' ); 18 } 28 19 29 // Save the updated options back to the database 30 update_option(UNDERSTORY_OPTION_KEY, $options); 31 } 20 $options['company'] = array( 21 'id' => $company_id, 22 'name' => $home_view_data['company']['name'] ?? null, 23 'languages' => $home_view_data['company']['languages'] ?? null, 24 'customization' => $home_view_data['company']['customization'] ?? null, 25 'defaultLanguage' => $home_view_data['company']['defaultLanguage'] ?? null, 26 ); 27 28 // Save the updated options back to the database 29 update_option( UNDERSTORY_OPTION_KEY, $options ); 30 } 32 31 } -
understory/trunk/includes/utils/class-experiences.php
r3369591 r3470118 3 3 namespace Understory\Utils; 4 4 5 if ( !defined('ABSPATH')) {6 exit; // Exit if accessed directly.5 if ( ! defined( 'ABSPATH' ) ) { 6 exit; // Exit if accessed directly. 7 7 } 8 class Experiences 9 { 10 public static function render($company_id, $language = null, $tag_ids = null, $storefront_id = null) 11 { 12 if (empty($language)) { 13 $language = \Understory_Settings::get_default_language(); 14 } 8 class Experiences { 15 9 16 if (empty($storefront_id)) { 17 $storefront_id = \Understory_Settings::get_default_storefront($company_id); 18 } 10 private static $translations = array( 11 'da' => array( 12 'from' => 'Fra', 13 'person' => 'person', 14 ), 15 'en' => array( 16 'from' => 'From', 17 'person' => 'guest', 18 ), 19 'de' => array( 20 'from' => 'von', 21 'person' => 'Person', 22 ), 23 'sv' => array( 24 'from' => 'Från', 25 'person' => 'person', 26 ), 27 'nb' => array( 28 'from' => 'Fra', 29 'person' => 'menneske', 30 ), 31 'nl' => array( 32 'from' => 'Vanaf', 33 'person' => 'persoon', 34 ), 35 'it' => array( 36 'from' => 'Da', 37 'person' => 'persona', 38 ), 39 'fr' => array( 40 'from' => 'À partir de', 41 'person' => 'personne', 42 ), 43 'es' => array( 44 'from' => 'Desde', 45 'person' => 'persona', 46 ), 47 'pt' => array( 48 'from' => 'A partir de', 49 'person' => 'pessoa', 50 ), 51 ); 19 52 20 $utils_translations = [ 21 "da" => [ 22 "from" => "Fra", 23 "person" => "person", 24 "currencySymbol" => "kr" 25 ], 26 "en" => [ 27 "from" => "From", 28 "person" => "guest", 29 "currencySymbol" => "kr" 30 ], 31 "de" => [ 32 "from" => "von", 33 "person" => "Person", 34 "currencySymbol" => "kr" 35 ], 36 "sv" => [ 37 "from" => "Från", 38 "person" => "person", 39 "currencySymbol" => "kr" 40 ], 41 "nb" => [ 42 "from" => "Fra", 43 "person" => "menneske", 44 "currencySymbol" => "kr" 45 ], 46 ]; 47 $card_url_base_prefix = !empty($language) ? '/' . $language . '/experience/' : '/en/experience/'; 48 $currency_symbol = !empty($language) ? $utils_translations[$language]['currencySymbol'] : $utils_translations['en']['currencySymbol']; 49 $price_prefix = !empty($language) ? $utils_translations[$language]['from'] : $utils_translations['en']['from']; 50 $fallback_price_suffix = (!empty($language) ? $utils_translations[$language]['person'] : $utils_translations['en']['person']); 53 public static function render( $company_id, $language = null, $tag_ids = null, $storefront_id = null ) { 54 if ( empty( $language ) ) { 55 $language = \Understory_Settings::get_default_language(); 56 } 51 57 52 $storefront = \Understory_Settings::get_storefront($company_id, $storefront_id); 53 if (empty($storefront)) { 54 return '<p>' . esc_html__('The selected storefront could not be found. Please, check your storefront ID and try again.', 'understory') . '</p>'; 55 } 56 $storefront_fqdn = $storefront['fqdn']; 57 $experience_ids = $storefront['experienceIds']; 58 if ( empty( $storefront_id ) ) { 59 $storefront_id = \Understory_Settings::get_default_storefront( $company_id ); 60 } 58 61 59 // Fetch data from API with storefront filtering 60 $data = ExperienceFetcher::fetch_experiences($company_id, $language, $tag_ids, $experience_ids); 62 $translation = self::$translations[ $language ] ?? self::$translations['en']; 61 63 62 if (empty($data)) { 63 return '';64 } 64 $card_url_base_prefix = ! empty( $language ) ? '/' . $language . '/experience/' : '/en/experience/'; 65 $price_prefix = $translation['from']; 66 $fallback_price_suffix = $translation['person']; 65 67 66 ob_start(); 68 $storefront = \Understory_Settings::get_storefront( $company_id, $storefront_id ); 69 if ( empty( $storefront ) ) { 70 return '<p>' . esc_html__( 'The selected storefront could not be found. Please, check your storefront ID and try again.', 'understory' ) . '</p>'; 71 } 72 $storefront_fqdn = $storefront['fqdn']; 73 $experience_ids = $storefront['experienceIds']; 67 74 68 $root_classnames = ['understory-experiences-widget']; 69 if (count($data) > 2) { 70 $root_classnames[] = 'has-max-three-columns'; 71 } 72 ?> 73 <div class="<?php echo esc_attr(implode(' ', $root_classnames)); ?>" 74 data-company-id="<?php echo esc_attr($company_id); ?>" data-storefront-id="<?php echo esc_attr($storefront_id); ?>" 75 <?php if (!empty($language)): ?> data-language="<?php echo esc_attr($language); ?>" <?php endif; ?> <?php if (!empty($tag_ids)): ?> data-tag-ids="<?php echo esc_attr($tag_ids); ?>" <?php endif; ?>> 76 <?php foreach ($data as $experience): ?> 77 <?php 78 // Sanitize output 79 $href = esc_url($card_url_base_prefix . $experience['id']); 80 $image_url = esc_url($experience['image']); 81 $name = esc_html($experience['name']); 82 $description = self::markdownToText($experience['description']); 83 $price_item = esc_html($experience['price'] . ' ' . $currency_symbol); 84 $price_suffix = '/ ' . (!empty($experience['priceName']) ? esc_html($experience['priceName']) : $fallback_price_suffix); 85 ExperienceCard::render($href, $image_url, $name, $description, $price_prefix, $price_item, strtolower($price_suffix), $storefront_fqdn); 86 ?> 87 <?php endforeach; ?> 88 </div> 89 <?php 90 return ob_get_clean(); 91 } 75 // Fetch data from API with storefront filtering 76 $data = ExperienceFetcher::fetch_experiences( $company_id, $language, $tag_ids, $experience_ids ); 92 77 93 private static function markdownToText($markdown) 94 { 95 $text = preg_replace('/[\r\n]+/', ' ', $markdown); 78 if ( empty( $data ) ) { 79 return ''; 80 } 96 81 97 // Remove markdown characters and backslashes 98 $text = str_replace(['#', '*', '\\'], '', $text); 99 $text = esc_html(trim($text)); 82 ob_start(); 100 83 101 return trim($text); 102 } 84 $root_classnames = array( 'understory-experiences-widget' ); 85 if ( count( $data ) > 2 ) { 86 $root_classnames[] = 'has-max-three-columns'; 87 } 88 ?> 89 <div class="<?php echo esc_attr( implode( ' ', $root_classnames ) ); ?>" 90 data-company-id="<?php echo esc_attr( $company_id ); ?>" data-storefront-id="<?php echo esc_attr( $storefront_id ); ?>" 91 <?php 92 if ( ! empty( $language ) ) : 93 ?> 94 data-language="<?php echo esc_attr( $language ); ?>" <?php endif; ?> 95 <?php 96 if ( ! empty( $tag_ids ) ) : 97 ?> 98 data-tag-ids="<?php echo esc_attr( $tag_ids ); ?>" <?php endif; ?>> 99 <?php foreach ( $data as $experience ) : ?> 100 <?php 101 // Skip experiences without currency to avoid showing wrong prices 102 if ( empty( $experience['currency'] ) ) { 103 continue; 104 } 105 // Sanitize output 106 $href = esc_url( $card_url_base_prefix . $experience['id'] ); 107 $image_url = esc_url( $experience['image'] ); 108 $name = esc_html( $experience['name'] ); 109 $description = self::markdown_to_text( $experience['description'] ); 110 $price_item = esc_html( self::format_price( $experience['price'], $experience['currency'], $language ) ); 111 $price_suffix = '/ ' . ( ! empty( $experience['priceName'] ) ? esc_html( $experience['priceName'] ) : $fallback_price_suffix ); 112 ExperienceCard::render( $href, $image_url, $name, $description, $price_prefix, $price_item, strtolower( $price_suffix ), $storefront_fqdn ); 113 ?> 114 <?php endforeach; ?> 115 </div> 116 <?php 117 return ob_get_clean(); 118 } 119 120 /** 121 * Format a price with the correct currency symbol using Intl NumberFormatter. 122 * 123 * @param int|float $amount The price amount. 124 * @param string $currency The ISO 4217 currency code (e.g. "eur", "dkk"). 125 * @param string $language The language code for locale formatting. 126 * @return string Formatted price string (e.g. "€125", "125 kr"). 127 */ 128 private static function format_price( $amount, $currency, $language ) { 129 $locale = self::language_to_locale( $language ); 130 $currency = strtoupper( $currency ); 131 $formatter = new \NumberFormatter( $locale, \NumberFormatter::CURRENCY ); 132 $formatter->setAttribute( \NumberFormatter::FRACTION_DIGITS, 0 ); 133 $formatted = $formatter->formatCurrency( (float) $amount, $currency ); 134 if ( false !== $formatted ) { 135 return $formatted; 136 } 137 138 return $currency . $amount; 139 } 140 141 /** 142 * Map a language code to a full locale for Intl formatting. 143 */ 144 private static function language_to_locale( $language ) { 145 $locales = array( 146 'da' => 'da_DK', 147 'en' => 'en_US', 148 'de' => 'de_DE', 149 'sv' => 'sv_SE', 150 'nb' => 'nb_NO', 151 'nl' => 'nl_NL', 152 'it' => 'it_IT', 153 'fr' => 'fr_FR', 154 'es' => 'es_ES', 155 'pt' => 'pt_PT', 156 ); 157 return $locales[ $language ] ?? 'en_US'; 158 } 159 160 private static function markdown_to_text( $markdown ) { 161 $text = preg_replace( '/[\r\n]+/', ' ', $markdown ); 162 163 // Remove markdown characters and backslashes 164 $text = str_replace( array( '#', '*', '\\' ), '', $text ); 165 $text = esc_html( trim( $text ) ); 166 167 return trim( $text ); 168 } 103 169 } -
understory/trunk/package-lock.json
r3465724 r3470118 1 1 { 2 2 "name": "understory", 3 "version": "1.8. 1",3 "version": "1.8.2", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "understory", 9 "version": "1.8. 1",9 "version": "1.8.2", 10 10 "dependencies": { 11 11 "@mui/material": "6.4.2", -
understory/trunk/readme.txt
r3465724 r3470118 4 4 Requires at least: 5.0 5 5 Tested up to: 6.8 6 Stable tag: 1.8. 16 Stable tag: 1.8.2 7 7 Requires PHP: 7.0 8 8 License: GPLv2 or later … … 77 77 78 78 == Changelog == 79 80 = 1.8.2 = 81 * Experiences widget: Fix incorrect or missing currency symbols by using the currency from each experience directly. 79 82 80 83 = 1.8.1 = -
understory/trunk/understory.php
r3465724 r3470118 3 3 Plugin Name: Understory 4 4 Description: Connect your WordPress site with Understory, to easily add your booking widget to posts and pages. 5 Version: 1.8. 15 Version: 1.8.2 6 6 Author: Understory 7 7 Text Domain: understory … … 18 18 define('UNDERSTORY_PLUGIN_URL', plugin_dir_url(__FILE__)); 19 19 define('UNDERSTORY_PLUGIN_SLUG', 'understory'); 20 define('UNDERSTORY_PLUGIN_VERSION', '1.8. 1');20 define('UNDERSTORY_PLUGIN_VERSION', '1.8.2'); 21 21 define('UNDERSTORY_OPTION_KEY', 'understory_options'); 22 22 define('UNDERSTORY_NONCE_KEY', 'understory_nonce');
Note: See TracChangeset
for help on using the changeset viewer.