post_content, 'advk_calendar' )
|| has_block( 'adventura/calendar', $post );
if ( ! $has ) {
return;
}
$opts = ADVK_Settings::get_options();
$title = trim( (string) $opts['title_line1'] . ' ' . (string) $opts['title_line2'] );
if ( '' === $title ) {
$title = get_the_title( $post );
}
$desc = (string) $opts['subtitle'];
$image = '';
if ( ! empty( $opts['share_image_id'] ) ) {
$image = (string) wp_get_attachment_image_url( (int) $opts['share_image_id'], 'large' );
}
if ( '' === $image && ! empty( $opts['share_image_url'] ) ) {
$image = (string) $opts['share_image_url']; // legacy fallback
}
if ( '' === $image && function_exists( 'get_site_icon_url' ) ) {
$image = get_site_icon_url( 512 );
}
$url = get_permalink( $post );
echo "\n";
printf( '%s', "\n" );
printf( '%s', esc_attr( $title ), "\n" );
if ( '' !== $desc ) {
printf( '%s', esc_attr( $desc ), "\n" );
}
printf( '%s', esc_url( $url ), "\n" );
if ( '' !== $image ) {
printf( '%s', esc_url( $image ), "\n" );
}
printf( '%s', '' !== $image ? 'summary_large_image' : 'summary', "\n" );
}
/**
* `[advk_winners]` — a public, privacy-friendly winners list. Shows the day,
* the winner's first name + last initial, and the prize. Attributes: `day`
* (0 = all), `limit` (0 = no limit).
*
* @param array $atts Shortcode attributes.
* @return string
*/
public function render_winners( $atts ) {
$atts = shortcode_atts(
array( 'day' => 0, 'limit' => 0 ),
$atts,
'advk_winners'
);
$day = max( 0, min( 24, (int) $atts['day'] ) );
$limit = max( 0, (int) $atts['limit'] );
$winners = ADVK_Entries::query(
array(
'per_page' => $limit > 0 ? $limit : 100000,
'offset' => 0,
'day' => $day,
'winners_only' => true,
'orderby' => 'day',
'order' => 'ASC',
)
);
$opts = ADVK_Settings::get_options();
$theme = in_array( $opts['theme'], array( 'beere', 'tanne', 'amber', 'winter', 'halloween', 'ostern', 'custom' ), true ) ? $opts['theme'] : 'beere';
$vars = $this->layout_css_vars( $opts );
if ( 'custom' === $theme ) {
$color_vars = $this->custom_css_vars( $opts );
if ( '' !== $color_vars ) {
$vars .= ';' . $color_vars;
}
}
$style_attr = '' !== $vars ? ' style="' . esc_attr( $vars ) . '"' : '';
wp_enqueue_style( 'advk-frontend' );
ob_start();
echo '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- attrs escaped above.
echo '
';
echo '
' . esc_html__( 'Unsere Gewinner', 'adventura-interactive-advent-calendar' ) . '
';
if ( empty( $winners ) ) {
echo '
' . esc_html__( 'Die Gewinner werden hier bekannt gegeben, sobald die Auslosung erfolgt ist.', 'adventura-interactive-advent-calendar' ) . '
';
} else {
echo '
';
foreach ( $winners as $w ) {
$name = trim( (string) $w->first_name );
$last = trim( (string) $w->last_name );
if ( '' !== $last ) {
// Default: privacy-friendly "Firstname L.". Optionally the full name.
$name .= ' ' . ( ! empty( $opts['winners_full_name'] ) ? $last : mb_substr( $last, 0, 1 ) . '.' );
}
$prize = ADVK_Winners::prize_for_winner( (int) $w->day, (int) $w->id );
echo '- ';
echo '' . esc_html( trim( (string) $opts['item_singular'] . ' ' . (int) $w->day ) ) . '';
echo '' . esc_html( $name ) . '';
if ( '' !== $prize ) {
echo '' . esc_html( $prize ) . '';
}
echo '
';
}
echo '
';
}
echo '
';
return ob_get_clean();
}
public function register_assets() {
// Fonts are bundled locally (no Google CDN request) for DSGVO /
// WordPress.org compliance.
wp_register_style( 'advk-fonts', ADVK_URL . 'assets/css/fonts.css', array(), ADVK_VERSION );
wp_register_style( 'advk-frontend', ADVK_URL . 'assets/css/adventura.css', array( 'advk-fonts' ), ADVK_VERSION );
wp_register_script( 'advk-frontend', ADVK_URL . 'assets/js/adventura.js', array(), ADVK_VERSION, true );
}
/**
* Build the inline CSS-variable overrides for the "custom" theme.
*
* @param array $opts Plugin options.
* @return string Inline CSS (without the surrounding selector).
*/
private function custom_css_vars( $opts ) {
$map = array(
'--bg' => $opts['custom_bg'],
'--bg-2' => $opts['custom_bg'],
'--ink' => $opts['custom_ink'],
'--accent' => $opts['custom_accent'],
'--accent-2' => $opts['custom_accent2'],
);
$parts = array();
foreach ( $map as $var => $val ) {
$hex = sanitize_hex_color( (string) $val );
if ( $hex ) {
$parts[] = $var . ':' . $hex;
}
}
return implode( ';', $parts );
}
/**
* Layout CSS custom properties (columns per row + calendar max-width) that
* apply to every theme, not just the custom palette. Rendered inline on the
* .advk-app wrapper so the frontend CSS can pick them up.
*
* @param array $opts Plugin options.
* @return string Inline CSS (without the surrounding selector).
*/
private function layout_css_vars( $opts ) {
$cols = (int) ( $opts['grid_columns'] ?? 6 );
if ( ! in_array( $cols, array( 3, 4, 6, 8 ), true ) ) {
$cols = 6;
}
// Fewer columns on small screens so tiles never get too tiny.
$cols_m = min( $cols, 4 );
$widths = array(
'compact' => '820px',
'normal' => '1060px',
'wide' => '1320px',
'full' => 'none',
);
$wkey = isset( $opts['calendar_width'] ) ? (string) $opts['calendar_width'] : 'normal';
$max = isset( $widths[ $wkey ] ) ? $widths[ $wkey ] : $widths['normal'];
return '--advk-cols:' . $cols . ';--advk-cols-m:' . $cols_m . ';--advk-shell-max:' . $max;
}
/**
* Prepare an admin-authored rich-text field for display in the frontend.
* The content is saved with wp_kses_post (only safe HTML), so here we just
* turn blank lines into paragraphs (wpautop) and re-sanitise defensively.
* The JS renders the result as HTML, so sponsors/prizes/donation copy may
* use standard formatting (paragraphs, links, bold, lists …).
*
* @param string $raw Stored text (may contain HTML or plain line breaks).
* @return string Display-ready HTML, or '' when empty.
*/
public static function rich_text( $raw ) {
$raw = (string) $raw;
if ( '' === trim( $raw ) ) {
return '';
}
return wp_kses_post( wpautop( $raw ) );
}
/**
* Strip the hidden `correct` index from quiz data before it reaches the
* browser — participants never see which answer is right.
*
* @param array $doors Calendar doors.
* @return array
*/
private function public_doors( $doors ) {
$out = array();
foreach ( $doors as $d ) {
$quiz = array();
foreach ( (array) $d['quiz'] as $q ) {
$quiz[] = array(
'q' => $q['q'],
'options' => array_values( $q['options'] ),
);
}
// Only expose URLs for channels actually offered on this door.
$offered = array_values( $d['social'] );
$social_urls = array();
$all_urls = isset( $d['socialUrls'] ) && is_array( $d['socialUrls'] ) ? $d['socialUrls'] : array();
foreach ( $offered as $skey ) {
if ( ! empty( $all_urls[ $skey ] ) ) {
$social_urls[ $skey ] = esc_url_raw( $all_urls[ $skey ] );
}
}
// Prizes are public display data; expose the whole list.
$prizes = array();
if ( isset( $d['prizes'] ) && is_array( $d['prizes'] ) ) {
foreach ( $d['prizes'] as $p ) {
$prizes[] = array(
'title' => isset( $p['title'] ) ? $p['title'] : '',
'text' => self::rich_text( isset( $p['text'] ) ? $p['text'] : '' ),
'value' => isset( $p['value'] ) ? $p['value'] : '',
'qty' => isset( $p['qty'] ) ? max( 1, (int) $p['qty'] ) : 1,
'image' => isset( $p['image'] ) ? $p['image'] : '',
);
}
}
$out[] = array(
'day' => (int) $d['day'],
'sponsor' => $d['sponsor'],
'sponsorText' => self::rich_text( $d['sponsorText'] ),
'sponsorUrl' => isset( $d['sponsorUrl'] ) ? esc_url_raw( $d['sponsorUrl'] ) : '',
'prize' => $d['prize'],
'prizeText' => self::rich_text( $d['prizeText'] ),
'value' => $d['value'],
'prizes' => $prizes,
'prizeTotal' => isset( $d['winnerCount'] ) ? (int) $d['winnerCount'] : count( $prizes ),
'logo' => $d['logo'],
'prizeImage' => $d['prizeImage'],
'image' => isset( $d['image'] ) ? $d['image'] : '',
'donateUrl' => isset( $d['donateUrl'] ) ? esc_url_raw( $d['donateUrl'] ) : '',
'donateText' => self::rich_text( isset( $d['donateText'] ) ? $d['donateText'] : '' ),
'social' => $offered,
'socialUrls' => $social_urls,
'quiz' => $quiz,
);
}
return $out;
}
public function render( $atts ) {
$opts = ADVK_Settings::get_options();
$atts = shortcode_atts(
array(
'theme' => $opts['theme'],
'layout' => $opts['layout'],
'door_style' => $opts['door_style'],
'quiz_mode' => $opts['quiz_mode'],
),
$atts,
'advk_calendar'
);
// Validate attribute overrides against known values.
$theme = in_array( $atts['theme'], array( 'beere', 'tanne', 'amber', 'winter', 'halloween', 'ostern', 'custom' ), true ) ? $atts['theme'] : $opts['theme'];
$layout = in_array( $atts['layout'], array( 'grid', 'scene' ), true ) ? $atts['layout'] : $opts['layout'];
$door_style = in_array( $atts['door_style'], array( 'classic', 'ornament', 'gift' ), true ) ? $atts['door_style'] : $opts['door_style'];
$quiz_mode = in_array( $atts['quiz_mode'], array( 'guided', 'single' ), true ) ? $atts['quiz_mode'] : $opts['quiz_mode'];
$doors = ADVK_Data::get_calendar();
$data = array(
'doors' => $this->public_doors( $doors ),
'today' => ADVK_Data::today_day(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'advk_submit' ),
'settings' => array(
'layout' => $layout,
'doorStyle' => $door_style,
'quizMode' => $quiz_mode,
'count' => ADVK_Data::door_count(),
'itemSingular' => (string) $opts['item_singular'],
'itemPlural' => (string) $opts['item_plural'],
'periodLabel' => (string) $opts['period_label'],
'particles' => (string) $opts['particles'],
'year' => (int) $opts['year'],
'eyebrow' => ( isset( $opts['eyebrow'] ) && '' !== trim( (string) $opts['eyebrow'] ) ) ? $opts['eyebrow'] : 'Adventura',
'title1' => $opts['title_line1'],
'title2' => $opts['title_line2'],
'subtitle' => $opts['subtitle'],
'sound' => ! empty( $opts['celebrate_sound'] ),
'melody' => isset( $opts['celebrate_melody'] ) ? $opts['celebrate_melody'] : 'chime',
'newsletter' => ! empty( $opts['newsletter_enabled'] ) ? array(
'label' => $opts['newsletter_label'],
) : false,
'reminder' => ! empty( $opts['reminder_enabled'] ) ? array(
'label' => $opts['reminder_label'],
) : false,
'referral' => ! empty( $opts['referral_enabled'] ),
'credit' => ! empty( $opts['credit_badge'] ),
'showSponsor' => ! empty( $opts['show_sponsor'] ),
'showPrizes' => ! empty( $opts['show_prizes'] ),
'share' => ! empty( $opts['share_enabled'] ),
// Logged-in admins may open and play through every door regardless
// of the date (preview/testing). The server mirrors this bypass.
'adminPreview' => current_user_can( 'manage_options' ),
'turnstile' => ( ! empty( $opts['turnstile_enabled'] ) && '' !== trim( (string) $opts['turnstile_site_key'] ) )
? array( 'enabled' => true, 'siteKey' => (string) $opts['turnstile_site_key'] )
: false,
),
'i18n' => self::ui_strings(),
);
wp_enqueue_style( 'advk-frontend' );
wp_enqueue_script( 'advk-frontend' );
// The calendar data is emitted as a JSON data island inside the markup
// (below) rather than via wp_localize_script. The frontend reads it with
// textContent + JSON.parse, so it keeps working even when a consent
// manager (e.g. Complianz) or a cache/optimizer rewrites inline '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON encoded with JSON_HEX_TAG|JSON_HEX_AMP, safe inside a