first commit: gustavoo-portfolio theme + gustavoo-portfolio-core plugin

This commit is contained in:
gustavooth
2026-07-23 21:06:39 -03:00
commit aff10f7880
60 changed files with 9778 additions and 0 deletions
@@ -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;
}
}
}
@@ -0,0 +1,57 @@
<?php
/**
* Not found template.
*
* @package Gustavo_Portfolio
*/
get_header();
$gustavoo_recent_posts = get_posts(
array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 3,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
)
);
?>
<main id="primary" class="site-main error-404">
<section class="page-hero page-hero--error" aria-labelledby="error-title">
<div class="site-shell page-hero__inner">
<p class="eyebrow">404</p>
<h1 id="error-title" class="page-hero__title"><?php esc_html_e( 'Esta página saiu do mapa.', 'gustavoo-portfolio' ); ?></h1>
<p class="page-hero__description"><?php esc_html_e( 'O endereço pode ter mudado ou não existir mais. Pesquise pelo conteúdo ou retorne ao início.', 'gustavoo-portfolio' ); ?></p>
<div class="page-hero__actions">
<a class="button button--primary" href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php esc_html_e( 'Voltar ao início', 'gustavoo-portfolio' ); ?> <span aria-hidden="true">→</span></a>
</div>
<div class="page-hero__search"><?php get_search_form(); ?></div>
</div>
</section>
<?php if ( $gustavoo_recent_posts ) : ?>
<section class="section section--error-posts" aria-labelledby="error-posts-title">
<div class="site-shell">
<header class="section__header">
<p class="eyebrow"><?php esc_html_e( 'Continue explorando', 'gustavoo-portfolio' ); ?></p>
<h2 id="error-posts-title" class="section__title"><?php esc_html_e( 'Artigos recentes', 'gustavoo-portfolio' ); ?></h2>
</header>
<div class="posts-grid">
<?php
global $post;
foreach ( $gustavoo_recent_posts as $post ) {
setup_postdata( $post );
get_template_part( 'template-parts/global/post-card' );
}
wp_reset_postdata();
?>
</div>
</div>
</section>
<?php endif; ?>
</main>
<?php
get_footer();
@@ -0,0 +1,44 @@
<?php
/**
* Category, tag, author, and date archives.
*
* @package Gustavo_Portfolio
*/
get_header();
?>
<main id="primary" class="site-main archive-main">
<header class="page-hero page-hero--archive">
<div class="site-shell page-hero__inner">
<p class="eyebrow"><?php esc_html_e( 'Arquivo', 'gustavoo-portfolio' ); ?></p>
<h1 class="page-hero__title"><?php echo esc_html( wp_strip_all_tags( get_the_archive_title() ) ); ?></h1>
<?php if ( get_the_archive_description() ) : ?>
<div class="page-hero__description prose"><?php echo wp_kses_post( get_the_archive_description() ); ?></div>
<?php endif; ?>
</div>
</header>
<div class="site-shell article-layout">
<div class="article-layout__main">
<?php if ( have_posts() ) : ?>
<div class="posts-grid">
<?php
while ( have_posts() ) {
the_post();
get_template_part( 'template-parts/global/post-card' );
}
?>
</div>
<?php gustavoo_portfolio_pagination(); ?>
<?php else : ?>
<?php get_template_part( 'template-parts/content/content', 'none' ); ?>
<?php endif; ?>
</div>
<?php get_sidebar(); ?>
</div>
</main>
<?php
get_footer();
@@ -0,0 +1,57 @@
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("../fonts/inter-variable.woff2") format("woff2-variations");
}
.editor-styles-wrapper {
color: #d4dde3;
background: #071624;
font-family: "Inter", system-ui, sans-serif;
font-size: 17px;
line-height: 1.75;
}
.editor-styles-wrapper .wp-block {
max-width: 760px;
}
.editor-styles-wrapper .wp-block[data-align="wide"] {
max-width: 1160px;
}
.editor-styles-wrapper h1,
.editor-styles-wrapper h2,
.editor-styles-wrapper h3,
.editor-styles-wrapper h4,
.editor-styles-wrapper h5,
.editor-styles-wrapper h6 {
color: #f5f8fa;
font-weight: 750;
line-height: 1.1;
letter-spacing: -0.035em;
}
.editor-styles-wrapper a {
color: #74d5c7;
}
.editor-styles-wrapper blockquote {
padding-left: 1.5rem;
color: #b8c5ce;
border-left: 3px solid #74d5c7;
}
.editor-styles-wrapper code,
.editor-styles-wrapper pre {
color: #d9fdf7;
background: #040d16;
border-radius: 8px;
font-family: "SFMono-Regular", Consolas, monospace;
}
.editor-styles-wrapper img {
border-radius: 10px;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,273 @@
(function () {
"use strict";
const ready = (callback) => {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", callback, { once: true });
return;
}
callback();
};
ready(function () {
const header = document.querySelector(".site-header");
const toggle = document.querySelector("[data-nav-toggle], .nav-toggle, .menu-toggle");
const navigation = document.querySelector("[data-primary-nav], .primary-nav, .primary-navigation");
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
let lastFocusedElement = null;
const updateHeader = () => {
if (!header) {
return;
}
header.classList.toggle("is-scrolled", window.scrollY > 24);
};
updateHeader();
window.addEventListener("scroll", updateHeader, { passive: true });
if (toggle && navigation) {
const drawerClose = navigation.querySelector("[data-nav-close]");
const navigationParent = navigation.parentNode;
const navigationNextSibling = navigation.nextSibling;
let navigationIsDrawer = false;
const placeNavigation = (asDrawer) => {
if (asDrawer && !navigationIsDrawer) {
document.body.appendChild(navigation);
navigationIsDrawer = true;
} else if (!asDrawer && navigationIsDrawer) {
navigationParent.insertBefore(navigation, navigationNextSibling);
navigationIsDrawer = false;
}
};
if (!navigation.id) {
navigation.id = "site-navigation";
}
toggle.setAttribute("aria-controls", navigation.id);
toggle.setAttribute("aria-expanded", "false");
navigation.setAttribute("aria-hidden", "true");
const focusableSelector = [
"a[href]",
"button:not([disabled])",
"input:not([disabled])",
"select:not([disabled])",
"textarea:not([disabled])",
"[tabindex]:not([tabindex='-1'])",
].join(",");
const closeMenu = (restoreFocus) => {
toggle.setAttribute("aria-expanded", "false");
const toggleLabel = toggle.querySelector(".menu-toggle__label");
if (toggleLabel && window.gustavooPortfolioNavigation) {
toggleLabel.textContent = window.gustavooPortfolioNavigation.expand;
}
navigation.setAttribute("aria-hidden", "true");
navigation.classList.remove("is-open");
navigation.classList.remove("has-more-content");
document.body.classList.remove("nav-open");
if (restoreFocus && lastFocusedElement) {
lastFocusedElement.focus();
}
};
const openMenu = () => {
lastFocusedElement = document.activeElement;
toggle.setAttribute("aria-expanded", "true");
const toggleLabel = toggle.querySelector(".menu-toggle__label");
if (toggleLabel && window.gustavooPortfolioNavigation) {
toggleLabel.textContent = window.gustavooPortfolioNavigation.collapse;
}
navigation.setAttribute("aria-hidden", "false");
navigation.classList.add("is-open");
document.body.classList.add("nav-open");
window.requestAnimationFrame(function () {
updateNavigationScrollHint();
});
const firstFocusable = navigation.querySelector(focusableSelector);
if (firstFocusable) {
window.requestAnimationFrame(() => firstFocusable.focus());
}
};
const updateNavigationScrollHint = () => {
const hasMoreContent = navigation.scrollHeight > navigation.clientHeight + 4
&& navigation.scrollTop + navigation.clientHeight < navigation.scrollHeight - 4;
navigation.classList.toggle("has-more-content", hasMoreContent);
};
toggle.addEventListener("click", function () {
if (toggle.getAttribute("aria-expanded") === "true") {
closeMenu(false);
} else {
openMenu();
}
});
if (drawerClose) {
drawerClose.addEventListener("click", function () {
closeMenu(true);
});
}
navigation.addEventListener("click", function (event) {
if (event.target.closest("a")) {
closeMenu(false);
}
});
navigation.addEventListener("scroll", updateNavigationScrollHint, { passive: true });
window.addEventListener("resize", updateNavigationScrollHint, { passive: true });
document.addEventListener("keydown", function (event) {
if (toggle.getAttribute("aria-expanded") !== "true") {
return;
}
if (event.key === "Escape") {
event.preventDefault();
closeMenu(true);
return;
}
if (event.key !== "Tab") {
return;
}
const focusable = Array.from(navigation.querySelectorAll(focusableSelector));
focusable.push(toggle);
if (!focusable.length) {
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
});
const desktopQuery = window.matchMedia("(min-width: 921px)");
const handleDesktop = (event) => {
placeNavigation(!event.matches);
if (event.matches) {
closeMenu(false);
navigation.removeAttribute("aria-hidden");
} else if (toggle.getAttribute("aria-expanded") !== "true") {
navigation.setAttribute("aria-hidden", "true");
}
};
handleDesktop(desktopQuery);
desktopQuery.addEventListener("change", handleDesktop);
}
if (window.location.hash.length > 1) {
const hashId = decodeURIComponent(window.location.hash.slice(1));
const hashTarget = document.getElementById(hashId);
if (hashTarget) {
window.requestAnimationFrame(function () {
window.setTimeout(function () {
hashTarget.scrollIntoView({ behavior: "auto", block: "start" });
}, 120);
});
}
}
document.querySelectorAll('a[href^="#"]:not([href="#"])').forEach(function (link) {
link.addEventListener("click", function (event) {
const id = link.getAttribute("href").slice(1);
const target = document.getElementById(id);
if (!target) {
return;
}
event.preventDefault();
// The header becomes fixed after the page is scrolled, so
// scrollIntoView() cannot scroll the document when targeting it.
if (id === "masthead") {
window.scrollTo({
top: 0,
behavior: reduceMotion.matches ? "auto" : "smooth",
});
} else {
target.scrollIntoView({
behavior: reduceMotion.matches ? "auto" : "smooth",
block: "start",
});
}
if (window.history && window.history.replaceState) {
window.history.replaceState(null, "", `#${id}`);
}
});
});
const revealItems = Array.from(document.querySelectorAll("[data-reveal]"));
if (revealItems.length && "IntersectionObserver" in window && !reduceMotion.matches) {
document.documentElement.classList.add("has-reveal");
const revealObserver = new IntersectionObserver(
function (entries, observer) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) {
return;
}
const delay = Number.parseInt(entry.target.dataset.revealDelay || "0", 10);
if (delay > 0) {
entry.target.style.transitionDelay = `${Math.min(delay, 500)}ms`;
}
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
});
},
{ rootMargin: "0px 0px -8%", threshold: 0.12 }
);
revealItems.forEach((item) => revealObserver.observe(item));
} else {
revealItems.forEach((item) => item.classList.add("is-visible"));
}
const progress = document.querySelector("[data-reading-progress]");
const article = document.querySelector(".entry-content");
if (progress && article) {
const updateProgress = () => {
const articleTop = article.getBoundingClientRect().top + window.scrollY;
const articleHeight = article.offsetHeight;
const viewportHeight = window.innerHeight;
const distance = articleHeight - viewportHeight;
const current = window.scrollY - articleTop;
const percentage = distance > 0 ? Math.min(100, Math.max(0, (current / distance) * 100)) : 100;
progress.style.setProperty("--reading-progress", `${percentage}%`);
progress.setAttribute("aria-valuenow", String(Math.round(percentage)));
};
updateProgress();
window.addEventListener("scroll", updateProgress, { passive: true });
window.addEventListener("resize", updateProgress, { passive: true });
}
});
})();
@@ -0,0 +1,34 @@
<?php
/** Category news archive. @package Gustavo_Portfolio */
get_header();
?>
<main id="primary" class="site-main news-index news-index--category">
<header class="news-page-heading">
<div class="site-shell">
<h1><?php single_cat_title(); ?></h1>
<?php if ( category_description() ) : ?><div class="news-page-heading__description"><?php echo wp_kses_post( category_description() ); ?></div><?php endif; ?>
</div>
</header>
<div class="site-shell news-index__content">
<?php if ( have_posts() ) : ?>
<div class="latest-news-grid">
<?php
$gustavoo_news_index = 0;
$gustavoo_displayed_post_ids = array();
while ( have_posts() ) {
the_post();
$gustavoo_displayed_post_ids[] = get_the_ID();
get_template_part( 'template-parts/global/latest-news-card', null, array( 'featured' => 0 === $gustavoo_news_index ) );
$gustavoo_news_index++;
}
?>
</div>
<?php get_template_part( 'template-parts/global/latest-news-list', null, array( 'exclude' => $gustavoo_displayed_post_ids, 'category_id' => get_queried_object_id() ) ); ?>
<?php gustavoo_portfolio_pagination(); ?>
<?php else : ?>
<?php get_template_part( 'template-parts/content/content', 'none' ); ?>
<?php endif; ?>
</div>
</main>
<?php get_footer(); ?>
@@ -0,0 +1,65 @@
<?php
/**
* Comments template.
*
* @package Gustavo_Portfolio
*/
if ( post_password_required() ) {
return;
}
?>
<section id="comments" class="comments-area">
<?php if ( have_comments() ) : ?>
<h2 class="comments-title">
<?php
$gustavoo_comment_count = get_comments_number();
printf(
/* translators: 1: comment count, 2: post title. */
esc_html( _n( '%1$s comentário em “%2$s”', '%1$s comentários em “%2$s”', $gustavoo_comment_count, 'gustavoo-portfolio' ) ),
esc_html( number_format_i18n( $gustavoo_comment_count ) ),
esc_html( get_the_title() )
);
?>
</h2>
<ol class="comment-list">
<?php
wp_list_comments(
array(
'avatar_size' => 64,
'short_ping' => true,
'style' => 'ol',
)
);
?>
</ol>
<?php
the_comments_navigation(
array(
'prev_text' => sprintf( '<span aria-hidden="true">←</span> %s', esc_html__( 'Comentários anteriores', 'gustavoo-portfolio' ) ),
'next_text' => sprintf( '%s <span aria-hidden="true">→</span>', esc_html__( 'Próximos comentários', 'gustavoo-portfolio' ) ),
)
);
?>
<?php endif; ?>
<?php if ( ! comments_open() && get_comments_number() ) : ?>
<p class="no-comments"><?php esc_html_e( 'Os comentários estão encerrados.', 'gustavoo-portfolio' ); ?></p>
<?php endif; ?>
<?php
comment_form(
array(
'class_submit' => 'submit button button--primary',
'label_submit' => __( 'Publicar comentário', 'gustavoo-portfolio' ),
'title_reply' => __( 'Deixe um comentário', 'gustavoo-portfolio' ),
'title_reply_before' => '<h2 id="reply-title" class="comment-reply-title">',
'title_reply_after' => '</h2>',
'comment_notes_before' => '<p class="comment-notes">' . esc_html__( 'Seu endereço de e-mail não será publicado. Campos obrigatórios são indicados.', 'gustavoo-portfolio' ) . '</p>',
)
);
?>
</section>
@@ -0,0 +1,66 @@
<?php
/**
* Site footer and the global newsletter CTA.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
get_template_part( 'template-parts/global/newsletter-cta' );
?>
<footer id="colophon" class="site-footer">
<div class="site-shell site-footer__main">
<div class="site-footer__brand">
<div class="site-footer__brand-row">
<?php gustavoo_portfolio_the_brand(); ?>
<p class="site-footer__name"><?php echo esc_html( get_bloginfo( 'name' ) ); ?></p>
</div>
<p><?php echo esc_html( gustavoo_portfolio_get_setting( 'hero_description' ) ); ?></p>
<?php gustavoo_portfolio_social_links( 'social-links social-links--footer' ); ?>
</div>
<?php if ( has_nav_menu( 'footer' ) ) : ?>
<nav class="footer-navigation" aria-label="<?php esc_attr_e( 'Navegação do rodapé', 'gustavoo-portfolio' ); ?>">
<?php
wp_nav_menu(
array(
'theme_location' => 'footer',
'menu_class' => 'footer-menu',
'container' => false,
'depth' => 1,
'fallback_cb' => false,
)
);
?>
</nav>
<?php endif; ?>
</div>
<div class="site-shell site-footer__bottom">
<p>
<?php
printf(
/* translators: 1: current year, 2: site name. */
esc_html__( '© %1$s %2$s. Todos os direitos reservados.', 'gustavoo-portfolio' ),
esc_html( wp_date( 'Y' ) ),
esc_html( get_bloginfo( 'name' ) )
);
?>
</p>
<a href="#masthead" class="back-to-top"><?php esc_html_e( 'Voltar ao topo', 'gustavoo-portfolio' ); ?> <span aria-hidden="true">↑</span></a>
</div>
</footer>
<?php
if ( is_front_page() ) {
gustavoo_portfolio_the_whatsapp_float();
}
?>
<?php wp_footer(); ?>
</body>
</html>
@@ -0,0 +1,41 @@
<?php
/**
* Portfolio landing page.
*
* @package Gustavo_Portfolio
*/
get_header();
?>
<main id="primary" class="site-main front-page">
<?php get_template_part( 'template-parts/front/hero' ); ?>
<?php get_template_part( 'template-parts/front/about' ); ?>
<?php get_template_part( 'template-parts/front/services' ); ?>
<?php get_template_part( 'template-parts/front/projects' ); ?>
<?php if ( is_page() && have_posts() ) : ?>
<?php
while ( have_posts() ) {
the_post();
$gustavoo_front_content = trim( (string) get_the_content() );
if ( $gustavoo_front_content ) {
?>
<div class="section section--custom-content">
<div class="site-shell entry-content">
<?php the_content(); ?>
</div>
</div>
<?php
}
}
?>
<?php endif; ?>
<?php get_template_part( 'template-parts/front/contact' ); ?>
<?php get_template_part( 'template-parts/front/posts' ); ?>
</main>
<?php
get_footer();
@@ -0,0 +1,30 @@
<?php
/**
* Gustavo Portfolio theme bootstrap.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$gustavoo_portfolio_includes = array(
'/inc/defaults.php',
'/inc/setup.php',
'/inc/enqueue.php',
'/inc/template-tags.php',
'/inc/customizer.php',
'/inc/forms.php',
'/inc/widgets.php',
);
foreach ( $gustavoo_portfolio_includes as $gustavoo_portfolio_file ) {
$gustavoo_portfolio_path = get_theme_file_path( $gustavoo_portfolio_file );
if ( file_exists( $gustavoo_portfolio_path ) ) {
require_once $gustavoo_portfolio_path;
}
}
unset( $gustavoo_portfolio_file, $gustavoo_portfolio_includes, $gustavoo_portfolio_path );
@@ -0,0 +1,70 @@
<?php
/**
* Site header.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?><!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<?php wp_body_open(); ?>
<a class="skip-link screen-reader-text" href="#primary"><?php esc_html_e( 'Pular para o conteúdo', 'gustavoo-portfolio' ); ?></a>
<?php
$gustavoo_is_editorial_page = is_home() || is_category() || is_singular( 'post' );
$gustavoo_primary_shortcut_attr = is_customize_preview() ? ' data-customize-partial-id="gustavoo_portfolio_menu_shortcut_primary"' : '';
?>
<header id="masthead" class="site-header<?php echo is_front_page() ? ' site-header--overlay' : ''; ?><?php echo $gustavoo_is_editorial_page ? ' site-header--editorial' : ''; ?>">
<div class="site-shell site-header__inner">
<div class="site-branding">
<?php gustavoo_portfolio_the_brand(); ?>
<a class="site-branding__text" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<span class="site-branding__name"><?php echo esc_html( get_bloginfo( 'name' ) ); ?></span>
<span class="site-branding__tagline site-branding__tagline--full"><?php esc_html_e( 'Desenvolvedor Full Stack | Web, Mobile, Infraestrutura & Automação', 'gustavoo-portfolio' ); ?></span>
<span class="site-branding__tagline site-branding__tagline--short"><?php esc_html_e( 'Desenvolvedor Full Stack | Web, Mobile, Infraestrutura & Automação', 'gustavoo-portfolio' ); ?></span>
</a>
</div>
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="primary-menu">
<span class="menu-toggle__label"><?php esc_html_e( 'Abrir menu', 'gustavoo-portfolio' ); ?></span>
<span class="menu-toggle__icon" aria-hidden="true"><span></span><span></span><span></span></span>
</button>
<nav id="site-navigation" class="primary-navigation" aria-label="<?php esc_attr_e( 'Navegação principal', 'gustavoo-portfolio' ); ?>"<?php echo $gustavoo_primary_shortcut_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static attribute. ?>>
<button class="menu-drawer-close" type="button" data-nav-close aria-label="<?php esc_attr_e( 'Fechar menu', 'gustavoo-portfolio' ); ?>">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</button>
<?php
wp_nav_menu(
array(
'theme_location' => 'primary',
'menu_id' => 'primary-menu',
'menu_class' => 'primary-menu',
'container' => false,
'fallback_cb' => 'gustavoo_portfolio_primary_menu_fallback',
'depth' => 2,
)
);
?>
<?php if ( $gustavoo_is_editorial_page ) : ?>
<?php gustavoo_portfolio_mobile_news_category_menu(); ?>
<?php endif; ?>
</nav>
</div>
</header>
<?php
if ( $gustavoo_is_editorial_page ) {
gustavoo_portfolio_news_category_menu();
}
@@ -0,0 +1,38 @@
<?php
/**
* Posts page.
*
* @package Gustavo_Portfolio
*/
get_header();
?>
<main id="primary" class="site-main news-index">
<header class="news-page-heading"><div class="site-shell"><h1><?php esc_html_e( 'Últimas notícias', 'gustavoo-portfolio' ); ?></h1></div></header>
<div class="site-shell news-index__content">
<?php if ( have_posts() ) : ?>
<div class="latest-news-grid">
<?php
$gustavoo_news_index = 0;
$gustavoo_displayed_post_ids = array();
while ( have_posts() ) {
the_post();
$gustavoo_displayed_post_ids[] = get_the_ID();
get_template_part( 'template-parts/global/latest-news-card', null, array( 'featured' => 0 === $gustavoo_news_index ) );
$gustavoo_news_index++;
}
?>
</div>
<?php get_template_part( 'template-parts/global/latest-news-list', null, array( 'exclude' => $gustavoo_displayed_post_ids ) ); ?>
<?php gustavoo_portfolio_pagination(); ?>
<?php else : ?>
<?php get_template_part( 'template-parts/content/content', 'none' ); ?>
<?php endif; ?>
</div>
</main>
<?php
get_footer();
@@ -0,0 +1,212 @@
<?php
/**
* Native WordPress Customizer integration and selective-refresh shortcuts.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** Sanitize a URL or an in-page anchor used by theme calls to action. */
function gustavoo_portfolio_customize_link( $value ) {
$value = trim( sanitize_text_field( (string) $value ) );
if ( preg_match( '/^#[A-Za-z][A-Za-z0-9_:.-]*$/', $value ) ) {
return $value;
}
return esc_url_raw( $value, array( 'http', 'https', 'mailto', 'tel' ) );
}
/** Render a template part for a selective-refresh partial. */
function gustavoo_portfolio_customize_render_part( $slug ) {
ob_start();
get_template_part( $slug );
return (string) ob_get_clean();
}
/** Register the portfolio content controls in Appearance > Customize. */
function gustavoo_portfolio_customize_register( $wp_customize ) {
$wp_customize->add_panel(
'gustavoo_portfolio_content',
array(
'title' => __( 'Conteúdo do tema', 'gustavoo-portfolio' ),
'description' => __( 'Edite os conteúdos exibidos no site e use os lápis na prévia para ir direto à seção desejada.', 'gustavoo-portfolio' ),
'priority' => 30,
)
);
$sections = array(
'hero' => __( 'Hero', 'gustavoo-portfolio' ),
'about' => __( 'Sobre', 'gustavoo-portfolio' ),
'services' => __( 'Serviços', 'gustavoo-portfolio' ),
'projects' => __( 'Projetos', 'gustavoo-portfolio' ),
'blog' => __( 'Blog', 'gustavoo-portfolio' ),
'contact' => __( 'Contato', 'gustavoo-portfolio' ),
'newsletter' => __( 'Newsletter', 'gustavoo-portfolio' ),
'social' => __( 'Redes sociais', 'gustavoo-portfolio' ),
);
foreach ( $sections as $id => $title ) {
$wp_customize->add_section(
'gustavoo_portfolio_' . $id,
array(
'title' => $title,
'panel' => 'gustavoo_portfolio_content',
'priority' => 10,
)
);
}
$fields = array(
'hero_eyebrow' => array( 'hero', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ),
'hero_title' => array( 'hero', __( 'Título', 'gustavoo-portfolio' ), 'text' ),
'hero_description' => array( 'hero', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ),
'hero_primary_label' => array( 'hero', __( 'Botão principal — texto', 'gustavoo-portfolio' ), 'text' ),
'hero_primary_url' => array( 'hero', __( 'Botão principal — link', 'gustavoo-portfolio' ), 'link' ),
'hero_secondary_label' => array( 'hero', __( 'Botão secundário — texto', 'gustavoo-portfolio' ), 'text' ),
'hero_secondary_url' => array( 'hero', __( 'Botão secundário — link', 'gustavoo-portfolio' ), 'link' ),
'about_eyebrow' => array( 'about', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ),
'about_title' => array( 'about', __( 'Título', 'gustavoo-portfolio' ), 'text' ),
'about_text' => array( 'about', __( 'Texto principal', 'gustavoo-portfolio' ), 'textarea' ),
'about_secondary_text' => array( 'about', __( 'Texto complementar', 'gustavoo-portfolio' ), 'textarea' ),
'services_eyebrow' => array( 'services', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ),
'services_title' => array( 'services', __( 'Título', 'gustavoo-portfolio' ), 'text' ),
'services_description' => array( 'services', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ),
'projects_eyebrow' => array( 'projects', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ),
'projects_title' => array( 'projects', __( 'Título', 'gustavoo-portfolio' ), 'text' ),
'projects_text' => array( 'projects', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ),
'projects_limit' => array( 'projects', __( 'Quantidade de projetos', 'gustavoo-portfolio' ), 'number' ),
'blog_eyebrow' => array( 'blog', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ),
'blog_title' => array( 'blog', __( 'Título', 'gustavoo-portfolio' ), 'text' ),
'blog_text' => array( 'blog', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ),
'blog_limit' => array( 'blog', __( 'Quantidade de posts', 'gustavoo-portfolio' ), 'number' ),
'contact_eyebrow' => array( 'contact', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ),
'contact_heading' => array( 'contact', __( 'Título', 'gustavoo-portfolio' ), 'text' ),
'contact_text' => array( 'contact', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ),
'email' => array( 'contact', __( 'E-mail', 'gustavoo-portfolio' ), 'email' ),
'whatsapp' => array( 'contact', __( 'WhatsApp — número', 'gustavoo-portfolio' ), 'text' ),
'whatsapp_message' => array( 'contact', __( 'WhatsApp — mensagem inicial', 'gustavoo-portfolio' ), 'textarea' ),
'newsletter_eyebrow' => array( 'newsletter', __( 'Sobretítulo', 'gustavoo-portfolio' ), 'text' ),
'newsletter_heading' => array( 'newsletter', __( 'Título', 'gustavoo-portfolio' ), 'text' ),
'newsletter_text' => array( 'newsletter', __( 'Descrição', 'gustavoo-portfolio' ), 'textarea' ),
'github_url' => array( 'social', __( 'GitHub', 'gustavoo-portfolio' ), 'url' ),
'linkedin_url' => array( 'social', __( 'LinkedIn', 'gustavoo-portfolio' ), 'url' ),
'instagram_url' => array( 'social', __( 'Instagram', 'gustavoo-portfolio' ), 'url' ),
'availability_label' => array( 'social', __( 'Status de disponibilidade', 'gustavoo-portfolio' ), 'text' ),
);
foreach ( $fields as $key => $field ) {
$setting_id = 'gustavoo_portfolio_' . $key;
$sanitize = 'textarea' === $field[2] ? 'sanitize_textarea_field' : ( 'number' === $field[2] ? 'absint' : ( in_array( $field[2], array( 'url', 'link' ), true ) ? 'gustavoo_portfolio_customize_link' : ( 'email' === $field[2] ? 'sanitize_email' : 'sanitize_text_field' ) ) );
$wp_customize->add_setting(
$setting_id,
array(
'default' => gustavoo_portfolio_get_setting( $key ),
'sanitize_callback' => $sanitize,
// Each front-page section is refreshed by its native Customizer
// partial, allowing WordPress to expose its own edit shortcut.
'transport' => 'postMessage',
)
);
$control_args = array(
'label' => $field[1],
'section' => 'gustavoo_portfolio_' . $field[0],
'type' => in_array( $field[2], array( 'textarea', 'number', 'email', 'url' ), true ) ? $field[2] : 'text',
);
if ( 'number' === $field[2] ) {
$control_args['input_attrs'] = array( 'min' => 1, 'max' => 24 );
}
$wp_customize->add_control( $setting_id, $control_args );
}
if ( ! isset( $wp_customize->selective_refresh ) ) {
return;
}
$partials = array(
'hero' => array( '.hero-stage', 'template-parts/front/hero', array( 'hero_eyebrow', 'hero_title', 'hero_description', 'hero_primary_label', 'hero_primary_url', 'hero_secondary_label', 'hero_secondary_url' ) ),
'about' => array( '#sobre', 'template-parts/front/about', array( 'about_eyebrow', 'about_title', 'about_text', 'about_secondary_text' ) ),
'services' => array( '#servicos', 'template-parts/front/services', array( 'services_eyebrow', 'services_title', 'services_description' ) ),
'projects' => array( '#projetos', 'template-parts/front/projects', array( 'projects_eyebrow', 'projects_title', 'projects_text', 'projects_limit' ) ),
'blog' => array( '#blog', 'template-parts/front/posts', array( 'blog_eyebrow', 'blog_title', 'blog_text', 'blog_limit' ) ),
'contact' => array( '#contato', 'template-parts/front/contact', array( 'contact_eyebrow', 'contact_heading', 'contact_text', 'email', 'whatsapp', 'whatsapp_message', 'github_url', 'linkedin_url', 'instagram_url' ) ),
);
foreach ( $partials as $id => $partial ) {
$settings = array_map(
static function ( $key ) {
return 'gustavoo_portfolio_' . $key;
},
$partial[2]
);
/*
* A native edit shortcut is tied to the partial's primary setting.
* Using the first actual setting as the partial ID makes the pencil
* focus a real control instead of an unresolvable synthetic partial.
*/
$wp_customize->selective_refresh->add_partial(
$settings[0],
array(
'selector' => $partial[0],
'settings' => $settings,
'primary_setting' => $settings[0],
'container_inclusive' => true,
'render_callback' => static function () use ( $partial ) {
return gustavoo_portfolio_customize_render_part( $partial[1] );
},
)
);
}
$wp_customize->selective_refresh->add_partial(
'gustavoo_portfolio_whatsapp_shortcut',
array(
'selector' => '.whatsapp-float-wrap',
'settings' => array( 'gustavoo_portfolio_whatsapp', 'gustavoo_portfolio_whatsapp_message' ),
'primary_setting' => 'gustavoo_portfolio_whatsapp',
'container_inclusive' => true,
'render_callback' => static function () {
return gustavoo_portfolio_customize_render_part( 'template-parts/global/whatsapp-float' );
},
)
);
}
add_action( 'customize_register', 'gustavoo_portfolio_customize_register' );
/** Register native Customizer shortcuts for the theme's menu locations. */
function gustavoo_portfolio_customize_menu_shortcuts( $wp_customize ) {
if ( ! isset( $wp_customize->selective_refresh ) ) {
return;
}
$menus = array(
'primary' => '#site-navigation',
'news_categories' => '.news-category-nav',
);
foreach ( $menus as $location => $selector ) {
$setting = 'nav_menu_locations[' . $location . ']';
if ( ! $wp_customize->get_setting( $setting ) ) {
continue;
}
$wp_customize->selective_refresh->add_partial(
'gustavoo_portfolio_menu_shortcut_' . $location,
array(
'selector' => $selector,
'settings' => array( $setting ),
'primary_setting' => $setting,
)
);
}
}
add_action( 'customize_register', 'gustavoo_portfolio_customize_menu_shortcuts', 20 );
@@ -0,0 +1,173 @@
<?php
/**
* Theme setting defaults and reusable portfolio content.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Return default settings without writing options to the database.
*
* @return array<string, mixed>
*/
function gustavoo_portfolio_get_defaults() {
$defaults = array(
'hero_eyebrow' => __( 'Desenvolvimento de ponta a ponta', 'gustavoo-portfolio' ),
'hero_title' => __( 'Transformo ideias em produtos digitais sólidos.', 'gustavoo-portfolio' ),
'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' ),
'hero_primary_label' => __( 'Ver projetos', 'gustavoo-portfolio' ),
'hero_primary_url' => '#projetos',
'hero_secondary_label' => __( 'Fale comigo', 'gustavoo-portfolio' ),
'hero_secondary_url' => '#contato',
'about_eyebrow' => __( 'Sobre mim', 'gustavoo-portfolio' ),
'about_title' => __( 'Responsabilidade técnica do planejamento à produção.', 'gustavoo-portfolio' ),
'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' ),
'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' ),
'services_eyebrow' => __( 'Como posso ajudar', 'gustavoo-portfolio' ),
'services_title' => __( 'Soluções completas para produtos digitais.', 'gustavoo-portfolio' ),
'services_description' => __( 'Estratégia, código e infraestrutura trabalhando juntos para criar experiências rápidas, seguras e preparadas para evoluir.', 'gustavoo-portfolio' ),
'projects_eyebrow' => __( 'Portfólio', 'gustavoo-portfolio' ),
'projects_title' => __( 'Projetos selecionados', 'gustavoo-portfolio' ),
'projects_text' => __( 'Uma seleção de produtos, experiências e soluções que ajudei a colocar no mundo.', 'gustavoo-portfolio' ),
'projects_limit' => 6,
'blog_eyebrow' => __( 'Blog', 'gustavoo-portfolio' ),
'blog_title' => __( 'Código, produto e bastidores', 'gustavoo-portfolio' ),
'blog_text' => __( 'Análises práticas sobre desenvolvimento, infraestrutura, automação e crescimento de produtos digitais.', 'gustavoo-portfolio' ),
'blog_limit' => 4,
'contact_eyebrow' => __( 'Vamos conversar', 'gustavoo-portfolio' ),
'contact_heading' => __( 'Tem um projeto em mente?', 'gustavoo-portfolio' ),
'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' ),
'contact_form_id' => 0,
'newsletter_eyebrow' => __( 'Newsletter', 'gustavoo-portfolio' ),
'newsletter_heading' => __( 'Ideias úteis, direto na sua caixa de entrada.', 'gustavoo-portfolio' ),
'newsletter_text' => __( 'Receba conteúdos sobre desenvolvimento, produto, infraestrutura e automação. Sem ruído e sem spam.', 'gustavoo-portfolio' ),
'newsletter_form_id' => 0,
'email' => (string) get_option( 'admin_email', '' ),
'whatsapp' => '',
'whatsapp_message' => __( 'Olá! Gostaria de falar sobre um projeto.', 'gustavoo-portfolio' ),
'github_url' => '',
'linkedin_url' => '',
'instagram_url' => '',
'projects_archive_url' => '',
'availability_label' => __( 'Disponível para novos projetos', 'gustavoo-portfolio' ),
);
/**
* Filter theme defaults before saved settings are merged.
*
* @param array<string, mixed> $defaults Default settings.
*/
return apply_filters( 'gustavoo_portfolio_defaults', $defaults );
}
/**
* Return the saved theme settings merged with safe local defaults.
*
* @return array<string, mixed>
*/
function gustavoo_portfolio_get_settings() {
$settings = get_option( 'gustavoo_portfolio_settings', array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
return wp_parse_args( $settings, gustavoo_portfolio_get_defaults() );
}
/**
* Read one theme setting. A few aliases preserve compatibility with early builds.
*
* @param string $key Setting key.
* @param mixed $fallback Optional fallback.
* @return mixed
*/
function gustavoo_portfolio_get_setting( $key, $fallback = null ) {
$customizer_key = 'gustavoo_portfolio_' . $key;
$customizer_values = get_theme_mods();
if ( is_array( $customizer_values ) && array_key_exists( $customizer_key, $customizer_values ) ) {
return $customizer_values[ $customizer_key ];
}
$settings = gustavoo_portfolio_get_settings();
$aliases = array(
'hero_eyebrow' => array( 'hero_kicker' ),
'hero_description' => array( 'hero_text' ),
'contact_form_id' => array( 'fluent_contact_form_id', 'lead_form_id' ),
'newsletter_form_id' => array( 'fluent_newsletter_form_id', 'fluentcrm_form_id' ),
'contact_heading' => array( 'contact_title' ),
'newsletter_heading' => array( 'newsletter_title' ),
);
if ( array_key_exists( $key, $settings ) && '' !== $settings[ $key ] && null !== $settings[ $key ] ) {
return $settings[ $key ];
}
if ( isset( $aliases[ $key ] ) ) {
foreach ( $aliases[ $key ] as $alias ) {
if ( array_key_exists( $alias, $settings ) && '' !== $settings[ $alias ] && null !== $settings[ $alias ] ) {
return $settings[ $alias ];
}
}
}
if ( null !== $fallback ) {
return $fallback;
}
$defaults = gustavoo_portfolio_get_defaults();
return $defaults[ $key ] ?? '';
}
/**
* Return the service cards used by the front page.
*
* @return array<int, array<string, string>>
*/
function gustavoo_portfolio_get_services() {
$services = array(
array(
'number' => '01',
'title' => __( 'Web, Mobile e E-commerce', 'gustavoo-portfolio' ),
'description' => __( 'Sites institucionais, aplicações para iOS e Android, softwares desktop, lojas e marketplaces personalizados com foco em experiência e conversão.', 'gustavoo-portfolio' ),
),
array(
'number' => '02',
'title' => __( 'WordPress avançado', 'gustavoo-portfolio' ),
'description' => __( 'Temas e plugins sob medida, integrações específicas, recuperação de sites e otimização para Core Web Vitals.', 'gustavoo-portfolio' ),
),
array(
'number' => '03',
'title' => __( 'Infraestrutura e servidores', 'gustavoo-portfolio' ),
'description' => __( 'Ambientes Linux, VPS e servidores dedicados configurados para manter aplicações estáveis, rápidas, seguras e escaláveis.', 'gustavoo-portfolio' ),
),
array(
'number' => '04',
'title' => __( 'Automação e integrações', 'gustavoo-portfolio' ),
'description' => __( 'APIs e fluxos com n8n para conectar sistemas, organizar dados e eliminar tarefas operacionais repetitivas.', 'gustavoo-portfolio' ),
),
array(
'number' => '05',
'title' => __( 'Games e experiências interativas', 'gustavoo-portfolio' ),
'description' => __( 'Jogos 2D e 3D para celulares, computadores e navegadores, incluindo ranking, multiplayer e compras dentro do jogo.', 'gustavoo-portfolio' ),
),
array(
'number' => '06',
'title' => __( 'Divulgação e crescimento', 'gustavoo-portfolio' ),
'description' => __( 'Estratégias de conteúdo, comunidades, parcerias e mídia paga para aproximar produtos digitais do público certo.', 'gustavoo-portfolio' ),
),
);
/**
* Filter the service cards shown on the front page.
*
* @param array<int, array<string, string>> $services Service cards.
*/
return apply_filters( 'gustavoo_portfolio_services', $services );
}
@@ -0,0 +1,75 @@
<?php
/**
* Front-end assets.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Return a cache-busting asset version.
*
* @param string $relative_path Theme-relative path.
* @return string
*/
function gustavoo_portfolio_asset_version( $relative_path ) {
$release = '1.1.0';
$path = get_theme_file_path( '/' . ltrim( $relative_path, '/' ) );
if ( file_exists( $path ) ) {
return $release . '-' . (string) filemtime( $path );
}
return $release;
}
/**
* Enqueue only local, versioned assets.
*
* @return void
*/
function gustavoo_portfolio_enqueue_assets() {
wp_enqueue_style(
'gustavoo-portfolio-style',
get_stylesheet_uri(),
array(),
gustavoo_portfolio_asset_version( 'style.css' )
);
if ( file_exists( get_theme_file_path( '/assets/css/main.css' ) ) ) {
wp_enqueue_style(
'gustavoo-portfolio-main',
get_theme_file_uri( '/assets/css/main.css' ),
array( 'gustavoo-portfolio-style' ),
gustavoo_portfolio_asset_version( 'assets/css/main.css' )
);
}
if ( file_exists( get_theme_file_path( '/assets/js/navigation.js' ) ) ) {
wp_enqueue_script(
'gustavoo-portfolio-navigation',
get_theme_file_uri( '/assets/js/navigation.js' ),
array(),
gustavoo_portfolio_asset_version( 'assets/js/navigation.js' ),
true
);
wp_script_add_data( 'gustavoo-portfolio-navigation', 'strategy', 'defer' );
wp_localize_script(
'gustavoo-portfolio-navigation',
'gustavooPortfolioNavigation',
array(
'expand' => __( 'Abrir menu', 'gustavoo-portfolio' ),
'collapse' => __( 'Fechar menu', 'gustavoo-portfolio' ),
)
);
}
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
wp_enqueue_script( 'comment-reply' );
}
}
add_action( 'wp_enqueue_scripts', 'gustavoo_portfolio_enqueue_assets' );
@@ -0,0 +1,247 @@
<?php
/**
* Fluent Forms presentation helpers.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Ensure the configured contact form contains the fields required by the site.
*
* This is intentionally idempotent so forms created by an older seed version
* are repaired even when the seeder state is already marked complete.
*
* @return void
*/
function gustavoo_portfolio_ensure_contact_form_fields() {
$form_id = absint( gustavoo_portfolio_get_setting( 'contact_form_id' ) );
if ( ! $form_id || ! class_exists( '\FluentForm\App\Models\Form' ) || ! function_exists( 'fluentformLoadFile' ) ) {
return;
}
$form = \FluentForm\App\Models\Form::find( $form_id );
if ( ! $form ) {
return;
}
$structure = json_decode( (string) $form->form_fields, true );
if ( ! is_array( $structure ) || ! isset( $structure['fields'] ) || ! is_array( $structure['fields'] ) ) {
return;
}
$defaults = fluentformLoadFile( 'Services/FormBuilder/DefaultElements.php' );
$message = $defaults['general']['textarea'] ?? array();
if ( ! $message ) {
return;
}
$fields_by_name = array();
$changed = false;
foreach ( $structure['fields'] as $field ) {
$name = (string) ( $field['attributes']['name'] ?? '' );
if ( ! in_array( $name, array( 'email', 'whatsapp', 'message' ), true ) ) {
$changed = true;
continue;
}
if ( isset( $fields_by_name[ $name ] ) ) {
$changed = true;
continue;
}
$fields_by_name[ $name ] = $field;
}
if ( isset( $fields_by_name['email'] ) ) {
$fields_by_name['email']['settings']['validation_rules']['required']['value'] = true;
}
if ( isset( $fields_by_name['whatsapp'] ) ) {
$whatsapp = $fields_by_name['whatsapp'];
$whatsapp['settings']['validation_rules']['required']['value'] = true;
$whatsapp['settings']['temp_mask'] = 'custom';
$whatsapp['settings']['data-mask-reverse'] = 'no';
$whatsapp['attributes']['data-mask'] = '(00) 00000-0000';
$fields_by_name['whatsapp'] = $whatsapp;
}
if ( ! isset( $fields_by_name['whatsapp'] ) ) {
$whatsapp = $defaults['general']['input_mask'] ?? array();
if ( $whatsapp ) {
$whatsapp['uniqElKey'] = 'el_gso_contact_whatsapp';
$whatsapp['attributes']['name'] = 'whatsapp';
$whatsapp['attributes']['placeholder'] = '(31) 99999-9999';
$whatsapp['attributes']['data-mask'] = '(00) 00000-0000';
$whatsapp['settings']['label'] = __( 'WhatsApp', 'gustavoo-portfolio' );
$whatsapp['settings']['admin_field_label'] = __( 'WhatsApp', 'gustavoo-portfolio' );
$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_by_name['whatsapp'] = $whatsapp;
}
}
if ( ! isset( $fields_by_name['message'] ) || 'textarea' !== (string) ( $fields_by_name['message']['element'] ?? '' ) ) {
$message['uniqElKey'] = 'el_gso_contact_message';
$message['attributes']['name'] = 'message';
$message['attributes']['placeholder'] = __( 'Descreva o motivo do contato e o que você quer construir.', 'gustavoo-portfolio' );
$message['attributes']['rows'] = 5;
$message['settings']['label'] = __( 'Como posso ajudar?', 'gustavoo-portfolio' );
$message['settings']['admin_field_label'] = __( 'Motivo do contato', 'gustavoo-portfolio' );
$message['settings']['validation_rules']['required']['value'] = true;
$fields_by_name['message'] = $message;
}
$fields = array();
foreach ( array( 'email', 'whatsapp', 'message' ) as $required_name ) {
if ( isset( $fields_by_name[ $required_name ] ) ) {
$fields[] = $fields_by_name[ $required_name ];
}
}
$changed = wp_json_encode( $structure['fields'] ) !== wp_json_encode( $fields );
if ( ! $changed ) {
return;
}
$structure['fields'] = $fields;
$form_fields = wp_json_encode( $structure, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
if ( class_exists( '\FluentForm\App\Services\Form\FormService' ) ) {
( new \FluentForm\App\Services\Form\FormService() )->update(
array(
'form_id' => $form->id,
'title' => $form->title,
'status' => $form->status,
'formFields' => $form_fields,
)
);
} else {
$form->form_fields = $form_fields;
$form->save();
}
}
add_action( 'init', 'gustavoo_portfolio_ensure_contact_form_fields', 30 );
/**
* Preserve contact fields in submissions while an older form schema is being
* migrated. Fluent Forms only whitelists fields known by its saved schema.
*
* @param array $form_data Sanitized submission data.
* @param int $form_id Submitted form ID.
* @param array $input_configs Fluent Forms input configuration.
* @return array
*/
function gustavoo_portfolio_preserve_contact_submission_fields( $form_data, $form_id, $input_configs ) {
$contact_form_id = absint( gustavoo_portfolio_get_setting( 'contact_form_id' ) );
if ( ! $contact_form_id || $contact_form_id !== absint( $form_id ) ) {
return $form_data;
}
if ( isset( $_POST['whatsapp'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Fluent Forms validates the submission.
$form_data['whatsapp'] = sanitize_text_field( wp_unslash( $_POST['whatsapp'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
if ( isset( $_POST['message'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Fluent Forms validates the submission.
$form_data['message'] = sanitize_textarea_field( wp_unslash( $_POST['message'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
}
return $form_data;
}
add_filter( 'fluentform/insert_response_data', 'gustavoo_portfolio_preserve_contact_submission_fields', 10, 3 );
/**
* Render a Fluent Forms form from a numeric, administrator-controlled ID.
*
* The theme deliberately never stores or executes arbitrary shortcodes. Contact
* synchronization is configured through the FluentCRM feed inside Fluent Forms.
*
* @param int $form_id Form ID.
* @param string $context Form context, used for classes and filters.
* @return string
*/
function gustavoo_portfolio_render_fluent_form( $form_id, $context = 'default' ) {
$form_id = absint( $form_id );
$context = sanitize_html_class( $context );
if ( $form_id && shortcode_exists( 'fluentform' ) ) {
if ( 'contact' === $context ) {
gustavoo_portfolio_ensure_contact_form_fields();
}
$shortcode = sprintf( '[fluentform id="%d"]', $form_id );
$markup = do_shortcode( $shortcode );
// Older persisted forms may still contain only the e-mail field. Keep the
// requested fields visible inside the real Fluent Forms <form>.
if ( 'contact' === $context && false !== stripos( $markup, '</form>' ) ) {
$missing_fields = '';
if ( false === stripos( $markup, 'name="whatsapp"' ) ) {
$missing_fields .= '<div class="ff-el-group"><div class="ff-el-input--label"><label for="gso-contact-whatsapp">' . esc_html__( 'WhatsApp', 'gustavoo-portfolio' ) . '</label></div><div class="ff-el-input--content"><input id="gso-contact-whatsapp" name="whatsapp" class="ff-el-form-control" type="tel" placeholder="(31) 99999-9999" inputmode="tel" data-mask="(00) 00000-0000" data-mask-reverse="false"></div></div>';
}
if ( false === stripos( $markup, 'name="message"' ) ) {
$missing_fields .= '<div class="ff-el-group"><div class="ff-el-input--label"><label for="gso-contact-message">' . esc_html__( 'Como posso ajudar?', 'gustavoo-portfolio' ) . '</label></div><div class="ff-el-input--content"><textarea id="gso-contact-message" name="message" class="ff-el-form-control" rows="5" placeholder="' . esc_attr__( 'Descreva o motivo do contato e o que você quer construir.', 'gustavoo-portfolio' ) . '"></textarea></div></div>';
}
if ( $missing_fields ) {
$markup = preg_replace( '#<button\b#i', $missing_fields . '<button', $markup, 1 );
}
}
/**
* Filter trusted Fluent Forms output before it is returned.
*
* @param string $markup Form markup generated by Fluent Forms.
* @param int $form_id Form ID.
* @param string $context Presentation context.
*/
return (string) apply_filters( 'gustavoo_portfolio_fluent_form_markup', $markup, $form_id, $context );
}
$email = sanitize_email( (string) gustavoo_portfolio_get_setting( 'email' ) );
$contact_url = gustavoo_portfolio_section_url( 'contato' );
$button_url = $email ? 'mailto:' . $email : $contact_url;
$button_label = 'newsletter' === $context
? __( 'Quero receber novidades', 'gustavoo-portfolio' )
: __( 'Enviar uma mensagem', 'gustavoo-portfolio' );
$fallback = '<div class="form-fallback form-fallback--' . esc_attr( $context ) . '">';
$fallback .= '<p>' . esc_html__( 'O formulário está sendo configurado. Você ainda pode entrar em contato diretamente.', 'gustavoo-portfolio' ) . '</p>';
$fallback .= '<a class="button button--primary" href="' . esc_url( $button_url ) . '">' . esc_html( $button_label ) . '</a>';
if ( current_user_can( 'manage_options' ) ) {
$fallback .= '<p class="form-fallback__admin"><a href="' . esc_url( admin_url( 'edit.php?post_type=gso_project&page=gso-portfolio-settings' ) ) . '">';
$fallback .= esc_html__( 'Configure o ID do Fluent Forms nas opções do portfólio.', 'gustavoo-portfolio' );
$fallback .= '</a></p>';
}
$fallback .= '</div>';
return $fallback;
}
/**
* Print a Fluent Forms form. Output originates either from the trusted plugin
* shortcode or from fully escaped fallback markup built above.
*
* @param int $form_id Form ID.
* @param string $context Form context.
* @return void
*/
function gustavoo_portfolio_the_fluent_form( $form_id, $context = 'default' ) {
echo gustavoo_portfolio_render_fluent_form( $form_id, $context ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Trusted plugin output or escaped local fallback.
}
@@ -0,0 +1,146 @@
<?php
/**
* Theme setup and core WordPress integrations.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register theme supports, menus, and editor features.
*
* @return void
*/
function gustavoo_portfolio_setup() {
load_theme_textdomain( 'gustavoo-portfolio', get_template_directory() . '/languages' );
add_theme_support( 'automatic-feed-links' );
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'responsive-embeds' );
add_theme_support( 'align-wide' );
add_theme_support( 'editor-styles' );
add_editor_style( 'assets/css/editor.css' );
add_theme_support(
'html5',
array(
'comment-form',
'comment-list',
'gallery',
'caption',
'script',
'search-form',
'style',
)
);
add_theme_support(
'custom-logo',
array(
'height' => 160,
'width' => 160,
'flex-height' => true,
'flex-width' => true,
)
);
register_nav_menus(
array(
'primary' => __( 'Menu principal', 'gustavoo-portfolio' ),
'news_categories' => __( 'Menu de categorias de notícias', 'gustavoo-portfolio' ),
'footer' => __( 'Menu do rodapé', 'gustavoo-portfolio' ),
)
);
add_image_size( 'gustavoo-project-card', 720, 480, true );
add_image_size( 'gustavoo-post-card', 720, 460, true );
}
add_action( 'after_setup_theme', 'gustavoo_portfolio_setup' );
/**
* Define a comfortable content width for embeds and media.
*
* @return void
*/
function gustavoo_portfolio_content_width() {
$GLOBALS['content_width'] = apply_filters( 'gustavoo_portfolio_content_width', 780 );
}
add_action( 'after_setup_theme', 'gustavoo_portfolio_content_width', 0 );
/**
* Add useful state classes without coupling templates to presentation logic.
*
* @param string[] $classes Existing body classes.
* @return string[]
*/
function gustavoo_portfolio_body_classes( $classes ) {
if ( is_front_page() ) {
$classes[] = 'has-overlay-header';
}
if ( ! is_front_page() && is_active_sidebar( 'sidebar-blog' ) ) {
$classes[] = 'has-blog-sidebar';
}
if ( ! is_singular() ) {
$classes[] = 'is-list-view';
}
return array_unique( $classes );
}
add_filter( 'body_class', 'gustavoo_portfolio_body_classes' );
/** Set the posts page browser title to the editorial section name. */
function gustavoo_portfolio_blog_document_title( $title ) {
if ( is_home() ) {
$title['title'] = __( 'Últimas notícias', 'gustavoo-portfolio' );
}
return $title;
}
add_filter( 'document_title_parts', 'gustavoo_portfolio_blog_document_title' );
/** Keep the editorial top grid focused on the five newest posts. */
function gustavoo_portfolio_news_posts_per_page( $query ) {
if ( is_admin() || ! $query->is_main_query() || ! ( $query->is_home() || $query->is_category() ) ) {
return;
}
$query->set( 'posts_per_page', 5 );
}
add_action( 'pre_get_posts', 'gustavoo_portfolio_news_posts_per_page' );
/** Refresh category rewrite rules once after the editorial templates are installed. */
function gustavoo_portfolio_refresh_editorial_rewrites() {
$version = '1.0.0';
if ( $version === get_option( 'gustavoo_portfolio_editorial_rewrite_version' ) ) {
return;
}
flush_rewrite_rules( false );
update_option( 'gustavoo_portfolio_editorial_rewrite_version', $version, false );
}
add_action( 'init', 'gustavoo_portfolio_refresh_editorial_rewrites', 99 );
/**
* Print a fallback icon only when WordPress has no configured Site Icon.
*
* @return void
*/
function gustavoo_portfolio_fallback_site_icon() {
if ( has_site_icon() || ! function_exists( 'gustavoo_portfolio_get_image_source' ) ) {
return;
}
$icon_url = gustavoo_portfolio_get_image_source( 'logo-mark', 'png' );
if ( $icon_url ) {
printf( '<link rel="icon" href="%s" sizes="192x192">' . "\n", esc_url( $icon_url ) );
}
}
add_action( 'wp_head', 'gustavoo_portfolio_fallback_site_icon', 2 );
@@ -0,0 +1,569 @@
<?php
/**
* Reusable template helpers.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Resolve an image source, preferring an existing format and falling back safely.
*
* @param string $basename File basename without extension.
* @param string $preferred Preferred extension.
* @return string
*/
function gustavoo_portfolio_get_image_source( $basename, $preferred = 'webp' ) {
$basename = preg_replace( '/[^a-z0-9-]/', '', strtolower( (string) $basename ) );
$preferred = in_array( $preferred, array( 'webp', 'png' ), true ) ? $preferred : 'webp';
$formats = array_unique( array( $preferred, 'webp', 'png' ) );
if ( ! $basename ) {
return '';
}
foreach ( $formats as $format ) {
$relative_path = '/assets/images/' . $basename . '.' . $format;
if ( file_exists( get_theme_file_path( $relative_path ) ) ) {
return get_theme_file_uri( $relative_path );
}
}
// Keep a deterministic URI while assets are being provisioned by deployment.
return get_theme_file_uri( '/assets/images/' . $basename . '.' . $preferred );
}
/**
* Return image URLs for a picture element.
*
* @param string $basename File basename without extension.
* @return array{webp:string,png:string,fallback:string}
*/
function gustavoo_portfolio_get_picture_sources( $basename ) {
$webp_path = '/assets/images/' . $basename . '.webp';
$png_path = '/assets/images/' . $basename . '.png';
$has_webp = file_exists( get_theme_file_path( $webp_path ) );
$has_png = file_exists( get_theme_file_path( $png_path ) );
return array(
'webp' => $has_webp ? get_theme_file_uri( $webp_path ) : '',
'png' => $has_png ? get_theme_file_uri( $png_path ) : '',
'fallback' => $has_png ? get_theme_file_uri( $png_path ) : get_theme_file_uri( $webp_path ),
);
}
/**
* Print the configured logo or the bundled brand mark.
*
* @return void
*/
function gustavoo_portfolio_the_brand() {
$site_name = get_bloginfo( 'name' );
if ( has_custom_logo() ) {
echo get_custom_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Core-generated custom logo markup.
return;
}
$sources = gustavoo_portfolio_get_picture_sources( 'logo-mark' );
?>
<a class="custom-logo-link" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<picture>
<?php if ( $sources['webp'] ) : ?>
<source srcset="<?php echo esc_url( $sources['webp'] ); ?>" type="image/webp">
<?php endif; ?>
<img class="custom-logo" src="<?php echo esc_url( $sources['fallback'] ); ?>" width="64" height="64" alt="<?php echo esc_attr( $site_name ); ?>">
</picture>
</a>
<?php
}
/**
* Return the appropriate URL for a front-page section.
*
* @param string $section_id Section anchor without hash.
* @return string
*/
function gustavoo_portfolio_section_url( $section_id ) {
$section_id = sanitize_html_class( $section_id );
if ( is_front_page() ) {
return '#' . $section_id;
}
return home_url( '/#' . $section_id );
}
/**
* Return the configured posts-page URL.
*
* @return string
*/
function gustavoo_portfolio_get_blog_url() {
$posts_page_id = absint( get_option( 'page_for_posts' ) );
if ( $posts_page_id ) {
$url = get_permalink( $posts_page_id );
if ( $url ) {
return $url;
}
}
return home_url( '/blog/' );
}
/** Render the editorial category navigation used on news pages. */
function gustavoo_portfolio_news_category_menu() {
if ( has_nav_menu( 'news_categories' ) ) {
?>
<nav class="news-category-nav" aria-label="<?php esc_attr_e( 'Categorias de notícias', 'gustavoo-portfolio' ); ?>">
<div class="news-category-nav__inner site-shell">
<?php
wp_nav_menu(
array(
'theme_location' => 'news_categories',
'menu_class' => 'news-category-nav__list',
'container' => false,
'fallback_cb' => false,
'depth' => 1,
)
);
?>
</div>
</nav>
<?php
return;
}
$categories = get_categories(
array(
'hide_empty' => true,
'exclude' => array( absint( get_option( 'default_category' ) ) ),
'orderby' => 'name',
'order' => 'ASC',
)
);
if ( empty( $categories ) ) {
return;
}
$current_category_id = is_category() ? (int) get_queried_object_id() : 0;
?>
<nav class="news-category-nav" aria-label="<?php esc_attr_e( 'Categorias de notícias', 'gustavoo-portfolio' ); ?>">
<div class="news-category-nav__inner site-shell">
<a class="news-category-nav__link<?php echo is_home() ? ' is-current' : ''; ?>" href="<?php echo esc_url( gustavoo_portfolio_get_blog_url() ); ?>"><?php esc_html_e( 'Últimas notícias', 'gustavoo-portfolio' ); ?></a>
<?php foreach ( $categories as $category ) : ?>
<a class="news-category-nav__link<?php echo $current_category_id === (int) $category->term_id ? ' is-current' : ''; ?>" href="<?php echo esc_url( get_category_link( $category ) ); ?>"><?php echo esc_html( $category->name ); ?></a>
<?php endforeach; ?>
</div>
</nav>
<?php
}
/** Print editorial categories inside the mobile hamburger panel. */
function gustavoo_portfolio_mobile_news_category_menu() {
if ( has_nav_menu( 'news_categories' ) ) {
?>
<div class="mobile-news-categories">
<p class="mobile-news-categories__title"><?php esc_html_e( 'Categorias', 'gustavoo-portfolio' ); ?></p>
<?php
wp_nav_menu(
array(
'theme_location' => 'news_categories',
'menu_class' => 'mobile-news-categories__list',
'container' => false,
'fallback_cb' => false,
'depth' => 1,
)
);
?>
</div>
<?php
return;
}
$categories = get_categories(
array(
'hide_empty' => true,
'exclude' => array( absint( get_option( 'default_category' ) ) ),
'orderby' => 'name',
'order' => 'ASC',
)
);
if ( empty( $categories ) ) {
return;
}
$current_category_id = is_category() ? (int) get_queried_object_id() : 0;
?>
<div class="mobile-news-categories">
<p class="mobile-news-categories__title"><?php esc_html_e( 'Categorias', 'gustavoo-portfolio' ); ?></p>
<ul class="mobile-news-categories__list">
<li><a class="<?php echo is_home() ? 'is-current' : ''; ?>" href="<?php echo esc_url( gustavoo_portfolio_get_blog_url() ); ?>"><?php esc_html_e( 'Últimas notícias', 'gustavoo-portfolio' ); ?></a></li>
<?php foreach ( $categories as $category ) : ?>
<li><a class="<?php echo $current_category_id === (int) $category->term_id ? 'is-current' : ''; ?>" href="<?php echo esc_url( get_category_link( $category ) ); ?>"><?php echo esc_html( $category->name ); ?></a></li>
<?php endforeach; ?>
</ul>
</div>
<?php
}
/**
* Resolve the image used by an editorial card.
*
* Seeded demo posts use a local theme image until an editor assigns a
* featured image in WordPress.
*
* @param int $post_id Post ID.
* @param string $size Registered image size.
* @return string
*/
function gustavoo_portfolio_get_news_image_url( $post_id = 0, $size = 'gustavoo-post-card' ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
$image = get_the_post_thumbnail_url( $post_id, $size );
if ( $image ) {
return $image;
}
return (string) get_post_meta( $post_id, '_gso_news_image', true );
}
/**
* Accessible fallback menu for a site that has not assigned a WordPress menu yet.
*
* @return void
*/
function gustavoo_portfolio_primary_menu_fallback() {
?>
<ul id="primary-menu" class="primary-menu">
<li class="menu-item"><a href="<?php echo esc_url( gustavoo_portfolio_section_url( 'sobre' ) ); ?>"><?php esc_html_e( 'Sobre', 'gustavoo-portfolio' ); ?></a></li>
<li class="menu-item"><a href="<?php echo esc_url( gustavoo_portfolio_section_url( 'servicos' ) ); ?>"><?php esc_html_e( 'Serviços', 'gustavoo-portfolio' ); ?></a></li>
<li class="menu-item"><a href="<?php echo esc_url( gustavoo_portfolio_section_url( 'projetos' ) ); ?>"><?php esc_html_e( 'Projetos', 'gustavoo-portfolio' ); ?></a></li>
<li class="menu-item"><a href="<?php echo esc_url( gustavoo_portfolio_get_blog_url() ); ?>"><?php esc_html_e( 'Blog', 'gustavoo-portfolio' ); ?></a></li>
<li class="menu-item"><a href="<?php echo esc_url( gustavoo_portfolio_section_url( 'contato' ) ); ?>"><?php esc_html_e( 'Contato', 'gustavoo-portfolio' ); ?></a></li>
</ul>
<?php
}
/**
* Return supported social profile links.
*
* @return array<string, array{label:string,url:string}>
*/
function gustavoo_portfolio_get_social_links() {
$profiles = array(
'linkedin' => array(
'label' => __( 'LinkedIn', 'gustavoo-portfolio' ),
'url' => (string) gustavoo_portfolio_get_setting( 'linkedin_url' ),
),
'instagram' => array(
'label' => __( 'Instagram', 'gustavoo-portfolio' ),
'url' => (string) gustavoo_portfolio_get_setting( 'instagram_url' ),
),
);
return array_filter(
$profiles,
static function ( $profile ) {
return ! empty( $profile['url'] );
}
);
}
/**
* Print social links with explicit new-window text for assistive technology.
*
* @param string $class Optional list class.
* @return void
*/
function gustavoo_portfolio_social_links( $class = 'social-links' ) {
$profiles = gustavoo_portfolio_get_social_links();
if ( empty( $profiles ) ) {
return;
}
printf( '<ul class="%s">', esc_attr( $class ) );
foreach ( $profiles as $slug => $profile ) {
printf(
'<li class="social-links__item social-links__item--%1$s"><a href="%2$s" target="_blank" rel="noopener noreferrer external"><span>%3$s</span><span class="screen-reader-text"> %4$s</span></a></li>',
esc_attr( $slug ),
esc_url( $profile['url'] ),
esc_html( $profile['label'] ),
esc_html__( '(abre em uma nova guia)', 'gustavoo-portfolio' )
);
}
echo '</ul>';
}
/**
* Print share links for a blog post using inline brand SVG icons.
*
* @param string $class Optional wrapper class.
* @return void
*/
function gustavoo_portfolio_post_share_links( $class = 'entry-share' ) {
$share_url = rawurlencode( get_permalink() );
$share_title = rawurlencode( get_the_title() );
$links = array(
'WhatsApp' => array(
'url' => 'https://api.whatsapp.com/send?text=' . $share_title . '%20' . $share_url,
'icon' => '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M20.5 3.5A11.8 11.8 0 0 0 12.1 0C5.6 0 .3 5.3.3 11.8c0 2.1.5 4.1 1.6 5.9L.2 23.8l6.3-1.7a11.8 11.8 0 0 0 5.6 1.4h.1c6.5 0 11.8-5.3 11.8-11.8 0-3.1-1.2-6-3.5-8.2Zm-8.4 17.9c-1.8 0-3.5-.5-5-1.4l-.4-.2-3.7 1 1-3.6-.2-.4a9.6 9.6 0 1 1 8.3 4.6Zm5.3-7.2c-.3-.2-1.7-.8-2-.9-.3-.1-.5-.2-.7.2-.2.3-.8.9-.9 1.1-.2.2-.3.2-.6.1-1.7-.8-2.8-1.5-3.9-3.4-.3-.5.3-.5.8-1.7.1-.2 0-.4 0-.6l-.9-2.1c-.2-.6-.5-.5-.7-.5h-.6c-.2 0-.5.1-.8.4-.3.3-1 1-1 2.4s1 2.8 1.1 3c.1.2 2 3.1 4.9 4.4 1.8.8 2.5.9 3.4.8.5-.1 1.7-.7 1.9-1.4.2-.7.2-1.3.1-1.4-.1-.2-.3-.3-.6-.4Z" fill="currentColor"/></svg>',
),
'Facebook' => array(
'url' => 'https://www.facebook.com/sharer/sharer.php?u=' . $share_url,
'icon' => '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M13.5 22v-8h2.7l.4-3h-3.1V9.1c0-.9.3-1.5 1.6-1.5h1.7V4.9c-.3 0-1.3-.1-2.5-.1-2.5 0-4.2 1.5-4.2 4.3V11H7.3v3h2.8v8h3.4Z" fill="currentColor"/></svg>',
),
'X' => array(
'url' => 'https://twitter.com/intent/tweet?text=' . $share_title . '&url=' . $share_url,
'icon' => '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M18.9 2h3.7l-8.1 9.3L24 22h-7.4l-5.8-7.6L4.2 22H.5l8.4-9.7L0 2h7.6l5.2 6.9L18.9 2Zm-1.3 17.9h2.1L6.4 4H4.1l13.5 15.9Z" fill="currentColor"/></svg>',
),
'LinkedIn' => array(
'url' => 'https://www.linkedin.com/sharing/share-offsite/?url=' . $share_url,
'icon' => '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5.2 3.4A2.2 2.2 0 1 1 .8 3.4a2.2 2.2 0 0 1 4.4 0ZM1.1 7h4.2v13.5H1.1V7Zm6.8 0h4v1.8h.1c.6-1.1 2-2.2 4.1-2.2 4.4 0 5.2 2.9 5.2 6.7v7.2h-4.2v-6.4c0-1.5 0-3.5-2.1-3.5s-2.4 1.7-2.4 3.4v6.5H7.9V7Z" fill="currentColor"/></svg>',
),
);
printf( '<div class="%1$s" aria-label="%2$s">', esc_attr( $class ), esc_attr__( 'Compartilhar postagem', 'gustavoo-portfolio' ) );
foreach ( $links as $label => $link ) {
printf(
'<a href="%1$s" target="_blank" rel="noopener noreferrer" aria-label="%2$s">%3$s<span class="screen-reader-text">%2$s</span></a>',
esc_url( $link['url'] ),
esc_attr( $label ),
$link['icon'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static SVG markup.
);
}
echo '</div>';
}
/**
* Convert a configured phone number to a WhatsApp URL.
*
* @param string $phone Raw phone number.
* @param string $message Optional pre-filled conversation message.
* @return string
*/
function gustavoo_portfolio_get_whatsapp_url( $phone, $message = '' ) {
$digits = preg_replace( '/\D+/', '', (string) $phone );
if ( ! $digits ) {
return '';
}
if ( in_array( strlen( $digits ), array( 10, 11 ), true ) ) {
$digits = '55' . $digits;
}
$url = 'https://wa.me/' . $digits;
$message = trim( wp_strip_all_tags( (string) $message ) );
if ( '' !== $message ) {
$url .= '?text=' . rawurlencode( $message );
}
return $url;
}
/** Print the configured floating WhatsApp contact link. */
function gustavoo_portfolio_the_whatsapp_float() {
$phone = (string) gustavoo_portfolio_get_setting( 'whatsapp' );
$message = (string) gustavoo_portfolio_get_setting( 'whatsapp_message' );
$url = gustavoo_portfolio_get_whatsapp_url( $phone, $message );
if ( ! $url ) {
return;
}
?>
<div class="whatsapp-float-wrap"<?php echo is_customize_preview() ? ' data-customize-partial-id="gustavoo_portfolio_whatsapp_shortcut"' : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Static attribute. ?>>
<a class="whatsapp-float" href="<?php echo esc_url( $url ); ?>" target="_blank" rel="noopener noreferrer external" aria-label="<?php esc_attr_e( 'Conversar pelo WhatsApp (abre em uma nova guia)', 'gustavoo-portfolio' ); ?>">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M20.5 3.5A11.8 11.8 0 0 0 12.1 0C5.6 0 .3 5.3.3 11.8c0 2.1.5 4.1 1.6 5.9L.2 23.8l6.3-1.7a11.8 11.8 0 0 0 5.6 1.4h.1c6.5 0 11.8-5.3 11.8-11.8 0-3.1-1.2-6-3.5-8.2ZM12.1 21.4c-1.8 0-3.5-.5-5-1.4l-.4-.2-3.7 1 1-3.6-.2-.4a9.6 9.6 0 1 1 8.3 4.6Zm5.3-7.2c-.3-.2-1.7-.8-2-.9-.3-.1-.5-.2-.7.2-.2.3-.8.9-.9 1.1-.2.2-.3.2-.6.1-1.7-.8-2.8-1.5-3.9-3.4-.3-.5.3-.5.8-1.7.1-.2 0-.4 0-.6l-.9-2.1c-.2-.6-.5-.5-.7-.5h-.6c-.2 0-.5.1-.8.4-.3.3-1 1-1 2.4s1 2.8 1.1 3c.1.2 2 3.1 4.9 4.4 1.8.8 2.5.9 3.4.8.5-.1 1.7-.7 1.9-1.4.2-.7.2-1.3.1-1.4-.1-.2-.3-.3-.6-.4Z"/></svg>
</a>
</div>
<?php
}
/**
* Validate an external project URL without making a network request.
*
* @param string $url Candidate URL.
* @return string Empty string when invalid.
*/
function gustavoo_portfolio_validate_project_url( $url ) {
$url = esc_url_raw( (string) $url, array( 'http', 'https' ) );
$scheme = wp_parse_url( $url, PHP_URL_SCHEME );
$host = wp_parse_url( $url, PHP_URL_HOST );
if ( ! $url || ! $host || ! in_array( strtolower( (string) $scheme ), array( 'http', 'https' ), true ) ) {
return '';
}
return $url;
}
/**
* Return featured projects first, then fill the requested limit by menu order.
*
* @param int $limit Maximum number of projects.
* @return WP_Post[]
*/
function gustavoo_portfolio_get_home_projects( $limit = 6 ) {
$limit = max( 1, min( 24, absint( $limit ) ) );
if ( ! post_type_exists( 'gso_project' ) ) {
return array();
}
$base_args = array(
'post_type' => 'gso_project',
'post_status' => 'publish',
'posts_per_page' => $limit * 3,
'orderby' => array(
'menu_order' => 'ASC',
'date' => 'DESC',
),
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'update_post_term_cache' => true,
);
$featured_args = $base_args;
$featured_args['meta_query'] = array(
array(
'key' => '_gso_project_featured',
'value' => array( '1', 'yes', 'on', 'true' ),
'compare' => 'IN',
),
);
$featured = get_posts( $featured_args );
$others = get_posts( $base_args );
$projects = array();
$seen = array();
foreach ( array_merge( $featured, $others ) as $project ) {
if ( isset( $seen[ $project->ID ] ) ) {
continue;
}
$url = gustavoo_portfolio_validate_project_url( get_post_meta( $project->ID, '_gso_project_url', true ) );
if ( ! $url ) {
continue;
}
$seen[ $project->ID ] = true;
$projects[] = $project;
if ( count( $projects ) >= $limit ) {
break;
}
}
return $projects;
}
/**
* Print post publication metadata.
*
* @return void
*/
function gustavoo_portfolio_posted_on() {
$published = sprintf(
'<time class="entry-date published" datetime="%1$s">%2$s</time>',
esc_attr( get_the_date( DATE_W3C ) ),
esc_html( get_the_date() )
);
printf(
'<span class="posted-on"><span class="screen-reader-text">%1$s </span>%2$s</span>',
esc_html__( 'Publicado em', 'gustavoo-portfolio' ),
$published // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped immediately above.
);
}
/**
* Print the post author link.
*
* @return void
*/
function gustavoo_portfolio_posted_by() {
printf(
'<span class="byline"><span class="screen-reader-text">%1$s </span><a class="url fn n" href="%2$s">%3$s</a></span>',
esc_html__( 'Por', 'gustavoo-portfolio' ),
esc_url( get_author_posts_url( (int) get_the_author_meta( 'ID' ) ) ),
esc_html( get_the_author() )
);
}
/**
* Estimate reading time from the current post content.
*
* @param int|null $post_id Optional post ID.
* @return string
*/
function gustavoo_portfolio_reading_time( $post_id = null ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
$content = wp_strip_all_tags( strip_shortcodes( (string) get_post_field( 'post_content', $post_id ) ) );
$words = preg_split( '/\s+/u', trim( $content ), -1, PREG_SPLIT_NO_EMPTY );
$minutes = max( 1, (int) ceil( count( is_array( $words ) ? $words : array() ) / 220 ) );
return sprintf(
/* translators: %d: reading time in minutes. */
_n( '%d min de leitura', '%d min de leitura', $minutes, 'gustavoo-portfolio' ),
$minutes
);
}
/**
* Print category and tag links for a post.
*
* @return void
*/
function gustavoo_portfolio_entry_terms() {
$categories = get_the_category_list( esc_html_x( ', ', 'category list separator', 'gustavoo-portfolio' ) );
$tags = get_the_tag_list( '', esc_html_x( ', ', 'tag list separator', 'gustavoo-portfolio' ) );
if ( $categories ) {
printf(
'<div class="entry-terms entry-terms--categories"><span>%1$s</span> %2$s</div>',
esc_html__( 'Categorias:', 'gustavoo-portfolio' ),
wp_kses_post( $categories )
);
}
if ( $tags ) {
printf(
'<div class="entry-terms entry-terms--tags"><span>%1$s</span> %2$s</div>',
esc_html__( 'Tags:', 'gustavoo-portfolio' ),
wp_kses_post( $tags )
);
}
}
/**
* Print main-query pagination with accessible labels.
*
* @return void
*/
function gustavoo_portfolio_pagination() {
the_posts_pagination(
array(
'mid_size' => 1,
'prev_text' => sprintf( '<span aria-hidden="true">←</span> %s', esc_html__( 'Anteriores', 'gustavoo-portfolio' ) ),
'next_text' => sprintf( '%s <span aria-hidden="true">→</span>', esc_html__( 'Próximos', 'gustavoo-portfolio' ) ),
'screen_reader_text' => __( 'Navegação entre páginas', 'gustavoo-portfolio' ),
)
);
}
@@ -0,0 +1,204 @@
<?php
/**
* Widget areas and the reusable newsletter widget.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register sidebars used by blog and footer templates.
*
* @return void
*/
function gustavoo_portfolio_register_sidebars() {
$shared = array(
'before_widget' => '<section id="%1$s" class="widget %2$s">',
'after_widget' => '</section>',
'before_title' => '<h2 class="widget__title">',
'after_title' => '</h2>',
);
register_sidebar(
array_merge(
$shared,
array(
'name' => __( 'Barra lateral do blog', 'gustavoo-portfolio' ),
'id' => 'sidebar-blog',
'description' => __( 'Widgets exibidos ao lado de posts, arquivos e buscas.', 'gustavoo-portfolio' ),
)
)
);
register_sidebar(
array_merge(
$shared,
array(
'name' => __( 'Rodapé — coluna 1', 'gustavoo-portfolio' ),
'id' => 'footer-1',
'description' => __( 'Primeira coluna de widgets do rodapé.', 'gustavoo-portfolio' ),
)
)
);
register_sidebar(
array_merge(
$shared,
array(
'name' => __( 'Rodapé — coluna 2', 'gustavoo-portfolio' ),
'id' => 'footer-2',
'description' => __( 'Segunda coluna de widgets do rodapé.', 'gustavoo-portfolio' ),
)
)
);
}
add_action( 'widgets_init', 'gustavoo_portfolio_register_sidebars' );
/**
* Newsletter widget backed by the same safe Fluent Forms renderer as the CTA.
*/
class Gustavao_Portfolio_Newsletter_Widget extends WP_Widget {
/**
* Register the widget with WordPress.
*/
public function __construct() {
parent::__construct(
'gustavoo_portfolio_newsletter',
__( 'Gustavo — Newsletter', 'gustavoo-portfolio' ),
array(
'classname' => 'widget_newsletter',
'description' => __( 'Exibe a chamada de newsletter integrada ao Fluent Forms e FluentCRM.', 'gustavoo-portfolio' ),
'customize_selective_refresh' => true,
)
);
}
/**
* Render widget front end.
*
* @param array<string, string> $args Sidebar wrappers.
* @param array<string, mixed> $instance Saved instance.
* @return void
*/
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : gustavoo_portfolio_get_setting( 'newsletter_heading' );
$text = ! empty( $instance['text'] ) ? $instance['text'] : gustavoo_portfolio_get_setting( 'newsletter_text' );
$form_id = ! empty( $instance['form_id'] ) ? absint( $instance['form_id'] ) : absint( gustavoo_portfolio_get_setting( '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_newsletter__text">%s</p>', esc_html( $text ) );
}
gustavoo_portfolio_the_fluent_form( $form_id, 'newsletter' );
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Registered sidebar wrapper.
}
/**
* Render widget controls.
*
* @param array<string, mixed> $instance Saved instance.
* @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' ); ?></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' ); ?></label>
<textarea class="widefat" rows="4" id="<?php echo esc_attr( $this->get_field_id( 'text' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'text' ) ); ?>"><?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 (vazio usa o global):', 'gustavoo-portfolio' ); ?></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( $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,
);
}
}
/**
* Register the newsletter widget after core has loaded WP_Widget.
*
* @return void
*/
function gustavoo_portfolio_register_widgets() {
register_widget( 'Gustavao_Portfolio_Newsletter_Widget' );
}
add_action( 'widgets_init', 'gustavoo_portfolio_register_widgets' );
/**
* Add one newsletter widget to the blog sidebar on first theme activation.
*
* Existing widget arrangements are preserved and the operation is idempotent.
*
* @return void
*/
function gustavoo_portfolio_seed_newsletter_widget() {
$widget_id_base = 'gustavoo_portfolio_newsletter';
$instances = get_option( 'widget_' . $widget_id_base, array() );
$instances = is_array( $instances ) ? $instances : array();
$sidebars = wp_get_sidebars_widgets();
$sidebars = is_array( $sidebars ) ? $sidebars : array();
foreach ( $sidebars as $widgets ) {
foreach ( (array) $widgets as $widget_id ) {
if ( str_starts_with( (string) $widget_id, $widget_id_base . '-' ) ) {
return;
}
}
}
$indexes = array_filter( array_keys( $instances ), 'is_int' );
$index = $indexes ? max( $indexes ) + 1 : 1;
$instances[ $index ] = array(
'title' => __( 'Newsletter', 'gustavoo-portfolio' ),
'text' => __( 'Receba novos artigos sobre desenvolvimento, infraestrutura e automação.', 'gustavoo-portfolio' ),
'form_id' => 0,
);
$instances['_multiwidget'] = 1;
$sidebars['sidebar-blog'] = array_values(
array_merge(
array( $widget_id_base . '-' . $index ),
isset( $sidebars['sidebar-blog'] ) ? (array) $sidebars['sidebar-blog'] : array()
)
);
update_option( 'widget_' . $widget_id_base, $instances, false );
update_option( 'sidebars_widgets', $sidebars, false );
}
add_action( 'after_switch_theme', 'gustavoo_portfolio_seed_newsletter_widget' );
@@ -0,0 +1,41 @@
<?php
/**
* Main fallback template.
*
* @package Gustavo_Portfolio
*/
get_header();
?>
<main id="primary" class="site-main content-index">
<header class="page-hero">
<div class="site-shell page-hero__inner">
<p class="eyebrow"><?php esc_html_e( 'Conteúdo', 'gustavoo-portfolio' ); ?></p>
<h1 class="page-hero__title"><?php esc_html_e( 'Publicações recentes', 'gustavoo-portfolio' ); ?></h1>
</div>
</header>
<div class="site-shell article-layout">
<div class="article-layout__main">
<?php if ( have_posts() ) : ?>
<div class="posts-grid">
<?php
while ( have_posts() ) {
the_post();
get_template_part( 'template-parts/global/post-card' );
}
?>
</div>
<?php gustavoo_portfolio_pagination(); ?>
<?php else : ?>
<?php get_template_part( 'template-parts/content/content', 'none' ); ?>
<?php endif; ?>
</div>
<?php get_sidebar(); ?>
</div>
</main>
<?php
get_footer();
@@ -0,0 +1,27 @@
<?php
/**
* Standard page template.
*
* @package Gustavo_Portfolio
*/
get_header();
?>
<main id="primary" class="site-main page-main">
<div class="site-shell content-narrow">
<?php
while ( have_posts() ) {
the_post();
get_template_part( 'template-parts/content/content', 'page' );
if ( comments_open() || get_comments_number() ) {
comments_template();
}
}
?>
</div>
</main>
<?php
get_footer();
Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

@@ -0,0 +1,61 @@
<?php
/**
* Search results.
*
* @package Gustavo_Portfolio
*/
get_header();
$gustavoo_search_query = get_search_query();
?>
<main id="primary" class="site-main search-main">
<header class="page-hero page-hero--search">
<div class="site-shell page-hero__inner">
<p class="eyebrow"><?php esc_html_e( 'Pesquisa', 'gustavoo-portfolio' ); ?></p>
<h1 class="page-hero__title">
<?php
printf(
/* translators: %s: search query. */
esc_html__( 'Resultados para “%s”', 'gustavoo-portfolio' ),
esc_html( $gustavoo_search_query )
);
?>
</h1>
<div class="page-hero__search"><?php get_search_form(); ?></div>
</div>
</header>
<div class="site-shell article-layout">
<div class="article-layout__main">
<?php if ( have_posts() ) : ?>
<p class="results-count">
<?php
global $wp_query;
printf(
/* translators: %s: number of search results. */
esc_html( _n( '%s resultado encontrado', '%s resultados encontrados', (int) $wp_query->found_posts, 'gustavoo-portfolio' ) ),
esc_html( number_format_i18n( (int) $wp_query->found_posts ) )
);
?>
</p>
<div class="posts-grid">
<?php
while ( have_posts() ) {
the_post();
get_template_part( 'template-parts/global/post-card' );
}
?>
</div>
<?php gustavoo_portfolio_pagination(); ?>
<?php else : ?>
<?php get_template_part( 'template-parts/content/content', 'none' ); ?>
<?php endif; ?>
</div>
<?php get_sidebar(); ?>
</div>
</main>
<?php
get_footer();
@@ -0,0 +1,23 @@
<?php
/**
* Accessible search form.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$gustavoo_search_id = wp_unique_id( 'site-search-' );
?>
<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
<label class="search-form__label" for="<?php echo esc_attr( $gustavoo_search_id ); ?>">
<span class="screen-reader-text"><?php echo esc_html_x( 'Pesquisar por:', 'label', 'gustavoo-portfolio' ); ?></span>
<input id="<?php echo esc_attr( $gustavoo_search_id ); ?>" type="search" class="search-form__field" placeholder="<?php echo esc_attr_x( 'Pesquisar no blog…', 'placeholder', 'gustavoo-portfolio' ); ?>" value="<?php echo esc_attr( get_search_query() ); ?>" name="s">
</label>
<button type="submit" class="search-form__submit">
<span><?php echo esc_html_x( 'Pesquisar', 'submit button', 'gustavoo-portfolio' ); ?></span>
<span aria-hidden="true">→</span>
</button>
</form>
@@ -0,0 +1,35 @@
<?php
/**
* Blog sidebar.
*
* @package Gustavo_Portfolio
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<aside id="secondary" class="sidebar" aria-label="<?php esc_attr_e( 'Barra lateral do blog', 'gustavoo-portfolio' ); ?>">
<?php if ( is_active_sidebar( 'sidebar-blog' ) ) : ?>
<?php dynamic_sidebar( 'sidebar-blog' ); ?>
<?php else : ?>
<section class="widget widget_search">
<h2 class="widget__title"><?php esc_html_e( 'Pesquisar', 'gustavoo-portfolio' ); ?></h2>
<?php get_search_form(); ?>
</section>
<section class="widget widget_categories">
<h2 class="widget__title"><?php esc_html_e( 'Categorias', 'gustavoo-portfolio' ); ?></h2>
<ul>
<?php
wp_list_categories(
array(
'title_li' => '',
'show_count' => true,
)
);
?>
</ul>
</section>
<?php endif; ?>
</aside>
@@ -0,0 +1,138 @@
<?php
/**
* Single blog post template.
*
* @package Gustavo_Portfolio
*/
get_header();
while ( have_posts() ) :
the_post();
?>
<main id="primary" class="site-main single-post-main">
<header class="entry-hero">
<div class="site-shell entry-hero__inner">
<nav class="entry-breadcrumbs" aria-label="<?php esc_attr_e( 'Navegação estrutural', 'gustavoo-portfolio' ); ?>">
<a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php esc_html_e( 'Início', 'gustavoo-portfolio' ); ?></a>
<span aria-hidden="true"></span>
<a href="<?php echo esc_url( gustavoo_portfolio_get_blog_url() ); ?>"><?php esc_html_e( 'Últimas notícias', 'gustavoo-portfolio' ); ?></a>
<?php $gustavoo_breadcrumb_categories = get_the_category(); ?>
<?php if ( ! empty( $gustavoo_breadcrumb_categories ) ) : ?>
<span aria-hidden="true"></span>
<a href="<?php echo esc_url( get_category_link( $gustavoo_breadcrumb_categories[0] ) ); ?>"><?php echo esc_html( $gustavoo_breadcrumb_categories[0]->name ); ?></a>
<?php endif; ?>
</nav>
<?php $gustavoo_categories = get_the_category(); ?>
<?php if ( ! empty( $gustavoo_categories ) ) : ?>
<a class="entry-category-badge" href="<?php echo esc_url( get_category_link( $gustavoo_categories[0] ) ); ?>"><?php echo esc_html( $gustavoo_categories[0]->name ); ?></a>
<?php endif; ?>
<?php the_title( '<h1 class="entry-hero__title">', '</h1>' ); ?>
</div>
</header>
<div class="site-shell entry-post-meta">
<div class="entry-post-meta__author">
<strong><?php echo esc_html( get_the_author() ); ?></strong>
<span aria-hidden="true">·</span>
<span><?php esc_html_e( 'Publicado em', 'gustavoo-portfolio' ); ?> <?php echo esc_html( get_the_date() ); ?></span>
<span aria-hidden="true">·</span>
<span><?php echo esc_html( gustavoo_portfolio_reading_time() ); ?></span>
</div>
<?php gustavoo_portfolio_post_share_links(); ?>
</div>
<?php if ( has_post_thumbnail() ) : ?>
<figure class="entry-featured-image">
<?php the_post_thumbnail( 'full', array( 'class' => 'entry-featured-image__image' ) ); ?>
</figure>
<?php endif; ?>
<div class="site-shell article-layout article-layout--single">
<article id="post-<?php the_ID(); ?>" <?php post_class( 'article article--single' ); ?>>
<div class="entry-content prose">
<?php the_content(); ?>
<?php
wp_link_pages(
array(
'before' => '<nav class="page-links" aria-label="' . esc_attr__( 'Páginas deste artigo', 'gustavoo-portfolio' ) . '"><span>' . esc_html__( 'Páginas:', 'gustavoo-portfolio' ) . '</span>',
'after' => '</nav>',
)
);
?>
</div>
<footer class="entry-footer">
<?php gustavoo_portfolio_entry_terms(); ?>
</footer>
<?php
$gustavoo_categories = get_the_category();
?>
<section class="post-share-panel" aria-labelledby="post-share-title">
<h2 id="post-share-title" class="post-share-panel__title"><?php esc_html_e( 'Compartilhar:', 'gustavoo-portfolio' ); ?></h2>
<?php gustavoo_portfolio_post_share_links( 'post-share-panel__links' ); ?>
</section>
<?php if ( ! empty( $gustavoo_categories ) ) : ?>
<?php
$gustavoo_related_posts = new WP_Query(
array(
'category__in' => wp_list_pluck( $gustavoo_categories, 'term_id' ),
'post__not_in' => array( get_the_ID() ),
'posts_per_page' => 3,
'ignore_sticky_posts' => true,
'orderby' => 'date',
'order' => 'DESC',
)
);
?>
<?php if ( $gustavoo_related_posts->have_posts() ) : ?>
<section class="related-posts" aria-labelledby="related-posts-title">
<div class="related-posts__heading"><span></span><h2 id="related-posts-title"><?php esc_html_e( 'Relacionadas', 'gustavoo-portfolio' ); ?></h2><span></span></div>
<div class="related-posts__grid">
<?php while ( $gustavoo_related_posts->have_posts() ) : $gustavoo_related_posts->the_post(); ?>
<article <?php post_class( 'related-post' ); ?>>
<a class="related-post__link" href="<?php the_permalink(); ?>">
<?php if ( has_post_thumbnail() ) : ?>
<?php the_post_thumbnail( 'gustavoo-post-card', array( 'class' => 'related-post__image', 'loading' => 'lazy', 'decoding' => 'async' ) ); ?>
<?php endif; ?>
<h3 class="related-post__title"><?php the_title(); ?></h3>
</a>
</article>
<?php endwhile; ?>
</div>
</section>
<?php endif; ?>
<?php wp_reset_postdata(); ?>
<?php endif; ?>
<?php $gustavoo_previous_post = get_previous_post(); ?>
<?php $gustavoo_next_post = get_next_post(); ?>
<?php if ( $gustavoo_previous_post || $gustavoo_next_post ) : ?>
<nav class="post-navigation post-navigation--editorial" aria-label="<?php esc_attr_e( 'Navegação entre artigos', 'gustavoo-portfolio' ); ?>">
<div class="nav-links">
<?php if ( $gustavoo_previous_post ) : ?>
<a class="nav-previous" href="<?php echo esc_url( get_permalink( $gustavoo_previous_post ) ); ?>"><span class="nav-subtitle">← <?php esc_html_e( 'Anterior', 'gustavoo-portfolio' ); ?></span><span class="nav-title"><?php echo esc_html( get_the_title( $gustavoo_previous_post ) ); ?></span></a>
<?php endif; ?>
<?php if ( $gustavoo_next_post ) : ?>
<a class="nav-next" href="<?php echo esc_url( get_permalink( $gustavoo_next_post ) ); ?>"><span class="nav-subtitle"><?php esc_html_e( 'Próximo', 'gustavoo-portfolio' ); ?> →</span><span class="nav-title"><?php echo esc_html( get_the_title( $gustavoo_next_post ) ); ?></span></a>
<?php endif; ?>
</div>
</nav>
<?php endif; ?>
<?php if ( comments_open() || get_comments_number() ) {
comments_template();
}
?>
</article>
<?php get_sidebar(); ?>
</div>
</main>
<?php
endwhile;
get_footer();
@@ -0,0 +1,13 @@
/*
Theme Name: Gustavo Portfolio
Theme URI: https://gustavoo.me/
Author: Gustavo Oliveira
Author URI: https://gustavoo.me/
Description: Tema clássico para o portfólio e blog de Gustavo Oliveira.
Version: 1.1.0
Requires at least: 6.5
Requires PHP: 8.1
Text Domain: gustavoo-portfolio
License: GNU General Public License v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
*/
@@ -0,0 +1,17 @@
<?php
/**
* Empty result state.
*
* @package Gustavo_Portfolio
*/
?>
<section class="no-results not-found" aria-labelledby="no-results-title">
<h2 id="no-results-title" class="no-results__title"><?php esc_html_e( 'Nada encontrado', 'gustavoo-portfolio' ); ?></h2>
<?php if ( is_search() ) : ?>
<p><?php esc_html_e( 'Não encontramos conteúdo com esses termos. Tente palavras diferentes.', 'gustavoo-portfolio' ); ?></p>
<?php get_search_form(); ?>
<?php else : ?>
<p><?php esc_html_e( 'Ainda não há publicações aqui.', 'gustavoo-portfolio' ); ?></p>
<?php endif; ?>
</section>
@@ -0,0 +1,32 @@
<?php
/**
* Page content.
*
* @package Gustavo_Portfolio
*/
?>
<article id="post-<?php the_ID(); ?>" <?php post_class( 'page-article' ); ?>>
<header class="page-article__header">
<p class="eyebrow"><?php esc_html_e( 'Página', 'gustavoo-portfolio' ); ?></p>
<?php the_title( '<h1 class="page-article__title">', '</h1>' ); ?>
</header>
<?php if ( has_post_thumbnail() ) : ?>
<figure class="page-article__media">
<?php the_post_thumbnail( 'full', array( 'class' => 'page-article__image' ) ); ?>
</figure>
<?php endif; ?>
<div class="entry-content prose">
<?php the_content(); ?>
<?php
wp_link_pages(
array(
'before' => '<nav class="page-links" aria-label="' . esc_attr__( 'Páginas deste conteúdo', 'gustavoo-portfolio' ) . '"><span>' . esc_html__( 'Páginas:', 'gustavoo-portfolio' ) . '</span>',
'after' => '</nav>',
)
);
?>
</div>
</article>
@@ -0,0 +1,41 @@
<?php
/**
* Front-page about section.
*
* @package Gustavo_Portfolio
*/
$gustavoo_about_text = (string) gustavoo_portfolio_get_setting( 'about_text' );
$gustavoo_about_secondary_text = (string) gustavoo_portfolio_get_setting( 'about_secondary_text' );
?>
<section id="sobre" class="section section--about" aria-labelledby="about-title">
<div class="site-shell about-layout">
<header class="section__header about-layout__header">
<p class="eyebrow"><?php echo esc_html( gustavoo_portfolio_get_setting( 'about_eyebrow' ) ); ?></p>
<h2 id="about-title" class="section__title"><?php echo esc_html( gustavoo_portfolio_get_setting( 'about_title' ) ); ?></h2>
</header>
<div class="about-layout__content">
<div class="prose prose--lead">
<?php echo wp_kses_post( wpautop( esc_html( $gustavoo_about_text ) ) ); ?>
<?php echo wp_kses_post( wpautop( esc_html( $gustavoo_about_secondary_text ) ) ); ?>
</div>
<dl class="about-facts" aria-label="<?php esc_attr_e( 'Diferenciais profissionais', 'gustavoo-portfolio' ); ?>">
<div class="about-fact">
<dt><?php esc_html_e( 'Escopo', 'gustavoo-portfolio' ); ?></dt>
<dd><?php esc_html_e( 'Da regra de negócio à infraestrutura', 'gustavoo-portfolio' ); ?></dd>
</div>
<div class="about-fact">
<dt><?php esc_html_e( 'Entrega', 'gustavoo-portfolio' ); ?></dt>
<dd><?php esc_html_e( 'Código limpo, documentado e sustentável', 'gustavoo-portfolio' ); ?></dd>
</div>
<div class="about-fact">
<dt><?php esc_html_e( 'Visão', 'gustavoo-portfolio' ); ?></dt>
<dd><?php esc_html_e( 'Tecnologia alinhada ao crescimento', 'gustavoo-portfolio' ); ?></dd>
</div>
</dl>
</div>
</div>
</section>
@@ -0,0 +1,46 @@
<?php
/**
* Front-page contact section.
*
* @package Gustavo_Portfolio
*/
$gustavoo_contact_form_id = absint( gustavoo_portfolio_get_setting( 'contact_form_id' ) );
$gustavoo_email = sanitize_email( (string) gustavoo_portfolio_get_setting( 'email' ) );
$gustavoo_privacy_url = get_privacy_policy_url();
?>
<section id="contato" class="section section--contact" aria-labelledby="contact-title">
<div class="site-shell contact-panel">
<div class="contact-panel__content">
<p class="eyebrow"><?php echo esc_html( gustavoo_portfolio_get_setting( 'contact_eyebrow' ) ); ?></p>
<h2 id="contact-title" class="section__title"><?php echo esc_html( gustavoo_portfolio_get_setting( 'contact_heading' ) ); ?></h2>
<p class="contact-panel__description"><?php echo esc_html( gustavoo_portfolio_get_setting( 'contact_text' ) ); ?></p>
<?php if ( $gustavoo_email ) : ?>
<ul class="contact-list">
<?php if ( $gustavoo_email ) : ?>
<li><span><?php esc_html_e( 'E-mail', 'gustavoo-portfolio' ); ?></span><a href="<?php echo esc_url( 'mailto:' . $gustavoo_email ); ?>"><?php echo esc_html( antispambot( $gustavoo_email ) ); ?></a></li>
<?php endif; ?>
</ul>
<?php endif; ?>
<?php gustavoo_portfolio_social_links( 'social-links social-links--contact' ); ?>
</div>
<div class="contact-panel__form">
<?php gustavoo_portfolio_the_fluent_form( $gustavoo_contact_form_id, 'contact' ); ?>
<?php if ( $gustavoo_privacy_url ) : ?>
<p class="form-privacy">
<?php
printf(
/* translators: %s: privacy policy URL. */
wp_kses_post( __( 'Ao enviar, você concorda com o tratamento dos dados conforme a <a href="%s">Política de Privacidade</a>.', 'gustavoo-portfolio' ) ),
esc_url( $gustavoo_privacy_url )
);
?>
</p>
<?php endif; ?>
</div>
</div>
</section>
@@ -0,0 +1,54 @@
<?php
/**
* Front-page hero.
*
* @package Gustavo_Portfolio
*/
$gustavoo_hero_eyebrow = (string) gustavoo_portfolio_get_setting( 'hero_eyebrow' );
$gustavoo_hero_title = (string) gustavoo_portfolio_get_setting( 'hero_title' );
$gustavoo_hero_description = (string) gustavoo_portfolio_get_setting( 'hero_description' );
$gustavoo_hero_primary_label = (string) gustavoo_portfolio_get_setting( 'hero_primary_label' );
$gustavoo_hero_primary_url = (string) gustavoo_portfolio_get_setting( 'hero_primary_url' );
$gustavoo_hero_secondary_label = (string) gustavoo_portfolio_get_setting( 'hero_secondary_label' );
$gustavoo_hero_secondary_url = (string) gustavoo_portfolio_get_setting( 'hero_secondary_url' );
$gustavoo_hero_sources = gustavoo_portfolio_get_picture_sources( 'hero-art' );
?>
<section class="hero-stage" aria-labelledby="hero-title">
<div class="site-shell hero">
<div class="hero__content">
<?php if ( $gustavoo_hero_eyebrow ) : ?>
<p class="eyebrow hero__eyebrow"><?php echo esc_html( $gustavoo_hero_eyebrow ); ?></p>
<?php endif; ?>
<h1 id="hero-title" class="hero__title"><?php echo esc_html( $gustavoo_hero_title ); ?></h1>
<p class="hero__description"><?php echo esc_html( $gustavoo_hero_description ); ?></p>
<div class="hero__actions">
<?php if ( $gustavoo_hero_primary_label && $gustavoo_hero_primary_url ) : ?>
<a class="button button--primary" href="<?php echo esc_url( $gustavoo_hero_primary_url ); ?>">
<?php echo esc_html( $gustavoo_hero_primary_label ); ?> <span aria-hidden="true">→</span>
</a>
<?php endif; ?>
<?php if ( $gustavoo_hero_secondary_label && $gustavoo_hero_secondary_url ) : ?>
<a class="button button--secondary" href="<?php echo esc_url( $gustavoo_hero_secondary_url ); ?>">
<?php echo esc_html( $gustavoo_hero_secondary_label ); ?>
</a>
<?php endif; ?>
</div>
</div>
<div class="hero__visual" aria-hidden="true">
<div class="hero__visual-glow"></div>
<picture>
<?php if ( $gustavoo_hero_sources['webp'] ) : ?>
<source srcset="<?php echo esc_url( $gustavoo_hero_sources['webp'] ); ?>" type="image/webp">
<?php endif; ?>
<img class="hero__image" src="<?php echo esc_url( $gustavoo_hero_sources['fallback'] ); ?>" width="1234" height="1280" alt="" loading="eager" decoding="async" fetchpriority="high">
</picture>
</div>
</div>
</section>
@@ -0,0 +1,47 @@
<?php
/**
* Front-page latest posts.
*
* @package Gustavo_Portfolio
*/
$gustavoo_blog_limit = max( 1, min( 12, absint( gustavoo_portfolio_get_setting( 'blog_limit', 4 ) ) ) );
$gustavoo_posts = new WP_Query(
array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $gustavoo_blog_limit,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
)
);
?>
<section id="blog" class="section section--posts" aria-labelledby="latest-posts-title">
<div class="site-shell">
<header class="section__header section__header--split">
<div>
<p class="eyebrow"><?php echo esc_html( gustavoo_portfolio_get_setting( 'blog_eyebrow' ) ); ?></p>
<h2 id="latest-posts-title" class="section__title"><?php echo esc_html( gustavoo_portfolio_get_setting( 'blog_title' ) ); ?></h2>
</div>
<p class="section__description"><?php echo esc_html( gustavoo_portfolio_get_setting( 'blog_text' ) ); ?></p>
</header>
<?php if ( $gustavoo_posts->have_posts() ) : ?>
<div class="posts-grid posts-grid--featured">
<?php
while ( $gustavoo_posts->have_posts() ) {
$gustavoo_posts->the_post();
get_template_part( 'template-parts/global/post-card' );
}
?>
</div>
<p class="section__action"><a class="button button--secondary" href="<?php echo esc_url( gustavoo_portfolio_get_blog_url() ); ?>"><?php esc_html_e( 'Explorar todos os artigos', 'gustavoo-portfolio' ); ?> <span aria-hidden="true">→</span></a></p>
<?php else : ?>
<div class="section__empty"><p><?php esc_html_e( 'O primeiro artigo está a caminho.', 'gustavoo-portfolio' ); ?></p></div>
<?php endif; ?>
<?php wp_reset_postdata(); ?>
</div>
</section>
@@ -0,0 +1,49 @@
<?php
/**
* Front-page project grid.
*
* @package Gustavo_Portfolio
*/
$gustavoo_project_limit = max( 1, min( 24, absint( gustavoo_portfolio_get_setting( 'projects_limit', 6 ) ) ) );
$gustavoo_projects = gustavoo_portfolio_get_home_projects( $gustavoo_project_limit );
$gustavoo_archive_url = esc_url_raw( (string) gustavoo_portfolio_get_setting( 'projects_archive_url' ) );
?>
<section id="projetos" class="section section--projects" aria-labelledby="projects-title">
<div class="site-shell">
<header class="section__header section__header--split">
<div>
<p class="eyebrow"><?php echo esc_html( gustavoo_portfolio_get_setting( 'projects_eyebrow' ) ); ?></p>
<h2 id="projects-title" class="section__title"><?php echo esc_html( gustavoo_portfolio_get_setting( 'projects_title' ) ); ?></h2>
</div>
<p class="section__description"><?php echo esc_html( gustavoo_portfolio_get_setting( 'projects_text' ) ); ?></p>
</header>
<?php if ( $gustavoo_projects ) : ?>
<div class="projects-grid">
<?php
global $post;
foreach ( $gustavoo_projects as $post ) {
setup_postdata( $post );
get_template_part( 'template-parts/global/project-card' );
}
wp_reset_postdata();
?>
</div>
<?php else : ?>
<div class="section__empty">
<p><?php esc_html_e( 'Novos projetos serão publicados aqui em breve.', 'gustavoo-portfolio' ); ?></p>
<?php if ( current_user_can( 'edit_posts' ) && post_type_exists( 'gso_project' ) ) : ?>
<a class="text-link" href="<?php echo esc_url( admin_url( 'post-new.php?post_type=gso_project' ) ); ?>"><?php esc_html_e( 'Adicionar o primeiro projeto', 'gustavoo-portfolio' ); ?> <span aria-hidden="true">→</span></a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if ( $gustavoo_archive_url ) : ?>
<p class="section__action"><a class="button button--secondary" href="<?php echo esc_url( $gustavoo_archive_url ); ?>"><?php esc_html_e( 'Ver todos os projetos', 'gustavoo-portfolio' ); ?> <span aria-hidden="true">→</span></a></p>
<?php endif; ?>
</div>
</section>
@@ -0,0 +1,31 @@
<?php
/**
* Front-page services section.
*
* @package Gustavo_Portfolio
*/
$gustavoo_services = gustavoo_portfolio_get_services();
?>
<section id="servicos" class="section section--services" aria-labelledby="services-title">
<div class="site-shell">
<header class="section__header section__header--split">
<div>
<p class="eyebrow"><?php echo esc_html( gustavoo_portfolio_get_setting( 'services_eyebrow' ) ); ?></p>
<h2 id="services-title" class="section__title"><?php echo esc_html( gustavoo_portfolio_get_setting( 'services_title' ) ); ?></h2>
</div>
<p class="section__description"><?php echo esc_html( gustavoo_portfolio_get_setting( 'services_description' ) ); ?></p>
</header>
<div class="services-grid">
<?php foreach ( $gustavoo_services as $gustavoo_service ) : ?>
<article class="service-card">
<p class="service-card__number" aria-hidden="true"><?php echo esc_html( $gustavoo_service['number'] ?? '' ); ?></p>
<h3 class="service-card__title"><?php echo esc_html( $gustavoo_service['title'] ?? '' ); ?></h3>
<p class="service-card__description"><?php echo esc_html( $gustavoo_service['description'] ?? '' ); ?></p>
</article>
<?php endforeach; ?>
</div>
</div>
</section>
@@ -0,0 +1,22 @@
<?php
/** Compact editorial news card. @package Gustavo_Portfolio */
$gustavoo_args = isset( $args ) && is_array( $args ) ? $args : array();
$gustavoo_is_featured = ! empty( $gustavoo_args['featured'] );
$gustavoo_categories = get_the_category();
$gustavoo_category = $gustavoo_categories ? $gustavoo_categories[0] : null;
$gustavoo_image_url = gustavoo_portfolio_get_news_image_url( get_the_ID(), $gustavoo_is_featured ? 'large' : 'gustavoo-post-card' );
?>
<article <?php post_class( 'latest-news-card' . ( $gustavoo_is_featured ? ' latest-news-card--featured' : '' ) ); ?>>
<a class="latest-news-card__media" href="<?php the_permalink(); ?>" aria-hidden="true" tabindex="-1">
<?php if ( $gustavoo_image_url ) : ?>
<img class="latest-news-card__image" src="<?php echo esc_url( $gustavoo_image_url ); ?>" loading="<?php echo $gustavoo_is_featured ? 'eager' : 'lazy'; ?>" decoding="async" alt="">
<?php else : ?><span class="latest-news-card__placeholder" aria-hidden="true"></span><?php endif; ?>
</a>
<div class="latest-news-card__body">
<?php if ( $gustavoo_category ) : ?><a class="latest-news-card__category" href="<?php echo esc_url( get_category_link( $gustavoo_category ) ); ?>"><?php echo esc_html( $gustavoo_category->name ); ?></a><?php endif; ?>
<h2 class="latest-news-card__title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<time class="latest-news-card__date" datetime="<?php echo esc_attr( get_the_date( DATE_W3C ) ); ?>"><?php echo esc_html( get_the_date() ); ?></time>
<?php if ( $gustavoo_is_featured && has_excerpt() ) : ?><p class="latest-news-card__excerpt"><?php echo esc_html( wp_trim_words( get_the_excerpt(), 28 ) ); ?></p><?php endif; ?>
</div>
</article>
@@ -0,0 +1,47 @@
<?php
/**
* Secondary compact news list for news and category pages.
*
* @package Gustavo_Portfolio
*/
$gustavoo_args = isset( $args ) && is_array( $args ) ? $args : array();
$gustavoo_query_args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 4,
'post__not_in' => array_map( 'absint', $gustavoo_args['exclude'] ?? array() ),
'ignore_sticky_posts' => true,
'orderby' => 'date',
'order' => 'DESC',
);
if ( ! empty( $gustavoo_args['category_id'] ) ) {
$gustavoo_query_args['cat'] = absint( $gustavoo_args['category_id'] );
}
$gustavoo_latest_posts = new WP_Query( $gustavoo_query_args );
if ( ! $gustavoo_latest_posts->have_posts() ) {
return;
}
?>
<section class="latest-news-list" aria-labelledby="latest-news-list-title">
<h2 id="latest-news-list-title" class="latest-news-list__heading"><?php esc_html_e( 'Últimas notícias', 'gustavoo-portfolio' ); ?></h2>
<div class="latest-news-list__items">
<?php while ( $gustavoo_latest_posts->have_posts() ) : $gustavoo_latest_posts->the_post(); ?>
<?php $gustavoo_categories = get_the_category(); $gustavoo_category = $gustavoo_categories ? $gustavoo_categories[0] : null; $gustavoo_image_url = gustavoo_portfolio_get_news_image_url(); ?>
<article <?php post_class( 'latest-news-list__item' ); ?>>
<a class="latest-news-list__image" href="<?php the_permalink(); ?>" aria-hidden="true" tabindex="-1">
<?php if ( $gustavoo_image_url ) : ?><img src="<?php echo esc_url( $gustavoo_image_url ); ?>" loading="lazy" decoding="async" alt=""><?php endif; ?>
</a>
<div class="latest-news-list__body">
<?php if ( $gustavoo_category ) : ?><a class="latest-news-card__category" href="<?php echo esc_url( get_category_link( $gustavoo_category ) ); ?>"><?php echo esc_html( $gustavoo_category->name ); ?></a><?php endif; ?>
<h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
<time datetime="<?php echo esc_attr( get_the_date( DATE_W3C ) ); ?>"><?php echo esc_html( get_the_date( 'D, d/m/Y · H:i' ) ); ?></time>
</div>
</article>
<?php endwhile; ?>
</div>
</section>
<?php wp_reset_postdata(); ?>
@@ -0,0 +1,35 @@
<?php
/**
* Global newsletter call to action, rendered before every site footer.
*
* @package Gustavo_Portfolio
*/
$gustavoo_newsletter_form_id = absint( gustavoo_portfolio_get_setting( 'newsletter_form_id' ) );
$gustavoo_privacy_url = get_privacy_policy_url();
?>
<section class="section section--newsletter" aria-labelledby="newsletter-title">
<div class="site-shell newsletter-panel">
<div class="newsletter-panel__content">
<p class="eyebrow"><?php echo esc_html( gustavoo_portfolio_get_setting( 'newsletter_eyebrow' ) ); ?></p>
<h2 id="newsletter-title" class="newsletter-panel__title"><?php echo esc_html( gustavoo_portfolio_get_setting( 'newsletter_heading' ) ); ?></h2>
<p class="newsletter-panel__description"><?php echo esc_html( gustavoo_portfolio_get_setting( 'newsletter_text' ) ); ?></p>
</div>
<div class="newsletter-panel__form">
<?php gustavoo_portfolio_the_fluent_form( $gustavoo_newsletter_form_id, 'newsletter' ); ?>
<?php if ( $gustavoo_privacy_url ) : ?>
<p class="form-privacy">
<?php
printf(
/* translators: %s: privacy policy URL. */
wp_kses_post( __( 'Você pode cancelar a inscrição a qualquer momento. Consulte a <a href="%s">Política de Privacidade</a>.', 'gustavoo-portfolio' ) ),
esc_url( $gustavoo_privacy_url )
);
?>
</p>
<?php endif; ?>
</div>
</div>
</section>
@@ -0,0 +1,45 @@
<?php
/**
* Blog post card.
*
* @package Gustavo_Portfolio
*/
$gustavoo_categories = get_the_category();
$gustavoo_category = $gustavoo_categories ? $gustavoo_categories[0] : null;
?>
<article <?php post_class( 'post-card' ); ?>>
<?php if ( has_post_thumbnail() ) : ?>
<a class="post-card__media" href="<?php the_permalink(); ?>" tabindex="-1" aria-hidden="true">
<?php
the_post_thumbnail(
'gustavoo-post-card',
array(
'class' => 'post-card__image',
'loading' => 'lazy',
'decoding' => 'async',
'alt' => '',
)
);
?>
</a>
<?php endif; ?>
<div class="post-card__body">
<div class="post-card__meta">
<?php if ( $gustavoo_category ) : ?>
<a class="post-card__category" href="<?php echo esc_url( get_category_link( $gustavoo_category ) ); ?>"><?php echo esc_html( $gustavoo_category->name ); ?></a>
<?php endif; ?>
<?php gustavoo_portfolio_posted_on(); ?>
<span class="reading-time"><?php echo esc_html( gustavoo_portfolio_reading_time() ); ?></span>
</div>
<h2 class="post-card__title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<p class="post-card__excerpt"><?php echo esc_html( wp_trim_words( get_the_excerpt(), 26 ) ); ?></p>
<a class="post-card__more" href="<?php the_permalink(); ?>">
<?php esc_html_e( 'Ler artigo', 'gustavoo-portfolio' ); ?> <span aria-hidden="true">→</span>
<span class="screen-reader-text">: <?php the_title(); ?></span>
</a>
</div>
</article>
@@ -0,0 +1,84 @@
<?php
/**
* Project card.
*
* @package Gustavo_Portfolio
*/
$gustavoo_project_id = get_the_ID();
$gustavoo_project_url = gustavoo_portfolio_validate_project_url( get_post_meta( $gustavoo_project_id, '_gso_project_url', true ) );
$gustavoo_project_client = sanitize_text_field( (string) get_post_meta( $gustavoo_project_id, '_gso_project_client', true ) );
$gustavoo_project_year = absint( get_post_meta( $gustavoo_project_id, '_gso_project_year', true ) );
$gustavoo_project_link_label = sanitize_text_field( (string) get_post_meta( $gustavoo_project_id, '_gso_project_link_label', true ) );
$gustavoo_project_link_label = $gustavoo_project_link_label ?: __( 'Ver projeto', 'gustavoo-portfolio' );
$gustavoo_project_types = taxonomy_exists( 'gso_project_type' ) ? get_the_terms( $gustavoo_project_id, 'gso_project_type' ) : array();
$gustavoo_project_tech = taxonomy_exists( 'gso_project_tech' ) ? get_the_terms( $gustavoo_project_id, 'gso_project_tech' ) : array();
if ( ! $gustavoo_project_url ) {
return;
}
?>
<article <?php post_class( 'project-card' ); ?>>
<a class="project-card__link" href="<?php echo esc_url( $gustavoo_project_url ); ?>" target="_blank" rel="noopener noreferrer external">
<figure class="project-card__media">
<?php if ( has_post_thumbnail() ) : ?>
<?php
$gustavoo_thumbnail_id = get_post_thumbnail_id();
$gustavoo_thumbnail_alt = get_post_meta( $gustavoo_thumbnail_id, '_wp_attachment_image_alt', true );
$gustavoo_thumbnail_alt = $gustavoo_thumbnail_alt ?: sprintf(
/* translators: %s: project title. */
__( 'Imagem do projeto %s', 'gustavoo-portfolio' ),
get_the_title()
);
echo get_the_post_thumbnail(
$gustavoo_project_id,
'gustavoo-project-card',
array(
'alt' => $gustavoo_thumbnail_alt,
'class' => 'project-card__image',
'loading' => 'lazy',
'decoding' => 'async',
)
); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Core image markup.
?>
<?php else : ?>
<span class="project-card__placeholder" aria-hidden="true">
<img src="<?php echo esc_url( gustavoo_portfolio_get_image_source( 'logo-mark' ) ); ?>" width="96" height="96" alt="">
</span>
<?php endif; ?>
</figure>
<div class="project-card__body">
<?php if ( ! is_wp_error( $gustavoo_project_types ) && $gustavoo_project_types ) : ?>
<p class="project-card__type"><?php echo esc_html( implode( ' · ', wp_list_pluck( $gustavoo_project_types, 'name' ) ) ); ?></p>
<?php endif; ?>
<h3 class="project-card__title"><?php the_title(); ?></h3>
<?php if ( has_excerpt() ) : ?>
<p class="project-card__description"><?php echo esc_html( wp_trim_words( get_the_excerpt(), 24 ) ); ?></p>
<?php endif; ?>
<?php if ( $gustavoo_project_client || $gustavoo_project_year ) : ?>
<p class="project-card__meta">
<?php echo esc_html( implode( ' · ', array_filter( array( $gustavoo_project_client, $gustavoo_project_year ? (string) $gustavoo_project_year : '' ) ) ) ); ?>
</p>
<?php endif; ?>
<?php if ( ! is_wp_error( $gustavoo_project_tech ) && $gustavoo_project_tech ) : ?>
<ul class="project-card__tags" aria-label="<?php esc_attr_e( 'Tecnologias', 'gustavoo-portfolio' ); ?>">
<?php foreach ( array_slice( $gustavoo_project_tech, 0, 4 ) as $gustavoo_technology ) : ?>
<li><?php echo esc_html( $gustavoo_technology->name ); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<span class="project-card__cta">
<?php echo esc_html( $gustavoo_project_link_label ); ?> <span aria-hidden="true">↗</span>
<span class="screen-reader-text"> <?php esc_html_e( '(abre em uma nova guia)', 'gustavoo-portfolio' ); ?></span>
</span>
</div>
</a>
</article>
@@ -0,0 +1,8 @@
<?php
/**
* Floating WhatsApp action used by the Customizer partial refresh.
*
* @package Gustavo_Portfolio
*/
gustavoo_portfolio_the_whatsapp_float();
@@ -0,0 +1,119 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"layout": {
"contentSize": "760px",
"wideSize": "1160px"
},
"color": {
"defaultPalette": false,
"palette": [
{ "slug": "background", "name": "Fundo", "color": "#071624" },
{ "slug": "background-alt", "name": "Fundo alternativo", "color": "#0a1c2d" },
{ "slug": "surface", "name": "Superfície", "color": "#0d2438" },
{ "slug": "text", "name": "Texto", "color": "#f5f8fa" },
{ "slug": "text-soft", "name": "Texto suave", "color": "#b8c5ce" },
{ "slug": "brand", "name": "Menta", "color": "#74d5c7" },
{ "slug": "brand-strong", "name": "Menta forte", "color": "#50e5cf" }
]
},
"spacing": {
"spacingScale": {
"steps": 0
},
"spacingSizes": [
{ "slug": "10", "name": "XS", "size": "0.5rem" },
{ "slug": "20", "name": "S", "size": "0.75rem" },
{ "slug": "30", "name": "M", "size": "1rem" },
{ "slug": "40", "name": "L", "size": "1.5rem" },
{ "slug": "50", "name": "XL", "size": "2rem" },
{ "slug": "60", "name": "2XL", "size": "3rem" },
{ "slug": "70", "name": "3XL", "size": "4rem" }
],
"units": ["px", "rem", "em", "%", "vw", "vh"]
},
"typography": {
"fluid": true,
"fontFamilies": [
{
"fontFamily": "Inter, system-ui, sans-serif",
"name": "Inter",
"slug": "inter",
"fontFace": [
{
"fontFamily": "Inter",
"fontStyle": "normal",
"fontWeight": "100 900",
"src": ["file:./assets/fonts/inter-variable.woff2"]
}
]
},
{
"fontFamily": "SFMono-Regular, Consolas, Liberation Mono, monospace",
"name": "Monoespaçada",
"slug": "mono"
}
],
"fontSizes": [
{ "slug": "small", "name": "Pequeno", "size": "0.875rem" },
{ "slug": "medium", "name": "Médio", "size": "1rem" },
{ "slug": "large", "name": "Grande", "size": "1.25rem" },
{ "slug": "x-large", "name": "Muito grande", "size": "clamp(1.75rem, 3vw, 2.75rem)" },
{ "slug": "hero", "name": "Hero", "size": "clamp(2.7rem, 5.4vw, 5.3rem)" }
]
},
"border": {
"color": true,
"radius": true,
"style": true,
"width": true
}
},
"styles": {
"color": {
"background": "#071624",
"text": "#f5f8fa"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--inter)",
"fontSize": "1.0625rem",
"lineHeight": "1.72"
},
"elements": {
"button": {
"border": { "radius": "12px" },
"color": { "background": "#74d5c7", "text": "#062722" },
"typography": { "fontWeight": "700" }
},
"caption": {
"color": { "text": "#879aa9" },
"typography": { "fontSize": "0.8rem" }
},
"heading": {
"color": { "text": "#f5f8fa" },
"typography": {
"fontFamily": "var(--wp--preset--font-family--inter)",
"fontWeight": "750",
"lineHeight": "1.1"
}
},
"link": {
"color": { "text": "#74d5c7" }
}
},
"blocks": {
"core/code": {
"border": { "color": "rgba(142,190,207,.18)", "radius": "10px", "width": "1px" },
"color": { "background": "#040d16", "text": "#d9fdf7" },
"typography": { "fontFamily": "var(--wp--preset--font-family--mono)", "fontSize": "0.9rem" }
},
"core/quote": {
"border": { "color": "#74d5c7", "style": "solid", "width": "0 0 0 3px" },
"color": { "text": "#b8c5ce" },
"spacing": { "padding": { "left": "1.5rem" } }
}
}
}
}