diff --git a/public/class-peptide-news-public.php b/public/class-peptide-news-public.php new file mode 100644 index 0000000..275af3a --- /dev/null +++ b/public/class-peptide-news-public.php @@ -0,0 +1,333 @@ +plugin_name = $plugin_name; + $this->version = $version; + } + + /** + * Register front-end CSS (enqueued on demand by the shortcode/widget). + */ + public function enqueue_styles() { + wp_register_style( + $this->plugin_name, + PEPTIDE_NEWS_PLUGIN_URL . 'public/css/public-style.css', + array(), + $this->version, + 'all' + ); + } + + /** + * Register front-end JS (enqueued on demand by the shortcode/widget). + */ + public function enqueue_scripts() { + wp_register_script( + $this->plugin_name, + PEPTIDE_NEWS_PLUGIN_URL . 'public/js/public-script.js', + array( 'jquery' ), + $this->version, + true + ); + + wp_localize_script( $this->plugin_name, 'peptideNewsPublic', array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'peptide_news_track' ), + 'session_id' => $this->get_or_create_session_id(), + ) ); + } + + /** + * Register the shortcode. + */ + public function register_shortcode() { + add_shortcode( 'peptide_news', array( $this, 'render_shortcode' ) ); + } + + /** + * Register the widget. + */ + public function register_widget() { + register_widget( 'Peptide_News_Widget' ); + } + + /** + * Render the [peptide_news] shortcode. + * + * @param array $atts Shortcode attributes. + * @return string HTML output. + */ + public function render_shortcode( $atts ) { + $atts = shortcode_atts( array( + 'count' => get_option( 'peptide_news_articles_count', 10 ), + 'layout' => 'card', // card | compact | list + ), $atts, 'peptide_news' ); + + $articles = $this->get_articles( absint( $atts['count'] ) ); + + if ( empty( $articles ) ) { + return '

' . esc_html__( 'No peptide news articles available yet. Check back soon!', 'peptide-news' ) . '

'; + } + + // Enqueue assets only when the shortcode is actually rendered. + wp_enqueue_style( $this->plugin_name ); + wp_enqueue_script( $this->plugin_name ); + + $fallback_thumb = get_option( 'peptide_news_thumbnail_fallback', '' ); + $layout = sanitize_key( $atts['layout'] ); + + ob_start(); + ?> +
+ thumbnail_url ) ? $article->thumbnail_url : $fallback_thumb; + $date = wp_date( 'M j, Y', strtotime( $article->published_at ) ); + ?> +
+ +
+ + <?php echo esc_attr( $article->title ); ?> + +
+ + +
+ + +

+ + title ); ?> + +

+ +

+ excerpt ); ?> +

+ + author ) ) : ?> + + author ); ?> + + + + categories ) ) : ?> +
+ categories ) ); + foreach ( array_slice( $cats, 0, 3 ) as $cat ) : + if ( empty( $cat ) ) continue; + ?> + + +
+ +
+
+ +
+ true ) ); + } + set_transient( $throttle_key, 1, 5 ); + + $meta = array( + 'ip' => '', + 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', + 'referrer' => isset( $_POST['referrer'] ) ? esc_url_raw( wp_unslash( $_POST['referrer'] ) ) : '', + 'page_url' => isset( $_POST['page_url'] ) ? esc_url_raw( wp_unslash( $_POST['page_url'] ) ) : '', + 'session_id' => $session_id, + ); + + $result = Peptide_News_Analytics::record_click( $article_id, $meta ); + + if ( $result ) { + wp_send_json_success(); + } else { + wp_send_json_error( 'Failed to record click.' ); + } + } + + /** + * Get active articles from the database. + * + * @param int $count Number of articles. + * @return array + */ + private function get_articles( $count ) { + global $wpdb; + + $table = $wpdb->prefix . 'peptide_news_articles'; + + // Use transient cache to avoid DB hits on every page load. + $cache_key = 'peptide_news_articles_' . $count; + $cached = get_transient( $cache_key ); + + if ( false !== $cached ) { + return $cached; + } + + $articles = $wpdb->get_results( $wpdb->prepare( + "SELECT id, source, source_url, title, excerpt, author, thumbnail_url, + published_at, categories + FROM {$table} + WHERE is_active = 1 + ORDER BY published_at DESC + LIMIT %d", + $count + ) ); + + // Cache for 5 minutes. + set_transient( $cache_key, $articles, 5 * MINUTE_IN_SECONDS ); + + return $articles; + } + + /** + * Generate or retrieve a session ID for visitor tracking. + * + * Sets the cookie in PHP if it doesn't exist so the value + * is consistent between the server-side localized script data + * and the client-side cookie. + * + * @return string + */ + private function get_or_create_session_id() { + if ( isset( $_COOKIE['pn_session_id'] ) ) { + return sanitize_text_field( wp_unslash( $_COOKIE['pn_session_id'] ) ); + } + + $session_id = wp_generate_uuid4(); + + // Set a 30-minute session cookie (matches JS-side behaviour). + if ( ! headers_sent() ) { + setcookie( + 'pn_session_id', + $session_id, + array( + 'expires' => time() + 1800, + 'path' => '/', + 'secure' => is_ssl(), + 'httponly' => false, // JS needs read access for sendBeacon. + 'samesite' => 'Lax', + ) + ); + } + + return $session_id; + } +} + +/** + * Peptide News Widget. + * + * @since 1.0.0 + */ +class Peptide_News_Widget extends WP_Widget { + + public function __construct() { + parent::__construct( + 'peptide_news_widget', + __( 'Peptide News', 'peptide-news' ), + array( + 'description' => __( 'Displays the latest peptide research news.', 'peptide-news' ), + 'classname' => 'peptide-news-widget', + ) + ); + } + + public function widget( $args, $instance ) { + $title = apply_filters( 'widget_title', $instance['title'] ?? __( 'Peptide News', 'peptide-news' ) ); + $count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5; + + echo $args['before_widget']; + + if ( $title ) { + echo $args['before_title'] . esc_html( $title ) . $args['after_title']; + } + + $public = new Peptide_News_Public( 'peptide-news', PEPTIDE_NEWS_VERSION ); + echo $public->render_shortcode( array( 'count' => $count, 'layout' => 'compact' ) ); + + echo $args['after_widget']; + } + + public function form( $instance ) { + $title = isset( $instance['title'] ) ? $instance['title'] : __( 'Peptide News', 'peptide-news' ); + $count = isset( $instance['count'] ) ? absint( $instance['count'] ) : 5; + ?> +

+ + +

+

+ + +

+