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,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' );