Release / Build & publish plugin zip (push) Successful in 7s
Lucide (https://lucide.dev) replaces the hand-rolled Tabler paths. Lucide is more compact and consistent, and ships under the ISC license (more permissive than Tabler's MIT and equally fine for plugin redistribution). Lucide has no brand icons for Reddit/WhatsApp/Pinterest, so we use generic substitutes that preserve the visual intent: - reddit.svg <- lucide 'share-2' (community share metaphor) - whatsapp.svg <- lucide 'message-circle' - pinterest.svg<- lucide 'pin' Avatar: shrunk to 40x40 per user testing — sizes above 41x41 were being clipped inside the round border on this host.
401 lines
14 KiB
PHP
401 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* The plugin bootstrap file
|
|
*
|
|
* @link https://keithsolomon.net
|
|
* @since 1.0.0
|
|
* @package Projects_Portfolio
|
|
*
|
|
* @wordpress-plugin
|
|
*
|
|
* Plugin Name: Projects Portfolio
|
|
* Description: Create a showcase directory for projects (plugins, themes, patterns) with custom post types, taxonomies, and download functionality.
|
|
* Plugin URI: https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio
|
|
* Version: 1.1.9
|
|
* Author: Keith Solomon
|
|
* Author URI: https://keithsolomon.net
|
|
* License: GPL-2.0+
|
|
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
|
|
* Text Domain: projects-wp
|
|
* Domain Path: /languages
|
|
* Update URI: https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio
|
|
*/
|
|
|
|
// If this file is called directly, abort.
|
|
if ( ! defined( 'WPINC' ) ) {
|
|
die;
|
|
}
|
|
|
|
require 'includes/plugin-update-checker/plugin-update-checker.php';
|
|
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
|
|
|
|
$myUpdateChecker = PucFactory::buildUpdateChecker(
|
|
'https://git.keithsolomon.net/Solo-Web-Works/Projects-Portfolio',
|
|
__FILE__,
|
|
'projects-portfolio'
|
|
);
|
|
// Note: PUC's setBranch() is only available when the host is recognized as a
|
|
// VCS provider (GitHub, GitLab, Bitbucket). The Gitea URL here falls back to
|
|
// PUC's plain JSON metadata mode, so we skip the call rather than throw.
|
|
|
|
// Current plugin version.
|
|
define( 'PROJECTS_PORTFOLIO_VERSION', '1.1.9' );
|
|
|
|
// Add the required files.
|
|
require 'admin/admin-settings.php';
|
|
require 'admin/cpt-taxonomy.php';
|
|
require 'admin/metabox.php';
|
|
require 'includes/helper-functions.php';
|
|
require 'includes/providers/interface-repository-provider.php';
|
|
require 'includes/providers/class-github-provider.php';
|
|
require 'includes/providers/class-gitea-provider.php';
|
|
require 'includes/providers/class-provider-factory.php';
|
|
|
|
/**
|
|
* Flush rewrite rules on activation.
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_flush_rewrite_rules() {
|
|
projects_portfolio_register_cpt_and_taxonomy();
|
|
projects_portfolio_add_rewrite_rules();
|
|
flush_rewrite_rules();
|
|
}
|
|
register_activation_hook( __FILE__, 'projects_portfolio_flush_rewrite_rules' );
|
|
|
|
/**
|
|
* Add rewrite rules for download endpoint.
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_add_rewrite_rules() {
|
|
add_rewrite_rule(
|
|
'^download/([0-9]+)/?$',
|
|
'index.php?project_download_id=$matches[1]',
|
|
'top'
|
|
);
|
|
}
|
|
add_action( 'init', 'projects_portfolio_add_rewrite_rules' );
|
|
|
|
/**
|
|
* WP_Query Vars
|
|
*
|
|
* @param array $vars Query variables array.
|
|
*
|
|
* @since 1.0.0
|
|
* @return array
|
|
*/
|
|
function projects_portfolio_query_vars( $vars ) {
|
|
$vars[] = 'project_download_id';
|
|
return $vars;
|
|
}
|
|
add_filter( 'query_vars', 'projects_portfolio_query_vars' );
|
|
|
|
/**
|
|
* Handle Download Redirect and Increment Download Count
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_handle_download_redirect() {
|
|
$project_id = get_query_var( 'project_download_id' );
|
|
|
|
if ( $project_id ) {
|
|
$download_url = projects_portfolio_get_release_url( (int) $project_id );
|
|
|
|
if ( $download_url ) {
|
|
// Increment download count.
|
|
$download_count = (int) get_post_meta( $project_id, '_projects_portfolio_download_count', true );
|
|
++$download_count;
|
|
update_post_meta( $project_id, '_projects_portfolio_download_count', $download_count );
|
|
|
|
// Redirect to the release ZIP on the project's host.
|
|
// Use wp_redirect() (not wp_safe_redirect()) because the destination
|
|
// is an external host the user has explicitly configured — wp_safe_redirect
|
|
// would reject it and fall back to admin_url().
|
|
wp_redirect( esc_url_raw( $download_url ), 302 ); // phpcs:ignore
|
|
exit;
|
|
} else {
|
|
wp_die(
|
|
esc_html__( 'Invalid download URL. Please check the repository URL.', 'projects-wp' ),
|
|
esc_html__( 'Download Error', 'projects-wp' ),
|
|
array( 'response' => 404 )
|
|
);
|
|
}
|
|
}
|
|
}
|
|
add_action( 'template_redirect', 'projects_portfolio_handle_download_redirect' );
|
|
|
|
/**
|
|
* Add Download Count Column to Projects Admin Table
|
|
*
|
|
* @since 1.0.0
|
|
* @param array $columns The columns array.
|
|
* @return mixed
|
|
*/
|
|
function projects_portfolio_add_download_count_column( $columns ) {
|
|
$columns['download_count'] = esc_html__( 'Download Count', 'projects-wp' );
|
|
return $columns;
|
|
}
|
|
add_filter( 'manage_projects_posts_columns', 'projects_portfolio_add_download_count_column' );
|
|
|
|
/**
|
|
* Display Download Count in Admin Column
|
|
*
|
|
* @since 1.0.0
|
|
* @param string $column The column name.
|
|
* @param int $post_id The post ID.
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_display_download_count_column( $column, $post_id ) {
|
|
if ( 'download_count' === $column ) {
|
|
$download_count = (int) get_post_meta( $post_id, '_projects_portfolio_download_count', true );
|
|
echo $download_count ? esc_html( $download_count ) : esc_html__( '0', 'projects-wp' );
|
|
}
|
|
}
|
|
add_action( 'manage_projects_posts_custom_column', 'projects_portfolio_display_download_count_column', 10, 2 );
|
|
|
|
/**
|
|
* Make the Download Count Column Sortable
|
|
*
|
|
* @since 1.0.0
|
|
* @param array $columns The columns array.
|
|
* @return mixed
|
|
*/
|
|
function projects_portfolio_sortable_columns( $columns ) {
|
|
$columns['download_count'] = 'download_count';
|
|
return $columns;
|
|
}
|
|
add_filter( 'manage_edit-projects_sortable_columns', 'projects_portfolio_sortable_columns' );
|
|
|
|
/**
|
|
* Handle Sorting for Download Count
|
|
*
|
|
* @since 1.0.0
|
|
* @param WP_Query $query The query object.
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_sort_download_count_column( $query ) {
|
|
if ( ! is_admin() || ! $query->is_main_query() ) {
|
|
return;
|
|
}
|
|
|
|
if ( 'download_count' === $query->get( 'orderby' ) ) {
|
|
$query->set( 'meta_key', '_projects_portfolio_download_count' );
|
|
$query->set( 'orderby', 'meta_value_num' );
|
|
}
|
|
}
|
|
add_action( 'pre_get_posts', 'projects_portfolio_sort_download_count_column' );
|
|
|
|
/**
|
|
* Register custom REST API endpoint for projects.
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_register_custom_endpoint() {
|
|
register_rest_route(
|
|
'projects/v1',
|
|
'/projects',
|
|
array(
|
|
'methods' => WP_REST_Server::READABLE,
|
|
'callback' => 'projects_portfolio_get_projects_data',
|
|
'permission_callback' => '__return_true',
|
|
)
|
|
);
|
|
}
|
|
add_action( 'rest_api_init', 'projects_portfolio_register_custom_endpoint' );
|
|
|
|
/**
|
|
* Load custom templates for single and archive project views.
|
|
*
|
|
* @since 1.0.0
|
|
* @param string $template The template file path.
|
|
* @return mixed
|
|
*/
|
|
function projects_portfolio_template_loader( $template ) {
|
|
if ( is_singular( 'projects' ) ) {
|
|
// Check for a single-projects.php template in the theme
|
|
$theme_template = locate_template( 'single-projects.php' );
|
|
return $theme_template ? $theme_template : plugin_dir_path( __FILE__ ) . 'templates/single-projects.php';
|
|
}
|
|
|
|
if ( is_post_type_archive( 'projects' ) ) {
|
|
// Check for an archive-projects.php template in the theme
|
|
$theme_template = locate_template( 'archive-projects.php' );
|
|
return $theme_template ? $theme_template : plugin_dir_path( __FILE__ ) . 'templates/archive-projects.php';
|
|
}
|
|
|
|
if ( is_tax( 'project-type' ) ) {
|
|
// Check for a taxonomy-project-type.php template in the theme
|
|
$theme_template = locate_template( 'archive-project-type.php' );
|
|
return $theme_template ? $theme_template : plugin_dir_path( __FILE__ ) . 'templates/taxonomy-project-type.php';
|
|
}
|
|
|
|
return $template;
|
|
}
|
|
add_filter( 'template_include', 'projects_portfolio_template_loader' );
|
|
|
|
/**
|
|
* Enqueue styles for project single and archive templates.
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_enqueue_project_styles() {
|
|
if ( is_singular( 'projects' ) || is_post_type_archive( 'projects' ) || is_tax( 'project-type' ) ) {
|
|
$is_fse_theme = wp_theme_has_theme_json() && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
|
|
|
|
// Register stylesheet for both theme types
|
|
wp_register_style(
|
|
'projects-wp-styles',
|
|
plugin_dir_url( __FILE__ ) . 'assets/css/style.css',
|
|
array(),
|
|
PROJECTS_PORTFOLIO_VERSION
|
|
);
|
|
|
|
if ( $is_fse_theme ) {
|
|
// Inline block style enqueue (needed for FSE themes)
|
|
add_action(
|
|
'wp_footer',
|
|
function () {
|
|
echo '<style id="projects-wp-styles">';
|
|
echo file_get_contents( plugin_dir_path( __FILE__ ) . 'assets/css/style.css' ); // phpcs:ignore
|
|
echo '</style>';
|
|
}
|
|
);
|
|
} else {
|
|
// Classic theme enqueue
|
|
wp_enqueue_style( 'projects-wp-styles' );
|
|
}
|
|
}
|
|
|
|
if ( is_singular( 'projects' ) ) {
|
|
// assets/js/buttons.js was used to render GitHub-style follow buttons.
|
|
// The Follow button now uses a plain styled link (works for any provider),
|
|
// so the script is no longer enqueued.
|
|
}
|
|
}
|
|
add_action( 'wp_enqueue_scripts', 'projects_portfolio_enqueue_project_styles' );
|
|
|
|
/**
|
|
* Enqueue admin styles.
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_admin_styles() {
|
|
wp_enqueue_style( 'projects-wp-admin-css', plugin_dir_url( __FILE__ ) . 'assets/css/admin-styles.css' );
|
|
}
|
|
add_action( 'admin_enqueue_scripts', 'projects_portfolio_admin_styles' );
|
|
|
|
/**
|
|
* Modify the number of posts per page for the 'projects' post type archive.
|
|
*
|
|
* Ensures only the 'projects' archive page is affected without changing global settings.
|
|
*
|
|
* @param WP_Query $query The WordPress query object.
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_custom_posts_per_page( $query ) {
|
|
if ( is_admin() || ! $query->is_main_query() ) {
|
|
return;
|
|
}
|
|
|
|
$projects_per_page = apply_filters( 'projects_portfolio_archives_projects_per_page', 12 );
|
|
|
|
if ( is_post_type_archive( 'projects' ) ) {
|
|
$query->set( 'posts_per_page', $projects_per_page );
|
|
}
|
|
}
|
|
add_action( 'pre_get_posts', 'projects_portfolio_custom_posts_per_page', 1 );
|
|
|
|
/**
|
|
* Modify the posts per page for the 'projects' post type archive in Full Site Editing (FSE) themes.
|
|
*
|
|
* This function ensures that the 'projects' post type archive displays 12 posts per page,
|
|
* even when using an FSE theme with a Query Loop block.
|
|
*
|
|
* @param array $query The existing query variables for the Query Loop block.
|
|
* @param object $block The current block object (not used but available if needed).
|
|
*
|
|
* @since 1.0.0
|
|
* @return array Modified query variables with 'posts_per_page' set for the projects archive.
|
|
*/
|
|
function modify_projects_posts_per_page_fse( $query, $block ) { // phpcs:ignore
|
|
// Ensure we are modifying the archive query for the 'projects' post type.
|
|
if ( isset( $query['post_type'] ) && $query['post_type'] === 'projects' ) {
|
|
$query['posts_per_page'] = 12;
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
add_filter( 'query_loop_block_query_vars', 'modify_projects_posts_per_page_fse', 10, 2 );
|
|
|
|
/**
|
|
* Add social sharing buttons with Tabler icons.
|
|
*
|
|
* @param int $project_id The project post ID.
|
|
*
|
|
* @since 1.0.0
|
|
* @return void
|
|
*/
|
|
function projects_portfolio_social_sharing_buttons( $project_id ) {
|
|
$url = rawurlencode( get_permalink( $project_id ) );
|
|
$title = rawurlencode( get_the_title( $project_id ) );
|
|
|
|
$html = '<div class="project-social-sharing">';
|
|
|
|
$svg_icon = function( string $name, string $file ) use ( &$html ) {
|
|
// Render each social icon as an <img> referencing a real .svg file
|
|
// shipped under assets/icons/. This bypasses any kses data: URI stripping
|
|
// and ensures the icon loads as a normal image via standard img rules.
|
|
$src = plugins_url( 'assets/icons/' . $file, __FILE__ );
|
|
$html .= '<img src="' . esc_url( $src ) . '" alt="' . esc_attr( $name ) . '" width="20" height="20" style="width:20px;height:20px;display:inline-block;vertical-align:middle;" />';
|
|
};
|
|
|
|
// Facebook.
|
|
$html .= '<a href="https://www.facebook.com/sharer/sharer.php?u=' . $url . '" target="_blank" rel="noopener noreferrer">';
|
|
$svg_icon( 'Facebook', 'facebook.svg' );
|
|
$html .= '</a>';
|
|
|
|
// X (Twitter).
|
|
$html .= '<a href="https://twitter.com/intent/tweet?text=' . $title . '&url=' . $url . '" target="_blank" rel="noopener noreferrer">';
|
|
$svg_icon( 'X', 'x.svg' );
|
|
$html .= '</a>';
|
|
|
|
// LinkedIn.
|
|
$html .= '<a href="https://www.linkedin.com/sharing/share-offsite/?url=' . $url . '" target="_blank" rel="noopener noreferrer">';
|
|
$svg_icon( 'LinkedIn', 'linkedin.svg' );
|
|
$html .= '</a>';
|
|
|
|
// Reddit.
|
|
$html .= '<a href="https://www.reddit.com/submit?url=' . $url . '&title=' . $title . '" target="_blank" rel="noopener noreferrer">';
|
|
$svg_icon( 'Reddit', 'reddit.svg' );
|
|
$html .= '</a>';
|
|
|
|
// WhatsApp.
|
|
$html .= '<a href="https://www.whatsapp.com/send?text=' . $title . '%20' . $url . '" target="_blank" rel="noopener noreferrer">';
|
|
$svg_icon( 'WhatsApp', 'whatsapp.svg' );
|
|
$html .= '</a>';
|
|
|
|
// Pinterest.
|
|
$html .= '<a href="https://pinterest.com/pin/create/button/?url=' . $url . '&description=' . $title . '" target="_blank" rel="noopener noreferrer">';
|
|
$svg_icon( 'Pinterest', 'pinterest.svg' );
|
|
$html .= '</a>';
|
|
|
|
// Email.
|
|
$html .= '<a href="mailto:?subject=' . $title . '&body=' . $url . '" target="_blank" rel="noopener noreferrer">';
|
|
$svg_icon( 'Email', 'mail.svg' );
|
|
$html .= '</a>';
|
|
|
|
$html .= '</div>';
|
|
|
|
echo wp_kses_post( $html );
|
|
}
|
|
add_action( 'projects_after_download_button', 'projects_portfolio_social_sharing_buttons', 999 );
|