Тема магазина по макету Figma
Каталог, карточка товара, корзина, оформление заказа и блог на WooCommerce. Стили разложены по слоям: токены из Figma, база, компоненты, страницы, брейкпоинты. Цвета, типографика, радиусы и отступы взяты из Variables и текстовых стилей макета, ниже по слоям хардкода нет. Иконки — Phosphor, вставляются инлайном как SVG, символами ничего не рисуется. Стили WooCommerce отключены, вёрстка своя, !important не используется.
This commit is contained in:
commit
29d10ce4cf
72 files changed
+7465
No files matched your search
+163
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
/**
|
||||
* Front-end assets.
|
||||
*
|
||||
* The stylesheet is layered: tokens -> base -> components -> style.css
|
||||
* (layout and pages). Each layer may only use what the layers before it
|
||||
* defined, which is what keeps the cascade free of override chains.
|
||||
*
|
||||
* @package VyatkaShop
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version a theme asset with a short token derived from the file itself.
|
||||
*
|
||||
* Four characters is enough to tell one build of a file from the next, and it
|
||||
* keeps the query string out of the way. Deriving it from the file rather than
|
||||
* randomising it per request is what lets the browser actually cache: the
|
||||
* token only moves when the file does.
|
||||
*
|
||||
* @param string $relative_path Path relative to the theme root.
|
||||
* @return string
|
||||
*/
|
||||
function vyatka_shop_asset_version( string $relative_path ): string {
|
||||
$file = get_template_directory() . '/' . ltrim( $relative_path, '/' );
|
||||
if ( ! is_readable( $file ) ) {
|
||||
return substr( hash( 'crc32b', $relative_path . VYATKA_SHOP_VERSION ), 0, 4 );
|
||||
}
|
||||
|
||||
return substr( hash( 'crc32b', $relative_path . filemtime( $file ) . filesize( $file ) ), 0, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Icons the front-end script has to render on its own (toasts, dismiss
|
||||
* buttons). Sharing them from PHP keeps `assets/icons/*.svg` the single
|
||||
* source of truth.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
function vyatka_shop_script_icons(): array {
|
||||
$icons = array();
|
||||
foreach ( array( 'check-circle-fill', 'x-circle-fill', 'warning-fill', 'info-fill', 'x', 'caret-down' ) as $name ) {
|
||||
$icons[ $name ] = vyatka_shop_get_icon( $name );
|
||||
}
|
||||
|
||||
return $icons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load public assets.
|
||||
*/
|
||||
function vyatka_shop_assets(): void {
|
||||
/*
|
||||
* Order is the cascade. Each layer may only build on the ones before it:
|
||||
* tokens define the vocabulary, base sets element defaults, components own
|
||||
* the controls, the page layers place them, and responsive comes last
|
||||
* because it overrides all of the above.
|
||||
*/
|
||||
$layers = array(
|
||||
'vyatka-tokens' => 'assets/css/tokens.css',
|
||||
'vyatka-base' => 'assets/css/base.css',
|
||||
'vyatka-components' => 'assets/css/components.css',
|
||||
'vyatka-layout' => 'assets/css/layout.css',
|
||||
'vyatka-home' => 'assets/css/home.css',
|
||||
'vyatka-catalog' => 'assets/css/catalog.css',
|
||||
'vyatka-product' => 'assets/css/product.css',
|
||||
'vyatka-blog' => 'assets/css/blog.css',
|
||||
'vyatka-cart' => 'assets/css/cart.css',
|
||||
'vyatka-responsive' => 'assets/css/responsive.css',
|
||||
'vyatka-misc' => 'assets/css/misc.css',
|
||||
);
|
||||
|
||||
$previous = array();
|
||||
foreach ( $layers as $handle => $path ) {
|
||||
wp_enqueue_style(
|
||||
$handle,
|
||||
get_template_directory_uri() . '/' . $path,
|
||||
$previous,
|
||||
vyatka_shop_asset_version( $path )
|
||||
);
|
||||
$previous = array( $handle );
|
||||
}
|
||||
|
||||
// style.css only carries the theme header WordPress reads, but it still has
|
||||
// to be enqueued so child themes can depend on it.
|
||||
wp_enqueue_style(
|
||||
'vyatka-shop',
|
||||
get_stylesheet_uri(),
|
||||
array( 'vyatka-misc' ),
|
||||
vyatka_shop_asset_version( 'style.css' )
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'vyatka-shop',
|
||||
get_template_directory_uri() . '/assets/js/theme.js',
|
||||
array(),
|
||||
vyatka_shop_asset_version( 'assets/js/theme.js' ),
|
||||
true
|
||||
);
|
||||
wp_localize_script(
|
||||
'vyatka-shop',
|
||||
'vyatkaShopCart',
|
||||
array(
|
||||
'ajaxUrl' => '/wp-admin/admin-ajax.php',
|
||||
'nonce' => wp_create_nonce( 'vyatka-cart' ),
|
||||
)
|
||||
);
|
||||
wp_localize_script( 'vyatka-shop', 'vyatkaShopIcons', vyatka_shop_script_icons() );
|
||||
}
|
||||
add_action( 'wp_enqueue_scripts', 'vyatka_shop_assets' );
|
||||
|
||||
/**
|
||||
* Drop WooCommerce's default presentation.
|
||||
*
|
||||
* The theme templates every WooCommerce surface it uses, so the plugin's
|
||||
* stylesheets only add rules that have to be fought with `!important`.
|
||||
* Removing them is what lets the component layer win on its own specificity.
|
||||
*
|
||||
* @param array<string,array<string,string>> $styles Registered WooCommerce styles.
|
||||
* @return array<string,array<string,string>>
|
||||
*/
|
||||
function vyatka_shop_remove_woocommerce_styles( array $styles ): array {
|
||||
unset(
|
||||
$styles['woocommerce-general'],
|
||||
$styles['woocommerce-layout'],
|
||||
$styles['woocommerce-smallscreen']
|
||||
);
|
||||
|
||||
return $styles;
|
||||
}
|
||||
add_filter( 'woocommerce_enqueue_styles', 'vyatka_shop_remove_woocommerce_styles' );
|
||||
|
||||
/**
|
||||
* Remove the block-editor front-end styles WooCommerce and core add for
|
||||
* surfaces this theme renders itself.
|
||||
*
|
||||
* Hooked on `wp_print_styles` rather than `wp_enqueue_scripts`: WooCommerce
|
||||
* Blocks registers its stylesheet from `enqueue_block_assets`, which runs
|
||||
* after the theme's own enqueue pass.
|
||||
*/
|
||||
function vyatka_shop_dequeue_block_styles(): void {
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handles = array(
|
||||
'wc-blocks-style',
|
||||
'wc-blocks-style-all-products',
|
||||
'wc-blocks-packages-style',
|
||||
'wp-block-library-theme',
|
||||
'classic-theme-styles',
|
||||
'global-styles',
|
||||
);
|
||||
|
||||
foreach ( $handles as $handle ) {
|
||||
wp_dequeue_style( $handle );
|
||||
wp_deregister_style( $handle );
|
||||
}
|
||||
}
|
||||
add_action( 'wp_print_styles', 'vyatka_shop_dequeue_block_styles', 100 );
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
/**
|
||||
* Inline SVG icon component.
|
||||
*
|
||||
* Every icon in the design is a Phosphor vector. Icons are stored as real
|
||||
* `.svg` files under `assets/icons/` and inlined into the document so they
|
||||
* inherit `currentColor`, can be sized from CSS and never cost an extra
|
||||
* request. Nothing in the theme may draw an icon with a text glyph.
|
||||
*
|
||||
* @package VyatkaShop
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an icon file and normalise it for inline use.
|
||||
*
|
||||
* @param string $name Icon file name without extension.
|
||||
* @return string Normalised `<svg>` markup, or an empty string when missing.
|
||||
*/
|
||||
function vyatka_shop_icon_source( string $name ): string {
|
||||
static $cache = array();
|
||||
|
||||
if ( isset( $cache[ $name ] ) ) {
|
||||
return $cache[ $name ];
|
||||
}
|
||||
|
||||
$file = get_template_directory() . '/assets/icons/' . $name . '.svg';
|
||||
if ( ! preg_match( '/^[a-z0-9-]+$/', $name ) || ! is_readable( $file ) ) {
|
||||
$cache[ $name ] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
$svg = (string) file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
|
||||
// Sizing belongs to CSS, colour to `currentColor`.
|
||||
$svg = preg_replace( '/\s(?:width|height)="[^"]*"/', '', $svg );
|
||||
$svg = preg_replace( '/\sfill="(?:black|#000(?:000)?)"/i', ' fill="currentColor"', $svg );
|
||||
$svg = preg_replace( '/<svg\b(?![^>]*\bfill=)/', '<svg fill="currentColor"', $svg );
|
||||
|
||||
$cache[ $name ] = trim( (string) $svg );
|
||||
return $cache[ $name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the markup for an inline icon.
|
||||
*
|
||||
* @param string $name Icon name, e.g. `cart`, `check-bold`.
|
||||
* @param array<string,string> $args {
|
||||
* Optional. Presentation options.
|
||||
*
|
||||
* @type string $class Extra class names appended to `icon icon-{name}`.
|
||||
* @type string $label Accessible label. When omitted the icon is hidden
|
||||
* from assistive technology.
|
||||
* @type string $size Pixel size applied inline, for one-off sizes.
|
||||
* }
|
||||
* @return string
|
||||
*/
|
||||
function vyatka_shop_get_icon( string $name, array $args = array() ): string {
|
||||
$svg = vyatka_shop_icon_source( $name );
|
||||
if ( '' === $svg ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$classes = trim( 'icon icon-' . $name . ' ' . ( $args['class'] ?? '' ) );
|
||||
$label = trim( (string) ( $args['label'] ?? '' ) );
|
||||
|
||||
$attributes = sprintf( ' class="%s"', esc_attr( $classes ) );
|
||||
if ( '' !== $label ) {
|
||||
$attributes .= sprintf( ' role="img" aria-label="%s"', esc_attr( $label ) );
|
||||
} else {
|
||||
$attributes .= ' aria-hidden="true" focusable="false"';
|
||||
}
|
||||
if ( ! empty( $args['size'] ) ) {
|
||||
$attributes .= sprintf( ' style="--icon-size:%dpx"', absint( $args['size'] ) );
|
||||
}
|
||||
|
||||
return (string) preg_replace( '/<svg\b/', '<svg' . $attributes, $svg, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Echo an inline icon.
|
||||
*
|
||||
* @param string $name Icon name.
|
||||
* @param array<string,string> $args See vyatka_shop_get_icon().
|
||||
*/
|
||||
function vyatka_shop_icon( string $name, array $args = array() ): void {
|
||||
echo vyatka_shop_get_icon( $name, $args ); // phpcs:ignore WordPress.Security.EscapingOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow inline SVG through `wp_kses_post()` so icons survive filtered output.
|
||||
*
|
||||
* @param array<string,array<string,bool>> $tags Allowed tags.
|
||||
* @return array<string,array<string,bool>>
|
||||
*/
|
||||
function vyatka_shop_allow_inline_svg( array $tags ): array {
|
||||
$shared = array(
|
||||
'class' => true,
|
||||
'style' => true,
|
||||
'fill' => true,
|
||||
'stroke' => true,
|
||||
'aria-hidden' => true,
|
||||
'aria-label' => true,
|
||||
'focusable' => true,
|
||||
'role' => true,
|
||||
);
|
||||
|
||||
$tags['svg'] = $shared + array(
|
||||
'xmlns' => true,
|
||||
'viewbox' => true,
|
||||
'width' => true,
|
||||
'height' => true,
|
||||
);
|
||||
$tags['path'] = $shared + array(
|
||||
'd' => true,
|
||||
'fill-rule' => true,
|
||||
'clip-rule' => true,
|
||||
'stroke-width' => true,
|
||||
'stroke-linecap' => true,
|
||||
'stroke-linejoin' => true,
|
||||
);
|
||||
$tags['g'] = $shared;
|
||||
$tags['circle'] = $shared + array( 'cx' => true, 'cy' => true, 'r' => true );
|
||||
$tags['rect'] = $shared + array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true );
|
||||
$tags['line'] = $shared + array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true );
|
||||
$tags['polyline'] = $shared + array( 'points' => true );
|
||||
$tags['polygon'] = $shared + array( 'points' => true );
|
||||
|
||||
return $tags;
|
||||
}
|
||||
add_filter( 'wp_kses_allowed_html', 'vyatka_shop_allow_inline_svg' );
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
/**
|
||||
* Theme supports, menus and the customizer surface.
|
||||
*
|
||||
* @package VyatkaShop
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure theme features and navigation.
|
||||
*/
|
||||
function vyatka_shop_setup(): void {
|
||||
load_theme_textdomain( 'vyatka-shop', get_template_directory() . '/languages' );
|
||||
add_theme_support( 'title-tag' );
|
||||
add_theme_support( 'post-thumbnails' );
|
||||
add_theme_support( 'html5', array( 'search-form', 'gallery', 'caption', 'style', 'script' ) );
|
||||
add_theme_support(
|
||||
'custom-logo',
|
||||
array(
|
||||
'height' => 100,
|
||||
'width' => 260,
|
||||
'flex-height' => true,
|
||||
'flex-width' => true,
|
||||
)
|
||||
);
|
||||
add_theme_support( 'woocommerce' );
|
||||
add_theme_support( 'wc-product-gallery-lightbox' );
|
||||
add_theme_support( 'wc-product-gallery-slider' );
|
||||
add_image_size( 'vyatka-product', 720, 560, true );
|
||||
add_image_size( 'vyatka-news', 860, 620, true );
|
||||
|
||||
register_nav_menus(
|
||||
array(
|
||||
'primary' => __( 'Главное меню', 'vyatka-shop' ),
|
||||
'footer' => __( 'Меню в подвале', 'vyatka-shop' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
add_action( 'after_setup_theme', 'vyatka_shop_setup' );
|
||||
|
||||
/**
|
||||
* Store options editable from Appearance > Customize.
|
||||
*
|
||||
* @param WP_Customize_Manager $customizer Customizer instance.
|
||||
*/
|
||||
function vyatka_shop_customize_register( WP_Customize_Manager $customizer ): void {
|
||||
$customizer->add_section(
|
||||
'vyatka_home',
|
||||
array(
|
||||
'title' => __( 'Главная страница', 'vyatka-shop' ),
|
||||
'priority' => 30,
|
||||
)
|
||||
);
|
||||
|
||||
$fields = array(
|
||||
'vyatka_hero_title' => array(
|
||||
'label' => __( 'Заголовок', 'vyatka-shop' ),
|
||||
'default' => __( 'Строим тёплые деревянные дома', 'vyatka-shop' ),
|
||||
),
|
||||
'vyatka_hero_text' => array(
|
||||
'label' => __( 'Описание', 'vyatka-shop' ),
|
||||
'default' => __( 'Учитывая ключевые сценарии поведения, существующая теория в значительной степени обусловливает важность дальнейших направлений развития.', 'vyatka-shop' ),
|
||||
),
|
||||
);
|
||||
|
||||
foreach ( $fields as $key => $field ) {
|
||||
$customizer->add_setting(
|
||||
$key,
|
||||
array(
|
||||
'default' => $field['default'],
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
)
|
||||
);
|
||||
$customizer->add_control(
|
||||
$key,
|
||||
array(
|
||||
'label' => $field['label'],
|
||||
'section' => 'vyatka_home',
|
||||
'type' => 'text',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
add_action( 'customize_register', 'vyatka_shop_customize_register' );
|
||||
|
||||
/**
|
||||
* Match the deterministic desktop blog grid from the design.
|
||||
*
|
||||
* @param WP_Query $query Current query.
|
||||
*/
|
||||
function vyatka_shop_blog_query( WP_Query $query ): void {
|
||||
if ( ! is_admin() && $query->is_main_query() && $query->is_home() ) {
|
||||
$query->set( 'posts_per_page', 9 );
|
||||
}
|
||||
}
|
||||
add_action( 'pre_get_posts', 'vyatka_shop_blog_query' );
|
||||
|
||||
/**
|
||||
* Keep the primary navigation labels aligned with the design even when a
|
||||
* previously saved WordPress menu contains an old typo.
|
||||
*
|
||||
* @param array<int,WP_Post> $items Menu items.
|
||||
* @param stdClass $args Menu arguments.
|
||||
* @return array<int,WP_Post>
|
||||
*/
|
||||
function vyatka_shop_primary_menu_labels( array $items, stdClass $args ): array {
|
||||
if ( 'primary' !== ( $args->theme_location ?? '' ) ) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
foreach ( $items as $item ) {
|
||||
$path = (string) wp_parse_url( (string) $item->url, PHP_URL_PATH );
|
||||
if ( '/blog/' === trailingslashit( $path ) ) {
|
||||
$item->title = __( 'Блог', 'vyatka-shop' );
|
||||
}
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
add_filter( 'wp_nav_menu_objects', 'vyatka_shop_primary_menu_labels', 10, 2 );
|
||||
|
||||
/**
|
||||
* Render a fallback primary menu before a menu is assigned.
|
||||
*/
|
||||
function vyatka_shop_fallback_menu(): void {
|
||||
$links = array(
|
||||
home_url( '/' ) => __( 'Главная', 'vyatka-shop' ),
|
||||
home_url( '/catalog/' ) => __( 'Каталог', 'vyatka-shop' ),
|
||||
get_post_type_archive_link( 'post' ) ?: home_url( '/blog/' ) => __( 'Блог', 'vyatka-shop' ),
|
||||
home_url( '/#contacts' ) => __( 'Контакты', 'vyatka-shop' ),
|
||||
);
|
||||
echo '<ul>';
|
||||
foreach ( $links as $url => $label ) {
|
||||
printf(
|
||||
'<li><a href="%1$s">%2$s</a></li>',
|
||||
esc_url( $url ),
|
||||
esc_html( $label )
|
||||
);
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* Reusable render helpers shared by the templates.
|
||||
*
|
||||
* @package VyatkaShop
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the same pagination component for posts and products.
|
||||
*
|
||||
* @param int $total_pages Number of available pages.
|
||||
* @param int $current_page Current page number.
|
||||
*/
|
||||
function vyatka_shop_pagination( int $total_pages, int $current_page = 0 ): void {
|
||||
if ( $total_pages < 2 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current_page = $current_page ?: max( 1, get_query_var( 'paged' ) );
|
||||
?>
|
||||
<nav class="navigation pagination" aria-label="<?php esc_attr_e( 'Навигация по страницам', 'vyatka-shop' ); ?>">
|
||||
<div class="nav-links">
|
||||
<?php if ( $current_page > 1 ) : ?>
|
||||
<a class="prev page-numbers" href="<?php echo esc_url( get_pagenum_link( $current_page - 1 ) ); ?>">
|
||||
<?php vyatka_shop_icon( 'arrow-right-fill', array( 'class' => 'icon-flip icon-20' ) ); ?>
|
||||
<span class="screen-reader-text"><?php esc_html_e( 'Предыдущая страница', 'vyatka-shop' ); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
echo wp_kses_post(
|
||||
paginate_links(
|
||||
array(
|
||||
'current' => $current_page,
|
||||
'total' => $total_pages,
|
||||
'mid_size' => 4,
|
||||
'end_size' => 1,
|
||||
'prev_next' => false,
|
||||
)
|
||||
)
|
||||
);
|
||||
?>
|
||||
<?php if ( $current_page < $total_pages ) : ?>
|
||||
<a class="next page-numbers" href="<?php echo esc_url( get_pagenum_link( $current_page + 1 ) ); ?>">
|
||||
<?php vyatka_shop_icon( 'arrow-right-fill', array( 'class' => 'icon-20' ) ); ?>
|
||||
<span class="screen-reader-text"><?php esc_html_e( 'Следующая страница', 'vyatka-shop' ); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</nav>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render compact breadcrumbs matching the design.
|
||||
*
|
||||
* @param string $current Current page title.
|
||||
*/
|
||||
function vyatka_shop_breadcrumbs( string $current = '' ): void {
|
||||
?>
|
||||
<ol class="breadcrumbs" aria-label="<?php esc_attr_e( 'Хлебные крошки', 'vyatka-shop' ); ?>">
|
||||
<li><a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php esc_html_e( 'Главная', 'vyatka-shop' ); ?></a></li>
|
||||
<?php if ( is_singular( 'product' ) || is_product_category() ) : ?>
|
||||
<li><a href="<?php echo esc_url( home_url( '/catalog/' ) ); ?>"><?php esc_html_e( 'Каталог', 'vyatka-shop' ); ?></a></li>
|
||||
<?php elseif ( is_singular( 'post' ) ) : ?>
|
||||
<li><a href="<?php echo esc_url( get_post_type_archive_link( 'post' ) ?: home_url( '/blog/' ) ); ?>"><?php esc_html_e( 'Блог', 'vyatka-shop' ); ?></a></li>
|
||||
<?php endif; ?>
|
||||
<?php if ( $current ) : ?>
|
||||
<li aria-current="page"><?php echo esc_html( $current ); ?></li>
|
||||
<?php endif; ?>
|
||||
</ol>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a reusable post card.
|
||||
*
|
||||
* @param WP_Post $post Post object.
|
||||
*/
|
||||
function vyatka_shop_news_card( WP_Post $post ): void {
|
||||
$excerpt = get_the_excerpt( $post );
|
||||
?>
|
||||
<a class="news-card" href="<?php echo esc_url( get_permalink( $post ) ); ?>">
|
||||
<h3><?php echo esc_html( get_the_title( $post ) ); ?></h3>
|
||||
<p><?php echo esc_html( wp_trim_words( $excerpt, 15 ) ); ?></p>
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the placeholder wordmark used until a real logo is uploaded.
|
||||
*
|
||||
* The design's logo is set in a condensed display face; shipping that as an
|
||||
* SVG outline keeps it sharp, drops a webfont the rest of the design system
|
||||
* never uses, and matches the rule that every mark on the page is a vector.
|
||||
*/
|
||||
function vyatka_shop_brand_mark(): void {
|
||||
$file = get_template_directory() . '/assets/images/logo.svg';
|
||||
if ( ! is_readable( $file ) ) {
|
||||
printf( '<span>%s</span>', esc_html__( 'Логотип', 'vyatka-shop' ) );
|
||||
return;
|
||||
}
|
||||
|
||||
$svg = (string) file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
echo (string) preg_replace( '/<svg\b/', '<svg class="brand-mark"', trim( $svg ), 1 ); // phpcs:ignore WordPress.Security.EscapingOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cart URL without failing when WooCommerce is not active.
|
||||
*/
|
||||
function vyatka_shop_cart_url(): string {
|
||||
return function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : home_url( '/cart/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return placeholder content for seeded products and posts.
|
||||
*/
|
||||
function vyatka_shop_placeholder_copy(): string {
|
||||
return 'Мы создаём продуманные пространства для жизни: проектируем, подбираем материалы, производим комплектующие и контролируем качество на каждом этапе. Конструкция рассчитана на круглогодичное использование и адаптируется под особенности участка.';
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* URL handling.
|
||||
*
|
||||
* @package VyatkaShop
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build links against the host the visitor actually asked for.
|
||||
*
|
||||
* WordPress stores one canonical home URL, so a site reachable under more than
|
||||
* one name (a bare host and a proxied domain, say) would otherwise send people
|
||||
* back to the stored one mid-session. Only the request host is trusted here,
|
||||
* and only when it matches a host WordPress already knows about, so this cannot
|
||||
* be driven by a forged Host header.
|
||||
*
|
||||
* @param string $url Complete URL generated by WordPress.
|
||||
* @return string
|
||||
*/
|
||||
function vyatka_shop_request_host_url( string $url ): string {
|
||||
$request_host = vyatka_shop_request_host();
|
||||
if ( '' === $request_host || ! vyatka_shop_host_is_known( $request_host ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$parts = wp_parse_url( $url );
|
||||
if ( ! is_array( $parts ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$forwarded_proto = isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] )
|
||||
? strtolower( trim( explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) )[0] ) )
|
||||
: '';
|
||||
$scheme = 'https' === $forwarded_proto || is_ssl() ? 'https' : 'http';
|
||||
|
||||
$rewritten = $scheme . '://' . $request_host;
|
||||
$rewritten .= $parts['path'] ?? '';
|
||||
$rewritten .= isset( $parts['query'] ) ? '?' . $parts['query'] : '';
|
||||
$rewritten .= isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
|
||||
return $rewritten;
|
||||
}
|
||||
add_filter( 'home_url', 'vyatka_shop_request_host_url' );
|
||||
add_filter( 'site_url', 'vyatka_shop_request_host_url' );
|
||||
|
||||
/**
|
||||
* The host from the current request, without its port.
|
||||
*/
|
||||
function vyatka_shop_request_host(): string {
|
||||
if ( ! isset( $_SERVER['HTTP_HOST'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return strtolower( preg_replace( '/:\d+$/', '', sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts this installation answers to: whatever is stored in the options, plus
|
||||
* anything a site owner adds through the filter.
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
function vyatka_shop_known_hosts(): array {
|
||||
$hosts = array();
|
||||
|
||||
foreach ( array( 'home', 'siteurl' ) as $option_name ) {
|
||||
$host = wp_parse_url( (string) get_option( $option_name ), PHP_URL_HOST );
|
||||
if ( is_string( $host ) && '' !== $host ) {
|
||||
$hosts[] = strtolower( $host );
|
||||
}
|
||||
}
|
||||
|
||||
// Sites behind a proxy answer to a name the stored options do not know.
|
||||
// Declare those in wp-config.php:
|
||||
// define( 'VYATKA_SHOP_HOSTS', 'shop.example.com, www.example.com' );
|
||||
if ( defined( 'VYATKA_SHOP_HOSTS' ) ) {
|
||||
foreach ( explode( ',', (string) VYATKA_SHOP_HOSTS ) as $host ) {
|
||||
$host = strtolower( trim( $host ) );
|
||||
if ( '' !== $host ) {
|
||||
$hosts[] = $host;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional hosts the site is served under.
|
||||
*
|
||||
* @param array<int,string> $hosts Known hostnames.
|
||||
*/
|
||||
return array_values( array_unique( (array) apply_filters( 'vyatka_shop_known_hosts', $hosts ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $host Hostname to test.
|
||||
*/
|
||||
function vyatka_shop_host_is_known( string $host ): bool {
|
||||
return in_array( strtolower( $host ), vyatka_shop_known_hosts(), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the origin from internal links in the finished HTML.
|
||||
*
|
||||
* Rewriting only the rendered document leaves redirects, REST responses and
|
||||
* WP-CLI output fully qualified, while the markup a browser receives stays
|
||||
* usable under whichever host served it.
|
||||
*
|
||||
* @param string $html Completed response body.
|
||||
* @return string
|
||||
*/
|
||||
function vyatka_shop_relative_frontend_urls( string $html ): string {
|
||||
foreach ( headers_list() as $header ) {
|
||||
if ( 0 === stripos( $header, 'Content-Type:' ) && false === stripos( $header, 'text/html' ) ) {
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
$hosts = vyatka_shop_known_hosts();
|
||||
$request_host = vyatka_shop_request_host();
|
||||
if ( '' !== $request_host ) {
|
||||
$hosts[] = $request_host;
|
||||
}
|
||||
|
||||
$absolute_bases = array();
|
||||
foreach ( array_unique( $hosts ) as $host ) {
|
||||
$absolute_bases[] = 'https://' . $host;
|
||||
$absolute_bases[] = 'http://' . $host;
|
||||
$absolute_bases[] = '//' . $host;
|
||||
}
|
||||
// Longest first, so "//host" never truncates "https://host".
|
||||
usort( $absolute_bases, static fn( string $left, string $right ): int => strlen( $right ) <=> strlen( $left ) );
|
||||
|
||||
return str_replace( $absolute_bases, '', $html );
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer ordinary frontend documents so their internal links can be made
|
||||
* relative without changing the site's canonical configuration.
|
||||
*/
|
||||
function vyatka_shop_start_relative_url_buffer(): void {
|
||||
if ( is_admin() || wp_doing_ajax() || wp_is_json_request() || is_feed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
ob_start( 'vyatka_shop_relative_frontend_urls' );
|
||||
}
|
||||
add_action( 'template_redirect', 'vyatka_shop_start_relative_url_buffer', 0 );
|
||||
|
||||
/**
|
||||
* Recover stale filtered category URLs when the selected page no longer exists.
|
||||
*
|
||||
* Filter forms always target page one, but bookmarked or cached /page/N/ URLs
|
||||
* can still reach WordPress directly and become a 404 after the result set is
|
||||
* narrowed. Preserve their query string and retry the category from page one.
|
||||
*/
|
||||
function vyatka_shop_redirect_stale_filtered_category_page(): void {
|
||||
if ( ! is_404() || empty( $_GET ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filter_keys = array(
|
||||
'filter_min_price',
|
||||
'filter_max_price',
|
||||
'feature',
|
||||
'collection',
|
||||
'per_page',
|
||||
'orderby',
|
||||
);
|
||||
|
||||
if ( ! array_intersect( $filter_keys, array_keys( $_GET ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
$request_path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
||||
if ( ! is_string( $request_path ) || ! preg_match( '#^(.*/product-category/[^/]+)/page/[1-9][0-9]*/?$#u', $request_path, $matches ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = add_query_arg( wp_unslash( $_GET ), trailingslashit( $matches[1] ) );
|
||||
wp_safe_redirect( $target, 302, 'Vyatka Shop' );
|
||||
exit;
|
||||
}
|
||||
add_action( 'template_redirect', 'vyatka_shop_redirect_stale_filtered_category_page', -10 );
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce integration: catalog behaviour, checkout fields and the
|
||||
* in-place cart endpoint.
|
||||
*
|
||||
* @package VyatkaShop
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove WooCommerce wrappers in favor of semantic theme templates.
|
||||
*/
|
||||
function vyatka_shop_woocommerce_hooks(): void {
|
||||
remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
|
||||
remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 );
|
||||
remove_action( 'woocommerce_sidebar', 'woocommerce_get_sidebar', 10 );
|
||||
}
|
||||
add_action( 'wp', 'vyatka_shop_woocommerce_hooks' );
|
||||
|
||||
/**
|
||||
* Respect the product-count control used by the archive toolbar.
|
||||
*/
|
||||
function vyatka_shop_products_per_page(): int {
|
||||
$requested = isset( $_GET['per_page'] ) ? absint( wp_unslash( $_GET['per_page'] ) ) : 25;
|
||||
return in_array( $requested, array( 12, 25, 50 ), true ) ? $requested : 25;
|
||||
}
|
||||
add_filter( 'loop_shop_per_page', 'vyatka_shop_products_per_page', 20 );
|
||||
|
||||
/**
|
||||
* Return the deterministic demo facets rendered by product cards.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
* @return array{feature:string,collection:string}
|
||||
*/
|
||||
function vyatka_shop_product_facets( int $product_id ): array {
|
||||
$features = array( 'conifer', 'maple', 'birch', 'sea-buckthorn', 'rowan', 'blueberry', 'lingonberry' );
|
||||
$collections = array( 'pine', 'linden', 'aspen', 'juniper', 'cranberry', 'blackberry', 'raspberry' );
|
||||
|
||||
return array(
|
||||
'feature' => $features[ $product_id % count( $features ) ],
|
||||
'collection' => $collections[ $product_id % count( $collections ) ],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the visible demo facets to the main product query so filtering,
|
||||
* sorting and pagination all operate on the same result set.
|
||||
*
|
||||
* @param WP_Query $query Main archive query.
|
||||
*/
|
||||
function vyatka_shop_filter_product_archive( WP_Query $query ): void {
|
||||
static $collecting_products = false;
|
||||
|
||||
if (
|
||||
$collecting_products ||
|
||||
is_admin() ||
|
||||
! $query->is_main_query() ||
|
||||
( ! $query->is_post_type_archive( 'product' ) && ! $query->is_tax( 'product_cat' ) )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$selected_features = isset( $_GET['feature'] )
|
||||
? array_values( array_diff( array_map( 'sanitize_key', (array) wp_unslash( $_GET['feature'] ) ), array( 'all', '' ) ) )
|
||||
: array();
|
||||
$selected_collection = isset( $_GET['collection'] ) ? sanitize_key( wp_unslash( $_GET['collection'] ) ) : '';
|
||||
$price_active = isset( $_GET['filter_min_price'], $_GET['filter_max_price'] );
|
||||
|
||||
if ( ! $selected_features && ! $selected_collection && ! $price_active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$minimum_price = $price_active ? max( 0, absint( wp_unslash( $_GET['filter_min_price'] ) ) ) : 0;
|
||||
$maximum_price = $price_active ? max( $minimum_price, absint( wp_unslash( $_GET['filter_max_price'] ) ) ) : PHP_INT_MAX;
|
||||
|
||||
$collecting_products = true;
|
||||
$product_ids = get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'fields' => 'ids',
|
||||
'posts_per_page' => -1,
|
||||
'no_found_rows' => true,
|
||||
'suppress_filters' => true,
|
||||
)
|
||||
);
|
||||
$collecting_products = false;
|
||||
|
||||
if ( $query->is_tax( 'product_cat' ) ) {
|
||||
$term = get_queried_object();
|
||||
if ( $term instanceof WP_Term ) {
|
||||
$product_ids = array_values(
|
||||
array_filter(
|
||||
$product_ids,
|
||||
static fn( int $product_id ): bool => has_term( $term->term_id, 'product_cat', $product_id )
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$matching_ids = array_values(
|
||||
array_filter(
|
||||
$product_ids,
|
||||
static function ( int $product_id ) use ( $selected_features, $selected_collection, $price_active, $minimum_price, $maximum_price ): bool {
|
||||
$facets = vyatka_shop_product_facets( $product_id );
|
||||
$price = (float) get_post_meta( $product_id, '_price', true );
|
||||
|
||||
return ( ! $selected_features || in_array( $facets['feature'], $selected_features, true ) )
|
||||
&& ( ! $selected_collection || $facets['collection'] === $selected_collection )
|
||||
&& ( ! $price_active || ( $price >= $minimum_price && $price <= $maximum_price ) );
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
$query->set( 'post__in', $matching_ids ?: array( 0 ) );
|
||||
$query->set( 'paged', max( 1, absint( $query->get( 'paged' ) ) ) );
|
||||
}
|
||||
add_action( 'pre_get_posts', 'vyatka_shop_filter_product_archive', 25 );
|
||||
|
||||
/**
|
||||
* Use the wording shown in the design for the default product order.
|
||||
*
|
||||
* @param array<string,string> $options Ordering labels.
|
||||
* @return array<string,string>
|
||||
*/
|
||||
function vyatka_shop_catalog_orderby( array $options ): array {
|
||||
if ( isset( $options['menu_order'] ) ) {
|
||||
$options['menu_order'] = __( 'По умолчанию', 'vyatka-shop' );
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
add_filter( 'woocommerce_catalog_orderby', 'vyatka_shop_catalog_orderby' );
|
||||
|
||||
/**
|
||||
* Adding a product is handled in place by the theme's AJAX control. Keeping
|
||||
* WooCommerce's redirect option disabled also prevents it from queueing a
|
||||
* second success message for the next page load.
|
||||
*/
|
||||
function vyatka_shop_disable_add_to_cart_redirect(): string {
|
||||
return 'no';
|
||||
}
|
||||
add_filter( 'pre_option_woocommerce_cart_redirect_after_add', 'vyatka_shop_disable_add_to_cart_redirect' );
|
||||
|
||||
/**
|
||||
* Success feedback is rendered by the client-side toast. Remove legacy
|
||||
* success notices left in the WooCommerce session before any frontend
|
||||
* template can print them into the document flow.
|
||||
*/
|
||||
function vyatka_shop_clear_legacy_success_notices(): void {
|
||||
if ( is_admin() || wp_doing_ajax() || ! function_exists( 'wc_get_notices' ) || ! function_exists( 'wc_set_notices' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notices = wc_get_notices();
|
||||
if ( ! isset( $notices['success'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset( $notices['success'] );
|
||||
wc_set_notices( $notices );
|
||||
}
|
||||
add_action( 'wp', 'vyatka_shop_clear_legacy_success_notices', 20 );
|
||||
|
||||
/**
|
||||
* Calculate regular, current and sale-discount totals from one shared source.
|
||||
*
|
||||
* @param WC_Cart|null $cart Cart to summarize. Defaults to the active cart.
|
||||
* @return array{regular_total: float, current_total: float, discount_total: float}
|
||||
*/
|
||||
function vyatka_shop_cart_price_summary( ?WC_Cart $cart = null ): array {
|
||||
$cart = $cart ?? WC()->cart;
|
||||
$regular_total = 0.0;
|
||||
$current_total = 0.0;
|
||||
|
||||
if ( ! $cart ) {
|
||||
return array(
|
||||
'regular_total' => 0.0,
|
||||
'current_total' => 0.0,
|
||||
'discount_total' => 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $cart->get_cart() as $cart_item ) {
|
||||
$product = $cart_item['data'] ?? null;
|
||||
$quantity = isset( $cart_item['quantity'] ) ? max( 0, (int) $cart_item['quantity'] ) : 0;
|
||||
|
||||
if ( ! $product instanceof WC_Product || ! $product->exists() || 0 === $quantity ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$current_price = max( 0.0, (float) $product->get_price() );
|
||||
$regular_price = (float) $product->get_regular_price();
|
||||
if ( $regular_price <= 0.0 ) {
|
||||
$regular_price = $current_price;
|
||||
}
|
||||
|
||||
$regular_total += $regular_price * $quantity;
|
||||
$current_total += $current_price * $quantity;
|
||||
}
|
||||
|
||||
return array(
|
||||
'regular_total' => $regular_total,
|
||||
'current_total' => $current_total,
|
||||
'discount_total' => max( 0.0, $regular_total - $current_total ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the current cart state for an AJAX response.
|
||||
*/
|
||||
function vyatka_shop_render_cart_fragment(): string {
|
||||
ob_start();
|
||||
if ( WC()->cart->is_empty() ) {
|
||||
wc_get_template( 'cart/cart-empty.php' );
|
||||
} else {
|
||||
wc_get_template( 'cart/cart.php' );
|
||||
}
|
||||
$html = (string) ob_get_clean();
|
||||
|
||||
return vyatka_shop_relative_frontend_urls( $html );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutate the WooCommerce cart without navigating away from the current page.
|
||||
*/
|
||||
function vyatka_shop_ajax_cart_mutation(): void {
|
||||
check_ajax_referer( 'vyatka-cart', 'nonce' );
|
||||
|
||||
if ( ! function_exists( 'WC' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Корзина недоступна.', 'vyatka-shop' ) ), 503 );
|
||||
}
|
||||
if ( null === WC()->cart && function_exists( 'wc_load_cart' ) ) {
|
||||
wc_load_cart();
|
||||
}
|
||||
if ( null === WC()->cart ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Корзина недоступна.', 'vyatka-shop' ) ), 503 );
|
||||
}
|
||||
|
||||
$operation = isset( $_POST['operation'] ) ? sanitize_key( wp_unslash( $_POST['operation'] ) ) : '';
|
||||
|
||||
if ( 'add' === $operation ) {
|
||||
$product_id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
|
||||
$quantity = isset( $_POST['quantity'] ) ? max( 1, absint( wp_unslash( $_POST['quantity'] ) ) ) : 1;
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product || ! $product->is_purchasable() || ! $product->is_in_stock() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Товар нельзя добавить в корзину.', 'vyatka-shop' ) ), 400 );
|
||||
}
|
||||
|
||||
$maximum = $product->get_max_purchase_quantity();
|
||||
if ( 0 < $maximum ) {
|
||||
$quantity = min( $quantity, $maximum );
|
||||
}
|
||||
|
||||
$existing_key = '';
|
||||
foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
|
||||
if ( $product_id === (int) $cart_item['product_id'] && empty( $cart_item['variation_id'] ) ) {
|
||||
$existing_key = $cart_item_key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$changed = '' !== $existing_key
|
||||
? WC()->cart->set_quantity( $existing_key, $quantity, false )
|
||||
: WC()->cart->add_to_cart( $product_id, $quantity );
|
||||
if ( false === $changed ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Не удалось добавить товар в корзину.', 'vyatka-shop' ) ), 400 );
|
||||
}
|
||||
} elseif ( 'update' === $operation ) {
|
||||
$cart_item_key = isset( $_POST['cart_item_key'] )
|
||||
? wc_clean( wp_unslash( $_POST['cart_item_key'] ) )
|
||||
: '';
|
||||
$quantity = isset( $_POST['quantity'] ) ? max( 1, absint( wp_unslash( $_POST['quantity'] ) ) ) : 1;
|
||||
$cart_item = WC()->cart->get_cart_item( $cart_item_key );
|
||||
|
||||
if ( ! $cart_item ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Товар в корзине не найден.', 'vyatka-shop' ) ), 404 );
|
||||
}
|
||||
|
||||
$product = $cart_item['data'];
|
||||
$maximum = $product instanceof WC_Product ? $product->get_max_purchase_quantity() : -1;
|
||||
if ( 0 < $maximum ) {
|
||||
$quantity = min( $quantity, $maximum );
|
||||
}
|
||||
if ( false === WC()->cart->set_quantity( $cart_item_key, $quantity, false ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Не удалось изменить количество.', 'vyatka-shop' ) ), 400 );
|
||||
}
|
||||
} elseif ( 'remove' === $operation ) {
|
||||
$cart_item_key = isset( $_POST['cart_item_key'] )
|
||||
? wc_clean( wp_unslash( $_POST['cart_item_key'] ) )
|
||||
: '';
|
||||
if ( '' === $cart_item_key || false === WC()->cart->remove_cart_item( $cart_item_key ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Не удалось удалить товар.', 'vyatka-shop' ) ), 400 );
|
||||
}
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Неизвестная операция с корзиной.', 'vyatka-shop' ) ), 400 );
|
||||
}
|
||||
|
||||
WC()->cart->calculate_totals();
|
||||
WC()->cart->set_session();
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'cartCount' => WC()->cart->get_cart_contents_count(),
|
||||
'html' => vyatka_shop_render_cart_fragment(),
|
||||
)
|
||||
);
|
||||
}
|
||||
add_action( 'wp_ajax_vyatka_cart_mutation', 'vyatka_shop_ajax_cart_mutation' );
|
||||
add_action( 'wp_ajax_nopriv_vyatka_cart_mutation', 'vyatka_shop_ajax_cart_mutation' );
|
||||
|
||||
/**
|
||||
* Keep only the three customer fields present in the checkout design.
|
||||
*
|
||||
* @param array<string,array<string,array<string,mixed>>> $fields Checkout fields.
|
||||
* @return array<string,array<string,array<string,mixed>>>
|
||||
*/
|
||||
function vyatka_shop_checkout_fields( array $fields ): array {
|
||||
// WooCommerce prefixes billing labels with "Выставление счёта", which turns
|
||||
// a validation message into "Выставление счёта Имя — обязательное поле".
|
||||
// The design labels these fields plainly, and the shorter label is also
|
||||
// what the inline field errors match on.
|
||||
$labels = array(
|
||||
'billing_first_name' => __( 'Имя', 'vyatka-shop' ),
|
||||
'billing_phone' => __( 'Телефон', 'vyatka-shop' ),
|
||||
'billing_email' => __( 'Email', 'vyatka-shop' ),
|
||||
);
|
||||
|
||||
$billing = array();
|
||||
foreach ( $labels as $key => $label ) {
|
||||
if ( isset( $fields['billing'][ $key ] ) ) {
|
||||
$billing[ $key ] = $fields['billing'][ $key ];
|
||||
$billing[ $key ]['required'] = true;
|
||||
$billing[ $key ]['label'] = $label;
|
||||
}
|
||||
}
|
||||
$fields['billing'] = $billing;
|
||||
$fields['shipping'] = array();
|
||||
$fields['order'] = array();
|
||||
return $fields;
|
||||
}
|
||||
add_filter( 'woocommerce_checkout_fields', 'vyatka_shop_checkout_fields' );
|
||||
|
||||
/**
|
||||
* Validate the delivery-only address field exposed by the checkout switch.
|
||||
*/
|
||||
function vyatka_shop_validate_delivery_fields(): void {
|
||||
$delivery_type = isset( $_POST['delivery_type'] ) ? sanitize_key( wp_unslash( $_POST['delivery_type'] ) ) : 'pickup';
|
||||
$address = isset( $_POST['delivery_address'] ) ? sanitize_text_field( wp_unslash( $_POST['delivery_address'] ) ) : '';
|
||||
|
||||
if ( 'delivery' === $delivery_type && '' === $address ) {
|
||||
wc_add_notice( __( 'Укажите адрес доставки.', 'vyatka-shop' ), 'error' );
|
||||
}
|
||||
}
|
||||
add_action( 'woocommerce_checkout_process', 'vyatka_shop_validate_delivery_fields' );
|
||||
|
||||
/**
|
||||
* Store custom delivery controls with the order.
|
||||
*
|
||||
* @param WC_Order $order Order being created.
|
||||
*/
|
||||
function vyatka_shop_save_delivery_fields( WC_Order $order ): void {
|
||||
$fields = array( 'delivery_type', 'pickup_address', 'delivery_address', 'delivery_unit' );
|
||||
foreach ( $fields as $field ) {
|
||||
if ( isset( $_POST[ $field ] ) ) {
|
||||
$order->update_meta_data( '_' . $field, sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action( 'woocommerce_checkout_create_order', 'vyatka_shop_save_delivery_fields' );
|
||||
Reference in new issue
Block a user