commit 6fb90520e4a8dae9c566235408af121ae112736e Author: Marco Antonio Vivas Date: Fri Sep 12 19:05:50 2025 -0300 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5c28e0 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Gerenciador de Eventos (eventos-manager) + +Plugin WordPress para cadastro, gerenciamento e exibição de eventos com calendário interativo, lista de próximos eventos e integração via shortcodes e widget. + +## Funcionalidades + +- Cadastro de eventos personalizados (CPT: `evento`) +- Metabox para data, hora e local do evento +- Taxonomia personalizada para tipos de evento +- Calendário interativo (FullCalendar.js) com AJAX +- Lista de próximos eventos filtrável por tipo e limite +- Shortcodes para exibição do calendário, lista e widget +- Página única customizada para eventos (`single-evento.php`) +- Widget para exibir próximos eventos na sidebar +- Página de ajuda no admin + +## Instalação + +1. Faça upload da pasta `eventos-manager` para o diretório `wp-content/plugins/`. +2. Ative o plugin no painel do WordPress. +3. O tipo de post "Evento" estará disponível no menu lateral. + +## Shortcodes Disponíveis + +- `[mostra-calendario]` — Exibe o calendário de eventos. +- `[mostra-prox-eventos limit="5" tipo=""]` — Lista os próximos eventos. Parâmetros: + - `limit`: número de eventos (padrão: 5) + - `tipo`: slug do tipo de evento (opcional) +- `[eventos-completo]` — Exibe calendário e lista de próximos eventos juntos. +- `[mostra-calendario-widget]` — Exibe um calendário compacto para sidebar ou widgets. + +## Widget + +- Vá em **Aparência > Widgets** e adicione o widget "Próximos Eventos" à sua sidebar. +- Configure o título e o limite de eventos a exibir. + +## Custom Post Type e Taxonomia + +- **Post Type:** `evento` +- **Taxonomia:** `tipo_evento` (hierárquica) + +## Templates + +- O plugin inclui o template `single-evento.php` para exibição individual dos eventos. + +## Scripts e Estilos + +- FullCalendar.js e Moment.js são carregados automaticamente. +- Estilos customizados para admin e frontend em `/assets/css/`. + +## AJAX + +- Os eventos do calendário são carregados via AJAX para melhor performance. + +## Página de Ajuda + +- Acesse em **Eventos > Ajuda** para instruções e exemplos de uso dos shortcodes. + +## Autor + +Marco Antonio Vivas + +--- + +**Observação:** +- Para personalizações avançadas, edite os arquivos em `includes/` e os templates conforme necessário. +- Compatível com WordPress 5.0+. \ No newline at end of file diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..6cdcd4c --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,57 @@ +/* No changes needed, but ensure this file exists as referenced in class-eventos-admin.php */ +.evento-metabox { + display: flex; + flex-direction: column; + gap: 15px; + padding: 15px 0; +} +.meta-field { + display: flex; + flex-direction: column; + gap: 5px; +} +.meta-field label { + font-weight: 600; +} +.meta-field input[type="date"], +.meta-field input[type="time"], +.meta-field input[type="text"] { + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + max-width: 300px; +} +#eventos-calendario-widget { + min-width: 280px; + min-height: 340px; + font-size: 15px; + max-height: 370px; + overflow: hidden; + border-radius: 18px; /* Mais arredondado */ + box-shadow: 0 2px 16px rgba(44,62,80,0.10); + border: none; + background: #fff; +} +#eventos-calendario-widget .fc { + border-radius: 18px; + box-shadow: 0 2px 16px rgba(44,62,80,0.10); + background: #fff; + padding: 16px 8px 8px 8px; +} +#eventos-calendario-widget .fc-day-number { + border-radius: 50%; +} +#eventos-calendario-widget .fc-center h2 { + color: #3498db; +} +#eventos-calendario-widget .fc-today .fc-day-number { + background: #3498db; + color: #fff; + box-shadow: 0 2px 8px rgba(52,152,219,0.15); +} +#eventos-calendario-widget .fc-has-event .fc-day-number { + background: #217dbb; + color: #fff; + border: 2px solid #3498db; + box-shadow: 0 2px 8px rgba(52,152,219,0.15); +} \ No newline at end of file diff --git a/assets/css/eventos-manager.css b/assets/css/eventos-manager.css new file mode 100644 index 0000000..8056cb3 --- /dev/null +++ b/assets/css/eventos-manager.css @@ -0,0 +1,288 @@ +/* CSS Mínimo para Estrutura do Gerenciador de Eventos */ + +.noticias-eventos-wrapper { + display: flex; + gap: 40px; +} + +.noticias-col { + flex: 2; +} + +.eventos-col { + flex: 1; + min-width: 280px; + display: flex; + flex-direction: column; + gap: 30px; +} +.proximos-eventos-lista { + list-style: none; + padding: 0; + margin: 0; +} + +.proximos-eventos-lista li { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 18px; +} + +.evento-titulo a { + text-decoration: none; +} + +.evento-tipo { + margin-left: auto; +} +@media (max-width: 900px) { + .noticias-eventos-wrapper { + flex-direction: column; + } + .eventos-col { + min-width: 0; + } +} + +/* --- Estilos do Novo Calendário Offline --- */ +.em-calendario-wrapper { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 5px rgba(0,0,0,0.05); +} +.em-calendario-wrapper[data-view="full"] .em-dia-celula { + height: 100px; /* Aumenta a altura para caber eventos */ +} +.em-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + user-select: none; +} +.em-toolbar-section { + display: flex; + align-items: center; + gap: 5px; +} +.em-toolbar-center { + justify-content: center; + flex-grow: 1; +} +.em-toolbar-right { + justify-content: flex-end; + flex-grow: 1; +} + +.em-mes-ano { + font-size: 1.2em; + font-weight: 600; + color: #333; + margin: 0; + text-align: center; + flex-grow: 0; +} + +.em-nav-btn { + background: #f7f7f7; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px 10px; + cursor: pointer; + font-weight: bold; + color: #555; + transition: background 0.2s; +} + +.em-view-btn { + background: none; + border: none; + font-size: 22px; + cursor: pointer; +} +.em-nav-btn:hover { + background: #e9e9e9; +} + +.em-today-btn { + font-weight: normal; + padding: 5px 12px; +} + +.em-dias-semana, .em-dias-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + text-align: center; +} + +.em-dias-semana span { + font-weight: 600; + color: #777; + font-size: 0.85em; + padding-bottom: 10px; +} + +.em-dia-celula { + position: relative; + padding: 4px; + height: 40px; + display: flex; + flex-direction: column; + align-items: flex-end; + font-size: 0.9em; + border-right: 1px solid #eee; + border-bottom: 1px solid #eee; +} + +.em-dia-celula.em-other-month { + background-color: #f9f9f9; +} + +.em-dia-celula.today span { + background-color: #e74c3c; + color: #fff; + border-radius: 50%; + width: 28px; + line-height: 26px; + display: inline-block; + text-align: center; +} + +.em-event-list { + width: 100%; + margin-top: 5px; + display: flex; + flex-direction: column; + gap: 3px; + overflow: hidden; +} + +.em-event { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 12px; + padding: 2px 6px; + border-radius: 3px; + color: #fff; + background-color: #3498db; + text-decoration: none; + transition: background-color 0.2s; +} + +.em-event:hover { + background-color: #217dbb; +} + +/* --- Estilos para Tela Cheia --- */ +body.em-fullscreen-active { + overflow: hidden; +} + +.em-calendario-wrapper.em-fullscreen-mode { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 10000; + border-radius: 0; + border: none; + padding: 20px; + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.em-calendario-wrapper.em-fullscreen-mode .em-dias-grid { + flex-grow: 1; /* Faz a grade de dias ocupar o espaço disponível */ +} + +.em-calendario-wrapper.em-fullscreen-mode .em-dia-celula { + height: auto; /* Altura automática para preencher o espaço */ +} + +.em-close-fullscreen-btn { + position: absolute; + top: 15px; + right: 20px; + background: none; + border: none; + font-size: 36px; + line-height: 1; + color: #333; + cursor: pointer; + z-index: 10001; +} + +.em-close-fullscreen-btn:hover { + color: #e74c3c; +} + +/* Estilos para single-evento.php */ +.single-evento-container { + max-width: 800px; /* Largura máxima para centralizar */ + margin: 0 auto; /* Centraliza horizontalmente */ + padding: 30px 20px; + background: #fff; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(44, 62, 80, 0.1); +} + +.evento-header { + text-align: center; + margin-bottom: 30px; +} + +.evento-header h1 { + font-size: 2rem; + color: #2c3e50; + font-weight: 700; + margin: 0; + line-height: 1.3; +} + +.evento-details { + display: flex; + flex-direction: column; + gap: 20px; +} + +.evento-info-list { + list-style: none; + padding: 0; + margin: 0; + gap: 12px; +} + +.evento-info-list li { + display: flex; + align-items: center; + gap: 10px; +} + +.evento-info-list li strong { + min-width: 80px; /* Ajuda a alinhar os rótulos */ +} + +/* Responsividade */ +@media (max-width: 600px) { + .single-evento-container { + padding: 20px 15px; + } + + .evento-info-list li { + flex-direction: column; + align-items: flex-start; + gap: 5px; + } + + .evento-info-list li strong { + min-width: auto; + } +} \ No newline at end of file diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..31e5ea7 --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,18 @@ +jQuery(document).ready(function($) { + $('#data_evento').on('change', function() { + var date = $(this).val(); + if (!date) { + alert('Por favor, selecione uma data válida.'); + $(this).focus(); + } + }); + + $('#hora_evento').on('change', function() { + var time = $(this).val(); + if (time && !/^\d{2}:\d{2}$/.test(time)) { + alert('Por favor, insira uma hora válida no formato HH:MM.'); + $(this).val(''); + $(this).focus(); + } + }); +}); \ No newline at end of file diff --git a/assets/js/eventos-manager.js b/assets/js/eventos-manager.js new file mode 100644 index 0000000..6d56358 --- /dev/null +++ b/assets/js/eventos-manager.js @@ -0,0 +1,150 @@ +jQuery(document).ready(function($) { + // Itera sobre cada instância de calendário na página + $('.em-calendario-wrapper').each(function() { + const calendarWrapper = $(this); + let currentDate = new Date(); + + const header = calendarWrapper.find('.em-mes-ano'); + const weekDaysContainer = calendarWrapper.find('.em-dias-semana'); + const daysGrid = calendarWrapper.find('.em-dias-grid'); + + // Nomes para internacionalização + const monthNames = ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"]; + const weekDayNames = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"]; + + function renderCalendar() { + daysGrid.html(''); // Usar .html('') é um pouco mais rápido que .empty() + weekDaysContainer.empty(); + + const month = currentDate.getMonth(); + const year = currentDate.getFullYear(); + + // Define o cabeçalho (Mês Ano) + header.text(monthNames[month] + ' ' + year); + + // Renderiza os dias da semana + weekDayNames.forEach(day => { + weekDaysContainer.append(`${day}`); + }); + + const firstDayOfMonth = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + // Preenche os dias vazios no início do mês + for (let i = 0; i < firstDayOfMonth; i++) { + daysGrid.append('
'); + } + + // Renderiza os dias do mês + for (let i = 1; i <= daysInMonth; i++) { + const dayCell = $(`
${i}
`); + + // Marca o dia de hoje + const today = new Date(); + if (i === today.getDate() && month === today.getMonth() && year === today.getFullYear()) { + dayCell.addClass('today'); + } + daysGrid.append(dayCell); + } + + loadEventsForMonth(year, month); + } + + function loadEventsForMonth(year, month) { + const startDate = `${year}-${String(month + 1).padStart(2, '0')}-01`; + const endDate = new Date(year, month + 1, 0).toISOString().split('T')[0]; + + $.ajax({ + url: eventosManager.ajaxurl, + type: 'POST', + data: { + action: 'get_eventos_calendario', + security: eventosManager.nonce, + start: startDate, + end: endDate + }, + success: function(response) { + if (response.success) { + markEventsOnCalendar(response.data); + } + }, + error: function() { + console.error('Erro ao carregar eventos.'); + } + }); + } + + function markEventsOnCalendar(events) { + // 1. Agrupar eventos por data + const eventsByDate = events.reduce((acc, event) => { + const eventDate = event.start.split('T')[0]; + if (!acc[eventDate]) { + acc[eventDate] = []; + } + acc[eventDate].push(event); + return acc; + }, {}); + + // 2. Limpar eventos antigos e renderizar os novos + daysGrid.find('.em-dia-celula').each(function() { + const cell = $(this); + const date = cell.data('date'); + + // Limpa conteúdo de eventos anteriores + cell.find('.em-event-list').remove(); + cell.removeClass('has-event'); + + if (eventsByDate[date]) { + cell.addClass('has-event'); + const eventList = $('
'); + eventsByDate[date].forEach(event => { + const eventEl = $(``); + eventEl.text(event.title); + eventList.append(eventEl); + }); + cell.append(eventList); + } + }); + } + + + // Navegação + calendarWrapper.find('.em-nav-btn').on('click', function() { + const direction = $(this).data('nav'); + const currentMonth = currentDate.getMonth(); + + if (direction === 'prev') { + currentDate.setMonth(currentMonth - 1); + } else if (direction === 'next') { + currentDate.setMonth(currentMonth + 1); + } else if (direction === 'today') { + currentDate = new Date(); + } + renderCalendar(); + }); + + // Botão de Tela Cheia + calendarWrapper.find('.em-view-btn[data-view="fullscreen"]').on('click', function() { + calendarWrapper.toggleClass('em-fullscreen-mode'); + $('body').toggleClass('em-fullscreen-active'); + + if (calendarWrapper.hasClass('em-fullscreen-mode')) { + // Adiciona botão de fechar + calendarWrapper.append(''); + } else { + // Remove botão de fechar + calendarWrapper.find('.em-close-fullscreen-btn').remove(); + } + }); + + // Fechar tela cheia com o botão 'X' (evento delegado) + calendarWrapper.on('click', '.em-close-fullscreen-btn', function() { + calendarWrapper.removeClass('em-fullscreen-mode'); + $('body').removeClass('em-fullscreen-active'); + $(this).remove(); + }); + + // Renderização inicial + renderCalendar(); + }); +}); \ No newline at end of file diff --git a/assets/js/eventos-manager.js.txt b/assets/js/eventos-manager.js.txt new file mode 100644 index 0000000..dbb555a --- /dev/null +++ b/assets/js/eventos-manager.js.txt @@ -0,0 +1,41 @@ +jQuery(document).ready(function($) { + // Inicializa o calendário se o elemento existir + if ($('#eventos-calendario').length) { + loadEventosCalendario(); + } + + function loadEventosCalendario() { + $.ajax({ + url: eventosManager.ajaxurl, + type: 'POST', + data: { + action: 'get_eventos_calendario', + security: eventosManager.nonce + }, + success: function(response) { + if (response.success) { + initFullCalendar(response.data); + } + } + }); + } + + function initFullCalendar(eventos) { + $('#eventos-calendario').fullCalendar({ + header: { + left: 'prev,next today', + center: 'title', + right: 'month,agendaWeek,agendaDay' + }, + defaultView: 'month', + editable: false, + events: eventos, + eventColor: '#3498db', + eventTextColor: '#ffffff', + timeFormat: 'H:mm', + eventRender: function(event, element) { + element.find('.fc-title').prepend('' + event.tipo + ' '); + } + }); + } +}); \ No newline at end of file diff --git a/eventos-manager.php b/eventos-manager.php new file mode 100644 index 0000000..77adaa8 --- /dev/null +++ b/eventos-manager.php @@ -0,0 +1,170 @@ + admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('eventos_manager_nonce') + ) + ); + } + + public function get_eventos_calendario() { + check_ajax_referer('eventos_manager_nonce', 'security'); + + $start_date = isset($_POST['start']) ? sanitize_text_field($_POST['start']) : date('Y-m-d', strtotime('-1 month')); + $end_date = isset($_POST['end']) ? sanitize_text_field($_POST['end']) : date('Y-m-d', strtotime('+1 month')); + + $args = array( + 'post_type' => 'evento', + 'posts_per_page' => -1, + 'meta_key' => 'data_evento', + 'orderby' => 'meta_value', + 'order' => 'ASC', + 'meta_query' => array( + array( + 'key' => 'data_evento', + 'value' => array($start_date, $end_date), + 'compare' => 'BETWEEN', + 'type' => 'DATE' + ) + ) + ); + + $eventos = new WP_Query($args); + $calendar_events = array(); + + if ($eventos->have_posts()) { + while ($eventos->have_posts()) { + $eventos->the_post(); + $data_evento = get_post_meta(get_the_ID(), 'data_evento', true); + $hora_evento = get_post_meta(get_the_ID(), 'hora_evento', true); + $tipos = get_the_terms(get_the_ID(), 'tipo_evento'); + $tipo = !empty($tipos) && !is_wp_error($tipos) ? $tipos[0]->name : ''; + + $calendar_events[] = array( + 'title' => get_the_title(), + 'start' => $data_evento . ($hora_evento ? 'T' . $hora_evento : ''), + 'tipo' => $tipo, + 'url' => get_permalink() + ); + } + wp_reset_postdata(); + } + + wp_send_json_success($calendar_events); + } + + public function load_single_template($template) { + global $post; + if ($post->post_type === 'evento') { + $plugin_template = EVENTOS_MANAGER_PLUGIN_DIR . 'single-evento.php'; + if (file_exists($plugin_template)) { + return $plugin_template; + } + } + return $template; + } +} + +new Eventos_Manager(); + +// Register widget +class Eventos_Upcoming_Widget extends WP_Widget { + public function __construct() { + parent::__construct( + 'eventos_upcoming_widget', + __('Próximos Eventos', 'eventos-manager'), + array('description' => __('Mostra os próximos eventos', 'eventos-manager')) + ); + } + + public function widget($args, $instance) { + echo $args['before_widget']; + if (!empty($instance['title'])) { + echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title']; + } + echo do_shortcode('[mostra-prox-eventos limit="' . esc_attr($instance['limit']) . '"]'); + echo $args['after_widget']; + } + + public function form($instance) { + $title = !empty($instance['title']) ? $instance['title'] : ''; + $limit = !empty($instance['limit']) ? $instance['limit'] : 5; + ?> +

+ + +

+

+ + +

+ ID, 'data_evento', true); + $hora_evento = get_post_meta($post->ID, 'hora_evento', true); + $local_evento = get_post_meta($post->ID, 'local_evento', true); + ?> +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

+

+

+ +
+ __('Eventos', 'eventos-manager'), + 'singular_name' => __('Evento', 'eventos-manager'), + 'menu_name' => __('Eventos', 'eventos-manager'), + 'add_new' => __('Adicionar Novo', 'eventos-manager'), + 'add_new_item' => __('Adicionar Novo Evento', 'eventos-manager'), + 'edit_item' => __('Editar Evento', 'eventos-manager'), + 'new_item' => __('Novo Evento', 'eventos-manager'), + 'view_item' => __('Ver Evento', 'eventos-manager'), + 'search_items' => __('Buscar Eventos', 'eventos-manager'), + 'not_found' => __('Nenhum evento encontrado', 'eventos-manager'), + 'not_found_in_trash' => __('Nenhum evento encontrado na lixeira', 'eventos-manager') + ); + + $args = array( + 'labels' => $labels, + 'public' => true, + 'has_archive' => true, + 'menu_icon' => 'dashicons-calendar-alt', + 'supports' => array('title', 'editor', 'thumbnail'), + 'rewrite' => array('slug' => 'eventos'), + 'show_in_rest' => true + ); + + register_post_type('evento', $args); + } + + public function register_taxonomy() { + $labels = array( + 'name' => __('Tipos de Evento', 'eventos-manager'), + 'singular_name' => __('Tipo de Evento', 'eventos-manager'), + 'search_items' => __('Buscar Tipos', 'eventos-manager'), + 'all_items' => __('Todos os Tipos', 'eventos-manager'), + 'edit_item' => __('Editar Tipo', 'eventos-manager'), + 'update_item' => __('Atualizar Tipo', 'eventos-manager'), + 'add_new_item' => __('Adicionar Novo Tipo', 'eventos-manager'), + 'new_item_name' => __('Novo Nome de Tipo', 'eventos-manager'), + 'menu_name' => __('Tipos de Evento', 'eventos-manager') + ); + + $args = array( + 'hierarchical' => true, + 'labels' => $labels, + 'show_ui' => true, + 'show_admin_column' => true, + 'query_var' => true, + 'rewrite' => array('slug' => 'tipo-evento'), + 'show_in_rest' => true + ); + + register_taxonomy('tipo_evento', 'evento', $args); + } +} \ No newline at end of file diff --git a/includes/class-eventos-shortcodes.php b/includes/class-eventos-shortcodes.php new file mode 100644 index 0000000..9b5f214 --- /dev/null +++ b/includes/class-eventos-shortcodes.php @@ -0,0 +1,135 @@ + +
+
+
+ + + +
+

+
+ +
+
+
+
+
+ 5, + 'tipo' => '' + ), $atts, 'mostra-prox-eventos'); + + $args = array( + 'post_type' => 'evento', + 'posts_per_page' => intval($atts['limit']), + 'meta_key' => 'data_evento', + 'orderby' => 'meta_value', + 'order' => 'ASC', + 'meta_query' => array( + array( + 'key' => 'data_evento', + 'value' => date('Y-m-d'), + 'compare' => '>=', + 'type' => 'DATE' + ) + ) + ); + + if (!empty($atts['tipo'])) { + $args['tax_query'] = array( + array( + 'taxonomy' => 'tipo_evento', + 'field' => 'slug', + 'terms' => sanitize_text_field($atts['tipo']) + ) + ); + } + + $eventos = new WP_Query($args); + + ob_start(); + ?> +
+

Próximos Eventos

+ +
+ +
+
+ render_calendario($atts); ?> +
+
+ render_proximos_eventos($atts); ?> +
+
+ +
+
+
+ + +
+

+
+
+
+
+
+ +
+
+

+
+
+
    +
  • Data:
  • + +
  • Hora:
  • + + +
  • Local:
  • + + +
  • Tipo: name); ?>
  • + +
+
+
+
+