Skip to content

Instantly share code, notes, and snippets.

@dedemenezes
Created September 11, 2025 12:53
Show Gist options
  • Select an option

  • Save dedemenezes/f3fc73c0d91bb9003a0c74d27ffef4ab to your computer and use it in GitHub Desktop.

Select an option

Save dedemenezes/f3fc73c0d91bb9003a0c74d27ffef4ab to your computer and use it in GitHub Desktop.
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e01dcf4..52dd050 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -39,7 +39,7 @@ class ApplicationController < ActionController::Base
"Programação": [
{
description: "Programação completa",
- path: ""
+ path: program_path
},
{
description: "Sessões com convidados",
diff --git a/app/controllers/programs_controller.rb b/app/controllers/programs_controller.rb
index c26bb81..e381b28 100644
--- a/app/controllers/programs_controller.rb
+++ b/app/controllers/programs_controller.rb
@@ -42,31 +42,31 @@ class ProgramsController < ApplicationController
selected_filters[:query] = selected_query
end
- if params[:mostrasFilter].present?
- selected_mostra = @mostras_filter.find { |c| c["permalink_pt"] == params[:mostrasFilter] }
- selected_filters[:mostrasFilter] = selected_mostra if selected_mostra
+ if params[:mostra].present?
+ selected_mostra = @mostras_filter.find { |c| c["permalink_pt"] == params[:mostra] }
+ selected_filters[:mostra] = selected_mostra if selected_mostra
if selected_mostra
- base_scope = base_scope.where(mostras: { permalink_pt: selected_filters[:mostrasFilter]["permalink_pt"] })
+ base_scope = base_scope.where(mostras: { permalink_pt: selected_filters[:mostra]["permalink_pt"] })
end
end
- if params[:cinemasFilter]
+ if params[:cinema]
selected_cinema = @cinemas_filter.find do |cinema_filter|
- (cinema_filter["id"].to_s === params[:cinemasFilter]) && (cinema_filter["edicao_id"] == EDICAO_ATUAL)
+ (cinema_filter["id"].to_s === params[:cinema]) && (cinema_filter["edicao_id"] == EDICAO_ATUAL)
end
if selected_cinema
- selected_filters[:cinemasFilter] = selected_cinema
+ selected_filters[:cinema] = selected_cinema
base_scope = base_scope.where(cinema_id: selected_cinema["id"])
end
end
- if params[:paisesFilter]
+ if params[:pais]
selected_pais = @paises_filter.find do |pais_filter|
- (pais_filter["id"].to_s === params[:paisesFilter])
+ (pais_filter["id"].to_s === params[:pais])
end
if selected_pais
- selected_filters[:paisesFilter] = selected_pais
+ selected_filters[:pais] = selected_pais
# base_scope = base_scope.where(paises_id: selected_pais["id"])
base_scope = base_scope.joins(pelicula: :paises).where(pelicula: { paises: { id: selected_pais["id"] } })
end
@@ -81,19 +81,53 @@ class ProgramsController < ApplicationController
end
end
- if params[:genresFilter].present?
- selected_genre = @genres_filter.find { |genre| (genre["filter_value"] === params[:genresFilter]) }
+ if params[:genre].present?
+ selected_genre = @genres_filter.find { |genre| (genre["filter_value"] === params[:genre]) }
if selected_genre
selected_filters[:genre] = selected_genre
locale_index = I18n.locale == :en ? -1 : 1
- # substring index is used to split the text in the database and select by index
- base_scope = base_scope.where(
- "SUBSTRING_INDEX(SUBSTRING_INDEX(peliculas.catalogo_ficha_2007, ' ', 1), '/', ?) LIKE ?",
+ # Use subquery instead of raw SQL on joined table
+ pelicula_ids = Pelicula.where(edicao_id: EDICAO_ATUAL).where(
+ "SUBSTRING_INDEX(SUBSTRING_INDEX(catalogo_ficha_2007, ' ', 1), '/', ?) LIKE ?",
locale_index,
"%#{selected_genre['filter_value']}%"
- )
+ ).pluck(:id)
+
+ base_scope = base_scope.where(pelicula_id: pelicula_ids)
+ end
+ end
+
+ if params["direção"].present?
+ selected_director = @directors_filter.find { |d| d["filter_value"] == params["direção"] }
+
+ if selected_director
+ selected_filters["direção"] = selected_director
+
+ # Get pelicula IDs first - clean, simple query
+ pelicula_ids = Pelicula.where(edicao_id: EDICAO_ATUAL)
+ .where(diretor_coord_int: selected_director["filter_value"])
+ .pluck(:id)
+
+ # Then filter programacoes by IDs - no complex joins
+ base_scope = base_scope.where(pelicula_id: pelicula_ids)
+ end
+ end
+
+ if params[:elenco].present?
+ actor_query = params[:elenco]
+
+ # Find peliculas with this actor
+ pelicula_ids = Pelicula.actor_to_pelicula_mapping(EDICAO_ATUAL)[actor_query] || []
+ if pelicula_ids.any?
+ selected_actor = {
+ "filter_display" => actor_query,
+ "filter_value" => actor_query,
+ "filter_label" => I18n.t("filter.elenco")
+ }
+ selected_filters[:elenco] = selected_actor
+ base_scope = base_scope.where(pelicula_id: pelicula_ids)
end
end
@@ -148,21 +182,25 @@ class ProgramsController < ApplicationController
items:,
elements: @programacoes,
pagy: @pagy,
- mostrasFilter: @mostras_filter,
- cinemasFilter: @cinemas_filter,
- paisesFilter: @paises_filter,
- genresFilter: @genres_filter,
+ mostras: @mostras_filter,
+ cinemas: @cinemas_filter,
+ paises: @paises_filter,
+ genres: @genres_filter,
sessoes: @sessoes,
+ directors: @directors_filter,
+ actors: @actors_filter,
menuTabs: @menu_tabs,
current_filters: { # those are the ones used as modelValue
query: selected_query,
- mostrasFilter: selected_mostra,
- cinemasFilter: selected_cinema,
- paisesFilter: selected_pais,
- genresFilter: selected_genre,
- sessao: selected_sessao
+ mostra: selected_mostra,
+ cinema: selected_cinema,
+ pais: selected_pais,
+ genre: selected_genre,
+ sessao: selected_sessao,
+ elenco: selected_actor,
+ "direção" => selected_director
},
- has_active_filters: params.permit(:query, :mostrasFilter).to_h.values.any?(&:present?),
+ has_active_filters: params.permit(:query, :mostra).to_h.values.any?(&:present?),
crumbs: breadcrumbs(
[ "", @root_url ],
[ "Programação", "" ],
@@ -205,10 +243,13 @@ class ProgramsController < ApplicationController
def build_tab_url(date, filters)
query_params = {}
- query_params[:mostrasFilter]= filters[:mostrasFilter]["permalink_pt"] if filters[:mostrasFilter].present?
- query_params[:cinemasFilter]= filters[:cinemasFilter]["id"] if filters[:cinemasFilter].present?
- query_params[:paisesFilter]= filters[:paisesFilter]["id"] if filters[:paisesFilter].present?
+ query_params[:mostra]= filters[:mostra]["filter_value"] if filters[:mostra].present?
+ query_params[:cinema]= filters[:cinema]["filter_value"] if filters[:cinema].present?
+ query_params[:pais]= filters[:pais]["filter_value"] if filters[:pais].present?
+ query_params[:genre]= filters[:genre]["filter_value"] if filters[:genre].present?
query_params[:sessao]= filters[:sessao]["filter_value"] if filters[:sessao].present?
+ query_params["direção"]= filters["direção"]["filter_value"] if filters["direção"].present?
+ query_params[:elenco]= filters[:elenco]["filter_value"] if filters[:elenco].present?
query_params[:date] = date
url_for(params: query_params, only_path: true)
end
@@ -221,7 +262,7 @@ class ProgramsController < ApplicationController
.sort_by { |it| it.nome_pais }
.as_json(
only: %i[id nome_pais],
- methods: %i[filter_display filter_value]
+ methods: %i[filter_display filter_value filter_label]
)
@mostras_filter = Mostra.where(edicao_id: EDICAO_ATUAL)
@@ -230,7 +271,7 @@ class ProgramsController < ApplicationController
.sort_by { |it| it.permalink_pt }
.as_json(
only: %i[id permalink_pt nome_abreviado],
- methods: [ :tag_class, :display_name, :filter_value, :filter_display ]
+ methods: [ :tag_class, :display_name, :filter_value, :filter_display, :filter_label ]
)
@cinemas_filter = Cinema.where(edicao_id: EDICAO_ATUAL)
.to_a
@@ -238,14 +279,16 @@ class ProgramsController < ApplicationController
.sort_by { |it| it.nome }
.as_json(
only: %i[id nome endereco edicao_id],
- methods: %i[filter_display filter_value]
+ methods: %i[filter_display filter_value filter_label]
)
@sessoes = Programacao.where(edicao_id: EDICAO_ATUAL).to_a.uniq { |p| p.sessao }.sort.as_json(
only: %i[sessao],
- methods: %i[display_sessao filter_value filter_display]
+ methods: %i[display_sessao filter_value filter_display filter_label]
)
@genres_filter = Pelicula.genres_for(EDICAO_ATUAL)
+ @directors_filter = Pelicula.directors_for(EDICAO_ATUAL)
+ @actors_filter = Pelicula.cast_for(EDICAO_ATUAL)
end
end
diff --git a/app/frontend/assets/icons/BoxArrowUp.svg b/app/frontend/assets/icons/BoxArrowUp.svg
new file mode 100644
index 0000000..6aee470
--- /dev/null
+++ b/app/frontend/assets/icons/BoxArrowUp.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="black">
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.955 16.875C7.955 17.0408 8.02085 17.1997 8.13806 17.3169C8.25527 17.4342 8.41424 17.5 8.58 17.5H16.875C17.3723 17.5 17.8492 17.3025 18.2008 16.9508C18.5525 16.5992 18.75 16.1223 18.75 15.625V3.125C18.75 2.62772 18.5525 2.15081 18.2008 1.79917C17.8492 1.44754 17.3723 1.25 16.875 1.25H4.375C3.87772 1.25 3.40081 1.44754 3.04917 1.79917C2.69754 2.15081 2.5 2.62772 2.5 3.125V11.42C2.5 11.5858 2.56585 11.7447 2.68306 11.8619C2.80027 11.9792 2.95924 12.045 3.125 12.045C3.29076 12.045 3.44973 11.9792 3.56694 11.8619C3.68415 11.7447 3.75 11.5858 3.75 11.42V3.125C3.75 2.95924 3.81585 2.80027 3.93306 2.68306C4.05027 2.56585 4.20924 2.5 4.375 2.5H16.875C17.0408 2.5 17.1997 2.56585 17.3169 2.68306C17.4342 2.80027 17.5 2.95924 17.5 3.125V15.625C17.5 15.7908 17.4342 15.9497 17.3169 16.0669C17.1997 16.1842 17.0408 16.25 16.875 16.25H8.58C8.41424 16.25 8.25527 16.3158 8.13806 16.4331C8.02085 16.5503 7.955 16.7092 7.955 16.875Z" fill="currentColor"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M13.7508 6.875C13.7508 6.70924 13.6849 6.55027 13.5677 6.43306C13.4505 6.31585 13.2915 6.25 13.1258 6.25H6.87579C6.71003 6.25 6.55106 6.31585 6.43385 6.43306C6.31664 6.55027 6.25079 6.70924 6.25079 6.875C6.25079 7.04076 6.31664 7.19973 6.43385 7.31694C6.55106 7.43415 6.71003 7.5 6.87579 7.5H11.617L1.43329 17.6825C1.31593 17.7999 1.25 17.959 1.25 18.125C1.25 18.291 1.31593 18.4501 1.43329 18.5675C1.55065 18.6849 1.70982 18.7508 1.87579 18.7508C2.04176 18.7508 2.20093 18.6849 2.31829 18.5675L12.5008 8.38375V13.125C12.5008 13.2908 12.5666 13.4497 12.6838 13.5669C12.8011 13.6842 12.96 13.75 13.1258 13.75C13.2915 13.75 13.4505 13.6842 13.5677 13.5669C13.6849 13.4497 13.7508 13.2908 13.7508 13.125V6.875Z" fill="currentColor"/>
+</svg>
diff --git a/app/frontend/components/common/icons/index.js b/app/frontend/components/common/icons/index.js
index 7c5a4eb..64debd5 100644
--- a/app/frontend/components/common/icons/index.js
+++ b/app/frontend/components/common/icons/index.js
@@ -11,6 +11,7 @@ export { default as IconCarretUp } from "./navigation/IconCarretUp.vue";
export { default as IconHome } from "./navigation/IconHome.vue";
export { default as IconChevronLeft } from "./navigation/IconChevronLeft.vue";
export { default as IconChevronRight } from "./navigation/IconChevronRight.vue";
+export { default as IconBoxArrowUp } from "./navigation/IconBoxArrowUp.vue";
// export { default as IconMenu } from "./navigation/IconMenu.vue";
// Status
diff --git a/app/frontend/components/common/icons/navigation/IconBoxArrowUp.vue b/app/frontend/components/common/icons/navigation/IconBoxArrowUp.vue
new file mode 100644
index 0000000..6edeb50
--- /dev/null
+++ b/app/frontend/components/common/icons/navigation/IconBoxArrowUp.vue
@@ -0,0 +1,21 @@
+<script setup>
+import { BaseIcon } from "@components/common/icons"
+
+const props = defineProps({
+ color: { type: String, default: undefined },
+ active: { type: Boolean, default: false },
+ width: { type: String, default: "20" },
+ height: { type: String, default: "20" }
+});
+</script>
+
+<template>
+ <BaseIcon viewBox="0 0 20 20" :width="props.width" :height="props.height" :active="active" :className="props.color">
+ <template #default="{ fill }">
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.955 16.875C7.955 17.0408 8.02085 17.1997 8.13806 17.3169C8.25527 17.4342 8.41424 17.5 8.58 17.5H16.875C17.3723 17.5 17.8492 17.3025 18.2008 16.9508C18.5525 16.5992 18.75 16.1223 18.75 15.625V3.125C18.75 2.62772 18.5525 2.15081 18.2008 1.79917C17.8492 1.44754 17.3723 1.25 16.875 1.25H4.375C3.87772 1.25 3.40081 1.44754 3.04917 1.79917C2.69754 2.15081 2.5 2.62772 2.5 3.125V11.42C2.5 11.5858 2.56585 11.7447 2.68306 11.8619C2.80027 11.9792 2.95924 12.045 3.125 12.045C3.29076 12.045 3.44973 11.9792 3.56694 11.8619C3.68415 11.7447 3.75 11.5858 3.75 11.42V3.125C3.75 2.95924 3.81585 2.80027 3.93306 2.68306C4.05027 2.56585 4.20924 2.5 4.375 2.5H16.875C17.0408 2.5 17.1997 2.56585 17.3169 2.68306C17.4342 2.80027 17.5 2.95924 17.5 3.125V15.625C17.5 15.7908 17.4342 15.9497 17.3169 16.0669C17.1997 16.1842 17.0408 16.25 16.875 16.25H8.58C8.41424 16.25 8.25527 16.3158 8.13806 16.4331C8.02085 16.5503 7.955 16.7092 7.955 16.875Z" :fill="fill"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M13.7508 6.875C13.7508 6.70924 13.6849 6.55027 13.5677 6.43306C13.4505 6.31585 13.2915 6.25 13.1258 6.25H6.87579C6.71003 6.25 6.55106 6.31585 6.43385 6.43306C6.31664 6.55027 6.25079 6.70924 6.25079 6.875C6.25079 7.04076 6.31664 7.19973 6.43385 7.31694C6.55106 7.43415 6.71003 7.5 6.87579 7.5H11.617L1.43329 17.6825C1.31593 17.7999 1.25 17.959 1.25 18.125C1.25 18.291 1.31593 18.4501 1.43329 18.5675C1.55065 18.6849 1.70982 18.7508 1.87579 18.7508C2.04176 18.7508 2.20093 18.6849 2.31829 18.5675L12.5008 8.38375V13.125C12.5008 13.2908 12.5666 13.4497 12.6838 13.5669C12.8011 13.6842 12.96 13.75 13.1258 13.75C13.2915 13.75 13.4505 13.6842 13.5677 13.5669C13.6849 13.4497 13.7508 13.2908 13.7508 13.125V6.875Z" :fill="fill"/>
+ </template>
+ </BaseIcon>
+</template>
+
+<style scoped></style>
diff --git a/app/frontend/components/common/tags/TagFilter.vue b/app/frontend/components/common/tags/TagFilter.vue
index ebfc335..d267691 100644
--- a/app/frontend/components/common/tags/TagFilter.vue
+++ b/app/frontend/components/common/tags/TagFilter.vue
@@ -13,7 +13,7 @@ const props = defineProps({
const emit = defineEmits(["remove-filter"]);
const removeSelf = () => {
- emit("remove-filter", props.filter.value);
+ emit("remove-filter", props.filter);
};
</script>
@@ -21,13 +21,13 @@ const removeSelf = () => {
<span
class="max-w-fit inline-flex items-center gap-100 px-200 py-100 border rounded-full border-neutrals-300 font-body text-xs text-neutrals-700 font-regular leading-[18px] shrink-0"
role="group"
- :aria-label="`Filter: ${props.filter.label}`"
+ :aria-label="`Filter: ${props.filter.filter_display}`"
>
- {{ props.filter.label }}
+ {{ props.text }}
<button
type="button"
- :aria-label="`Remove ${props.filter.label} filter`"
+ :aria-label="`Remove ${props.filter.filter_display} filter`"
class="p-50 -m-50 bg-transparent border-0 rounded
cursor-pointer
hover:bg-neutrals-100 hover:text-neutrals-800
diff --git a/app/frontend/components/features/filters/ProgramsFilterForm.vue b/app/frontend/components/features/filters/ProgramsFilterForm.vue
index bb3696a..ad5c5ca 100644
--- a/app/frontend/components/features/filters/ProgramsFilterForm.vue
+++ b/app/frontend/components/features/filters/ProgramsFilterForm.vue
@@ -8,67 +8,55 @@ import SelectComponent from "@/components/ui/SelectComponent.vue";
const props = defineProps({
modelValue: { type: Object, required: true },
updateField: { type: Function, required: true },
- mostrasFilter: { type: Array, default: () => [] }, // Program-specific prop
- cinemasFilter: { type: Array, default: () => [] }, // Program-specific prop
- paisesFilter: { type: Array, default: () => [] },
- genresFilter: { type: Array, default: () => [] },
+ mostras: { type: Array, default: () => [] }, // Program-specific prop
+ cinemas: { type: Array, default: () => [] }, // Program-specific prop
+ paises: { type: Array, default: () => [] },
+ genres: { type: Array, default: () => [] },
sessoes: { type: Array, default: () => [] },
+ directors: { type: Array, default: () => [] },
+ actors: { type: Array, default: () => [] },
});
-// Transform cadernos prop for ComboboxComponent format
-const mostrasFilterOptions = computed(() => {
- return props.mostrasFilter.map(caderno => ({
- label: caderno.nome_abreviado,
- value: caderno.permalink_pt,
+const mapFilterOptions = (filterList) => {
+ return filterList.map(option => ({
+ label: option.filter_display,
+ value: option.filter_value,
}));
-});
-// Transform cinema prop for ComboboxComponent format
-const cinemasFilterOptions = computed(() => {
- return props.cinemasFilter.map(cinema => ({
- label: cinema.nome,
- value: cinema.id,
- }));
-});
-// Transform cinema prop for ComboboxComponent format
-const paisesFilterOptions = computed(() => {
- return props.paisesFilter.map(pais => ({
- label: pais.nome_pais,
- value: pais.id,
- }));
-});
-// Transform cinema prop for ComboboxComponent format
-const genresFilterOptions = computed(() => {
- return props.genresFilter.map(genre => ({
- label: genre.filter_display,
- value: genre.filter_value,
- }));
-});
+}
+
+const mostrasFilterOptions = computed(() => mapFilterOptions(props.mostras));
+const mostraLabel = computed(() => props.mostras[0].filter_label)
+
+const actorsFilterOptions = computed(() => mapFilterOptions(props.actors));
+const actorsLabel = computed(() => props.actors[0]?.filter_label)
+
+const cinemasFilterOptions = computed(() => mapFilterOptions(props.cinemas));
+const cinemaLabel = computed(() => props.cinemas[0].filter_label)
+
+const paisesFilterOptions = computed(() => mapFilterOptions(props.paises));
+const paisLabel = computed(() => props.paises[0].filter_label)
+
+const genresFilterOptions = computed(() => mapFilterOptions(props.genres));
+const genreLabel = computed(() => props.genres[0].filter_label)
+
+const directorsOptions = computed(() => mapFilterOptions(props.directors));
+const directorLabel = computed(() => props.directors[0].filter_label)
+
// Transform cinema prop for ComboboxComponent format
const sessoesFilterOptions = computed(() => {
+ // TODO: TRANSLATE
return props.sessoes.map(sessao => ({
label: `Início às ${sessao.filter_display}`,
value: sessao.filter_value,
}));
});
+const sessaoLabel = computed(() => props.sessoes[0].filter_label)
-const getMostraObjectFromTagClas = (filter_value) => {
- return props.mostrasFilter.find(c => c.filter_value === filter_value) || null;
-};
-const getSessaoObject = (filter_value) => {
- return props.sessoes.find(c => c.filter_value === filter_value) || null;
-};
-const getCinemaObject = (filter_value) => {
- return props.cinemasFilter.find(c => c.filter_value === filter_value) || null;
-};
-
-const getPaisObject = (filter_value) => {
- return props.paisesFilter.find(c => c.filter_value === filter_value) || null;
+const getSelectedFrom = (collectionName, value) => {
+ return props[collectionName].find(option => option.filter_value == value)
}
-const getGenreObject = (filter_value) => {
- return props.genresFilter.find(c => c.filter_value === filter_value) || null;
-}
const getQueryObject = (filter_value) => {
// TODO: REFACTOR
// I'm building here beause the other get here as collection
@@ -80,7 +68,6 @@ const getQueryObject = (filter_value) => {
</script>
<template>
- <!-- Article-specific filter content -->
<div class="pt-400">
<SearchBar
:modelValue="props.modelValue.query?.filter_value"
@@ -88,31 +75,15 @@ const getQueryObject = (filter_value) => {
/>
</div>
- <!-- GENRES -->
- <AccordionGroup
- text="Gênero"
- :isOpen="!!props.modelValue.genresFilter"
- >
- <template v-slot:content>
- <div class="overflow-hidden w-full">
- <ComboboxComponent
- :collection="genresFilterOptions"
- :modelValue="props.modelValue.genresFilter?.filter_value || null"
- @update:modelValue="(val) => props.updateField('genresFilter', getGenreObject(val))"
- />
- </div>
- </template>
- </AccordionGroup>
-
<!-- HORARIO -->
<AccordionGroup
- text="Horário"
+ :text="sessaoLabel"
:isOpen="!!props.modelValue.sessao"
>
<template v-slot:content>
<SelectComponent
:modelValue="props.modelValue.sessao?.filter_value || null"
- @update:modelValue="(val) => props.updateField('sessao', getSessaoObject(val))"
+ @update:modelValue="(val) => props.updateField('sessao', getSelectedFrom('sessoes', val))"
:collection="sessoesFilterOptions"
/>
</template>
@@ -120,15 +91,15 @@ const getQueryObject = (filter_value) => {
<!-- MOSTRAS -->
<AccordionGroup
- text="Mostra"
+ :text="mostraLabel"
:isOpen="!!props.modelValue.mostrasFilter"
>
<template v-slot:content>
<div class="overflow-hidden w-full">
<ComboboxComponent
:collection="mostrasFilterOptions"
- :modelValue="props.modelValue.mostrasFilter?.filter_value || null"
- @update:modelValue="(val) => props.updateField('mostrasFilter', getMostraObjectFromTagClas(val))"
+ :modelValue="props.modelValue.mostra?.filter_value || null"
+ @update:modelValue="(val) => props.updateField('mostra', getSelectedFrom('mostras', val))"
/>
</div>
</template>
@@ -136,15 +107,31 @@ const getQueryObject = (filter_value) => {
<!-- CINEMAS -->
<AccordionGroup
- text="Cinema"
- :isOpen="!!props.modelValue.cinemasFilter"
+ :text="cinemaLabel"
+ :isOpen="!!props.modelValue.cinema"
>
<template v-slot:content>
<div class="overflow-hidden w-full">
<ComboboxComponent
:collection="cinemasFilterOptions"
- :modelValue="props.modelValue.cinemasFilter?.filter_value || null"
- @update:modelValue="(val) => props.updateField('cinemasFilter', getCinemaObject(val))"
+ :modelValue="props.modelValue.cinema?.filter_value || null"
+ @update:modelValue="(val) => props.updateField('cinema', getSelectedFrom('cinemas', val))"
+ />
+ </div>
+ </template>
+ </AccordionGroup>
+
+ <!-- GENRES -->
+ <AccordionGroup
+ :text="genreLabel"
+ :isOpen="!!props.modelValue.genre"
+ >
+ <template v-slot:content>
+ <div class="overflow-hidden w-full">
+ <ComboboxComponent
+ :collection="genresFilterOptions"
+ :modelValue="props.modelValue.genre?.filter_value || null"
+ @update:modelValue="(val) => props.updateField('genre', getSelectedFrom('genres', val))"
/>
</div>
</template>
@@ -152,15 +139,47 @@ const getQueryObject = (filter_value) => {
<!-- PAISES -->
<AccordionGroup
- text="Pais"
- :isOpen="!!props.modelValue.paisesFilter"
+ :text="paisLabel"
+ :isOpen="!!props.modelValue.pais"
>
<template v-slot:content>
<div class="overflow-hidden w-full">
<ComboboxComponent
:collection="paisesFilterOptions"
- :modelValue="props.modelValue.paisesFilter?.filter_value || null"
- @update:modelValue="(val) => props.updateField('paisesFilter', getPaisObject(val))"
+ :modelValue="props.modelValue.pais?.filter_value || null"
+ @update:modelValue="(val) => props.updateField('pais', getSelectedFrom('paises', val))"
+ />
+ </div>
+ </template>
+ </AccordionGroup>
+
+ <!-- DIRETORES -->
+ <AccordionGroup
+ :text="directorLabel"
+ :isOpen="!!props.modelValue['direção']"
+ >
+ <template v-slot:content>
+ <div class="overflow-hidden w-full">
+ <ComboboxComponent
+ :collection="directorsOptions"
+ :modelValue="props.modelValue['direção']?.filter_value || null"
+ @update:modelValue="(val) => props.updateField('direção', getSelectedFrom('directors', val))"
+ />
+ </div>
+ </template>
+ </AccordionGroup>
+
+ <!-- ACTORS -->
+ <AccordionGroup
+ :text="actorsLabel"
+ :isOpen="!!props.modelValue.elenco"
+ >
+ <template v-slot:content>
+ <div class="overflow-hidden w-full">
+ <ComboboxComponent
+ :collection="actorsFilterOptions"
+ :modelValue="props.modelValue.elenco?.filter_value || null"
+ @update:modelValue="(val) => props.updateField('elenco', getSelectedFrom('actors', val))"
/>
</div>
</template>
diff --git a/app/frontend/components/features/filters/ResponsiveFilterMenu.vue b/app/frontend/components/features/filters/ResponsiveFilterMenu.vue
index f196cf0..4c2e0e3 100644
--- a/app/frontend/components/features/filters/ResponsiveFilterMenu.vue
+++ b/app/frontend/components/features/filters/ResponsiveFilterMenu.vue
@@ -39,9 +39,9 @@ const emit = defineEmits([
<p class="text-header-sm text-neutrals-900 uppercase">
FILTROS
</p>
- <button @click="emit('close-filter-menu')" class="text-neutrals-900 cursor-pointer absolute -right-[.425rem]">
+ <!-- <button @click="emit('close-filter-menu')" class="text-neutrals-900 cursor-pointer absolute -right-[.425rem]">
<IconClose height="32px" width="32px" />
- </button>
+ </button> -->
</div>
<!-- Desktop Filter Content -->
@@ -67,46 +67,58 @@ const emit = defineEmits([
</div>
<!-- Mobile Layout: Fullscreen modal (only when open) -->
- <div
- v-show="props.isOpen"
- style="margin-top: 0"
- class="fixed inset-0 z-50 bg-white flex md:hidden flex-col w-full max-w-full h-[100vh] right-0 shadow-lg overflow-y-auto"
- >
- <TwContainer class="h-full">
- <div class="flex flex-col h-full">
- <!-- Mobile Filter header -->
+ <Teleport to="body">
+ <transition
+ name="slide"
+ enter-active-class="transition-transform duration-300 ease-out"
+ leave-active-class="transition-transform duration-300 ease-in"
+ enter-from-class="transform -translate-x-full"
+ enter-to-class="transform translate-x-0"
+ leave-from-class="transform translate-x-0"
+ leave-to-class="transform -translate-x-full"
+ >
<div
- class="shrink-0 flex justify-between items-center py-400 sticky top-0 bg-white-transp-1000 z-50"
+ v-show="props.isOpen"
+ style="margin-top: 0"
+ class="fixed inset-0 z-50 bg-white flex md:hidden flex-col w-full max-w-full h-[100vh] right-0 shadow-lg overflow-y-auto"
>
- <p class="text-header-sm text-neutrals-900 uppercase">
- FILTROS
- </p>
- <button @click="emit('close-filter-menu')" class="text-neutrals-900 cursor-pointer absolute -right-[.425rem]">
- <IconClose height="32px" width="32px" />
- </button>
- </div>
+ <TwContainer class="h-full">
+ <div class="flex flex-col h-full">
+ <!-- Mobile Filter header -->
+ <div
+ class="shrink-0 flex justify-between items-center py-400 sticky top-0 bg-white-transp-1000 z-50"
+ >
+ <p class="text-header-sm text-neutrals-900 uppercase">
+ FILTROS
+ </p>
+ <button @click="emit('close-filter-menu')" class="text-neutrals-900 cursor-pointer absolute -right-[.425rem]">
+ <IconClose height="32px" width="32px" />
+ </button>
+ </div>
- <!-- Mobile Filter Content -->
- <SearchFilter
- v-model="internalFilters"
- @update:modelValue="(val) => emit('update:modelValue', val)"
- @filtersApplied="emit('filtersApplied', internalFilters)"
- @filtersCleared="emit('filtersCleared')"
- @close-filter-menu="emit('close-filter-menu')"
- >
- <template #filters="slotProps">
- <slot
- name="filters"
- :modelValue="internalFilters"
- :updateField="(field, value) => {
- internalFilters[field] = value
- }"
- />
- </template>
- </SearchFilter>
- </div>
- </TwContainer>
- </div>
+ <!-- Mobile Filter Content -->
+ <SearchFilter
+ v-model="internalFilters"
+ @update:modelValue="(val) => emit('update:modelValue', val)"
+ @filtersApplied="emit('filtersApplied', internalFilters)"
+ @filtersCleared="emit('filtersCleared')"
+ @close-filter-menu="emit('close-filter-menu')"
+ >
+ <template #filters="slotProps">
+ <slot
+ name="filters"
+ :modelValue="internalFilters"
+ :updateField="(field, value) => {
+ internalFilters[field] = value
+ }"
+ />
+ </template>
+ </SearchFilter>
+ </div>
+ </TwContainer>
+ </div>
+ </transition>
+ </Teleport>
</template>
<style scoped></style>
diff --git a/app/frontend/components/layout/navbar/MobileMenu.vue b/app/frontend/components/layout/navbar/MobileMenu.vue
index 51a6aa6..fdf0dec 100644
--- a/app/frontend/components/layout/navbar/MobileMenu.vue
+++ b/app/frontend/components/layout/navbar/MobileMenu.vue
@@ -1,9 +1,15 @@
<script setup>
-import { ref } from "vue";
-import { Link } from "@inertiajs/vue3";
+import { ref, onMounted, onUnmounted, useTemplateRef} from "vue";
+import { Link, usePage } from "@inertiajs/vue3";
import AccordionGroup from "@/components/AccordionGroup.vue";
import TheLanguageSwitcher from "@components/TheLanguageSwitcher.vue";
+import { IconBoxArrowUp } from "@/components/common/icons";
+
+// import { FocusTrap } from "focus-trap-vue"; //=> Imported globally inside inertia.js
+const closeMenuButton = useTemplateRef('close-menu-button')
+
+const page = usePage()
// Controls menu visibility
const isMobileMenuOpen = ref(false);
@@ -16,6 +22,20 @@ const closeMenu = () => {
isMobileMenuOpen.value = false;
document.body.style.overflow = ""; // restore scroll
};
+
+const handleKeydown = (e) => {
+ if (e.key === 'Escape' && isMobileMenuOpen.value) {
+ closeMenu();
+ }
+};
+
+onMounted(() => {
+ document.addEventListener('keydown', handleKeydown);
+});
+
+onUnmounted(() => {
+ document.removeEventListener('keydown', handleKeydown);
+});
</script>
<template>
<div class="md:hidden pt-100">
@@ -43,77 +63,78 @@ const closeMenu = () => {
</button>
<!-- Mobile menu -->
<transition name="slide">
- <div
- v-if="isMobileMenuOpen"
- class="fixed inset-0 z-50 bg-white flex flex-col w-full max-w-full right-0 shadow-lg overflow-y-auto"
- >
- <!-- Close Button -->
- <div class="flex justify-between p-400">
- <Link @click="closeMenu" href="/">
- <img
- src="@assets/logos/festival-logo-mobile.svg"
- alt="Logo Festival do Rio"
- />
- </Link>
-
- <TheLanguageSwitcher />
- <button @click="closeMenu" class="text-neutrals-900">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="32"
- height="33"
- viewBox="0 0 32 33"
- fill="none"
- >
- <path
- d="M9.29379 9.79183C9.38668 9.69871 9.49703 9.62482 9.61852 9.57441C9.74001 9.524 9.87025 9.49805 10.0018 9.49805C10.1333 9.49805 10.2636 9.524 10.3851 9.57441C10.5065 9.62482 10.6169 9.69871 10.7098 9.79183L16.0018 15.0858L21.2938 9.79183C21.3868 9.69886 21.4971 9.62511 21.6186 9.57479C21.7401 9.52447 21.8703 9.49857 22.0018 9.49857C22.1333 9.49857 22.2635 9.52447 22.385 9.57479C22.5064 9.62511 22.6168 9.69886 22.7098 9.79183C22.8028 9.88481 22.8765 9.99519 22.9268 10.1167C22.9771 10.2381 23.003 10.3683 23.003 10.4998C23.003 10.6313 22.9771 10.7615 22.9268 10.883C22.8765 11.0045 22.8028 11.1149 22.7098 11.2078L17.4158 16.4998L22.7098 21.7918C22.8028 21.8848 22.8765 21.9952 22.9268 22.1167C22.9771 22.2381 23.003 22.3683 23.003 22.4998C23.003 22.6313 22.9771 22.7615 22.9268 22.883C22.8765 23.0045 22.8028 23.1149 22.7098 23.2078C22.6168 23.3008 22.5064 23.3746 22.385 23.4249C22.2635 23.4752 22.1333 23.5011 22.0018 23.5011C21.8703 23.5011 21.7401 23.4752 21.6186 23.4249C21.4971 23.3746 21.3868 23.3008 21.2938 23.2078L16.0018 17.9138L10.7098 23.2078C10.6168 23.3008 10.5064 23.3746 10.385 23.4249C10.2635 23.4752 10.1333 23.5011 10.0018 23.5011C9.8703 23.5011 9.7401 23.4752 9.61862 23.4249C9.49714 23.3746 9.38676 23.3008 9.29379 23.2078C9.20081 23.1149 9.12706 23.0045 9.07674 22.883C9.02642 22.7615 9.00052 22.6313 9.00052 22.4998C9.00052 22.3683 9.02642 22.2381 9.07674 22.1167C9.12706 21.9952 9.20081 21.8848 9.29379 21.7918L14.5878 16.4998L9.29379 11.2078C9.20066 11.1149 9.12677 11.0046 9.07636 10.8831C9.02595 10.7616 9 10.6314 9 10.4998C9 10.3683 9.02595 10.2381 9.07636 10.1166C9.12677 9.99508 9.20066 9.88473 9.29379 9.79183Z"
- fill="#3B3935"
- />
- </svg>
- </button>
- </div>
-
- <!-- Menu Content -->
- <nav class="flex-1 px-400 py-600 space-y-800">
- <AccordionGroup text="Programação" :isOpen="true">
- <template v-slot:content>
- <ul class="ps-600 pt-400 space-y-400">
- <li>
- <Link @click="closeMenu" href="/programacao">
- Programação completa
- </Link>
- </li>
- <li>Sessões com convidados</li>
- <li>Mudanças na programação</li>
- <li>Programação gratuita</li>
- </ul>
- </template>
- </AccordionGroup>
-
- <AccordionGroup text="edição 2024">
- <ul class="ps-600 pt-400 space-y-400"></ul>
- </AccordionGroup>
-
- <AccordionGroup text="sobre nós">
- <ul class="ps-600 pt-400 space-y-400"></ul>
- </AccordionGroup>
-
- <a
- href="#"
- class="font-body font-semibold text-neutrals-900 leadgin-[19.6px] uppercase flex justify-between pb-300 border-b"
- >Notícias</a
+ <FocusTrap v-model:active="isMobileMenuOpen" :initial-focus="() => $refs.closeMenuButton">
+ <div>
+ <div
+ v-if="isMobileMenuOpen"
+ class="fixed inset-0 z-50 bg-white flex flex-col w-full max-w-full right-0 shadow-lg overflow-y-auto"
+ id="mobile-menu"
>
+ <!-- Close Button -->
+ <div class="flex justify-between p-400">
+ <Link @click="closeMenu" href="/">
+ <img
+ src="@assets/logos/festival-logo-mobile.svg"
+ alt="Logo Festival do Rio"
+ />
+ </Link>
+
+ <TheLanguageSwitcher />
+ <button ref="close-menu-button" @click="closeMenu" class="text-neutrals-900">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="32"
+ height="33"
+ viewBox="0 0 32 33"
+ fill="none"
+ >
+ <path
+ d="M9.29379 9.79183C9.38668 9.69871 9.49703 9.62482 9.61852 9.57441C9.74001 9.524 9.87025 9.49805 10.0018 9.49805C10.1333 9.49805 10.2636 9.524 10.3851 9.57441C10.5065 9.62482 10.6169 9.69871 10.7098 9.79183L16.0018 15.0858L21.2938 9.79183C21.3868 9.69886 21.4971 9.62511 21.6186 9.57479C21.7401 9.52447 21.8703 9.49857 22.0018 9.49857C22.1333 9.49857 22.2635 9.52447 22.385 9.57479C22.5064 9.62511 22.6168 9.69886 22.7098 9.79183C22.8028 9.88481 22.8765 9.99519 22.9268 10.1167C22.9771 10.2381 23.003 10.3683 23.003 10.4998C23.003 10.6313 22.9771 10.7615 22.9268 10.883C22.8765 11.0045 22.8028 11.1149 22.7098 11.2078L17.4158 16.4998L22.7098 21.7918C22.8028 21.8848 22.8765 21.9952 22.9268 22.1167C22.9771 22.2381 23.003 22.3683 23.003 22.4998C23.003 22.6313 22.9771 22.7615 22.9268 22.883C22.8765 23.0045 22.8028 23.1149 22.7098 23.2078C22.6168 23.3008 22.5064 23.3746 22.385 23.4249C22.2635 23.4752 22.1333 23.5011 22.0018 23.5011C21.8703 23.5011 21.7401 23.4752 21.6186 23.4249C21.4971 23.3746 21.3868 23.3008 21.2938 23.2078L16.0018 17.9138L10.7098 23.2078C10.6168 23.3008 10.5064 23.3746 10.385 23.4249C10.2635 23.4752 10.1333 23.5011 10.0018 23.5011C9.8703 23.5011 9.7401 23.4752 9.61862 23.4249C9.49714 23.3746 9.38676 23.3008 9.29379 23.2078C9.20081 23.1149 9.12706 23.0045 9.07674 22.883C9.02642 22.7615 9.00052 22.6313 9.00052 22.4998C9.00052 22.3683 9.02642 22.2381 9.07674 22.1167C9.12706 21.9952 9.20081 21.8848 9.29379 21.7918L14.5878 16.4998L9.29379 11.2078C9.20066 11.1149 9.12677 11.0046 9.07636 10.8831C9.02595 10.7616 9 10.6314 9 10.4998C9 10.3683 9.02595 10.2381 9.07636 10.1166C9.12677 9.99508 9.20066 9.88473 9.29379 9.79183Z"
+ fill="#3B3935"
+ />
+ </svg>
+ </button>
+ </div>
- <AccordionGroup text="mídias">
- <ul class="ps-600 pt-400 space-y-400"></ul>
- </AccordionGroup>
- <AccordionGroup text="informações">
- <ul class="ps-600 pt-400 space-y-400"></ul>
- </AccordionGroup>
+ <!-- Menu Content -->
+ <nav class="flex-1 px-400 py-600 space-y-800">
+ <AccordionGroup
+ :isOpen="false"
+ v-for="[navText, subItems] in Object.entries(page.props.mainItems)"
+ :key="navText"
+ :text="navText"
+ >
+ <template v-slot:content>
+ <ul class="ps-600 pt-400 space-y-800">
+ <li
+ v-for="item in subItems"
+ :key="item.path"
+ >
+ <Link
+ @click="closeMenu"
+ :href="item.path"
+ >
+ {{ item.description }}
+ </Link>
+ </li>
+ </ul>
+ </template>
+ </AccordionGroup>
+ <Link
+ v-for="item in page.props.secondaryItems"
+ :key="item.name"
+ @click="closeMenu"
+ :href="item.href"
+ class="block font-body font-semibold text-neutrals-900 leadgin-[19.6px] text-sm uppercase pb-300"
+ >
+ {{ item.name }}
+ </Link>
+ <Link href="#" class="block font-body font-semibold leadgin-[19.6px] text-sm uppercase pb-300 text-vermelho-600 hover:opacity-80 flex items-center gap-300">RIOMARKET <IconBoxArrowUp height="16" width="16" color="inherit"/></Link>
+ </nav>
+ </div>
+ </div>
+ </FocusTrap>
- <a href="#" class="text-vermelho-600 font-bold">RIOMARKET</a>
- </nav>
- </div>
</transition>
</div>
</template>
diff --git a/app/frontend/components/ui/SelectComponent.vue b/app/frontend/components/ui/SelectComponent.vue
index 005f72f..3fac4ad 100644
--- a/app/frontend/components/ui/SelectComponent.vue
+++ b/app/frontend/components/ui/SelectComponent.vue
@@ -1,4 +1,5 @@
<script setup>
+// TODO: DESelected element if clicking same value
import {
Select,
SelectContent,
diff --git a/app/frontend/entrypoints/inertia.js b/app/frontend/entrypoints/inertia.js
index bba21d6..ba7bcda 100644
--- a/app/frontend/entrypoints/inertia.js
+++ b/app/frontend/entrypoints/inertia.js
@@ -1,6 +1,7 @@
import { createInertiaApp } from '@inertiajs/vue3'
import { createApp, h } from 'vue'
import PageLayout from '@pages/PageLayout.vue'
+import { FocusTrap } from 'focus-trap-vue'
createInertiaApp({
// Set default page title
@@ -30,6 +31,7 @@ createInertiaApp({
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
+ .component('FocusTrap', FocusTrap)
.use(plugin)
.mount(el)
},
diff --git a/app/frontend/pages/ProgramPage.vue b/app/frontend/pages/ProgramPage.vue
index 3c8c3c4..42d6a7c 100644
--- a/app/frontend/pages/ProgramPage.vue
+++ b/app/frontend/pages/ProgramPage.vue
@@ -1,4 +1,5 @@
<script setup>
+// TODO: Close form filter only after success submit?
// TODO: CHANGE TEXT WHEN NO RESULT FOR FILTERING
// TODO: FIX LIMPAR FILTRO
// TODO: Click cleansearchbar should close mobile filter menu?
@@ -32,11 +33,13 @@ const props = defineProps({
items: { type: Array, required: true }
,elements: { type: Object, required: true }
,pagy: { type: Object, required: true }
- ,mostrasFilter: { type: Array, default: () => [] }
- ,cinemasFilter: { type: Array, default: () => [] }
- ,paisesFilter: { type: Array, default: () => [] }
- ,genresFilter: { type: Array, default: () => [] }
+ ,mostras: { type: Array, default: () => [] }
+ ,cinemas: { type: Array, default: () => [] }
+ ,paises: { type: Array, default: () => [] }
+ ,genres: { type: Array, default: () => [] }
,sessoes: { type: Array, default: () => [] }
+ ,directors: { type: Array, default: () => [] }
+ ,actors: { type: Array, default: () => [] }
// NEW LIFE
,menuTabs: { type: Array, required: true }
,current_filters: { type: Object, default: () => ({}) }
@@ -72,9 +75,30 @@ const filterSearch = (filtersFromChild) => {
};
const removeQuery = (what) => {
- debugger
- // remove the correct queryparams from url
+ const newParams = new URLSearchParams()
+ // TODO: ADD ALL TRANSLATIONS OR REFACTOR AI CAN ADD TRANSLATIONS
+ if (["Country", "Pais"].includes(what.filter_label)) {
+ localFilters.value['pais'] = null
+ }
+
+ if (["Showcase", "Mostra"].includes(what.filter_label)) {
+ localFilters.value['mostra'] = null
+ }
// make new request with the up to date filters
+ debugger
+ if (localFilters.value[what["filter_label"].toLowerCase()]) {
+ localFilters.value[what["filter_label"].toLowerCase()] = null
+ }
+ Object.entries(localFilters.value).forEach(([key, value]) => {
+ if (value !== null && value !== undefined && value !== "") {
+ newParams.set(key, value.filter_value);
+ }
+ })
+ router.get(props.tabBaseUrl, newParams, {
+ preserveState: true,
+ preserveScroll: true,
+ only: ['elements', 'pagy', 'current_filters', 'has_active_filters', 'menuTabs']
+ })
}
// Called when filters cleared from MobileFilterMenu
@@ -104,16 +128,8 @@ const { sentinel, isSticky } = useStickyMenuTabs()
<MobileTrigger @open-menu="openMenu" />
</div>
- <!-- filtered tag -->
- <!-- { "query": null,
- "mostrasFilter": null,
- "cinemasFilter": null,
- "paisesFilter": null,
- "sessao": { "sessao": "2000-01-01T19:00:00.000Z",
- "display_sessao": "19:00",
- "filter_value": "19h00",
- "filter_display": "19h00" }
- } -->
+ <!-- TODO: REFAC into reusable components -->
+ <!-- MOBILE TAG FILTER -->
<div
class="flex lg:hidden gap-300 pt-200 pb-300 overflow-x-auto no-scroll-bar"
v-if="Object.values(props.current_filters).some((item) => item !== null)"
@@ -121,10 +137,10 @@ const { sentinel, isSticky } = useStickyMenuTabs()
<TagFilter
v-for="[key, value] in Object.entries(props.current_filters).filter(([k, v]) => v !== null)"
:key="key"
- :filter="{ label: value.filter_display, value: value.filter_value }"
+ :filter="value"
:text="value.filter_display"
@remove-filter="removeQuery"
- />
+ />
</div>
<!-- filtered tag -->
@@ -137,18 +153,22 @@ const { sentinel, isSticky } = useStickyMenuTabs()
:tabs="menuTabs"
class="h-15"
/>
+
+ <!-- DESKTOP TAG FILTER -->
<div
class="hidden lg:flex gap-300 pt-200 pb-300 overflow-x-auto no-scroll-bar sticky top-15 z-10 bg-white"
v-if="Object.values(props.current_filters).some((item) => item !== null)"
>
<TagFilter
v-for="[key, value] in Object.entries(props.current_filters).filter(([k, v]) => v !== null)"
- :key="key"
- :filter="{ label: value.filter_display, value: value.filter_value }"
+ :key="value.filter_value"
+ :filter="value"
:text="value.filter_display"
@remove-filter="removeQuery"
- />
+ />
</div>
+
+ <!-- CONTENT -->
<InfiniteScrollLayout #content="{ allElements }"
:elements="props.elements"
:pagy="props.pagy"
@@ -171,11 +191,13 @@ const { sentinel, isSticky } = useStickyMenuTabs()
<ProgramsFilterForm
:model-value="modelValue"
:update-field="updateField"
- :mostrasFilter="props.mostrasFilter"
- :cinemasFilter="props.cinemasFilter"
- :paisesFilter="props.paisesFilter"
- :genresFilter="props.genresFilter"
+ :mostras="props.mostras"
+ :cinemas="props.cinemas"
+ :paises="props.paises"
+ :genres="props.genres"
:sessoes="props.sessoes"
+ :directors="props.directors"
+ :actors="props.actors"
/>
</template>
</ResponsiveFilterMenu>
diff --git a/app/models/cinema.rb b/app/models/cinema.rb
index 9ca1bd6..c57b37f 100644
--- a/app/models/cinema.rb
+++ b/app/models/cinema.rb
@@ -10,4 +10,8 @@ class Cinema < ApplicationRecord
def filter_display
nome
end
+
+ def filter_label
+ I18n.t("filter.cinema")
+ end
end
diff --git a/app/models/mostra.rb b/app/models/mostra.rb
index 034b02a..b4b4754 100644
--- a/app/models/mostra.rb
+++ b/app/models/mostra.rb
@@ -31,6 +31,14 @@ class Mostra < ApplicationRecord
end
def filter_display
- display_name
+ if I18n.locale == :pt
+ nome_pt
+ else
+ nome_en
+ end
+ end
+
+ def filter_label
+ I18n.t("filter.submostra")
end
end
diff --git a/app/models/pais.rb b/app/models/pais.rb
index c382cd2..da02a5b 100644
--- a/app/models/pais.rb
+++ b/app/models/pais.rb
@@ -1,4 +1,6 @@
class Pais < ApplicationRecord
+ include ActiveSupport::Inflector
+
has_many :paises_peliculas
def filter_value
@@ -6,6 +8,15 @@ class Pais < ApplicationRecord
end
def filter_display
- nome_pais
+ nome_without_special_char = transliterate(self.nome_pais, :pt).downcase.gsub(" ", "_")
+ if I18n.locale == :pt
+ I18n.t("countries.#{nome_without_special_char}")
+ else
+ I18n.t("countries.#{nome_without_special_char}")
+ end
+ end
+
+ def filter_label
+ I18n.t("filter.pais")
end
end
diff --git a/app/models/pelicula.rb b/app/models/pelicula.rb
index 578cb31..75c53e2 100644
--- a/app/models/pelicula.rb
+++ b/app/models/pelicula.rb
@@ -23,10 +23,55 @@ class Pelicula < ApplicationRecord
.uniq
.sort
- genres.map { |g| { "filter_display" => g, "filter_value" => g } }
+ genres.map do |genre|
+ {
+ "filter_display" => genre,
+ "filter_value" => genre,
+ "filter_label" => I18n.t("filter.genero")
+ }
+ end
# end
end
+ def self.directors_for(edicao_id)
+ # Rails.cache.fetch("directors-for-edicao-#{edicao_id}", expires_in: 12.hours) do
+ where(edicao_id: edicao_id)
+ .where.not(diretor_coord_int: [ nil, "" ])
+ .pluck(:diretor_coord_int)
+ .map(&:strip)
+ .uniq
+ .compact
+ .sort
+ .map do |director|
+ {
+ "filter_display" => director,
+ "filter_value" => director,
+ "filter_label" => I18n.t("filter.direcao")
+ }
+ end
+ # end
+ end
+
+ # Filter options
+ def self.cast_for(edicao_id)
+ # Filter collection must be cached
+ all_cast_for(edicao_id).map do |cast|
+ {
+ "filter_display": cast,
+ "filter_value": cast,
+ "filter_label": I18n.t("filter.elenco")
+ }
+ end
+ end
+
+ def self.all_cast_for(edicao_id)
+ where(edicao_id: edicao_id)
+ .where.not(elenco_coord_int: [ nil, "" ])
+ .pluck(:elenco_coord_int)
+ .flat_map { |cast| cast.split(",").map(&:strip) }
+ .reject(&:blank?).uniq.sort
+ end
+
def genre
return "TBD" unless catalogo_ficha_2007
@@ -47,4 +92,39 @@ class Pelicula < ApplicationRecord
all_paises.first
end
end
+
+ # Caches actor names with pelicula id
+ def self.actor_to_pelicula_mapping(edicao_id)
+ Rails.cache.fetch("actor-pelicula-mapping-#{edicao_id}", expires_in: 6.hours) do
+ mapping = {}
+
+ where(edicao_id: edicao_id)
+ .where.not(elenco_coord_int: [ nil, "" ])
+ .pluck(:id, :elenco_coord_int)
+ .each do |pelicula_id, cast_string|
+ cast_string.split(",").each do |actor|
+ clean_actor = actor.strip
+ next if clean_actor.blank?
+
+ mapping[clean_actor] ||= []
+ mapping[clean_actor.downcase] ||= []
+ # Store both exact name and normalized version
+ mapping[clean_actor] << pelicula_id
+ mapping[clean_actor.downcase] << pelicula_id
+ end
+ end
+
+ # Remove duplicates and return
+ mapping.each { |ky, value| mapping[ky] = value.uniq }
+ mapping
+ end
+ end
+
+ # get from cache and search for pelicula ids
+ def self.with_actor(actor_name, edicao_id)
+ mapping = actor_to_pelicula_mapping(edicao_id)
+ pelicula_ids = mapping[actor_name] || mapping[actor_name.downcase] || []
+
+ where(id: pelicula_ids)
+ end
end
diff --git a/app/models/programacao.rb b/app/models/programacao.rb
index 639fa4b..086897a 100644
--- a/app/models/programacao.rb
+++ b/app/models/programacao.rb
@@ -18,4 +18,8 @@ class Programacao < ApplicationRecord
def filter_display
sessao.strftime("%Hh%M")
end
+
+ def filter_label
+ I18n.t("filter.time")
+ end
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 15ced19..b23ad61 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -34,7 +34,7 @@ en:
date: "Date"
time: "Time"
submostra: "Showcase"
- cinema: "Cinema"
+ cinema: "Theater"
genero: "Genre"
pais: "Country"
direcao: "Director"
@@ -58,3 +58,121 @@ en:
placeholder:
select: "Pick one"
+
+ countries:
+ afeganistao: "Afghanistan"
+ albania: "Albania"
+ argelia: "Algeria"
+ angola: "Angola"
+ argentina: "Argentina"
+ armenia: "Armenia"
+ australia: "Australia"
+ austria: "Austria"
+ belgica: "Belgium"
+ butao: "Bhutan"
+ bolivia: "Bolivia"
+ bosnia_herzegovina: "Bosnia and Herzegovina"
+ brasil: "Brazil"
+ bulgaria: "Bulgaria"
+ camboja: "Cambodia"
+ canada: "Canada"
+ chile: "Chile"
+ china: "China"
+ colombia: "Colombia"
+ congo: "Congo"
+ congo_republica_democratica: "Democratic Republic of the Congo"
+ costa_rica: "Costa Rica"
+ croacia: "Croatia"
+ cuba: "Cuba"
+ chipre: "Cyprus"
+ republica_tcheca: "Czech Republic"
+ tchecoslovaquia: "Czechoslovakia"
+ dinamarca: "Denmark"
+ republica_dominicana: "Dominican Republic"
+ equador: "Ecuador"
+ egito: "Egypt"
+ estonia: "Estonia"
+ finlandia: "Finland"
+ franca: "France"
+ georgia: "Georgia"
+ alemanha: "Germany"
+ alemanha_ocidental: "West Germany"
+ grecia: "Greece"
+ guatemala: "Guatemala"
+ guiana: "Guyana"
+ hong_kong: "Hong Kong"
+ hungria: "Hungary"
+ islandia: "Iceland"
+ india: "India"
+ indonesia: "Indonesia"
+ ira: "Iran"
+ irlanda: "Ireland"
+ israel: "Israel"
+ italia: "Italy"
+ japao: "Japan"
+ jordania: "Jordan"
+ casaquistao: "Kazakhstan"
+ quenia: "Kenya"
+ kyrgyztan: "Kyrgyzstan"
+ libano: "Lebanon"
+ lituania: "Lithuania"
+ luxemburgo: "Luxembourg"
+ macedonia: "North Macedonia"
+ macedonia_antiga_iugoslavia: "North Macedonia"
+ mali: "Mali"
+ mauritania: "Mauritania"
+ mexico: "Mexico"
+ moldavia: "Moldova"
+ mongolia: "Mongolia"
+ marrocos: "Morocco"
+ mocambique: "Mozambique"
+ myanmar: "Myanmar"
+ nepal: "Nepal"
+ holanda: "Netherlands"
+ paises_baixos: "Netherlands"
+ nova_zelandia: "New Zealand"
+ niger: "Niger"
+ nigeria: "Nigeria"
+ noruega: "Norway"
+ paquistao: "Pakistan"
+ palestina: "Palestine"
+ panama: "Panama"
+ paraguai: "Paraguay"
+ peru: "Peru"
+ filipinas: "Philippines"
+ polonia: "Poland"
+ portugal: "Portugal"
+ catar: "Qatar"
+ qatar: "Qatar"
+ romenia: "Romania"
+ russia: "Russia"
+ ruanda: "Rwanda"
+ arabia_saudita: "Saudi Arabia"
+ senegal: "Senegal"
+ servia: "Serbia"
+ singapura: "Singapore"
+ eslovaquia: "Slovakia"
+ eslovenia: "Slovenia"
+ africa_do_sul: "South Africa"
+ coreia_do_sul: "South Korea"
+ espanha: "Spain"
+ sri_lanka: "Sri Lanka"
+ sri_lanca: "Sri Lanka"
+ sudao: "Sudan"
+ suecia: "Sweden"
+ suica: "Switzerland"
+ siria: "Syria"
+ taiwan: "Taiwan"
+ tailandia: "Thailand"
+ tunisia: "Tunisia"
+ turquia: "Turkey"
+ emirados_arabes_unidos: "United Arab Emirates"
+ ucrania: "Ukraine"
+ reino_unido: "United Kingdom"
+ estados_unidos: "United States"
+ estados_unidos_da_america: "United States of America"
+ uruguai: "Uruguay"
+ vietname: "Vietnam"
+ iemen: "Yemen"
+ zambia: "Zambia"
+ acrotiri_e_decelia: "Acrotíri e Decelia"
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index bc71dcf..476fdc7 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -84,3 +84,121 @@ pt:
- Out
- Nov
- Dez
+
+ countries:
+ acrotiri_e_decelia: "Acrotíri e Decelia"
+ afeganistao: "Afeganistão"
+ albania: "Albânia"
+ argelia: "Argélia"
+ angola: "Angola"
+ argentina: "Argentina"
+ armenia: "Armênia"
+ australia: "Austrália"
+ austria: "Áustria"
+ belgica: "Bélgica"
+ butao: "Butão"
+ bolivia: "Bolívia"
+ bosnia_herzegovina: "Bósnia e Herzegovina"
+ brasil: "Brasil"
+ bulgaria: "Bulgária"
+ camboja: "Camboja"
+ canada: "Canadá"
+ chile: "Chile"
+ china: "China"
+ colombia: "Colômbia"
+ congo: "Congo"
+ congo_republica_democratica: "Congo República Democrática"
+ costa_rica: "Costa Rica"
+ croacia: "Croácia"
+ cuba: "Cuba"
+ chipre: "Chipre"
+ republica_tcheca: "República Tcheca"
+ tchecoslovaquia: "Tchecoslováquia"
+ dinamarca: "Dinamarca"
+ republica_dominicana: "República Dominicana"
+ equador: "Equador"
+ egito: "Egito"
+ estonia: "Estônia"
+ finlandia: "Finlândia"
+ franca: "França"
+ georgia: "Geórgia"
+ alemanha: "Alemanha"
+ alemanha_ocidental: "Alemanha Ocidental"
+ grecia: "Grécia"
+ guatemala: "Guatemala"
+ guiana: "Guiana"
+ hong_kong: "Hong Kong"
+ hungria: "Hungria"
+ islandia: "Islândia"
+ india: "Índia"
+ indonesia: "Indonésia"
+ ira: "Irã"
+ irlanda: "Irlanda"
+ israel: "Israel"
+ italia: "Itália"
+ japao: "Japão"
+ jordania: "Jordânia"
+ casaquistao: "Casaquistão"
+ quenia: "Quênia"
+ kyrgyztan: "Kyrgyztan"
+ libano: "Líbano"
+ lituania: "Lituânia"
+ luxemburgo: "Luxemburgo"
+ macedonia: "Macedônia"
+ macedonia_antiga_iugoslavia: "Macedônia, antiga Iugoslávia"
+ mali: "Mali"
+ mauritania: "Mauritânia"
+ mexico: "México"
+ moldavia: "Moldávia"
+ mongolia: "Mongólia"
+ marrocos: "Marrocos"
+ mocambique: "Moçambique"
+ myanmar: "Myanmar"
+ nepal: "Nepal"
+ holanda: "Holanda"
+ paises_baixos: "Países Baixos"
+ nova_zelandia: "Nova Zelândia"
+ niger: "Niger"
+ nigeria: "Nigéria"
+ noruega: "Noruega"
+ paquistao: "Paquistão"
+ palestina: "Palestina"
+ panama: "Panamá"
+ paraguai: "Paraguai"
+ peru: "Peru"
+ filipinas: "Filipinas"
+ polonia: "Polônia"
+ portugal: "Portugal"
+ catar: "Catar"
+ qatar: "Qatar"
+ romenia: "Romênia"
+ russia: "Rússia"
+ ruanda: "Ruanda"
+ arabia_saudita: "Arábia Saudita"
+ senegal: "Senegal"
+ servia: "Sérvia"
+ singapura: "Singapura"
+ eslovaquia: "Eslováquia"
+ eslovenia: "Eslovênia"
+ africa_do_sul: "África do Sul"
+ coreia_do_sul: "Coreia do Sul"
+ espanha: "Espanha"
+ sri_lanka: "Sri Lanka"
+ sri_lanca: "Sri Lanca"
+ sudao: "Sudão"
+ suecia: "Suécia"
+ suica: "Suíça"
+ siria: "Síria"
+ taiwan: "Taiwan"
+ tailandia: "Tailândia"
+ tunisia: "Tunísia"
+ turquia: "Turquia"
+ emirados_arabes_unidos: "Emirados Árabes Unidos"
+ ucrania: "Ucrânia"
+ reino_unido: "Reino Unido"
+ estados_unidos: "Estados Unidos"
+ estados_unidos_da_america: "Estados Unidos da América"
+ uruguai: "Uruguai"
+ vietname: "Vietname"
+ iemen: "Iémen"
+ zambia: "Zâmbia"
diff --git a/package-lock.json b/package-lock.json
index c7287e5..4f81d37 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
- "name": "riff-inertia",
"dependencies": {
"@inertiajs/vue3": "^2.1.3",
"@tailwindcss/forms": "^0.5.10",
@@ -15,6 +14,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-vue": "^8.6.0",
+ "focus-trap": "^7.6.5",
+ "focus-trap-vue": "^4.1.0",
"lucide-vue-next": "^0.542.0",
"reka-ui": "^2.5.0",
"tailwind-merge": "^3.3.1",
@@ -2063,6 +2064,25 @@
"node": ">=8"
}
},
+ "node_modules/focus-trap": {
+ "version": "7.6.5",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz",
+ "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==",
+ "license": "MIT",
+ "dependencies": {
+ "tabbable": "^6.2.0"
+ }
+ },
+ "node_modules/focus-trap-vue": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/focus-trap-vue/-/focus-trap-vue-4.1.0.tgz",
+ "integrity": "sha512-RvK28mED+4fqoqnJQyadfoiKZU2G/QEez+EnJtdsg83DjiuKetimvDGas9ot4p5bn1J09rDmKLtKFMBkwQc+JA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "focus-trap": "^7.0.0",
+ "vue": "^3.0.0"
+ }
+ },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -3063,6 +3083,12 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "license": "MIT"
+ },
"node_modules/tailwind-merge": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
diff --git a/package.json b/package.json
index ee484dd..140639c 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"type": "module",
- "scripts": {
+ "scripts": {
"test": "vitest"
},
"devDependencies": {
@@ -19,6 +19,8 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-vue": "^8.6.0",
+ "focus-trap": "^7.6.5",
+ "focus-trap-vue": "^4.1.0",
"lucide-vue-next": "^0.542.0",
"reka-ui": "^2.5.0",
"tailwind-merge": "^3.3.1",
diff --git a/test/controllers/programs_controller/director_filter_test.rb b/test/controllers/programs_controller/director_filter_test.rb
new file mode 100644
index 0000000..92c6382
--- /dev/null
+++ b/test/controllers/programs_controller/director_filter_test.rb
@@ -0,0 +1,356 @@
+require "test_helper"
+
+class ProgramsController::DirectorFilterTest < ActionDispatch::IntegrationTest
+ test "filters by director - Christopher Nolan" do
+ get program_url, params: { director: "Christopher Nolan" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["Batman"], element["titulo"]
+ # Verify the director matches the filter
+ end
+ end
+
+ test "filters by director - Wachowskis" do
+ get program_url, params: { director: "Wachowskis" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ # Should include just Matrix os 17 sao outro dia
+ assert_equal 1, elements.length
+
+ # Check total count via pagination
+ total_elements = props["pagy"]["count"]
+ assert_equal 1, total_elements
+ end
+
+ test "filters by director - João Silva" do
+ get program_url, params: { director: "João Silva" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["Cidade Perdida"], element["titulo"]
+ end
+ end
+
+ test "filters by director - Ana Pereira" do
+ get program_url, params: { director: "Ana Pereira" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["Amor em Brasília"], element["titulo"]
+ end
+ end
+
+ test "filters by director - Hans Mueller" do
+ get program_url, params: { director: "Hans Mueller" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["Berlin Nights"], element["titulo"]
+ end
+ end
+
+ test "filters by director - Pierre Dubois" do
+ get program_url, params: { director: "Pierre Dubois" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["Paris Stories"], element["titulo"]
+ end
+ end
+
+ test "filters by director - Roberto Oliveira" do
+ get program_url, params: { director: "Roberto Oliveira" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["Amazônia Selvagem"], element["titulo"]
+ end
+ end
+
+ test "filters by director - Marina Costa" do
+ get program_url, params: { director: "Marina Costa" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["Cidade em Transformação"], element["titulo"]
+ end
+ end
+
+ test "filters by director - Marcos Jorge" do
+ get program_url, params: { director: "Marcos Jorge" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes ["São Paulo"], element["titulo"]
+ end
+ end
+
+ # COMBINED FILTERS TESTS WITH DIRECTOR
+ test "combines search query and director filter" do
+ get program_url, params: {
+ query: "Batman",
+ director: "Christopher Nolan"
+ }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+ assert_equal "Batman", elements.first["titulo"]
+
+ # Verify both filters are preserved
+ assert_equal "Batman", props["current_filters"]["query"]["filter_value"]
+ assert_equal "Christopher Nolan", props["current_filters"]["director"]["filter_value"]
+ end
+
+ test "combines search query and director filter with no results" do
+ get program_url, params: {
+ query: "Batman",
+ director: "Wachowskis"
+ }
+
+ assert_response :success
+ props = inertia_props
+
+ # Batman is directed by Christopher Nolan, not Wachowskis
+ elements = props["elements"]
+ assert_equal 0, elements.length
+
+ # Filters should still be preserved
+ assert_equal "Batman", props["current_filters"]["query"]["filter_value"]
+ assert_equal "Wachowskis", props["current_filters"]["director"]["filter_value"]
+ end
+
+ test "search finds movies across different directors" do
+ get program_url, params: { query: "Cidade" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 2, elements.length
+
+ titles = elements.map { |e| e["titulo"] }
+
+ assert_includes titles, "Cidade Perdida" # directed by João Silva
+ assert_includes titles, "Cidade em Transformação" # directed by Marina Costa
+ end
+
+ test "combines director filter with mostra filter" do
+ get program_url, params: {
+ director: "João Silva",
+ mostra: "competicao-nacional"
+ }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+ assert_equal "Cidade Perdida", elements.first["titulo"]
+
+ # Verify both filters are preserved
+ assert_equal "João Silva", props["current_filters"]["director"]["filter_value"]
+ assert_equal "competicao-nacional", props["current_filters"]["mostra"]["permalink_pt"]
+ end
+
+ test "combines director filter with mostra filter with no results" do
+ get program_url, params: {
+ director: "Christopher Nolan",
+ mostra: "competicao-nacional"
+ }
+
+ assert_response :success
+ props = inertia_props
+
+ # Christopher Nolan's Batman is in sci_fi mostra, not competicao-nacional
+ elements = props["elements"]
+ assert_equal 0, elements.length
+
+ # Filters should still be preserved
+ assert_equal "Christopher Nolan", props["current_filters"]["director"]["filter_value"]
+ assert_equal "competicao-nacional", props["current_filters"]["mostra"]["permalink_pt"]
+ end
+
+ test "combines director filter with cinema filter" do
+ cinepolis = cinemas(:cinepolis)
+ get program_url, params: {
+ director: "João Silva",
+ cinema: cinepolis.id
+ }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ # This depends on which cinemas show João Silva's movies
+ # Adjust expected count based on fixtures
+ assert elements.length >= 0
+
+ elements.each do |element|
+ end
+
+ # Verify both filters are preserved
+ assert_equal "João Silva", props["current_filters"]["director"]["filter_value"]
+ assert_equal cinepolis.id, props["current_filters"]["cinema"]["id"]
+ end
+
+ test "director filter affects available dates" do
+ get program_url, params: { director: "Christopher Nolan" }
+
+ assert_response :success
+ props = inertia_props
+
+ available_dates = props["menuTabs"].map { _1["date"] }
+ # Should only show dates where Christopher Nolan movies are programmed
+ # Adjust expected dates based on your programacoes fixtures
+ assert available_dates.length >= 0
+ end
+
+ test "preserves director filter when navigating dates" do
+ get program_url, params: {
+ director: "Wachowskis",
+ date: "2024-10-07" # Adjust date based on when Wachowskis movies are shown
+ }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ elements.each do |element|
+ end
+
+ # Filter should be preserved
+ assert_equal "Wachowskis", props["current_filters"]["director"]["filter_value"]
+ end
+
+ test "combines all filters - search, director, mostra, cinema, and date" do
+ cinepolis = cinemas(:cinepolis)
+ get program_url, params: {
+ query: "Matrix",
+ director: "Wachowskis",
+ mostra: "sci-fi", # Adjust based on actual mostra permalink
+ cinema: cinepolis.id,
+ date: "2024-10-06" # Adjust based on when Matrix is shown at Cinépolis
+ }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ # Expect 1 result if all filters align, 0 if they don't
+ elements.each do |element|
+ assert_includes element["titulo"], "Matrix"
+ end
+
+ # All filters should be preserved
+ assert_equal "Matrix", props["current_filters"]["query"]["filter_value"]
+ assert_equal "Wachowskis", props["current_filters"]["director"]["filter_value"]
+ # Adjust mostra assertion based on actual data structure
+ assert_equal cinepolis.id, props["current_filters"]["cinema"]["id"]
+ end
+
+ test "returns correct director options in props" do
+ get program_url
+
+ assert_response :success
+ props = inertia_props
+
+ directors_filter = props["directors"]
+ assert directors_filter.is_a?(Array)
+ assert directors_filter.length >= 8 # At least the directors from fixtures
+
+ # Check that key directors are included
+ director_names = directors_filter.map { |d| d["filter_value"] || d["nome"] || d }
+ assert_includes director_names, "Christopher Nolan"
+ assert_includes director_names, "Wachowskis"
+ assert_includes director_names, "João Silva"
+ assert_includes director_names, "Ana Pereira"
+ assert_includes director_names, "Hans Mueller"
+ assert_includes director_names, "Pierre Dubois"
+ assert_includes director_names, "Roberto Oliveira"
+ assert_includes director_names, "Marina Costa"
+ assert_includes director_names, "Marcos Jorge"
+
+ # Check structure of director objects (adjust based on actual implementation)
+ unless directors_filter.empty?
+ director = directors_filter.first
+ # This depends on how the controller structures the director filter options
+ # Common patterns: simple array of strings, or array of hashes with keys
+ assert (director.is_a?(String) || director.is_a?(Hash))
+ end
+ end
+
+ test "handles empty director filter gracefully" do
+ get program_url, params: { director: "" }
+
+ assert_response :success
+ props = inertia_props
+
+ # Empty filter should behave like no filter - return all elements
+ elements = props["elements"]
+ assert elements.length > 0
+
+ selected_filters = props["current_filters"]
+ # Empty filter should not be preserved
+ assert_nil selected_filters["director"]
+ end
+
+ test "director filter with special characters" do
+ # Test with director that has special characters if any exist in your fixtures
+ # This is more relevant if you have international directors with accents, etc.
+ get program_url, params: { director: "Marcos Jorge" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+ assert_equal "São Paulo", elements.first["titulo"]
+ end
+end
diff --git a/test/controllers/programs_controller_test.rb b/test/controllers/programs_controller_test.rb
index fc152b3..7f6b187 100644
--- a/test/controllers/programs_controller_test.rb
+++ b/test/controllers/programs_controller_test.rb
@@ -277,7 +277,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
end
test "filters by mostra - competicao nacional" do
- get program_url, params: { mostrasFilter: "competicao-nacional" }
+ get program_url, params: { mostra: "competicao-nacional" }
assert_response :success
props = inertia_props
@@ -291,7 +291,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
end
test "filters by mostra - mostra internacional" do
- get program_url, params: { mostrasFilter: "mostra-internacional" }
+ get program_url, params: { mostra: "mostra-internacional" }
assert_response :success
props = inertia_props
@@ -305,7 +305,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
end
test "filters by mostra - documentarios" do
- get program_url, params: { mostrasFilter: "documentarios" }
+ get program_url, params: { mostra: "documentarios" }
assert_response :success
props = inertia_props
@@ -319,7 +319,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
end
test "handles invalid mostra filter gracefully" do
- get program_url, params: { mostrasFilter: "non-existent-mostra" }
+ get program_url, params: { mostra: "non-existent-mostra" }
assert_response :success
props = inertia_props
@@ -330,14 +330,14 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
# selectedFilters should be empty
selected_filters = props["current_filters"]
- assert_nil selected_filters["mostrasFilter"]
+ assert_nil selected_filters["mostra"]
end
# COMBINED FILTERS TESTS
test "combines search query and mostra filter" do
get program_url, params: {
query: "Cidade",
- mostrasFilter: "competicao-nacional"
+ mostra: "competicao-nacional"
}
assert_response :success
@@ -349,13 +349,13 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
# Verify both filters are preserved
assert_equal "Cidade", props["current_filters"]["query"]["filter_value"]
- assert_equal "competicao-nacional", props["current_filters"]["mostrasFilter"]["permalink_pt"]
+ assert_equal "competicao-nacional", props["current_filters"]["mostra"]["permalink_pt"]
end
test "combines search query and mostra filter with no results" do
get program_url, params: {
query: "Paris",
- mostrasFilter: "competicao-nacional"
+ mostra: "competicao-nacional"
}
assert_response :success
@@ -367,7 +367,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
# Filters should still be preserved
assert_equal "Paris", props["current_filters"]["query"]["filter_value"]
- assert_equal "competicao-nacional", props["current_filters"]["mostrasFilter"]["permalink_pt"]
+ assert_equal "competicao-nacional", props["current_filters"]["mostra"]["permalink_pt"]
end
test "search finds movies across different mostras" do
@@ -386,7 +386,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
test "mostra filter affects available dates" do
# Documentarios only has content on 2024-10-05, 2024-10-06 and 2024-10-07
- get program_url, params: { mostrasFilter: "documentarios" }
+ get program_url, params: { mostra: "documentarios" }
assert_response :success
props = inertia_props
@@ -398,7 +398,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
test "preserves mostra filter when navigating dates" do
get program_url, params: {
- mostrasFilter: "competicao-nacional",
+ mostra: "competicao-nacional",
date: "2024-10-06"
}
@@ -411,13 +411,13 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
assert_equal "Cidade Perdida", elements.first["titulo"]
# Filter should be preserved
- assert_equal "competicao-nacional", props["current_filters"]["mostrasFilter"]["permalink_pt"]
+ assert_equal "competicao-nacional", props["current_filters"]["mostra"]["permalink_pt"]
end
test "combines all filters - search, mostra, and date" do
get program_url, params: {
query: "Cidade",
- mostrasFilter: "documentarios",
+ mostra: "documentarios",
date: "2024-10-07"
}
@@ -430,13 +430,13 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
# All filters should be preserved
assert_equal "Cidade", props["current_filters"]["query"]["filter_value"]
- assert_equal "documentarios", props["current_filters"]["mostrasFilter"]["permalink_pt"]
+ assert_equal "documentarios", props["current_filters"]["mostra"]["permalink_pt"]
assert_includes props["menuTabs"].map { _1["date"] }, "2024-10-07"
end
test "mostra filter with pagination" do
get program_url, params: {
- mostrasFilter: "competicao-nacional"
+ mostra: "competicao-nacional"
}
assert_response :success
@@ -455,13 +455,13 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
assert_equal 1, pagy["page"]
end
- test "returns correct mostrasFilter options in props" do
+ test "returns correct mostras options in props" do
get program_url
assert_response :success
props = inertia_props
- mostras_filter = props["mostrasFilter"]
+ mostras_filter = props["mostras"]
assert_equal 4, mostras_filter.length
# Check that all mostras are included
@@ -481,13 +481,13 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
get program_url
assert_response :success
props = inertia_props
- cinema_options = props["cinemasFilter"]
+ cinema_options = props["cinemas"]
assert_equal 2, cinema_options.length
end
test "filters by cinema - cine brasilia" do
cine_brasilia = cinemas(:cine_brasilia)
- get program_url, params: { cinemasFilter: cine_brasilia.id }
+ get program_url, params: { cinema: cine_brasilia.id }
assert_response :success
props = inertia_props
@@ -501,7 +501,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
end
test "filters by cinema - cinepolis" do
cinepolis = cinemas(:cinepolis)
- get program_url, params: { cinemasFilter: cinepolis.id }
+ get program_url, params: { cinema: cinepolis.id }
assert_response :success
props = inertia_props
@@ -515,7 +515,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
end
test "handles invalid cinema filter gracefully" do
- get program_url, params: { cinemasFilter: 999_999 }
+ get program_url, params: { cinema: 999_999 }
assert_response :success
props = inertia_props
@@ -524,14 +524,14 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
assert props["elements"].length > 0
selected_filters = props["current_filters"]
- assert_nil selected_filters["cinemasFilter"]
+ assert_nil selected_filters["cinema"]
end
test "combines search query and cinema filter" do
cinepolis = cinemas(:cinepolis)
get program_url, params: {
query: "Batman",
- cinemasFilter: cinepolis.id
+ cinema: cinepolis.id
}
assert_response :success
@@ -543,14 +543,14 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
# Filters preserved
assert_equal "Batman", props["current_filters"]["query"]["filter_value"]
- assert_equal cinepolis.id, props["current_filters"]["cinemasFilter"]["id"]
+ assert_equal cinepolis.id, props["current_filters"]["cinema"]["id"]
end
test "combines search query and cinema filter with no results" do
cine_brasilia = cinemas(:cine_brasilia)
get program_url, params: {
query: "Batman",
- cinemasFilter: cine_brasilia.id
+ cinema: cine_brasilia.id
}
assert_response :success
@@ -560,7 +560,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
# Filters preserved
assert_equal "Batman", props["current_filters"]["query"]["filter_value"]
- assert_equal cine_brasilia.id, props["current_filters"]["cinemasFilter"]["id"]
+ assert_equal cine_brasilia.id, props["current_filters"]["cinema"]["id"]
end
test "search finds movies across different cinemas" do
@@ -578,7 +578,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
test "cinema filter affects available dates" do
cine_brasilia = cinemas(:cine_brasilia)
- get program_url, params: { cinemasFilter: cine_brasilia.id }
+ get program_url, params: { cinema: cine_brasilia.id }
assert_response :success
props = inertia_props
@@ -590,7 +590,7 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
test "preserves cinema filter when navigating dates" do
cinepolis = cinemas(:cinepolis)
get program_url, params: {
- cinemasFilter: cinepolis.id,
+ cinema: cinepolis.id,
date: "2024-10-06"
}
@@ -602,14 +602,14 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
assert_includes titles, "Amazônia Selvagem"
assert_includes titles, "Matrix"
- assert_equal cinepolis.id, props["current_filters"]["cinemasFilter"]["id"]
+ assert_equal cinepolis.id, props["current_filters"]["cinema"]["id"]
end
test "combines all filters - search, cinema, and date" do
cinepolis = cinemas(:cinepolis)
get program_url, params: {
query: "Cidade",
- cinemasFilter: cinepolis.id,
+ cinema: cinepolis.id,
date: "2024-10-07"
}
@@ -621,13 +621,13 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
assert_equal "Cidade em Transformação", elements.first["titulo"]
assert_equal "Cidade", props["current_filters"]["query"]["filter_value"]
- assert_equal cinepolis.id, props["current_filters"]["cinemasFilter"]["id"]
+ assert_equal cinepolis.id, props["current_filters"]["cinema"]["id"]
assert_includes props["menuTabs"].map { _1["date"] }, "2024-10-07"
end
test "cinema filter with pagination" do
cine_brasilia = cinemas(:cine_brasilia)
- get program_url, params: { cinemasFilter: cine_brasilia.id }
+ get program_url, params: { cinema: cine_brasilia.id }
assert_response :success
props = inertia_props
@@ -637,13 +637,13 @@ class ProgramsControllerTest < ActionDispatch::IntegrationTest
assert_equal 17, props["pagy"]["count"]
end
- test "returns correct cinemasFilter options in props" do
+ test "returns correct cinemas options in props" do
get program_url
assert_response :success
props = inertia_props
- cinemas_filter = props["cinemasFilter"]
+ cinemas_filter = props["cinemas"]
assert_equal 2, cinemas_filter.length
names = cinemas_filter.map { _1["nome"] }
diff --git a/test/fixtures/peliculas.yml b/test/fixtures/peliculas.yml
index aa89741..50a404c 100644
--- a/test/fixtures/peliculas.yml
+++ b/test/fixtures/peliculas.yml
@@ -96,7 +96,7 @@ cidade_perdida:
imagem: "cidade_perdida.jpg"
duracao_coord_int: 120
diretor_coord_int: "João Silva"
- elenco_coord_int: "Maria Santos, Pedro Costa"
+ elenco_coord_int: "Maria Santos , Pedro Costa "
paiscompleto_coord_int: "Brasil"
sinopse_port_export: "Drama sobre uma cidade em transformação."
ativo: 1
@@ -156,7 +156,7 @@ paris_stories:
imagem: "paris_stories.jpg"
duracao_coord_int: 105
diretor_coord_int: "Pierre Dubois"
- elenco_coord_int: "Jean Martin, Sophie Laurent"
+ elenco_coord_int: "Jean Martin, Sophie Laurent, François Truffaut, Jean-Luc Godard"
paiscompleto_coord_int: "França"
sinopse_port_export: "Comédia francesa sobre a vida parisiense."
ativo: 1
@@ -176,7 +176,7 @@ amazonia_selvagem:
imagem: "amazonia.jpg"
duracao_coord_int: 85
diretor_coord_int: "Roberto Oliveira"
- elenco_coord_int: "Documentário"
+ elenco_coord_int: "Ailton Krenak"
paiscompleto_coord_int: "Brasil"
sinopse_port_export: "Documentário sobre a fauna amazônica."
ativo: 1
@@ -196,7 +196,7 @@ cidade_transformacao:
imagem: "cidade_transformacao.jpg"
duracao_coord_int: 75
diretor_coord_int: "Marina Costa"
- elenco_coord_int: "Documentário"
+ elenco_coord_int: ""
paiscompleto_coord_int: "Brasil"
sinopse_port_export: "Documentário sobre urbanização."
ativo: 1
diff --git a/test/models/peliculas_test.rb b/test/models/peliculas_test.rb
new file mode 100644
index 0000000..a47c758
--- /dev/null
+++ b/test/models/peliculas_test.rb
@@ -0,0 +1,163 @@
+require "test_helper"
+
+class PeliculaTest < ActiveSupport::TestCase
+ test "actor_to_pelicula_mapping returns hash with actor names as keys" do
+ pelicula = peliculas(:batman)
+
+ mapping = Pelicula.actor_to_pelicula_mapping(pelicula.edicao_id)
+
+ assert_instance_of Hash, mapping
+
+ assert_includes mapping.keys, "Christian Bale"
+ assert_includes mapping.keys, "Michael Caine"
+
+ assert_instance_of Array, mapping["Christian Bale"]
+ assert_instance_of Array, mapping["Michael Caine"]
+ end
+
+ test "actor_to_pelicula_mapping handles comma-separated actors correctly" do
+ pelicula = peliculas(:matrix)
+
+ mapping = Pelicula.actor_to_pelicula_mapping(pelicula.edicao_id)
+
+ assert_includes mapping.keys, "Keanu Reeves"
+ assert_includes mapping.keys, "Laurence Fishburne"
+
+ assert_includes mapping["Keanu Reeves"], pelicula.id
+ assert_includes mapping["Laurence Fishburne"], pelicula.id
+ end
+
+ test "actor_to_pelicula_mapping stores both exact and lowercase versions" do
+ pelicula = peliculas(:batman)
+
+ mapping = Pelicula.actor_to_pelicula_mapping(pelicula.edicao_id)
+
+ assert_includes mapping.keys, "Christian Bale"
+ assert_includes mapping.keys, "christian bale"
+
+ assert_includes mapping["Christian Bale"], pelicula.id
+ assert_includes mapping["christian bale"], pelicula.id
+
+ assert_equal mapping["Christian Bale"], mapping["christian bale"]
+ end
+
+ test "actor_to_pelicula_mapping removes duplicate pelicula IDs" do
+ pelicula1 = peliculas(:matrix)
+ pelicula2 = peliculas(:test_0)
+
+ # Ensure both have the same actor and same edicao_id
+ assert_equal pelicula1.edicao_id, pelicula2.edicao_id
+
+ mapping = Pelicula.actor_to_pelicula_mapping(pelicula1.edicao_id)
+
+ # Should have both pelicula IDs for Christian Bale
+ keanu_reeves_movies = mapping["Keanu Reeves"]
+ assert_includes keanu_reeves_movies, pelicula1.id
+ assert_includes keanu_reeves_movies, pelicula2.id
+
+ # Should not have duplicates (uniq was called)
+ assert_equal keanu_reeves_movies.length, keanu_reeves_movies.uniq.length
+ end
+
+ test "actor_to_pelicula_mapping ignores blank/nil elenco_coord_int" do
+ pelicula_with_nil = peliculas(:cidade_transformacao)
+ pelicula_with_cast = peliculas(:batman)
+
+ edicao_id = pelicula_with_cast.edicao_id
+
+ mapping = Pelicula.actor_to_pelicula_mapping(edicao_id)
+
+ all_pelicula_ids = mapping.values.flatten.uniq
+
+ refute_includes all_pelicula_ids, pelicula_with_nil.id
+ assert_includes all_pelicula_ids, pelicula_with_cast.id
+ end
+
+ test "actor_to_pelicula_mapping handles actors with extra spaces" do
+ pelicula = peliculas(:cidade_perdida)
+
+ mapping = Pelicula.actor_to_pelicula_mapping(pelicula.edicao_id)
+
+ assert_includes mapping.keys, "Pedro Costa"
+ assert_includes mapping.keys, "Maria Santos"
+
+ refute_includes mapping.keys, " Pedro Costa"
+ refute_includes mapping.keys, "Maria Santos "
+ refute_includes mapping.keys, " Pedro Costa "
+
+ assert_includes mapping["Pedro Costa"], pelicula.id
+ assert_includes mapping["Maria Santos"], pelicula.id
+ end
+
+ test "with_actor finds peliculas by exact actor name" do
+ pelicula = peliculas(:batman)
+ edicao_id = pelicula.edicao_id
+
+ results = Pelicula.with_actor("Christian Bale", edicao_id)
+
+ assert_includes results.ids, pelicula.id
+
+ results.each do |p|
+ assert_equal edicao_id, p.edicao_id
+ end
+ end
+
+ test "with_actor returns empty when actor not found" do
+ edicao_id = peliculas(:batman).edicao_id
+
+ results = Pelicula.with_actor("Non Existent Actor", edicao_id)
+
+ assert_empty results
+ assert_equal 0, results.count
+ end
+
+ test "with_actor works with actors containing special characters" do
+ # Test with actors that have accents, hyphens, etc.
+ pelicula = peliculas(:paris_stories) # Assuming elenco_coord_int: "François Truffaut, Jean-Luc Godard"
+ edicao_id = pelicula.edicao_id
+
+ # Search for actors with special characters
+ results_accent = Pelicula.with_actor("François Truffaut", edicao_id)
+ results_hyphen = Pelicula.with_actor("Jean-Luc Godard", edicao_id)
+
+ # Should find the peliculas
+ assert_includes results_accent.ids, pelicula.id
+ assert_includes results_hyphen.ids, pelicula.id
+
+ # Should handle the special characters correctly
+ refute_empty results_accent
+ refute_empty results_hyphen
+ end
+
+ test "caching works - second call doesn't hit database" do
+ Rails.cache.clear
+ original_cache = Rails.cache
+ Rails.cache = ActiveSupport::Cache::MemoryStore.new
+ pelicula = peliculas(:batman)
+ edicao_id = pelicula.edicao_id
+
+ # Clear cache to ensure clean test
+ Rails.cache.delete("actor-pelicula-mapping-#{edicao_id}")
+
+ # First call should hit database
+ assert_queries_count(1) do
+ first_mapping = Pelicula.actor_to_pelicula_mapping(edicao_id)
+ end
+
+ # Second call should use cache (no database queries)
+ assert_no_queries do
+ second_mapping = Pelicula.actor_to_pelicula_mapping(edicao_id)
+ end
+
+ # Results should be identical
+ first_mapping = Pelicula.actor_to_pelicula_mapping(edicao_id)
+ second_mapping = Pelicula.actor_to_pelicula_mapping(edicao_id)
+
+ assert_equal first_mapping, second_mapping
+
+ # Verify cache key exists
+ assert Rails.cache.exist?("actor-pelicula-mapping-#{edicao_id}")
+
+ Rails.cache = original_cache
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 9f2d5a5..7966786 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -5,7 +5,7 @@ require "rails/test_help"
module ActiveSupport
class TestCase
# Run tests in parallel with specified workers
- parallelize(workers: :number_of_processors)
+ parallelize(workers: 1)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment