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 ''; } 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'] : '', '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, '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'] ), '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' ); wp_localize_script( 'advk-frontend', 'ADVK_DATA', $data ); // Cloudflare Turnstile widget script (explicit render), only when enabled. if ( ! empty( $opts['turnstile_enabled'] ) && '' !== trim( (string) $opts['turnstile_site_key'] ) ) { wp_enqueue_script( 'advk-turnstile', 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit', array(), null, // versionless CDN script true ); } $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 ) . '"' : ''; $html = '
'; $html .= '
'; $html .= '
'; $html .= '
'; return $html; } /** * All user-facing strings rendered by the frontend JavaScript. Passed to * the script via wp_localize_script so they are translatable (Loco / * .po-.mo) just like the PHP strings. Placeholders use printf-style * tokens (%s, %1$s …) that the JS `fmt()` helper fills in. * * @return array */ public static function ui_strings() { $opts = ADVK_Settings::get_options(); $privacy_url = isset( $opts['privacy_url'] ) ? (string) $opts['privacy_url'] : ''; $terms_url = isset( $opts['terms_url'] ) ? (string) $opts['terms_url'] : ''; $terms_open = '' !== $terms_url ? '' : ''; $privacy_open = '' !== $privacy_url ? '' : ''; /* translators: 1: opening tag for terms, 2: closing , 3: opening tag for privacy, 4: closing . */ $consent_tpl = __( 'Ich akzeptiere die %1$sTeilnahmebedingungen%2$s und die %3$sDatenschutzerklärung%4$s. Meine Daten werden ausschließlich zur Gewinnabwicklung verwendet.', 'adventura-interactive-advent-calendar' ); return array( // Calendar header / progress. /* translators: 1: number played so far, 2: item noun (plural, e.g. Türchen). */ 'playedSuffix' => __( ' von %1$s %2$s gespielt', 'adventura-interactive-advent-calendar' ), /* translators: %s: number of consecutive days played. */ 'streak' => __( '🔥 Serie: %s', 'adventura-interactive-advent-calendar' ), /* translators: %s: item noun (plural, e.g. Türchen). */ 'allDone' => __( 'Stark! Du hast alle %s gespielt.', 'adventura-interactive-advent-calendar' ), /* translators: %s: item noun (singular, e.g. Türchen). */ 'countdownLabel' => __( 'Nächstes %s in ', 'adventura-interactive-advent-calendar' ), /* translators: 1: item noun (singular), 2: number. */ 'doorOpenAria' => __( '%1$s %2$s öffnen', 'adventura-interactive-advent-calendar' ), /* translators: 1: item noun (singular), 2: number. */ 'doorLockedAria' => __( '%1$s %2$s — noch geschlossen', 'adventura-interactive-advent-calendar' ), 'today' => __( 'Heute', 'adventura-interactive-advent-calendar' ), 'december' => __( 'Dezember', 'adventura-interactive-advent-calendar' ), 'adminPreviewNote' => __( 'Adminvorschau: Für dich sind alle Türchen entsperrt und spielbar — Besucher sehen die normale Tages-Sperre.', 'adventura-interactive-advent-calendar' ), 'zoomHint' => __( 'Zum Vergrößern klicken', 'adventura-interactive-advent-calendar' ), 'shareText' => __( 'Mach mit beim Gewinnspiel!', 'adventura-interactive-advent-calendar' ), 'shareEmail' => __( 'E-Mail', 'adventura-interactive-advent-calendar' ), 'shareCalendar' => __( 'Kalender teilen:', 'adventura-interactive-advent-calendar' ), // Validation / errors. 'errFirst' => __( 'Bitte Vornamen angeben.', 'adventura-interactive-advent-calendar' ), 'errLast' => __( 'Bitte Nachnamen angeben.', 'adventura-interactive-advent-calendar' ), 'errEmail' => __( 'Bitte gültige E-Mail angeben.', 'adventura-interactive-advent-calendar' ), 'errConsent' => __( 'Bitte Datenschutz bestätigen.', 'adventura-interactive-advent-calendar' ), 'errGeneric' => __( 'Es ist ein Fehler aufgetreten. Bitte versuche es erneut.', 'adventura-interactive-advent-calendar' ), 'errNetwork' => __( 'Verbindungsfehler. Bitte versuche es erneut.', 'adventura-interactive-advent-calendar' ), // Entry closed (door opened on a day other than its own). 'expiredTitle' => __( 'Teilnahme nicht mehr möglich', 'adventura-interactive-advent-calendar' ), 'expiredText' => __( 'Dieses Türchen kann nur am jeweiligen Tag gespielt werden. Ansehen geht weiterhin — eine Teilnahme ist leider nicht mehr möglich.', 'adventura-interactive-advent-calendar' ), // Quiz. /* translators: 1: current question number, 2: total questions. */ 'qCount' => __( 'Frage %1$s von %2$s', 'adventura-interactive-advent-calendar' ), 'quizEyebrow' => __( 'Mitmachen', 'adventura-interactive-advent-calendar' ), 'quizTitle' => __( 'Quizfrage', 'adventura-interactive-advent-calendar' ), 'quizHint' => __( 'Wähle deine Antwort und tippe auf „Weiter“.', 'adventura-interactive-advent-calendar' ), 'quizAllTitle' => __( 'Beantworte alle Fragen', 'adventura-interactive-advent-calendar' ), 'quizAllHint' => __( 'Wähle bei jeder Frage eine Antwort.', 'adventura-interactive-advent-calendar' ), // Sponsor. 'sponsorEyebrow' => __( 'Heutiger Partner', 'adventura-interactive-advent-calendar' ), 'sponsorTitle' => __( 'Präsentiert von …', 'adventura-interactive-advent-calendar' ), /* translators: %s: sponsor name. */ 'logoAlt' => __( 'Logo · %s', 'adventura-interactive-advent-calendar' ), /* translators: %s: sponsor name. */ 'sponsorVisit' => __( '%s besuchen', 'adventura-interactive-advent-calendar' ), 'teaserLabel' => __( 'Heute zu gewinnen', 'adventura-interactive-advent-calendar' ), // Prize. 'prizeEyebrow' => __( 'Der Gewinn', 'adventura-interactive-advent-calendar' ), 'prizesEyebrow' => __( 'Die Gewinne', 'adventura-interactive-advent-calendar' ), /* translators: %s: number of prizes. */ 'prizesTitle' => __( '%s Preise zu gewinnen', 'adventura-interactive-advent-calendar' ), /* translators: %s: prize name. */ 'prizePhotoAlt' => __( 'Gewinn-Foto · %s', 'adventura-interactive-advent-calendar' ), /* translators: %s: monetary value of the prize. */ 'prizeValue' => __( 'Warenwert %s', 'adventura-interactive-advent-calendar' ), // Social. 'socialEyebrow' => __( 'Optionaler Schritt', 'adventura-interactive-advent-calendar' ), /* translators: %s: sponsor name. */ 'socialTitle' => __( 'Folge %s', 'adventura-interactive-advent-calendar' ), 'socialText' => __( 'Bleib auf dem Laufenden — das Folgen ist freiwillig und keine Voraussetzung für die Teilnahme, aber erhöht die Gewinnchancen.', 'adventura-interactive-advent-calendar' ), 'socialOpen' => __( 'Profil öffnen ↗', 'adventura-interactive-advent-calendar' ), 'socialNow' => __( 'Du folgst jetzt', 'adventura-interactive-advent-calendar' ), 'socialFollowing' => __( 'Folge ich ✓', 'adventura-interactive-advent-calendar' ), 'socialFollow' => __( 'Folgen', 'adventura-interactive-advent-calendar' ), 'socialSkipNote' => __( 'Dieser Schritt ist optional, erhöht aber die Gewinnchancen.', 'adventura-interactive-advent-calendar' ), // Donation (optional step). 'donateEyebrow' => __( 'Gutes tun & Extra-Chance', 'adventura-interactive-advent-calendar' ), 'donateTitle' => __( 'Spenden & Zusatzlos sichern', 'adventura-interactive-advent-calendar' ), 'donateText' => __( 'Mit einer Spende erhältst du ein Zusatzlos — eine zweite Gewinnchance. Bei einer größeren Spende gibt es sogar einen Direktgewinn.', 'adventura-interactive-advent-calendar' ), 'donateBtn' => __( 'Jetzt spenden', 'adventura-interactive-advent-calendar' ), 'donateNote' => __( 'Die Spende läuft über PayPal. Dein Zusatzlos bzw. der Direktgewinn wird nach Zahlungseingang berücksichtigt.', 'adventura-interactive-advent-calendar' ), // Form. 'formEyebrow' => __( 'Letzter Schritt', 'adventura-interactive-advent-calendar' ), 'formTitle' => __( 'Deine Teilnahme', 'adventura-interactive-advent-calendar' ), 'formText' => __( 'Damit wir dich im Gewinnfall benachrichtigen können.', 'adventura-interactive-advent-calendar' ), 'labelFirst' => __( 'Vorname', 'adventura-interactive-advent-calendar' ), 'labelLast' => __( 'Nachname', 'adventura-interactive-advent-calendar' ), 'labelEmail' => __( 'E-Mail-Adresse', 'adventura-interactive-advent-calendar' ), 'phFirst' => __( 'Maria', 'adventura-interactive-advent-calendar' ), 'phLast' => __( 'Muster', 'adventura-interactive-advent-calendar' ), 'phEmail' => __( 'maria@beispiel.de', 'adventura-interactive-advent-calendar' ), 'hpLabel' => __( 'Bitte dieses Feld leer lassen', 'adventura-interactive-advent-calendar' ), 'consentHtml' => sprintf( $consent_tpl, $terms_open, '', $privacy_open, '' ), 'nlFallback' => __( 'Newsletter abonnieren.', 'adventura-interactive-advent-calendar' ), 'remFallback' => __( 'Erinnere mich an neue Türchen.', 'adventura-interactive-advent-calendar' ), // Done / referral. 'doneTitle' => __( 'Du bist dabei!', 'adventura-interactive-advent-calendar' ), /* translators: 1: door number, 2: prize name (already wrapped in bold). */ /* translators: 1: day number, 2: prize name (bold), 3: item noun (singular). */ 'doneText' => __( 'Vielen Dank für deine Teilnahme an %3$s %1$s. Wir drücken die Daumen für den %2$s.', 'adventura-interactive-advent-calendar' ), 'losnrLabel' => __( 'Deine Los-Nummer', 'adventura-interactive-advent-calendar' ), /* translators: %s: e-mail address the confirmation was sent to. */ 'confirmSent' => __( 'Eine Bestätigung haben wir an %s gesendet.', 'adventura-interactive-advent-calendar' ), 'yourEmail' => __( 'deine E-Mail', 'adventura-interactive-advent-calendar' ), 'copy' => __( 'Kopieren', 'adventura-interactive-advent-calendar' ), 'copied' => __( 'Kopiert ✓', 'adventura-interactive-advent-calendar' ), 'inviteTitle' => __( 'Freunde einladen', 'adventura-interactive-advent-calendar' ), 'inviteText' => __( 'Teile deinen persönlichen Link — jede Teilnahme über deinen Link zählt als Empfehlung.', 'adventura-interactive-advent-calendar' ), // Footer / navigation. 'toCalendar' => __( 'Zum Kalender', 'adventura-interactive-advent-calendar' ), 'back' => __( 'Zurück', 'adventura-interactive-advent-calendar' ), 'skip' => __( 'Überspringen', 'adventura-interactive-advent-calendar' ), 'sending' => __( 'Senden …', 'adventura-interactive-advent-calendar' ), 'participate' => __( 'Jetzt teilnehmen', 'adventura-interactive-advent-calendar' ), 'next' => __( 'Weiter', 'adventura-interactive-advent-calendar' ), 'close' => __( 'Schließen', 'adventura-interactive-advent-calendar' ), ); } }