first commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Gustavo Portfolio Core
|
||||
* Plugin URI: https://gustavoo.me/
|
||||
* Description: Conteúdo, configurações e integrações do portfólio de Gustavo Oliveira.
|
||||
* Version: 1.6.0
|
||||
* Requires at least: 6.5
|
||||
* Requires PHP: 8.0
|
||||
* Requires Plugins: fluentform, fluent-crm, fluent-smtp
|
||||
* Author: Gustavo Oliveira
|
||||
* Author URI: https://gustavoo.me/
|
||||
* Text Domain: gustavoo-portfolio-core
|
||||
* License: GPL-2.0-or-later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
*
|
||||
* @package GustavoPortfolioCore
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
define( 'GSO_PORTFOLIO_CORE_VERSION', '1.6.0' );
|
||||
define( 'GSO_PORTFOLIO_CORE_FILE', __FILE__ );
|
||||
define( 'GSO_PORTFOLIO_CORE_DIR', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
require_once GSO_PORTFOLIO_CORE_DIR . 'includes/class-gso-projects.php';
|
||||
require_once GSO_PORTFOLIO_CORE_DIR . 'includes/class-gso-portfolio-settings.php';
|
||||
require_once GSO_PORTFOLIO_CORE_DIR . 'includes/class-gso-newsletter-widget.php';
|
||||
require_once GSO_PORTFOLIO_CORE_DIR . 'includes/class-gso-seeder.php';
|
||||
require_once GSO_PORTFOLIO_CORE_DIR . 'includes/class-gso-portfolio-core.php';
|
||||
|
||||
register_activation_hook( __FILE__, array( 'GSO_Portfolio_Core', 'activate' ) );
|
||||
register_deactivation_hook( __FILE__, array( 'GSO_Portfolio_Core', 'deactivate' ) );
|
||||
|
||||
GSO_Portfolio_Core::init();
|
||||
|
||||
if ( ! function_exists( 'gso_portfolio_get_settings' ) ) {
|
||||
/**
|
||||
* Return all portfolio settings merged with defaults.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function gso_portfolio_get_settings() {
|
||||
return GSO_Portfolio_Settings::get_settings();
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'gso_portfolio_get_setting' ) ) {
|
||||
/**
|
||||
* Return one portfolio setting.
|
||||
*
|
||||
* @param string $key Setting key.
|
||||
* @param mixed $default Fallback value.
|
||||
* @return mixed
|
||||
*/
|
||||
function gso_portfolio_get_setting( $key, $default = null ) {
|
||||
$settings = gso_portfolio_get_settings();
|
||||
|
||||
return array_key_exists( $key, $settings ) ? $settings[ $key ] : $default;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'gso_render_fluent_form' ) ) {
|
||||
/**
|
||||
* Render a Fluent Form using a trusted numeric ID.
|
||||
*
|
||||
* @param int $form_id Fluent Forms form ID.
|
||||
* @return string
|
||||
*/
|
||||
function gso_render_fluent_form( $form_id ) {
|
||||
$form_id = absint( $form_id );
|
||||
|
||||
if ( ! $form_id || ! shortcode_exists( 'fluentform' ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return do_shortcode( sprintf( '[fluentform id="%d"]', $form_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'gso_portfolio_render_fluent_form' ) ) {
|
||||
/**
|
||||
* Backwards-compatible descriptive alias for the form helper.
|
||||
*
|
||||
* @param int $form_id Fluent Forms form ID.
|
||||
* @return string
|
||||
*/
|
||||
function gso_portfolio_render_fluent_form( $form_id ) {
|
||||
return gso_render_fluent_form( $form_id );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
/**
|
||||
* Newsletter widget powered by Fluent Forms and FluentCRM.
|
||||
*
|
||||
* @package GustavoPortfolioCore
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Keeps the newsletter CTA available even if the presentation theme changes.
|
||||
*/
|
||||
final class GSO_Newsletter_Widget extends WP_Widget {
|
||||
/**
|
||||
* Register the widget.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'gso_newsletter_widget',
|
||||
__( 'Gustavo — Newsletter (FluentCRM)', 'gustavoo-portfolio-core' ),
|
||||
array(
|
||||
'classname' => 'widget_gso_newsletter',
|
||||
'description' => __( 'Exibe o formulário global de newsletter configurado no Portfólio.', 'gustavoo-portfolio-core' ),
|
||||
'customize_selective_refresh' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the widget.
|
||||
*
|
||||
* @param array<string, string> $args Sidebar wrappers.
|
||||
* @param array<string, mixed> $instance Saved settings.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ) {
|
||||
$settings = GSO_Portfolio_Settings::get_settings();
|
||||
$title = ! empty( $instance['title'] ) ? (string) $instance['title'] : (string) $settings['newsletter_heading'];
|
||||
$text = ! empty( $instance['text'] ) ? (string) $instance['text'] : (string) $settings['newsletter_text'];
|
||||
$form_id = ! empty( $instance['form_id'] ) ? absint( $instance['form_id'] ) : absint( $settings['newsletter_form_id'] );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper.
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper.
|
||||
echo esc_html( $title );
|
||||
echo $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper.
|
||||
}
|
||||
|
||||
if ( $text ) {
|
||||
printf( '<p class="widget_gso_newsletter__text">%s</p>', esc_html( $text ) );
|
||||
}
|
||||
|
||||
$form = gso_render_fluent_form( $form_id );
|
||||
if ( $form ) {
|
||||
echo $form; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is generated by Fluent Forms.
|
||||
} elseif ( current_user_can( 'manage_options' ) ) {
|
||||
printf(
|
||||
'<p><a href="%1$s">%2$s</a></p>',
|
||||
esc_url( admin_url( 'edit.php?post_type=' . GSO_Projects::POST_TYPE . '&page=' . GSO_Portfolio_Settings::PAGE_SLUG ) ),
|
||||
esc_html__( 'Configure o formulário de newsletter.', 'gustavoo-portfolio-core' )
|
||||
);
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper.
|
||||
}
|
||||
|
||||
/**
|
||||
* Render widget settings.
|
||||
*
|
||||
* @param array<string, mixed> $instance Saved settings.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ) {
|
||||
$title = isset( $instance['title'] ) ? (string) $instance['title'] : '';
|
||||
$text = isset( $instance['text'] ) ? (string) $instance['text'] : '';
|
||||
$form_id = isset( $instance['form_id'] ) ? absint( $instance['form_id'] ) : 0;
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Título:', 'gustavoo-portfolio-core' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>"><?php esc_html_e( 'Texto:', 'gustavoo-portfolio-core' ); ?></label>
|
||||
<textarea class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>" rows="4"><?php echo esc_textarea( $text ); ?></textarea>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'form_id' ) ); ?>"><?php esc_html_e( 'ID do Fluent Forms (opcional):', 'gustavoo-portfolio-core' ); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'form_id' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'form_id' ) ); ?>" type="number" min="0" step="1" value="<?php echo esc_attr( (string) $form_id ); ?>">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget settings.
|
||||
*
|
||||
* @param array<string, mixed> $new_instance New values.
|
||||
* @param array<string, mixed> $old_instance Previous values.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ) {
|
||||
unset( $old_instance );
|
||||
|
||||
return array(
|
||||
'title' => isset( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '',
|
||||
'text' => isset( $new_instance['text'] ) ? sanitize_textarea_field( $new_instance['text'] ) : '',
|
||||
'form_id' => isset( $new_instance['form_id'] ) ? absint( $new_instance['form_id'] ) : 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Main plugin coordinator.
|
||||
*
|
||||
* @package GustavoPortfolioCore
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Coordinates the plugin modules.
|
||||
*/
|
||||
final class GSO_Portfolio_Core {
|
||||
/**
|
||||
* Whether hooks have already been registered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private static $initialized = false;
|
||||
|
||||
/**
|
||||
* Register plugin hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
if ( self::$initialized ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::$initialized = true;
|
||||
|
||||
add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) );
|
||||
|
||||
GSO_Projects::init();
|
||||
GSO_Portfolio_Settings::init();
|
||||
GSO_Seeder::init();
|
||||
|
||||
add_action( 'widgets_init', array( __CLASS__, 'register_widgets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load translations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function load_textdomain() {
|
||||
load_plugin_textdomain(
|
||||
'gustavoo-portfolio-core',
|
||||
false,
|
||||
dirname( plugin_basename( GSO_PORTFOLIO_CORE_FILE ) ) . '/languages'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register bundled widgets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_widgets() {
|
||||
if ( class_exists( 'Gustavao_Portfolio_Newsletter_Widget' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
register_widget( 'GSO_Newsletter_Widget' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run activation tasks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate() {
|
||||
GSO_Projects::register_content_types();
|
||||
GSO_Portfolio_Settings::ensure_defaults();
|
||||
GSO_Seeder::activate();
|
||||
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh rewrite rules without deleting content or settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate() {
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
<?php
|
||||
/**
|
||||
* Portfolio settings screen and option contract.
|
||||
*
|
||||
* @package GustavoPortfolioCore
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Owns the shared option consumed by the portfolio theme.
|
||||
*/
|
||||
final class GSO_Portfolio_Settings {
|
||||
const OPTION_NAME = 'gustavoo_portfolio_settings';
|
||||
const OPTION_GROUP = 'gustavoo_portfolio_settings_group';
|
||||
const PAGE_SLUG = 'gso-portfolio-settings';
|
||||
|
||||
/**
|
||||
* Register hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'admin_menu', array( __CLASS__, 'add_settings_page' ), 20 );
|
||||
add_action( 'admin_init', array( __CLASS__, 'register_settings' ) );
|
||||
add_filter( 'plugin_action_links_' . plugin_basename( GSO_PORTFOLIO_CORE_FILE ), array( __CLASS__, 'plugin_action_links' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults based on the existing gustavoo.me content.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function defaults() {
|
||||
return array(
|
||||
'hero_eyebrow' => __( 'Desenvolvimento de ponta a ponta', 'gustavoo-portfolio-core' ),
|
||||
'hero_title' => __( 'Transformo ideias em produtos digitais sólidos.', 'gustavoo-portfolio-core' ),
|
||||
'hero_description' => __( 'Gustavo, Desenvolvedor Full Stack Sênior. Web, mobile, infraestrutura e automação para tirar projetos do papel e fazê-los crescer.', 'gustavoo-portfolio-core' ),
|
||||
'hero_primary_label' => __( 'Ver projetos', 'gustavoo-portfolio-core' ),
|
||||
'hero_primary_url' => '#projetos',
|
||||
'hero_secondary_label' => __( 'Fale comigo', 'gustavoo-portfolio-core' ),
|
||||
'hero_secondary_url' => '#contato',
|
||||
'about_title' => __( 'Responsabilidade técnica do planejamento à produção.', 'gustavoo-portfolio-core' ),
|
||||
'about_text' => __( 'Mais do que escrever código, meu objetivo é garantir que seu projeto saia do papel e funcione exatamente como planejado. Assumo a responsabilidade técnica de ponta a ponta para que você foque no que importa: crescer.', 'gustavoo-portfolio-core' ),
|
||||
'about_secondary_text' => __( 'Já participei do desenvolvimento e lançamento de múltiplos projetos digitais, atuando da arquitetura técnica à divulgação e ao crescimento das plataformas. Essa combinação de visão técnica e entendimento de mercado orienta cada entrega.', 'gustavoo-portfolio-core' ),
|
||||
'contact_heading' => __( 'Tem um projeto em mente?', 'gustavoo-portfolio-core' ),
|
||||
'contact_text' => __( 'Conte o que você precisa construir ou melhorar. Responderei com os próximos passos para transformar a ideia em uma entrega concreta.', 'gustavoo-portfolio-core' ),
|
||||
'contact_form_id' => 0,
|
||||
'newsletter_heading' => __( 'Ideias úteis, direto na sua caixa de entrada.', 'gustavoo-portfolio-core' ),
|
||||
'newsletter_text' => __( 'Receba conteúdos sobre desenvolvimento, produto, infraestrutura e automação. Sem ruído e sem spam.', 'gustavoo-portfolio-core' ),
|
||||
'newsletter_form_id' => 0,
|
||||
'projects_title' => __( 'Projetos selecionados', 'gustavoo-portfolio-core' ),
|
||||
'projects_text' => __( 'Uma seleção de produtos, experiências e soluções que ajudei a colocar no mundo.', 'gustavoo-portfolio-core' ),
|
||||
'projects_limit' => 6,
|
||||
'blog_title' => __( 'Código, produto e bastidores', 'gustavoo-portfolio-core' ),
|
||||
'blog_text' => __( 'Análises práticas sobre desenvolvimento, infraestrutura, automação e crescimento de produtos digitais.', 'gustavoo-portfolio-core' ),
|
||||
'blog_limit' => 4,
|
||||
'email' => 'contato@gustavoo.me',
|
||||
'whatsapp' => '+55 31 99516-8069',
|
||||
'whatsapp_message' => __( 'Olá! Gostaria de falar sobre um projeto.', 'gustavoo-portfolio-core' ),
|
||||
'github_url' => 'https://github.com/gustavooth',
|
||||
'linkedin_url' => '',
|
||||
'instagram_url' => '',
|
||||
'availability_label' => __( 'Disponível para novos projetos', 'gustavoo-portfolio-core' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing defaults without overwriting existing values.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ensure_defaults() {
|
||||
$existing = get_option( self::OPTION_NAME, null );
|
||||
|
||||
if ( null === $existing || ! is_array( $existing ) ) {
|
||||
add_option( self::OPTION_NAME, self::defaults(), '', false );
|
||||
return;
|
||||
}
|
||||
|
||||
$merged = array_merge( self::defaults(), $existing );
|
||||
if ( $merged !== $existing ) {
|
||||
update_option( self::OPTION_NAME, $merged, false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return settings merged with defaults.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function get_settings() {
|
||||
$settings = get_option( self::OPTION_NAME, array() );
|
||||
|
||||
return array_merge( self::defaults(), is_array( $settings ) ? $settings : array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Settings submenu below Portfolio.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_settings_page() {
|
||||
add_submenu_page(
|
||||
'edit.php?post_type=' . GSO_Projects::POST_TYPE,
|
||||
__( 'Configurações do portfólio', 'gustavoo-portfolio-core' ),
|
||||
__( 'Configurações', 'gustavoo-portfolio-core' ),
|
||||
'manage_options',
|
||||
self::PAGE_SLUG,
|
||||
array( __CLASS__, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the option, sections and fields.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_settings() {
|
||||
register_setting(
|
||||
self::OPTION_GROUP,
|
||||
self::OPTION_NAME,
|
||||
array(
|
||||
'type' => 'array',
|
||||
'description' => __( 'Configurações compartilhadas pelo tema Gustavo Portfolio.', 'gustavoo-portfolio-core' ),
|
||||
'sanitize_callback' => array( __CLASS__, 'sanitize_settings' ),
|
||||
'default' => self::defaults(),
|
||||
)
|
||||
);
|
||||
|
||||
$sections = self::sections();
|
||||
$fields = self::fields();
|
||||
|
||||
foreach ( $sections as $section_id => $section ) {
|
||||
add_settings_section(
|
||||
$section_id,
|
||||
$section['title'],
|
||||
array( __CLASS__, 'render_section_description' ),
|
||||
self::PAGE_SLUG,
|
||||
array( 'description' => $section['description'] )
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $fields as $key => $field ) {
|
||||
add_settings_field(
|
||||
'gso-setting-' . str_replace( '_', '-', $key ),
|
||||
$field['label'],
|
||||
array( __CLASS__, 'render_field' ),
|
||||
self::PAGE_SLUG,
|
||||
$field['section'],
|
||||
array_merge( $field, array( 'key' => $key ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings page section definitions.
|
||||
*
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
private static function sections() {
|
||||
return array(
|
||||
'gso-settings-hero' => array(
|
||||
'title' => __( 'Hero', 'gustavoo-portfolio-core' ),
|
||||
'description' => __( 'Mensagem principal e chamadas para ação da página inicial.', 'gustavoo-portfolio-core' ),
|
||||
),
|
||||
'gso-settings-about' => array(
|
||||
'title' => __( 'Sobre', 'gustavoo-portfolio-core' ),
|
||||
'description' => __( 'Apresentação profissional exibida na seção Sobre.', 'gustavoo-portfolio-core' ),
|
||||
),
|
||||
'gso-settings-content' => array(
|
||||
'title' => __( 'Projetos e blog', 'gustavoo-portfolio-core' ),
|
||||
'description' => __( 'Títulos, textos de apoio e limites das listagens da página inicial.', 'gustavoo-portfolio-core' ),
|
||||
),
|
||||
'gso-settings-contact' => array(
|
||||
'title' => __( 'Contato', 'gustavoo-portfolio-core' ),
|
||||
'description' => __( 'Canais diretos e formulário principal do Fluent Forms.', 'gustavoo-portfolio-core' ),
|
||||
),
|
||||
'gso-settings-newsletter' => array(
|
||||
'title' => __( 'Newsletter', 'gustavoo-portfolio-core' ),
|
||||
'description' => __( 'Conteúdo e formulário usados pelo CTA e pelo widget de newsletter.', 'gustavoo-portfolio-core' ),
|
||||
),
|
||||
'gso-settings-social' => array(
|
||||
'title' => __( 'Redes sociais', 'gustavoo-portfolio-core' ),
|
||||
'description' => __( 'Perfis públicos exibidos no tema.', 'gustavoo-portfolio-core' ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings field schema.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private static function fields() {
|
||||
return array(
|
||||
'hero_eyebrow' => self::field( 'gso-settings-hero', __( 'Sobretítulo', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'hero_title' => self::field( 'gso-settings-hero', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'hero_description' => self::field( 'gso-settings-hero', __( 'Descrição', 'gustavoo-portfolio-core' ), 'textarea' ),
|
||||
'hero_primary_label' => self::field( 'gso-settings-hero', __( 'CTA principal — texto', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'hero_primary_url' => self::field( 'gso-settings-hero', __( 'CTA principal — destino', 'gustavoo-portfolio-core' ), 'link', __( 'Aceita URL HTTP(S), mailto:, tel:, caminho relativo ou âncora como #projetos.', 'gustavoo-portfolio-core' ) ),
|
||||
'hero_secondary_label' => self::field( 'gso-settings-hero', __( 'CTA secundário — texto', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'hero_secondary_url' => self::field( 'gso-settings-hero', __( 'CTA secundário — destino', 'gustavoo-portfolio-core' ), 'link', __( 'Aceita URL HTTP(S), mailto:, tel:, caminho relativo ou âncora.', 'gustavoo-portfolio-core' ) ),
|
||||
'about_title' => self::field( 'gso-settings-about', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'about_text' => self::field( 'gso-settings-about', __( 'Texto', 'gustavoo-portfolio-core' ), 'textarea' ),
|
||||
'projects_title' => self::field( 'gso-settings-content', __( 'Projetos — título', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'projects_text' => self::field( 'gso-settings-content', __( 'Projetos — texto', 'gustavoo-portfolio-core' ), 'textarea' ),
|
||||
'projects_limit' => self::field( 'gso-settings-content', __( 'Projetos — quantidade', 'gustavoo-portfolio-core' ), 'number', __( 'Entre 1 e 24.', 'gustavoo-portfolio-core' ), array( 'min' => 1, 'max' => 24 ) ),
|
||||
'blog_title' => self::field( 'gso-settings-content', __( 'Blog — título', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'blog_text' => self::field( 'gso-settings-content', __( 'Blog — texto', 'gustavoo-portfolio-core' ), 'textarea' ),
|
||||
'blog_limit' => self::field( 'gso-settings-content', __( 'Blog — quantidade', 'gustavoo-portfolio-core' ), 'number', __( 'Entre 1 e 24.', 'gustavoo-portfolio-core' ), array( 'min' => 1, 'max' => 24 ) ),
|
||||
'contact_heading' => self::field( 'gso-settings-contact', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'contact_text' => self::field( 'gso-settings-contact', __( 'Texto', 'gustavoo-portfolio-core' ), 'textarea' ),
|
||||
'contact_form_id' => self::field( 'gso-settings-contact', __( 'ID do formulário de contato', 'gustavoo-portfolio-core' ), 'number', __( 'ID numérico do Fluent Forms. O seed preenche este campo quando possível.', 'gustavoo-portfolio-core' ), array( 'min' => 0 ) ),
|
||||
'email' => self::field( 'gso-settings-contact', __( 'E-mail', 'gustavoo-portfolio-core' ), 'email' ),
|
||||
'whatsapp' => self::field( 'gso-settings-contact', __( 'WhatsApp', 'gustavoo-portfolio-core' ), 'tel', __( 'Inclua o DDI, por exemplo +55 31 99999-9999.', 'gustavoo-portfolio-core' ) ),
|
||||
'whatsapp_message' => self::field( 'gso-settings-contact', __( 'WhatsApp — mensagem inicial', 'gustavoo-portfolio-core' ), 'textarea', __( 'Texto que será preenchido automaticamente ao abrir o botão flutuante.', 'gustavoo-portfolio-core' ) ),
|
||||
'newsletter_heading' => self::field( 'gso-settings-newsletter', __( 'Título', 'gustavoo-portfolio-core' ), 'text' ),
|
||||
'newsletter_text' => self::field( 'gso-settings-newsletter', __( 'Texto', 'gustavoo-portfolio-core' ), 'textarea' ),
|
||||
'newsletter_form_id' => self::field( 'gso-settings-newsletter', __( 'ID do formulário de newsletter', 'gustavoo-portfolio-core' ), 'number', __( 'ID numérico do Fluent Forms. O seed preenche este campo quando possível.', 'gustavoo-portfolio-core' ), array( 'min' => 0 ) ),
|
||||
'github_url' => self::field( 'gso-settings-social', __( 'GitHub', 'gustavoo-portfolio-core' ), 'url' ),
|
||||
'linkedin_url' => self::field( 'gso-settings-social', __( 'LinkedIn', 'gustavoo-portfolio-core' ), 'url' ),
|
||||
'instagram_url' => self::field( 'gso-settings-social', __( 'Instagram', 'gustavoo-portfolio-core' ), 'url' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build one field definition.
|
||||
*
|
||||
* @param string $section Section ID.
|
||||
* @param string $label Label.
|
||||
* @param string $type Field type.
|
||||
* @param string $description Help text.
|
||||
* @param array<string, mixed> $extra Extra attributes.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function field( $section, $label, $type, $description = '', $extra = array() ) {
|
||||
return array_merge(
|
||||
array(
|
||||
'section' => $section,
|
||||
'label' => $label,
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
),
|
||||
$extra
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize every documented setting and retain unknown extension keys.
|
||||
*
|
||||
* @param mixed $input Raw submitted option.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function sanitize_settings( $input ) {
|
||||
$current = get_option( self::OPTION_NAME, array() );
|
||||
$output = array_merge( self::defaults(), is_array( $current ) ? $current : array() );
|
||||
$input = is_array( $input ) ? $input : array();
|
||||
|
||||
foreach ( self::fields() as $key => $field ) {
|
||||
$value = $input[ $key ] ?? '';
|
||||
|
||||
switch ( $field['type'] ) {
|
||||
case 'textarea':
|
||||
$output[ $key ] = sanitize_textarea_field( $value );
|
||||
break;
|
||||
case 'email':
|
||||
$output[ $key ] = sanitize_email( $value );
|
||||
break;
|
||||
case 'tel':
|
||||
$output[ $key ] = self::sanitize_phone( $value );
|
||||
break;
|
||||
case 'url':
|
||||
$output[ $key ] = self::sanitize_public_url( $value );
|
||||
break;
|
||||
case 'link':
|
||||
$output[ $key ] = self::sanitize_link( $value );
|
||||
break;
|
||||
case 'number':
|
||||
$number = absint( $value );
|
||||
$min = isset( $field['min'] ) ? absint( $field['min'] ) : 0;
|
||||
$max = isset( $field['max'] ) ? absint( $field['max'] ) : PHP_INT_MAX;
|
||||
$output[ $key ] = min( $max, max( $min, $number ) );
|
||||
break;
|
||||
default:
|
||||
$output[ $key ] = sanitize_text_field( $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a public social URL.
|
||||
*
|
||||
* @param mixed $value Raw value.
|
||||
* @return string
|
||||
*/
|
||||
private static function sanitize_public_url( $value ) {
|
||||
$url = esc_url_raw( trim( (string) $value ), array( 'http', 'https' ) );
|
||||
|
||||
return $url && wp_http_validate_url( $url ) ? $url : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a CTA link, including safe same-page anchors.
|
||||
*
|
||||
* @param mixed $value Raw value.
|
||||
* @return string
|
||||
*/
|
||||
private static function sanitize_link( $value ) {
|
||||
$value = trim( sanitize_text_field( $value ) );
|
||||
|
||||
if ( preg_match( '/^#[A-Za-z][A-Za-z0-9_:.\-]*$/', $value ) ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return esc_url_raw( $value, array( 'http', 'https', 'mailto', 'tel' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep common telephone punctuation and discard everything else.
|
||||
*
|
||||
* @param mixed $value Raw phone value.
|
||||
* @return string
|
||||
*/
|
||||
private static function sanitize_phone( $value ) {
|
||||
$value = sanitize_text_field( $value );
|
||||
$value = preg_replace( '/[^0-9+()\-\s]/', '', $value );
|
||||
|
||||
return is_string( $value ) ? trim( $value ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a section introduction.
|
||||
*
|
||||
* @param array<string, mixed> $args Section arguments.
|
||||
* @return void
|
||||
*/
|
||||
public static function render_section_description( $args ) {
|
||||
if ( ! empty( $args['description'] ) ) {
|
||||
echo '<p>' . esc_html( $args['description'] ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a settings field from its schema.
|
||||
*
|
||||
* @param array<string, mixed> $args Field arguments.
|
||||
* @return void
|
||||
*/
|
||||
public static function render_field( $args ) {
|
||||
$settings = self::get_settings();
|
||||
$key = $args['key'];
|
||||
$value = $settings[ $key ] ?? '';
|
||||
$id = 'gso-setting-' . str_replace( '_', '-', $key );
|
||||
$name = self::OPTION_NAME . '[' . $key . ']';
|
||||
|
||||
if ( 'textarea' === $args['type'] ) {
|
||||
printf(
|
||||
'<textarea class="large-text" id="%1$s" name="%2$s" rows="4">%3$s</textarea>',
|
||||
esc_attr( $id ),
|
||||
esc_attr( $name ),
|
||||
esc_textarea( (string) $value )
|
||||
);
|
||||
} else {
|
||||
$html_type = in_array( $args['type'], array( 'email', 'number', 'tel', 'url' ), true ) ? $args['type'] : 'text';
|
||||
$attrs = '';
|
||||
|
||||
if ( isset( $args['min'] ) ) {
|
||||
$attrs .= ' min="' . esc_attr( (string) $args['min'] ) . '"';
|
||||
}
|
||||
if ( isset( $args['max'] ) ) {
|
||||
$attrs .= ' max="' . esc_attr( (string) $args['max'] ) . '"';
|
||||
}
|
||||
|
||||
printf(
|
||||
'<input class="%1$s" id="%2$s" name="%3$s" type="%4$s" value="%5$s"%6$s>',
|
||||
'number' === $html_type ? 'small-text' : 'regular-text',
|
||||
esc_attr( $id ),
|
||||
esc_attr( $name ),
|
||||
esc_attr( $html_type ),
|
||||
esc_attr( (string) $value ),
|
||||
$attrs // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attributes are assembled from escaped integers.
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $args['description'] ) ) {
|
||||
echo '<p class="description">' . esc_html( $args['description'] ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the admin settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_settings_page() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'Configurações do portfólio', 'gustavoo-portfolio-core' ); ?></h1>
|
||||
<p><?php esc_html_e( 'Estes valores são compartilhados com o tema e ficam preservados ao trocar a apresentação visual.', 'gustavoo-portfolio-core' ); ?></p>
|
||||
<?php self::render_seed_status(); ?>
|
||||
<form action="options.php" method="post">
|
||||
<?php
|
||||
settings_fields( self::OPTION_GROUP );
|
||||
do_settings_sections( self::PAGE_SLUG );
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a concise status for optional form integrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function render_seed_status() {
|
||||
$state = GSO_Seeder::get_state();
|
||||
$fluent_forms = class_exists( '\\FluentForm\\App\\Models\\Form' );
|
||||
$fluent_crm = function_exists( 'FluentCrmApi' );
|
||||
$contact_id = absint( $state['contact_form_id'] ?? 0 );
|
||||
$newsletter_id = absint( $state['newsletter_form_id'] ?? 0 );
|
||||
$feeds_installed = ! empty( $state['crm_feeds'] );
|
||||
?>
|
||||
<div class="notice notice-info inline">
|
||||
<p><strong><?php esc_html_e( 'Integrações automáticas', 'gustavoo-portfolio-core' ); ?></strong></p>
|
||||
<ul>
|
||||
<li><?php echo esc_html( sprintf( 'Fluent Forms: %s', $fluent_forms ? __( 'ativo', 'gustavoo-portfolio-core' ) : __( 'inativo', 'gustavoo-portfolio-core' ) ) ); ?></li>
|
||||
<li><?php echo esc_html( sprintf( 'Formulário de contato: %s', $contact_id ? '#' . $contact_id : __( 'aguardando', 'gustavoo-portfolio-core' ) ) ); ?></li>
|
||||
<li><?php echo esc_html( sprintf( 'Formulário de newsletter: %s', $newsletter_id ? '#' . $newsletter_id : __( 'aguardando', 'gustavoo-portfolio-core' ) ) ); ?></li>
|
||||
<li><?php echo esc_html( sprintf( 'FluentCRM: %s', $fluent_crm ? ( $feeds_installed ? __( 'feeds configurados', 'gustavoo-portfolio-core' ) : __( 'ativo; configuração pendente', 'gustavoo-portfolio-core' ) ) : __( 'inativo', 'gustavoo-portfolio-core' ) ) ); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shortcut from the Plugins screen.
|
||||
*
|
||||
* @param array<int, string> $links Existing links.
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function plugin_action_links( $links ) {
|
||||
$url = admin_url( 'edit.php?post_type=' . GSO_Projects::POST_TYPE . '&page=' . self::PAGE_SLUG );
|
||||
|
||||
array_unshift(
|
||||
$links,
|
||||
'<a href="' . esc_url( $url ) . '">' . esc_html__( 'Configurações', 'gustavoo-portfolio-core' ) . '</a>'
|
||||
);
|
||||
|
||||
return $links;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
<?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',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
<?php
|
||||
/**
|
||||
* Idempotent starter content and Fluent Forms integration.
|
||||
*
|
||||
* @package GustavoPortfolioCore
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Creates useful starter content once, without overwriting later admin edits.
|
||||
*/
|
||||
final class GSO_Seeder {
|
||||
const VERSION = '1.6.0';
|
||||
const STATE_OPTION = 'gso_portfolio_seed_state';
|
||||
const LOCK_OPTION = 'gso_portfolio_seed_lock';
|
||||
const SITE_OPTION = 'gso_portfolio_site_state';
|
||||
const PROJECT_META = '_gso_seed_key';
|
||||
const FORM_META = '_gustavoo_seed_key';
|
||||
const FEED_META = '_gustavoo_seed_fluentcrm_feed_id';
|
||||
|
||||
/**
|
||||
* Register deferred integration setup.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'init', array( __CLASS__, 'maybe_seed' ), 20 );
|
||||
add_action( 'after_switch_theme', array( __CLASS__, 'maybe_seed' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed content available during activation. Fluent integrations run on init.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate() {
|
||||
self::seed_site_configuration();
|
||||
self::seed_projects();
|
||||
|
||||
$state = self::get_state();
|
||||
$state['projects_seeded'] = true;
|
||||
$state['status'] = 'pending';
|
||||
update_option( self::STATE_OPTION, $state, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stable status contract for the admin screen.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function get_state() {
|
||||
$state = get_option( self::STATE_OPTION, array() );
|
||||
|
||||
return wp_parse_args(
|
||||
is_array( $state ) ? $state : array(),
|
||||
array(
|
||||
'version' => '',
|
||||
'status' => 'pending',
|
||||
'projects_seeded' => false,
|
||||
'contact_form_id' => 0,
|
||||
'newsletter_form_id' => 0,
|
||||
'crm_feeds' => false,
|
||||
'message' => '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete integration setup when Fluent Forms and FluentCRM are ready.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function maybe_seed() {
|
||||
self::seed_site_configuration();
|
||||
|
||||
$state = self::get_state();
|
||||
|
||||
if ( self::VERSION === $state['version'] && 'complete' === $state['status'] ) {
|
||||
self::maybe_migrate_contact_form();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! post_type_exists( GSO_Projects::POST_TYPE ) ) {
|
||||
GSO_Projects::register_content_types();
|
||||
}
|
||||
|
||||
self::seed_projects();
|
||||
|
||||
if ( ! self::fluent_is_ready() ) {
|
||||
$state['projects_seeded'] = true;
|
||||
$state['status'] = 'waiting';
|
||||
$state['message'] = __( 'Aguardando Fluent Forms e FluentCRM ativos.', 'gustavoo-portfolio-core' );
|
||||
update_option( self::STATE_OPTION, $state, false );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! add_option( self::LOCK_OPTION, time(), '', false ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$contact_id = self::upsert_form( 'contact', __( 'Site — Contato e orçamento', 'gustavoo-portfolio-core' ), true );
|
||||
$newsletter_id = self::upsert_form( 'newsletter', __( 'Site — Newsletter', 'gustavoo-portfolio-core' ), false );
|
||||
|
||||
self::upsert_crm_feed( $contact_id, 'contact' );
|
||||
self::upsert_crm_feed( $newsletter_id, 'newsletter' );
|
||||
self::save_form_ids( $contact_id, $newsletter_id );
|
||||
|
||||
update_option(
|
||||
self::STATE_OPTION,
|
||||
array(
|
||||
'version' => self::VERSION,
|
||||
'status' => 'complete',
|
||||
'projects_seeded' => true,
|
||||
'contact_form_id' => $contact_id,
|
||||
'newsletter_form_id' => $newsletter_id,
|
||||
'crm_feeds' => true,
|
||||
'message' => __( 'Formulários e feeds do FluentCRM configurados.', 'gustavoo-portfolio-core' ),
|
||||
),
|
||||
false
|
||||
);
|
||||
} catch ( Throwable $error ) {
|
||||
$state['status'] = 'error';
|
||||
$state['message'] = sanitize_text_field( $error->getMessage() );
|
||||
update_option( self::STATE_OPTION, $state, false );
|
||||
} finally {
|
||||
delete_option( self::LOCK_OPTION );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the WordPress pieces that make the bundled theme work on a
|
||||
* fresh installation.
|
||||
*
|
||||
* Existing sites are left untouched: each value is written only when the
|
||||
* corresponding WordPress setting still has its fresh-install value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function seed_site_configuration() {
|
||||
$state = get_option( self::SITE_OPTION, array() );
|
||||
|
||||
if ( ! is_array( $state ) || empty( $state['pages'] ) ) {
|
||||
$home_id = self::ensure_seed_page( 'inicio', __( 'Início', 'gustavoo-portfolio-core' ) );
|
||||
$blog_id = self::ensure_seed_page( 'blog', __( 'Blog', 'gustavoo-portfolio-core' ) );
|
||||
|
||||
if ( $home_id && $blog_id && 'posts' === get_option( 'show_on_front' ) ) {
|
||||
update_option( 'show_on_front', 'page' );
|
||||
update_option( 'page_on_front', $home_id );
|
||||
update_option( 'page_for_posts', $blog_id );
|
||||
}
|
||||
|
||||
if ( $home_id && $blog_id ) {
|
||||
$state = is_array( $state ) ? $state : array();
|
||||
$state['pages'] = true;
|
||||
update_option( self::SITE_OPTION, $state, false );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $state['widgets'] ) ) {
|
||||
self::seed_theme_widgets();
|
||||
$state = is_array( $state ) ? $state : array();
|
||||
$state['widgets'] = true;
|
||||
update_option( self::SITE_OPTION, $state, false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a required empty page once and identify it by a private marker.
|
||||
*
|
||||
* @param string $slug Page slug.
|
||||
* @param string $title Page title.
|
||||
* @return int
|
||||
*/
|
||||
private static function ensure_seed_page( $slug, $title ) {
|
||||
$existing = get_posts(
|
||||
array(
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_key' => '_gso_seed_page',
|
||||
'meta_value' => $slug,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $existing ) {
|
||||
return absint( $existing[0] );
|
||||
}
|
||||
|
||||
$page = get_page_by_path( $slug, OBJECT, 'page' );
|
||||
if ( $page ) {
|
||||
update_post_meta( $page->ID, '_gso_seed_page', $slug );
|
||||
return absint( $page->ID );
|
||||
}
|
||||
|
||||
$page_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => 'page',
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $title,
|
||||
'post_name' => $slug,
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
if ( is_wp_error( $page_id ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
update_post_meta( $page_id, '_gso_seed_page', $slug );
|
||||
return absint( $page_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reproduce the sidebar and footer widget arrangement from this site.
|
||||
* Widgets are added only to empty areas, so a user's arrangement wins.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function seed_theme_widgets() {
|
||||
$sidebars = wp_get_sidebars_widgets();
|
||||
$sidebars = is_array( $sidebars ) ? $sidebars : array();
|
||||
|
||||
$blog_widgets = isset( $sidebars['sidebar-blog'] ) ? (array) $sidebars['sidebar-blog'] : array();
|
||||
$can_seed_blog = empty( $blog_widgets ) || array( 'gustavoo_portfolio_newsletter-1' ) === $blog_widgets;
|
||||
|
||||
if ( $can_seed_blog ) {
|
||||
$newsletter = get_option( 'widget_gustavoo_portfolio_newsletter', array() );
|
||||
$newsletter = is_array( $newsletter ) ? $newsletter : array();
|
||||
$newsletter[1] = array(
|
||||
'title' => __( 'Newsletter', 'gustavoo-portfolio-core' ),
|
||||
'text' => __( 'Receba novos artigos sobre desenvolvimento, infraestrutura e automação.', 'gustavoo-portfolio-core' ),
|
||||
'form_id' => 0,
|
||||
);
|
||||
$newsletter['_multiwidget'] = 1;
|
||||
update_option( 'widget_gustavoo_portfolio_newsletter', $newsletter, false );
|
||||
|
||||
$blocks = get_option( 'widget_block', array() );
|
||||
$blocks = is_array( $blocks ) ? $blocks : array();
|
||||
$blocks[2] = array( 'content' => '<!-- wp:search /-->' );
|
||||
$blocks[3] = array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>Posts recentes</h2><!-- /wp:heading --><!-- wp:latest-posts /--></div><!-- /wp:group -->' );
|
||||
$blocks[4] = array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>Comentários</h2><!-- /wp:heading --><!-- wp:latest-comments {"displayAvatar":false,"displayDate":false,"displayExcerpt":false} /--></div><!-- /wp:group -->' );
|
||||
$blocks[5] = array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>Arquivos</h2><!-- /wp:heading --><!-- wp:archives /--></div><!-- /wp:group -->' );
|
||||
$blocks[6] = array( 'content' => '<!-- wp:group --><div class="wp-block-group"><!-- wp:heading --><h2>Categorias</h2><!-- /wp:heading --><!-- wp:categories /--></div><!-- /wp:group -->' );
|
||||
$blocks['_multiwidget'] = 1;
|
||||
update_option( 'widget_block', $blocks, false );
|
||||
|
||||
$sidebars['sidebar-blog'] = array( 'gustavoo_portfolio_newsletter-1', 'block-2', 'block-3', 'block-4' );
|
||||
}
|
||||
|
||||
if ( empty( $sidebars['footer-1'] ) ) {
|
||||
$sidebars['footer-1'] = array( 'block-5', 'block-6' );
|
||||
}
|
||||
|
||||
update_option( 'sidebars_widgets', $sidebars, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair the seeded contact form when an earlier migration did not persist.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function maybe_migrate_contact_form() {
|
||||
if ( ! self::fluent_is_ready() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$form = self::find_seeded_form( 'contact' );
|
||||
|
||||
if ( ! $form ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = json_decode( (string) $form->form_fields, true );
|
||||
$has_message = false;
|
||||
|
||||
foreach ( (array) ( $fields['fields'] ?? array() ) as $field ) {
|
||||
if ( 'message' === (string) ( $field['attributes']['name'] ?? '' ) && 'textarea' === (string) ( $field['element'] ?? '' ) ) {
|
||||
$has_message = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $has_message ) {
|
||||
try {
|
||||
self::upsert_form( 'contact', __( 'Site — Contato e orçamento', 'gustavoo-portfolio-core' ), true );
|
||||
} catch ( Throwable $error ) {
|
||||
// Keep the front end available if Fluent Forms is temporarily unavailable.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the optional integrations without triggering autoload errors.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function fluent_is_ready() {
|
||||
return defined( 'FLUENTFORM' )
|
||||
&& defined( 'FLUENTCRM' )
|
||||
&& function_exists( 'fluentformLoadFile' )
|
||||
&& function_exists( 'FluentCrmApi' )
|
||||
&& class_exists( '\\FluentForm\\App\\Models\\Form' )
|
||||
&& class_exists( '\\FluentForm\\App\\Models\\FormMeta' )
|
||||
&& class_exists( '\\FluentForm\\App\\Services\\Form\\FormService' )
|
||||
&& class_exists( '\\FluentForm\\App\\Services\\Integrations\\FormIntegrationService' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or reuse a seed-owned Fluent Form.
|
||||
*
|
||||
* @param string $key Deterministic form key.
|
||||
* @param string $title Form title.
|
||||
* @param bool $include_whatsapp Include the contact message field.
|
||||
* @return int
|
||||
* @throws Exception When Fluent Forms cannot create the form.
|
||||
*/
|
||||
private static function upsert_form( $key, $title, $include_whatsapp ) {
|
||||
$form = self::find_seeded_form( $key );
|
||||
|
||||
$defaults = fluentformLoadFile( 'Services/FormBuilder/DefaultElements.php' );
|
||||
$blank = \FluentForm\App\Models\Form::resolvePredefinedForm(
|
||||
array(
|
||||
'predefined' => 'blank_form',
|
||||
'type' => 'form',
|
||||
)
|
||||
);
|
||||
$structure = json_decode( (string) $blank['form_fields'], true );
|
||||
|
||||
if ( ! is_array( $defaults ) || ! is_array( $structure ) ) {
|
||||
throw new Exception( 'Não foi possível carregar a estrutura padrão do Fluent Forms.' );
|
||||
}
|
||||
|
||||
$email = $defaults['general']['input_email'];
|
||||
$email['uniqElKey'] = 'el_gso_' . $key . '_email';
|
||||
$email['attributes']['name'] = 'email';
|
||||
$email['attributes']['placeholder'] = __( 'Seu melhor e-mail', 'gustavoo-portfolio-core' );
|
||||
$email['settings']['label'] = __( 'E-mail', 'gustavoo-portfolio-core' );
|
||||
$email['settings']['admin_field_label'] = __( 'E-mail', 'gustavoo-portfolio-core' );
|
||||
$email['settings']['validation_rules']['required']['value'] = true;
|
||||
|
||||
$fields = array( $email );
|
||||
|
||||
if ( $include_whatsapp ) {
|
||||
$whatsapp = $defaults['general']['input_mask'];
|
||||
$whatsapp['uniqElKey'] = 'el_gso_' . $key . '_whatsapp';
|
||||
$whatsapp['attributes']['name'] = 'whatsapp';
|
||||
$whatsapp['attributes']['placeholder'] = '(31) 99999-9999';
|
||||
$whatsapp['attributes']['data-mask'] = '(00) 00000-0000';
|
||||
$whatsapp['settings']['label'] = __( 'WhatsApp', 'gustavoo-portfolio-core' );
|
||||
$whatsapp['settings']['admin_field_label'] = __( 'WhatsApp', 'gustavoo-portfolio-core' );
|
||||
$whatsapp['settings']['mobile_keyboard_type'] = 'tel';
|
||||
$whatsapp['settings']['temp_mask'] = 'custom';
|
||||
$whatsapp['settings']['data-mask-reverse'] = 'no';
|
||||
$whatsapp['settings']['validation_rules']['required']['value'] = true;
|
||||
$fields[] = $whatsapp;
|
||||
|
||||
$message = $defaults['general']['textarea'];
|
||||
$message['uniqElKey'] = 'el_gso_' . $key . '_message';
|
||||
$message['attributes']['name'] = 'message';
|
||||
$message['attributes']['placeholder'] = __( 'Descreva o motivo do contato e o que você quer construir.', 'gustavoo-portfolio-core' );
|
||||
$message['attributes']['rows'] = 5;
|
||||
$message['settings']['label'] = __( 'Como posso ajudar?', 'gustavoo-portfolio-core' );
|
||||
$message['settings']['admin_field_label'] = __( 'Motivo do contato', 'gustavoo-portfolio-core' );
|
||||
$message['settings']['validation_rules']['required']['value'] = true;
|
||||
$fields[] = $message;
|
||||
}
|
||||
|
||||
$structure['fields'] = $fields;
|
||||
$structure['submitButton']['settings']['button_ui']['text'] = $include_whatsapp
|
||||
? __( 'Solicitar contato', 'gustavoo-portfolio-core' )
|
||||
: __( 'Quero receber', 'gustavoo-portfolio-core' );
|
||||
|
||||
$service = new \FluentForm\App\Services\Form\FormService();
|
||||
if ( $form ) {
|
||||
$service->update(
|
||||
array(
|
||||
'form_id' => $form->id,
|
||||
'title' => $title,
|
||||
'status' => 'published',
|
||||
'formFields' => wp_json_encode( $structure, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ),
|
||||
)
|
||||
);
|
||||
self::localize_form_confirmation( $form->id, $include_whatsapp );
|
||||
return absint( $form->id );
|
||||
}
|
||||
|
||||
$form = $service->store(
|
||||
array(
|
||||
'predefined' => 'blank_form',
|
||||
'type' => 'form',
|
||||
)
|
||||
);
|
||||
$form = $service->update(
|
||||
array(
|
||||
'form_id' => $form->id,
|
||||
'title' => $title,
|
||||
'status' => 'published',
|
||||
'formFields' => wp_json_encode( $structure, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ),
|
||||
)
|
||||
);
|
||||
|
||||
\FluentForm\App\Models\FormMeta::persist( $form->id, self::FORM_META, $key );
|
||||
self::localize_form_confirmation( $form->id, $include_whatsapp );
|
||||
|
||||
return absint( $form->id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate a form owned by this seed.
|
||||
*
|
||||
* @param string $key Seed key.
|
||||
* @return object|null
|
||||
*/
|
||||
private static function find_seeded_form( $key ) {
|
||||
$markers = \FluentForm\App\Models\FormMeta::where( 'meta_key', self::FORM_META )
|
||||
->where( 'value', $key )
|
||||
->get();
|
||||
|
||||
foreach ( $markers as $marker ) {
|
||||
$form = \FluentForm\App\Models\Form::find( absint( $marker->form_id ) );
|
||||
if ( $form ) {
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a concise Portuguese success message.
|
||||
*
|
||||
* @param int $form_id Form ID.
|
||||
* @param bool $is_contact_form Whether this is the lead form.
|
||||
* @return void
|
||||
*/
|
||||
private static function localize_form_confirmation( $form_id, $is_contact_form ) {
|
||||
$settings = \FluentForm\App\Models\FormMeta::retrieve( 'formSettings', $form_id, array() );
|
||||
$settings = is_array( $settings ) ? $settings : array();
|
||||
|
||||
$settings['confirmation'] = wp_parse_args(
|
||||
array(
|
||||
'redirectTo' => 'samePage',
|
||||
'messageToShow' => $is_contact_form
|
||||
? __( 'Recebi seus dados. Entrarei em contato em breve.', 'gustavoo-portfolio-core' )
|
||||
: __( 'Inscrição recebida. Confira seu e-mail para confirmar.', 'gustavoo-portfolio-core' ),
|
||||
'samePageFormBehavior' => 'hide_form',
|
||||
),
|
||||
isset( $settings['confirmation'] ) && is_array( $settings['confirmation'] ) ? $settings['confirmation'] : array()
|
||||
);
|
||||
|
||||
\FluentForm\App\Models\FormMeta::persist( $form_id, 'formSettings', $settings );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the FluentCRM feed for one seed-owned form.
|
||||
*
|
||||
* @param int $form_id Form ID.
|
||||
* @param string $context Contact or newsletter.
|
||||
* @return void
|
||||
*/
|
||||
private static function upsert_crm_feed( $form_id, $context ) {
|
||||
$existing_id = absint( \FluentForm\App\Models\FormMeta::retrieve( self::FEED_META, $form_id, 0 ) );
|
||||
$defaults = apply_filters( 'fluentform/get_integration_defaults_fluentcrm', array(), $form_id );
|
||||
$defaults = is_array( $defaults ) ? $defaults : array();
|
||||
$is_contact = 'contact' === $context;
|
||||
$feed = array_replace(
|
||||
$defaults,
|
||||
array(
|
||||
'name' => $is_contact ? 'Site / Contato' : 'Site / Newsletter',
|
||||
'email' => 'email',
|
||||
'enabled' => true,
|
||||
'double_opt_in' => ! $is_contact,
|
||||
'other_fields' => $is_contact
|
||||
? array(
|
||||
array(
|
||||
'label' => 'phone',
|
||||
'item_value' => '{inputs.whatsapp}',
|
||||
),
|
||||
)
|
||||
: array(),
|
||||
'conditionals' => array(
|
||||
'status' => false,
|
||||
'type' => 'all',
|
||||
'conditions' => array(),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$result = ( new \FluentForm\App\Services\Integrations\FormIntegrationService() )->update(
|
||||
array(
|
||||
'form_id' => $form_id,
|
||||
'integration_id' => $existing_id,
|
||||
'integration_name' => 'fluentcrm',
|
||||
'data_type' => 'array',
|
||||
'status' => true,
|
||||
'integration' => $feed,
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $result['integration_id'] ) ) {
|
||||
\FluentForm\App\Models\FormMeta::persist( $form_id, self::FEED_META, absint( $result['integration_id'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share form IDs with the theme settings option.
|
||||
*
|
||||
* @param int $contact_id Contact form ID.
|
||||
* @param int $newsletter_id Newsletter form ID.
|
||||
* @return void
|
||||
*/
|
||||
private static function save_form_ids( $contact_id, $newsletter_id ) {
|
||||
$settings = GSO_Portfolio_Settings::get_settings();
|
||||
$settings['contact_form_id'] = absint( $contact_id );
|
||||
$settings['newsletter_form_id'] = absint( $newsletter_id );
|
||||
update_option( GSO_Portfolio_Settings::OPTION_NAME, $settings, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add starter projects only when their deterministic markers are absent.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function seed_projects() {
|
||||
$projects = array(
|
||||
'aurelia-online' => array(
|
||||
'title' => 'Aurelia Online',
|
||||
'excerpt' => __( 'Plataforma online desenvolvida com foco em performance, experiência do usuário e evolução contínua.', 'gustavoo-portfolio-core' ),
|
||||
'type' => 'Plataforma web',
|
||||
'technologies' => array( 'Web', 'APIs', 'Infraestrutura' ),
|
||||
'featured' => true,
|
||||
),
|
||||
'lumenix-engine' => array(
|
||||
'title' => 'Lumenix Engine',
|
||||
'excerpt' => __( 'Engine e ecossistema técnico para experiências interativas e produtos digitais.', 'gustavoo-portfolio-core' ),
|
||||
'type' => 'Games',
|
||||
'technologies' => array( 'C++', 'Games', 'Performance' ),
|
||||
'featured' => true,
|
||||
),
|
||||
'aurabet' => array(
|
||||
'title' => 'AuraBet',
|
||||
'excerpt' => __( 'Produto digital com arquitetura, integrações e infraestrutura preparadas para escala.', 'gustavoo-portfolio-core' ),
|
||||
'type' => 'Produto digital',
|
||||
'technologies' => array( 'Full Stack', 'Docker', 'APIs' ),
|
||||
'featured' => true,
|
||||
),
|
||||
'visionforge' => array(
|
||||
'title' => 'VisionForge',
|
||||
'excerpt' => __( 'Solução criada para transformar processos complexos em uma experiência clara e eficiente.', 'gustavoo-portfolio-core' ),
|
||||
'type' => 'Software',
|
||||
'technologies' => array( 'Automação', 'Dados', 'Web' ),
|
||||
'featured' => true,
|
||||
),
|
||||
'papel-e-cor' => array(
|
||||
'title' => 'Papel & Cor',
|
||||
'excerpt' => __( 'Experiência de e-commerce otimizada para catálogo, compra e conversão.', 'gustavoo-portfolio-core' ),
|
||||
'type' => 'E-commerce',
|
||||
'technologies' => array( 'WordPress', 'WooCommerce', 'UX' ),
|
||||
'featured' => false,
|
||||
),
|
||||
'lumina-hub' => array(
|
||||
'title' => 'Lumina Hub',
|
||||
'excerpt' => __( 'Hub digital que reúne conteúdo, serviços e integrações em uma única plataforma.', 'gustavoo-portfolio-core' ),
|
||||
'type' => 'Plataforma web',
|
||||
'technologies' => array( 'WordPress', 'APIs', 'Automação' ),
|
||||
'featured' => false,
|
||||
),
|
||||
);
|
||||
|
||||
$order = 0;
|
||||
foreach ( $projects as $key => $project ) {
|
||||
$existing = get_posts(
|
||||
array(
|
||||
'post_type' => GSO_Projects::POST_TYPE,
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_key' => self::PROJECT_META,
|
||||
'meta_value' => $key,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $existing ) {
|
||||
++$order;
|
||||
continue;
|
||||
}
|
||||
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => GSO_Projects::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $project['title'],
|
||||
'post_excerpt' => $project['excerpt'],
|
||||
'menu_order' => $order,
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
if ( is_wp_error( $post_id ) ) {
|
||||
++$order;
|
||||
continue;
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, self::PROJECT_META, $key );
|
||||
update_post_meta( $post_id, GSO_Projects::META_URL, 'https://gustavoo.me/#projetos' );
|
||||
update_post_meta( $post_id, GSO_Projects::META_CLIENT, __( 'Projeto selecionado', 'gustavoo-portfolio-core' ) );
|
||||
update_post_meta( $post_id, GSO_Projects::META_YEAR, (int) gmdate( 'Y' ) );
|
||||
update_post_meta( $post_id, GSO_Projects::META_LINK_LABEL, __( 'Conhecer projeto', 'gustavoo-portfolio-core' ) );
|
||||
update_post_meta( $post_id, GSO_Projects::META_FEATURED, $project['featured'] ? '1' : '' );
|
||||
wp_set_object_terms( $post_id, $project['type'], GSO_Projects::TAX_TYPE );
|
||||
wp_set_object_terms( $post_id, $project['technologies'], GSO_Projects::TAX_TECH );
|
||||
++$order;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user