Plugin Directory

Changeset 3470118


Ignore:
Timestamp:
02/26/2026 10:04:37 AM (5 weeks ago)
Author:
holdbar
Message:

Update to version 1.8.2 from GitHub

Location:
understory
Files:
12 edited
1 copied

Legend:

Unmodified
Added
Removed
  • understory/tags/1.8.2/CLAUDE.md

    r3465724 r3470118  
    204204### Version Management
    205205
    206 Current version: 1.8.1 (defined in both `package.json` and `understory.php`)
     206Current 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  
    33namespace Understory\Utils;
    44
    5 if (!defined('ABSPATH')) {
    6     exit; // Exit if accessed directly.
     5if ( ! defined( 'ABSPATH' ) ) {
     6    exit; // Exit if accessed directly.
    77}
    88
    9 class CompanyDataUpdater
    10 {
    11     public static function update($company_id)
    12     {
    13         $options = get_option(UNDERSTORY_OPTION_KEY);
     9class CompanyDataUpdater {
    1410
    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 );
    2013
    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        }
    2819
    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    }
    3231}
  • understory/tags/1.8.2/includes/utils/class-experiences.php

    r3369591 r3470118  
    33namespace Understory\Utils;
    44
    5 if (!defined('ABSPATH')) {
    6     exit; // Exit if accessed directly.
     5if ( ! defined( 'ABSPATH' ) ) {
     6    exit; // Exit if accessed directly.
    77}
    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         }
     8class Experiences {
    159
    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    );
    1952
    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        }
    5157
    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        }
    5861
    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'];
    6163
    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'];
    6567
    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'];
    6774
    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 );
    9277
    93     private static function markdownToText($markdown)
    94     {
    95         $text = preg_replace('/[\r\n]+/', ' ', $markdown);
     78        if ( empty( $data ) ) {
     79            return '';
     80        }
    9681
    97         // Remove markdown characters and backslashes
    98         $text = str_replace(['#', '*', '\\'], '', $text);
    99         $text = esc_html(trim($text));
     82        ob_start();
    10083
    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    }
    103169}
  • understory/tags/1.8.2/package-lock.json

    r3465724 r3470118  
    11{
    22  "name": "understory",
    3   "version": "1.8.1",
     3  "version": "1.8.2",
    44  "lockfileVersion": 3,
    55  "requires": true,
     
    77    "": {
    88      "name": "understory",
    9       "version": "1.8.1",
     9      "version": "1.8.2",
    1010      "dependencies": {
    1111        "@mui/material": "6.4.2",
  • understory/tags/1.8.2/readme.txt

    r3465724 r3470118  
    44Requires at least: 5.0
    55Tested up to: 6.8
    6 Stable tag: 1.8.1
     6Stable tag: 1.8.2
    77Requires PHP: 7.0
    88License: GPLv2 or later
     
    7777
    7878== Changelog ==
     79
     80= 1.8.2 =
     81* Experiences widget: Fix incorrect or missing currency symbols by using the currency from each experience directly.
    7982
    8083= 1.8.1 =
  • understory/tags/1.8.2/understory.php

    r3465724 r3470118  
    33Plugin Name: Understory
    44Description: Connect your WordPress site with Understory, to easily add your booking widget to posts and pages.
    5 Version: 1.8.1
     5Version: 1.8.2
    66Author: Understory
    77Text Domain: understory
     
    1818define('UNDERSTORY_PLUGIN_URL', plugin_dir_url(__FILE__));
    1919define('UNDERSTORY_PLUGIN_SLUG', 'understory');
    20 define('UNDERSTORY_PLUGIN_VERSION', '1.8.1');
     20define('UNDERSTORY_PLUGIN_VERSION', '1.8.2');
    2121define('UNDERSTORY_OPTION_KEY', 'understory_options');
    2222define('UNDERSTORY_NONCE_KEY', 'understory_nonce');
  • understory/trunk/CLAUDE.md

    r3465724 r3470118  
    204204### Version Management
    205205
    206 Current version: 1.8.1 (defined in both `package.json` and `understory.php`)
     206Current version: 1.8.2 (defined in both `package.json` and `understory.php`)
  • understory/trunk/includes/utils/class-company-data-updater.php

    r3351241 r3470118  
    33namespace Understory\Utils;
    44
    5 if (!defined('ABSPATH')) {
    6     exit; // Exit if accessed directly.
     5if ( ! defined( 'ABSPATH' ) ) {
     6    exit; // Exit if accessed directly.
    77}
    88
    9 class CompanyDataUpdater
    10 {
    11     public static function update($company_id)
    12     {
    13         $options = get_option(UNDERSTORY_OPTION_KEY);
     9class CompanyDataUpdater {
    1410
    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 );
    2013
    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        }
    2819
    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    }
    3231}
  • understory/trunk/includes/utils/class-experiences.php

    r3369591 r3470118  
    33namespace Understory\Utils;
    44
    5 if (!defined('ABSPATH')) {
    6     exit; // Exit if accessed directly.
     5if ( ! defined( 'ABSPATH' ) ) {
     6    exit; // Exit if accessed directly.
    77}
    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         }
     8class Experiences {
    159
    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    );
    1952
    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        }
    5157
    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        }
    5861
    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'];
    6163
    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'];
    6567
    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'];
    6774
    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 );
    9277
    93     private static function markdownToText($markdown)
    94     {
    95         $text = preg_replace('/[\r\n]+/', ' ', $markdown);
     78        if ( empty( $data ) ) {
     79            return '';
     80        }
    9681
    97         // Remove markdown characters and backslashes
    98         $text = str_replace(['#', '*', '\\'], '', $text);
    99         $text = esc_html(trim($text));
     82        ob_start();
    10083
    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    }
    103169}
  • understory/trunk/package-lock.json

    r3465724 r3470118  
    11{
    22  "name": "understory",
    3   "version": "1.8.1",
     3  "version": "1.8.2",
    44  "lockfileVersion": 3,
    55  "requires": true,
     
    77    "": {
    88      "name": "understory",
    9       "version": "1.8.1",
     9      "version": "1.8.2",
    1010      "dependencies": {
    1111        "@mui/material": "6.4.2",
  • understory/trunk/readme.txt

    r3465724 r3470118  
    44Requires at least: 5.0
    55Tested up to: 6.8
    6 Stable tag: 1.8.1
     6Stable tag: 1.8.2
    77Requires PHP: 7.0
    88License: GPLv2 or later
     
    7777
    7878== Changelog ==
     79
     80= 1.8.2 =
     81* Experiences widget: Fix incorrect or missing currency symbols by using the currency from each experience directly.
    7982
    8083= 1.8.1 =
  • understory/trunk/understory.php

    r3465724 r3470118  
    33Plugin Name: Understory
    44Description: Connect your WordPress site with Understory, to easily add your booking widget to posts and pages.
    5 Version: 1.8.1
     5Version: 1.8.2
    66Author: Understory
    77Text Domain: understory
     
    1818define('UNDERSTORY_PLUGIN_URL', plugin_dir_url(__FILE__));
    1919define('UNDERSTORY_PLUGIN_SLUG', 'understory');
    20 define('UNDERSTORY_PLUGIN_VERSION', '1.8.1');
     20define('UNDERSTORY_PLUGIN_VERSION', '1.8.2');
    2121define('UNDERSTORY_OPTION_KEY', 'understory_options');
    2222define('UNDERSTORY_NONCE_KEY', 'understory_nonce');
Note: See TracChangeset for help on using the changeset viewer.