Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a8957fe
Merge branch 'master_rails_5_1' of github.com:ufcvirtual/solar into b…
biancastephani Aug 27, 2025
5b60be4
Reposicionando o código js no form de questões
biancastephani Sep 1, 2025
dfd9d5f
Merge branch 'master_rails_5_1' of github.com:ufcvirtual/solar into b…
biancastephani Sep 1, 2025
7a70ec5
Reposicionando o javascript no form de questões (#36)
biancastephani Sep 1, 2025
6aa310d
Update README.md
biancastephani Sep 8, 2025
d23dead
Update README.md
biancastephani Sep 9, 2025
d625d7c
Adiciona recurso para FAQ (Perguntas e Respostas)
franciscojuliao-star Dec 15, 2025
052cd03
Adiciona menu administrativo para FAQ
franciscojuliao-star Dec 15, 2025
3ecfb18
Adiciona permissões de administrador para FAQ
franciscojuliao-star Dec 15, 2025
2320ee6
Adiciona modelos e migrations para sistema de FAQ
franciscojuliao-star Dec 17, 2025
dddf99f
Adiciona controller, views e assets do sistema de FAQ
franciscojuliao-star Dec 17, 2025
94c4c0d
Configura rotas e rake tasks para FAQ
franciscojuliao-star Dec 17, 2025
fc41f4a
Adiciona traduções das FAQs em português e inglês
franciscojuliao-star Dec 17, 2025
ab630e8
Adiciona testes e fixtures para sistema de FAQ
franciscojuliao-star Dec 17, 2025
ce66733
Atualiza dependências e fixtures do menu administrativo
franciscojuliao-star Dec 17, 2025
ef55bf6
Adiciona traduções para menu admin de FAQ
franciscojuliao-star Dec 19, 2025
23e7bea
Adiciona rota para toggle_active de FAQ
franciscojuliao-star Dec 19, 2025
da388f4
Adiciona método toggle_active no controller de FAQ
franciscojuliao-star Dec 19, 2025
3ae1881
Adiciona partial e view JS para FAQ
franciscojuliao-star Dec 19, 2025
3eb8dcf
Atualiza página index de FAQ com accordion e toggle de status
franciscojuliao-star Dec 19, 2025
e966be9
Merge branch 'development' into feature/faq
franciscojuliao-star Dec 19, 2025
ca50f82
Adiciona funcionalidade de ordenação de FAQs
franciscojuliao-star Dec 22, 2025
e89bb5d
Atualiza interface da lista de FAQs com seleção múltipla
franciscojuliao-star Dec 22, 2025
a66b9b7
Adiciona rota para alteração de ordem de FAQs
franciscojuliao-star Dec 22, 2025
a98eb1b
Atualiza traduções da interface de FAQs
franciscojuliao-star Dec 22, 2025
deffb43
Corrige problema de seleção após toggle de status
franciscojuliao-star Dec 22, 2025
66400ef
Corrige sintaxe HAML no arquivo de toggle
franciscojuliao-star Dec 22, 2025
b70260a
Ajusta interpolação Ruby no arquivo JS do toggle
franciscojuliao-star Dec 22, 2025
a8b3532
Torna função toggleActionButtons global para acesso via AJAX
franciscojuliao-star Dec 22, 2025
f019e95
Altera ícone do accordion de seta para chevron
franciscojuliao-star Dec 22, 2025
fa9b98d
Esconde tooltip ao alternar status do FAQ
franciscojuliao-star Dec 22, 2025
16ccc4f
Merge branch 'feature/faq' into feature/faq-resource
franciscojuliao-star Jan 5, 2026
9d7cad5
Implementar formulário FAQ em popup com fancybox
franciscojuliao-star Jan 9, 2026
e93b9ce
Adicionar validação de ordem única para FAQs ativos
franciscojuliao-star Jan 9, 2026
f6c80be
Merge branch 'development' into feature/faq-resource
biancastephani Jan 14, 2026
d5702e0
fix(faq): correções gerais e melhorias de acessibilidade
franciscojuliao-star Jan 23, 2026
0f75012
Merge remote-tracking branch 'origin/development' into feature/faq-re…
franciscojuliao-star Jan 23, 2026
0167035
fix(faq): correções de padrão, acessibilidade e alto contraste
franciscojuliao-star Feb 8, 2026
79402f8
Merge branch 'development' into feature/faq-resource
biancastephani Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
source "http://rubygems.org"

ruby "2.7.2"
ruby "2.7.8"

#gem "rails", "~> 3.2.16"
gem "rails", "5.1.7"
Expand Down
2 changes: 2 additions & 0 deletions app/assets/javascripts/application.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

// = require initialize-jspanel

// = require faqs

/**
* General
*/
Expand Down
185 changes: 185 additions & 0 deletions app/assets/javascripts/faqs.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<%# @encoding: UTF-8 %>

function faq_save(){
if (typeof CKEDITOR !== 'undefined') {
for (var instanceName in CKEDITOR.instances) {
CKEDITOR.instances[instanceName].updateElement();
}
}

$('form#faq_form').serialize_and_submit({
replace_list: $('.list_faqs')
});
}

function initFaqCKEditor(textareaId) {
if (typeof CKEDITOR !== 'undefined') {
if (CKEDITOR.instances[textareaId]) {
CKEDITOR.instances[textareaId].destroy();
}
CKEDITOR.replace(textareaId);
}
}

function updateFaqRemoveButtons() {
$('#translations-container .translation-fields:visible').each(function() {
var locale = $(this).find('select[name*="[locale]"]').val();
if (locale === 'pt_BR' || locale === 'en_US') {
$(this).find('.remove-translation').hide();
} else {
$(this).find('.remove-translation').show();
}
});
}

$(document).off('click.faqForm', '#add-translation').on('click.faqForm', '#add-translation', function(e) {
e.preventDefault();
e.stopImmediatePropagation();

var translationIndex = $('#translations-container .translation-fields').length;
var $template = $('#translations-container .translation-fields').first().clone();

$template.find('.cke').remove();

$template.find('input[type="text"], textarea, select').val('');
$template.find('input[type="hidden"]').not('.destroy-flag').val('');
$template.find('.destroy-flag').val('false');

var newAnswerId;

$template.find('input, select, textarea').each(function() {
var $field = $(this);

if ($field.attr('name')) {
$field.attr('name', $field.attr('name').replace(/\[\d+\]/, '[' + translationIndex + ']'));
}

if ($field.attr('id')) {
var newId = $field.attr('id').replace(/_\d+_/, '_' + translationIndex + '_');
$field.attr('id', newId);

if ($field.attr('name') && $field.attr('name').indexOf('[answer]') > -1) {
newAnswerId = newId;
}
}
});

$template.find('label').each(function() {
if ($(this).attr('for')) {
$(this).attr('for', $(this).attr('for').replace(/_\d+_/, '_' + translationIndex + '_'));
}
});

if ($template.find('.remove-translation').length === 0) {
$template.prepend('<button type="button" class="remove-translation" aria-label="<%= I18n.t("faqs.form.remove_aria", default: "Remover esta tradução") %>"><i class="icon-remove" aria-hidden="true"></i> <%= I18n.t("faqs.form.remove") %></button>');
}

$('#translations-container').append($template);

if (newAnswerId) {
setTimeout(function() {
initFaqCKEditor(newAnswerId);
}, 100);
}

updateFaqRemoveButtons();
});

$(document).off('click.faqForm', '.remove-translation').on('click.faqForm', '.remove-translation', function(e) {
e.preventDefault();
e.stopImmediatePropagation();

var $translationField = $(this).closest('.translation-fields');
var visibleFields = $('#translations-container .translation-fields:visible').length;

if (visibleFields <= 2) {
alert('<%= I18n.t("faqs.form.min_translations_alert") %>');
return;
}

var $answerField = $translationField.find('textarea[name*="[answer]"]');
if ($answerField.length && $answerField.attr('id')) {
var editorId = $answerField.attr('id');
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances[editorId]) {
CKEDITOR.instances[editorId].destroy();
}
}

// Se já existe no banco, marca para destruição; senão, remove do DOM
var $idField = $translationField.find('input[name*="[id]"]');
if ($idField.length && $idField.val() !== '') {
$translationField.find('.destroy-flag').val('true');
$translationField.hide();
} else {
$translationField.remove();
}

updateFaqRemoveButtons();
});

jQuery(function ($) {
$(".link_new_faq").call_fancybox({
width: '70%'
});

$(".link_show_faq").call_fancybox({
width: '70%'
});

$(".btn_edit[data-link-edit]").on('click', function(e) {
e.preventDefault();

var checkedBoxes = $('.ckb_faq:checked');
if (checkedBoxes.length === 1 && !$(this).attr('disabled')) {
var faqId = checkedBoxes.first().val();
var editUrl = $(this).data('link-edit').replace(':id', faqId);
$(this).call_fancybox({href: editUrl, open: true, width: '70%'});
}
return false;
});
});

jQuery(function ($) {
if ($(".tb_faqs").length === 0) {
return;
}

$(".up, .down").sort_row({
announceSelector: '#faq_announce',

getItemData: function($row) {
return {
id: $row.data('faq').id,
question: $row.data('question').trim()
};
},

isValidTarget: function($row) {
return !!$row.data('faq');
},

buildUrl: function(item, $targetRow) {
var $table = $targetRow.closest('table');
var urlTemplate = $table.data('reorderUrl');
var targetData = $targetRow.data('faq');
return urlTemplate.replace(':id', item.id).replace(':change_id', targetData.id);
},

formatBoundaryMessage: function(item, direction) {
if (direction === 'up') {
return 'A FAQ "' + item.question + '" já está no topo da lista';
} else {
return 'A FAQ "' + item.question + '" já está no final da lista';
}
},

formatMoveMessage: function(item, position) {
return 'FAQ "' + item.question + '" movida para a posição ' + position;
},

handleError: function(error) {
console.error("Falha ao atualizar a ordem:", error);
alert('<%= I18n.t("faqs.error.order") %>');
}
});
});
3 changes: 3 additions & 0 deletions app/assets/stylesheets/faq.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Place all the styles related to the Faq controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
1 change: 1 addition & 0 deletions app/assets/stylesheets/partials/_all.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@

@import 'tutorials';
@import 'panels';
@import 'faq';

@import 'load';

Expand Down
47 changes: 47 additions & 0 deletions app/assets/stylesheets/partials/_faq.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.faq-answer {
padding: 15px;
margin-top: 5px;
background-color: $background_light;
border-left: 3px solid $color_main;
}

.translation-content {
margin-bottom: 20px;
padding: 15px;
background-color: $background_light;
border-left: 3px solid $color_main;
}

#faq_form {
.faq_order input {
width: 80px !important;
}

.translation-fields input[type="text"] {
width: 100% !important;
}

.faq_active {
display: flex;
align-items: center;
gap: 15px;

.control-label {
margin-bottom: 0;
}

span.radio {
display: inline-flex !important;
align-items: center;
margin: 0;

label {
display: inline-flex !important;
align-items: center;
gap: 5px;
margin: 0;
white-space: nowrap;
}
}
}
}
123 changes: 123 additions & 0 deletions app/controllers/faqs_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
class FaqsController < ApplicationController

layout false, except: [:index, :apresentation]

before_action :require_admin
before_action :set_faq, only: [:show, :edit, :update, :destroy, :toggle_active, :order]
skip_before_action :require_admin, only:[:apresentation]

def index
@faqs = Faq.all
render layout: false if params[:layout].present? && params[:layout] == 'false'
end

def show
end

def new
@faq = Faq.new(active: true)
@faq.faq_translations.build(locale: 'pt_BR')
@faq.faq_translations.build(locale: 'en_US')
end

def create
@faq = Faq.new(faq_params)

begin
@faq.save!
render json: {success: true, notice: t('faqs.success.created')}
rescue
render :new
end
end

def edit
@faq.translation_pt
@faq.translation_en
end

def update
begin
@faq.update!(faq_params)
render json: {success: true, notice: t('faqs.success.updated')}
rescue
render :edit
end
end

def destroy
@faq.destroy

respond_to do |format|
format.html { redirect_to admin_faq_index_path, notice: t('faqs.success.deleted') }
format.json { render json: { success: true, notice: t('faqs.success.deleted') } }
end
end

def toggle_active
@faq.active = !@faq.active
@toggle_success = @faq.save
@toggle_errors = @faq.errors.full_messages.join(', ') unless @toggle_success

respond_to do |format|
format.js
format.json do
if @toggle_success
render json: { success: true, active: @faq.active }
else
render json: { success: false, errors: @faq.errors.full_messages }, status: :unprocessable_entity
end
end
end
end

def order
faq1 = @faq
faq2 = Faq.find(params[:change_id])

order1 = faq1.order
order2 = faq2.order

Faq.connection.execute(
"UPDATE faqs SET \"order\" = CASE
WHEN id = #{faq1.id} THEN #{order2}
WHEN id = #{faq2.id} THEN #{order1}
END
WHERE id IN (#{faq1.id}, #{faq2.id})"
)

render json: { success: true }
rescue => e
render json: { success: false, error: e.message }, status: :unprocessable_entity
end

def apresentation
@faq_order = Faq.faq_translations
render :faq
end

private
def require_admin
unless current_user.admin?
redirect_to home_path, alert: t(:no_permission)
end
end

def set_faq
@faq = Faq.find(params[:id])
end

def faq_params
params.require(:faq).permit(
:order,
:active,
faq_translations_attributes: [
:id,
:locale,
:question,
:answer,
:_destroy
]
)
end
end
2 changes: 2 additions & 0 deletions app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def apps
end

def faq
# Busca FAQs ativos com traduções (usa scope :faq_translations do model Faq)
@faq_order = Faq.faq_translations
end

def privacy_policy
Expand Down
Loading