Files
gustavoo-me/wp-content/plugins/gustavoo-portfolio-core/includes/class-gso-projects.php
T
2026-07-23 20:32:08 -03:00

564 lines
20 KiB
PHP

<?php
/**
* Project content model and admin UI.
*
* @package GustavoPortfolioCore
*/
defined( 'ABSPATH' ) || exit;
/**
* Registers the portfolio project post type, taxonomies and metadata.
*/
final class GSO_Projects {
const POST_TYPE = 'gso_project';
const TAX_TYPE = 'gso_project_type';
const TAX_TECH = 'gso_project_tech';
const META_URL = '_gso_project_url';
const META_CLIENT = '_gso_project_client';
const META_YEAR = '_gso_project_year';
const META_FEATURED = '_gso_project_featured';
const META_LINK_LABEL = '_gso_project_link_label';
const META_ACCENT = '_gso_project_accent';
/**
* Register hooks.
*
* @return void
*/
public static function init() {
add_action( 'init', array( __CLASS__, 'register_content_types' ) );
add_action( 'add_meta_boxes_' . self::POST_TYPE, array( __CLASS__, 'add_meta_boxes' ) );
add_action( 'save_post_' . self::POST_TYPE, array( __CLASS__, 'save_meta_box' ) );
add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( __CLASS__, 'admin_columns' ) );
add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( __CLASS__, 'render_admin_column' ), 10, 2 );
add_filter( 'manage_edit-' . self::POST_TYPE . '_sortable_columns', array( __CLASS__, 'sortable_columns' ) );
add_action( 'pre_get_posts', array( __CLASS__, 'apply_admin_sorting' ) );
add_action( 'restrict_manage_posts', array( __CLASS__, 'taxonomy_filters' ) );
}
/**
* Register the post type, taxonomies and REST-aware metadata.
*
* @return void
*/
public static function register_content_types() {
register_post_type(
self::POST_TYPE,
array(
'labels' => array(
'name' => __( 'Projetos', 'gustavoo-portfolio-core' ),
'singular_name' => __( 'Projeto', 'gustavoo-portfolio-core' ),
'menu_name' => __( 'Portfólio', 'gustavoo-portfolio-core' ),
'name_admin_bar' => __( 'Projeto', 'gustavoo-portfolio-core' ),
'add_new' => __( 'Adicionar projeto', 'gustavoo-portfolio-core' ),
'add_new_item' => __( 'Adicionar novo projeto', 'gustavoo-portfolio-core' ),
'edit_item' => __( 'Editar projeto', 'gustavoo-portfolio-core' ),
'new_item' => __( 'Novo projeto', 'gustavoo-portfolio-core' ),
'view_item' => __( 'Ver projeto', 'gustavoo-portfolio-core' ),
'search_items' => __( 'Buscar projetos', 'gustavoo-portfolio-core' ),
'not_found' => __( 'Nenhum projeto encontrado.', 'gustavoo-portfolio-core' ),
'not_found_in_trash' => __( 'Nenhum projeto encontrado na lixeira.', 'gustavoo-portfolio-core' ),
'all_items' => __( 'Projetos', 'gustavoo-portfolio-core' ),
'featured_image' => __( 'Imagem do projeto', 'gustavoo-portfolio-core' ),
'set_featured_image' => __( 'Definir imagem do projeto', 'gustavoo-portfolio-core' ),
'remove_featured_image' => __( 'Remover imagem do projeto', 'gustavoo-portfolio-core' ),
),
'public' => false,
'publicly_queryable' => false,
'exclude_from_search' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_admin_bar' => true,
'show_in_nav_menus' => false,
'show_in_rest' => true,
'menu_position' => 25,
'menu_icon' => 'dashicons-portfolio',
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'has_archive' => false,
'rewrite' => false,
'query_var' => false,
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'page-attributes', 'revisions', 'custom-fields' ),
)
);
register_taxonomy(
self::TAX_TYPE,
self::POST_TYPE,
array(
'labels' => array(
'name' => __( 'Tipos de projeto', 'gustavoo-portfolio-core' ),
'singular_name' => __( 'Tipo de projeto', 'gustavoo-portfolio-core' ),
'menu_name' => __( 'Tipos', 'gustavoo-portfolio-core' ),
'all_items' => __( 'Todos os tipos', 'gustavoo-portfolio-core' ),
'edit_item' => __( 'Editar tipo', 'gustavoo-portfolio-core' ),
'add_new_item' => __( 'Adicionar tipo', 'gustavoo-portfolio-core' ),
'search_items' => __( 'Buscar tipos', 'gustavoo-portfolio-core' ),
),
'public' => false,
'publicly_queryable' => false,
'show_ui' => true,
'show_admin_column' => false,
'show_in_rest' => true,
'hierarchical' => true,
'rewrite' => false,
)
);
register_taxonomy(
self::TAX_TECH,
self::POST_TYPE,
array(
'labels' => array(
'name' => __( 'Tecnologias', 'gustavoo-portfolio-core' ),
'singular_name' => __( 'Tecnologia', 'gustavoo-portfolio-core' ),
'menu_name' => __( 'Tecnologias', 'gustavoo-portfolio-core' ),
'all_items' => __( 'Todas as tecnologias', 'gustavoo-portfolio-core' ),
'edit_item' => __( 'Editar tecnologia', 'gustavoo-portfolio-core' ),
'add_new_item' => __( 'Adicionar tecnologia', 'gustavoo-portfolio-core' ),
'search_items' => __( 'Buscar tecnologias', 'gustavoo-portfolio-core' ),
'separate_items_with_commas' => __( 'Separe tecnologias com vírgulas', 'gustavoo-portfolio-core' ),
),
'public' => false,
'publicly_queryable' => false,
'show_ui' => true,
'show_admin_column' => false,
'show_in_rest' => true,
'hierarchical' => false,
'rewrite' => false,
)
);
self::register_meta();
}
/**
* Register project metadata.
*
* @return void
*/
private static function register_meta() {
$common = array(
'single' => true,
'show_in_rest' => true,
'auth_callback' => array( __CLASS__, 'authorize_meta' ),
);
register_post_meta(
self::POST_TYPE,
self::META_URL,
array_merge(
$common,
array(
'type' => 'string',
'sanitize_callback' => array( __CLASS__, 'sanitize_external_url' ),
)
)
);
foreach ( array( self::META_CLIENT, self::META_LINK_LABEL ) as $meta_key ) {
register_post_meta(
self::POST_TYPE,
$meta_key,
array_merge(
$common,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
)
)
);
}
register_post_meta(
self::POST_TYPE,
self::META_YEAR,
array_merge(
$common,
array(
'type' => 'integer',
'sanitize_callback' => array( __CLASS__, 'sanitize_year' ),
)
)
);
register_post_meta(
self::POST_TYPE,
self::META_FEATURED,
array_merge(
$common,
array(
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
)
)
);
register_post_meta(
self::POST_TYPE,
self::META_ACCENT,
array_merge(
$common,
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_hex_color',
)
)
);
}
/**
* Restrict meta updates to users who can edit the project.
*
* @param bool $allowed Current authorization result.
* @param string $meta_key Meta key.
* @param int $post_id Project ID.
* @return bool
*/
public static function authorize_meta( $allowed, $meta_key, $post_id ) {
unset( $allowed, $meta_key );
return current_user_can( 'edit_post', (int) $post_id );
}
/**
* Allow only valid external HTTP(S) URLs.
*
* @param mixed $value Raw URL.
* @return string
*/
public static function sanitize_external_url( $value ) {
$url = esc_url_raw( trim( (string) $value ), array( 'http', 'https' ) );
return $url && wp_http_validate_url( $url ) ? $url : '';
}
/**
* Validate a four-digit project year.
*
* @param mixed $value Raw year.
* @return int
*/
public static function sanitize_year( $value ) {
$year = absint( $value );
$max_year = (int) gmdate( 'Y' ) + 5;
return $year >= 1900 && $year <= $max_year ? $year : 0;
}
/**
* Register the project details metabox.
*
* @return void
*/
public static function add_meta_boxes() {
add_meta_box(
'gso-project-details',
__( 'Detalhes do projeto', 'gustavoo-portfolio-core' ),
array( __CLASS__, 'render_meta_box' ),
self::POST_TYPE,
'normal',
'high'
);
}
/**
* Render project fields.
*
* @param WP_Post $post Current project.
* @return void
*/
public static function render_meta_box( $post ) {
$url = (string) get_post_meta( $post->ID, self::META_URL, true );
$client = (string) get_post_meta( $post->ID, self::META_CLIENT, true );
$year = absint( get_post_meta( $post->ID, self::META_YEAR, true ) );
$featured = (bool) get_post_meta( $post->ID, self::META_FEATURED, true );
$link_label = (string) get_post_meta( $post->ID, self::META_LINK_LABEL, true );
$accent = sanitize_hex_color( get_post_meta( $post->ID, self::META_ACCENT, true ) );
if ( ! $accent ) {
$accent = '#7cf3da';
}
wp_nonce_field( 'gso_save_project_details', 'gso_project_details_nonce' );
?>
<table class="form-table" role="presentation">
<tbody>
<tr>
<th scope="row"><label for="gso-project-url"><?php esc_html_e( 'URL externa', 'gustavoo-portfolio-core' ); ?></label></th>
<td>
<input class="regular-text" id="gso-project-url" name="gso_project_url" type="url" value="<?php echo esc_attr( $url ); ?>" inputmode="url" placeholder="https://">
<p class="description"><?php esc_html_e( 'Aceita somente uma URL completa usando HTTP ou HTTPS.', 'gustavoo-portfolio-core' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><label for="gso-project-client"><?php esc_html_e( 'Cliente ou marca', 'gustavoo-portfolio-core' ); ?></label></th>
<td><input class="regular-text" id="gso-project-client" name="gso_project_client" type="text" value="<?php echo esc_attr( $client ); ?>" maxlength="160"></td>
</tr>
<tr>
<th scope="row"><label for="gso-project-year"><?php esc_html_e( 'Ano', 'gustavoo-portfolio-core' ); ?></label></th>
<td><input class="small-text" id="gso-project-year" name="gso_project_year" type="number" value="<?php echo $year ? esc_attr( (string) $year ) : ''; ?>" min="1900" max="<?php echo esc_attr( (string) ( (int) gmdate( 'Y' ) + 5 ) ); ?>" step="1"></td>
</tr>
<tr>
<th scope="row"><label for="gso-project-link-label"><?php esc_html_e( 'Texto do CTA', 'gustavoo-portfolio-core' ); ?></label></th>
<td><input class="regular-text" id="gso-project-link-label" name="gso_project_link_label" type="text" value="<?php echo esc_attr( $link_label ); ?>" maxlength="80" placeholder="<?php esc_attr_e( 'Ver projeto', 'gustavoo-portfolio-core' ); ?>"></td>
</tr>
<tr>
<th scope="row"><label for="gso-project-accent"><?php esc_html_e( 'Cor de acento', 'gustavoo-portfolio-core' ); ?></label></th>
<td><input id="gso-project-accent" name="gso_project_accent" type="color" value="<?php echo esc_attr( $accent ); ?>"></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Destaque', 'gustavoo-portfolio-core' ); ?></th>
<td>
<label for="gso-project-featured">
<input id="gso-project-featured" name="gso_project_featured" type="checkbox" value="1" <?php checked( $featured ); ?>>
<?php esc_html_e( 'Exibir entre os projetos em destaque', 'gustavoo-portfolio-core' ); ?>
</label>
</td>
</tr>
</tbody>
</table>
<?php
}
/**
* Save project fields after nonce and capability checks.
*
* @param int $post_id Project ID.
* @return void
*/
public static function save_meta_box( $post_id ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified immediately below.
$nonce = isset( $_POST['gso_project_details_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['gso_project_details_nonce'] ) ) : '';
if ( ! $nonce || ! wp_verify_nonce( $nonce, 'gso_save_project_details' ) ) {
return;
}
if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( self::POST_TYPE !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified above.
$url = isset( $_POST['gso_project_url'] ) ? self::sanitize_external_url( wp_unslash( $_POST['gso_project_url'] ) ) : '';
$client = isset( $_POST['gso_project_client'] ) ? sanitize_text_field( wp_unslash( $_POST['gso_project_client'] ) ) : '';
$year = isset( $_POST['gso_project_year'] ) ? self::sanitize_year( wp_unslash( $_POST['gso_project_year'] ) ) : 0;
$link_label = isset( $_POST['gso_project_link_label'] ) ? sanitize_text_field( wp_unslash( $_POST['gso_project_link_label'] ) ) : '';
$accent = isset( $_POST['gso_project_accent'] ) ? sanitize_hex_color( wp_unslash( $_POST['gso_project_accent'] ) ) : '';
$featured = isset( $_POST['gso_project_featured'] ) ? '1' : '';
// phpcs:enable WordPress.Security.NonceVerification.Missing
self::update_or_delete_meta( $post_id, self::META_URL, $url );
self::update_or_delete_meta( $post_id, self::META_CLIENT, $client );
self::update_or_delete_meta( $post_id, self::META_YEAR, $year );
self::update_or_delete_meta( $post_id, self::META_LINK_LABEL, $link_label );
self::update_or_delete_meta( $post_id, self::META_ACCENT, $accent );
self::update_or_delete_meta( $post_id, self::META_FEATURED, $featured );
}
/**
* Store a non-empty value, otherwise remove the metadata row.
*
* @param int $post_id Project ID.
* @param string $meta_key Metadata key.
* @param mixed $value Sanitized value.
* @return void
*/
private static function update_or_delete_meta( $post_id, $meta_key, $value ) {
if ( '' === $value || 0 === $value || null === $value ) {
delete_post_meta( $post_id, $meta_key );
return;
}
update_post_meta( $post_id, $meta_key, $value );
}
/**
* Define project list columns.
*
* @param array<string, string> $columns Existing columns.
* @return array<string, string>
*/
public static function admin_columns( $columns ) {
return array(
'cb' => $columns['cb'] ?? '<input type="checkbox">',
'gso_thumbnail' => __( 'Imagem', 'gustavoo-portfolio-core' ),
'title' => __( 'Projeto', 'gustavoo-portfolio-core' ),
'gso_client' => __( 'Cliente', 'gustavoo-portfolio-core' ),
'gso_type' => __( 'Tipo', 'gustavoo-portfolio-core' ),
'gso_tech' => __( 'Tecnologias', 'gustavoo-portfolio-core' ),
'gso_year' => __( 'Ano', 'gustavoo-portfolio-core' ),
'gso_featured' => __( 'Destaque', 'gustavoo-portfolio-core' ),
'gso_url' => __( 'URL', 'gustavoo-portfolio-core' ),
'gso_menu_order' => __( 'Ordem', 'gustavoo-portfolio-core' ),
'date' => $columns['date'] ?? __( 'Data', 'gustavoo-portfolio-core' ),
);
}
/**
* Render one project list cell.
*
* @param string $column Column ID.
* @param int $post_id Project ID.
* @return void
*/
public static function render_admin_column( $column, $post_id ) {
switch ( $column ) {
case 'gso_thumbnail':
if ( has_post_thumbnail( $post_id ) ) {
echo wp_kses_post( get_the_post_thumbnail( $post_id, array( 64, 48 ), array( 'style' => 'width:64px;height:48px;object-fit:cover;border-radius:4px;' ) ) );
} else {
echo '<span aria-hidden="true">—</span><span class="screen-reader-text">' . esc_html__( 'Sem imagem', 'gustavoo-portfolio-core' ) . '</span>';
}
break;
case 'gso_client':
echo esc_html( (string) get_post_meta( $post_id, self::META_CLIENT, true ) ?: '—' );
break;
case 'gso_type':
self::render_terms_column( $post_id, self::TAX_TYPE );
break;
case 'gso_tech':
self::render_terms_column( $post_id, self::TAX_TECH );
break;
case 'gso_year':
$year = absint( get_post_meta( $post_id, self::META_YEAR, true ) );
echo $year ? esc_html( (string) $year ) : '—';
break;
case 'gso_featured':
if ( get_post_meta( $post_id, self::META_FEATURED, true ) ) {
echo '<span class="dashicons dashicons-star-filled" aria-hidden="true"></span><span class="screen-reader-text">' . esc_html__( 'Sim', 'gustavoo-portfolio-core' ) . '</span>';
} else {
echo '<span aria-hidden="true">—</span><span class="screen-reader-text">' . esc_html__( 'Não', 'gustavoo-portfolio-core' ) . '</span>';
}
break;
case 'gso_url':
$url = self::sanitize_external_url( get_post_meta( $post_id, self::META_URL, true ) );
if ( $url ) {
$host = wp_parse_url( $url, PHP_URL_HOST );
printf(
'<a href="%1$s" target="_blank" rel="noopener noreferrer external">%2$s<span class="screen-reader-text"> %3$s</span></a>',
esc_url( $url ),
esc_html( $host ?: $url ),
esc_html__( '(abre em nova aba)', 'gustavoo-portfolio-core' )
);
} else {
echo '—';
}
break;
case 'gso_menu_order':
echo esc_html( (string) get_post_field( 'menu_order', $post_id ) );
break;
}
}
/**
* Render taxonomy terms in an admin column.
*
* @param int $post_id Project ID.
* @param string $taxonomy Taxonomy name.
* @return void
*/
private static function render_terms_column( $post_id, $taxonomy ) {
$terms = get_the_terms( $post_id, $taxonomy );
if ( ! $terms || is_wp_error( $terms ) ) {
echo '—';
return;
}
echo esc_html( implode( ', ', wp_list_pluck( $terms, 'name' ) ) );
}
/**
* Make selected columns sortable.
*
* @param array<string, string> $columns Existing sortable columns.
* @return array<string, string>
*/
public static function sortable_columns( $columns ) {
$columns['gso_client'] = 'gso_client';
$columns['gso_year'] = 'gso_year';
$columns['gso_featured'] = 'gso_featured';
$columns['gso_menu_order'] = 'menu_order';
return $columns;
}
/**
* Apply safe sorting to the main project admin query.
*
* @param WP_Query $query Current query.
* @return void
*/
public static function apply_admin_sorting( $query ) {
if ( ! is_admin() || ! $query->is_main_query() || self::POST_TYPE !== $query->get( 'post_type' ) ) {
return;
}
switch ( $query->get( 'orderby' ) ) {
case 'gso_client':
$query->set( 'meta_key', self::META_CLIENT );
$query->set( 'orderby', 'meta_value' );
break;
case 'gso_year':
$query->set( 'meta_key', self::META_YEAR );
$query->set( 'orderby', 'meta_value_num' );
break;
case 'gso_featured':
$query->set( 'meta_key', self::META_FEATURED );
$query->set( 'orderby', 'meta_value_num' );
break;
case 'menu_order':
$query->set( 'orderby', 'menu_order title' );
break;
}
}
/**
* Add type and technology filters above the project list.
*
* @param string $post_type Current list post type.
* @return void
*/
public static function taxonomy_filters( $post_type ) {
if ( self::POST_TYPE !== $post_type ) {
return;
}
foreach ( array( self::TAX_TYPE, self::TAX_TECH ) as $taxonomy ) {
$tax_object = get_taxonomy( $taxonomy );
if ( ! $tax_object ) {
continue;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only list filter.
$selected = isset( $_GET[ $taxonomy ] ) ? sanitize_title( wp_unslash( $_GET[ $taxonomy ] ) ) : '';
wp_dropdown_categories(
array(
'show_option_all' => sprintf(
/* translators: %s: taxonomy label. */
__( 'Todos: %s', 'gustavoo-portfolio-core' ),
$tax_object->labels->name
),
'taxonomy' => $taxonomy,
'name' => $taxonomy,
'orderby' => 'name',
'selected' => $selected,
'hide_empty' => false,
'hierarchical' => $tax_object->hierarchical,
'value_field' => 'slug',
)
);
}
}
}