Skip to content

Instantly share code, notes, and snippets.

@dedemenezes
Created September 15, 2025 13:28
Show Gist options
  • Select an option

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

Select an option

Save dedemenezes/5abecacf986db67d8cd19f37bb2a065a to your computer and use it in GitHub Desktop.
diff --git a/app/controllers/noticias_controller.rb b/app/controllers/noticias_controller.rb
index 0de505c..5bd0bb9 100644
--- a/app/controllers/noticias_controller.rb
+++ b/app/controllers/noticias_controller.rb
@@ -4,39 +4,55 @@ class NoticiasController < ApplicationController
include InfiniteScrollable
# TODO: Breakdown into smaller,
# more readable methods
+ before_action :set_filter_options, only: :index
+
def index
scope = Noticia.includes(:caderno).published
+ selected_filters = {}
- if params[:search].present?
- term = "%#{params[:search].downcase}%"
- scope = scope.where(
- "LOWER(titulo) LIKE ? OR LOWER(chamada) LIKE ?", term, term
- )
+ # if params[:search].present?
+ # term = "%#{params[:search].downcase}%"
+ # scope = scope.where(
+ # "LOWER(titulo) LIKE ? OR LOWER(chamada) LIKE ?", term, term
+ # )
+ # end
+
+ if params[:data]
+ selected_date = { filter_display: params[:data], filter_value: params[:data], filter_label: I18n.t("filter.date") }
+ # Noticia.where(data: (Date.parse("2025-06-19")..Date.today))
+ date_range = (Date.parse(selected_date[:filter_value])..Date.today)
+ scope = scope.where(data: date_range)
end
- if params[:cadernos].present?
- scope = scope.where(caderno: { permalink_pt: params[:cadernos] })
+ if params[:caderno].present?
+ selected_caderno = @cadernos.find { |c| c["filter_value"] == params[:caderno] }
+ selected_filters[:caderno] = selected_caderno if selected_caderno
+ if I18n.locale == :pt
+ scope = scope.where(caderno: { permalink_pt: params[:caderno] })
+ else
+ scope = scope.where(caderno: { permalink_en: params[:caderno] })
+ end
end
+ current_page = params[:page].to_i ||= 1
+
ordered = scope.order(created: :desc)
- @pagy, @noticias = pagy_infinite(ordered, params[:page])
+ @pagy, @noticias = pagy_infinite(ordered, current_page)
- cadernos = Caderno.for_filters
# Only set selectedCadernos if the param exists
- selected_filters = {}
- if params[:cadernos].present?
- selected_caderno = cadernos.find { |c| c["permalink_pt"] == params[:cadernos] }
- selected_filters[:cadernos] = selected_caderno if selected_caderno
- end
+
+ # tabBaseUrl must be defined with path + selected filters
render inertia: "Noticias/Index", props: {
rootUrl: @root_url,
- cadernos: cadernos,
+ tabBaseUrl: noticias_url,
+ dataLabel: I18n.t("filter.date"),
+ cadernos: @cadernos,
breadcrumbs: breadcrumbs(
[ "", @root_url ],
[ "Notícias", "" ],
),
- noticias: @noticias.as_json(
+ elements: @noticias.as_json(
only: %i[id titulo permalink chamada imagem],
methods: [ :caderno_nome, :display_date ]
),
@@ -45,7 +61,10 @@ class NoticiasController < ApplicationController
pages: @pagy.pages,
last: @pagy.last
},
- selectedFilters: selected_filters
+ current_filters: {
+ data: selected_date,
+ caderno: selected_caderno
+ }
}
end
@@ -63,4 +82,15 @@ class NoticiasController < ApplicationController
)
}
end
+
+ private
+
+ def set_filter_options
+ # cadernos = Caderno.for_filters
+ if I18n.locale == :pt
+ @cadernos = Caderno.collection_without_edition_for(:permalink_pt, :nome_pt, :caderno)
+ else
+ @cadernos = Caderno.collection_without_edition_for(:permalink_en, :nome_en, :caderno)
+ end
+ end
end
diff --git a/app/frontend/components/features/filters/NoticiasFilterForm.vue b/app/frontend/components/features/filters/NoticiasFilterForm.vue
index 6309089..b1ef3e4 100644
--- a/app/frontend/components/features/filters/NoticiasFilterForm.vue
+++ b/app/frontend/components/features/filters/NoticiasFilterForm.vue
@@ -1,39 +1,61 @@
<script setup>
import { computed, defineAsyncComponent } from "vue";
import AccordionGroup from "@/components/AccordionGroup.vue";
+import DatePickerComponent from "@/components/ui/DatePickerComponent.vue";
const ComboboxComponent = defineAsyncComponent(() => import('@/components/ui/ComboboxComponent.vue'))
const props = defineProps({
modelValue: { type: Object, required: true },
updateField: { type: Function, required: true },
+ dataLabel: String,
cadernos: { type: Array, default: () => [] }, // Article-specific prop
});
// Transform cadernos prop for ComboboxComponent format
const cadernosOptions = computed(() => {
return props.cadernos.map(caderno => ({
- label: caderno.nome_pt,
- value: caderno.permalink_pt,
+ label: caderno.filter_display,
+ value: caderno.filter_value,
}));
});
+const cadernoLabel = computed(() => props.cadernos[0].filter_label)
-const getCadernoObjectFromPermalink = (permalink) => {
- return props.cadernos.find(c => c.permalink_pt === permalink) || null;
+const getCadernoObjectFromPermalink = (inputValue) => {
+ return props.cadernos.find(c => c.filter_value === inputValue) || null;
};
+
+const getDateFromInput = (value) => {
+ // debugger
+ return { "filter_display": value, "filter_value": value, filter_label: props.dataLabel } || null;
+}
</script>
<template>
+ <AccordionGroup
+ :text="props.dataLabel"
+ :isOpen="!!props.modelValue.data?.filter_value"
+ >
+ <template v-slot:content>
+ <div class="w-full">
+ <DatePickerComponent
+ :model-value="props.modelValue.data?.filter_value"
+ placeholder="Pick session date"
+ @update:model-value="updateField('data', getDateFromInput($event))"
+ />
+ </div>
+ </template>
+ </AccordionGroup>
<!-- Article-specific filter content -->
<AccordionGroup
- text="Cadernos"
- :isOpen="!!props.modelValue.cadernos"
+ :text="cadernoLabel"
+ :isOpen="!!props.modelValue.caderno"
>
<template v-slot:content>
<div class="pt-400 overflow-hidden w-full">
<ComboboxComponent
:collection="cadernosOptions"
- :modelValue="props.modelValue.cadernos?.permalink_pt || null"
- @update:modelValue="(val) => props.updateField('cadernos', getCadernoObjectFromPermalink(val))"
+ :modelValue="props.modelValue.caderno?.filter_value || null"
+ @update:modelValue="(val) => props.updateField('caderno', getCadernoObjectFromPermalink(val))"
/>
</div>
</template>
diff --git a/app/frontend/components/features/filters/composables/usaSearchFilter.js b/app/frontend/components/features/filters/composables/usaSearchFilter.js
new file mode 100644
index 0000000..c51cbeb
--- /dev/null
+++ b/app/frontend/components/features/filters/composables/usaSearchFilter.js
@@ -0,0 +1,147 @@
+import { ref, watch } from "vue";
+import { router } from "@inertiajs/vue3"
+import { extractFilterValues } from "@/lib/filterUtils";
+
+/**
+ * FILTER STATE MANAGEMENT - SINGLE SOURCE OF TRUTH
+ */
+export function useSearchFilter(props, filtersFromController = {}) {
+ /**
+ * Initialize empty filter structure
+ */
+ console.log(filtersFromController);
+
+ const initializeFilters = () => (filtersFromController)
+
+ /**
+ * Override empty filters with current filter values from controller
+ */
+ const overrideFiltersValues = () => {
+ return { ...initializeFilters(), ...props.current_filters }
+ }
+
+ // Props that will change when updating filters
+ const propsToUpdate = ['elements', 'pagy', 'current_filters', 'has_active_filters', 'menuTabs']
+
+ // Main filter state - this is passed to SearchFilter via ResponsiveFilterMenu
+ const filters = ref(overrideFiltersValues())
+
+ // Watch for prop changes from server (shouldn't happen often but good to have)
+ watch(() => props.current_filters, (newFilters) => {
+ console.log('ProgramPage: current_filters changed from server:', newFilters);
+ filters.value = overrideFiltersValues()
+ }, { immediate: true, deep: true })
+
+ // ============================================================================
+ // FILTER OPERATIONS - CALLED BY SEARCHFILTER VIA EVENTS
+ // ============================================================================
+
+ /**
+ * Called when SearchFilter emits filtersApplied
+ * This makes the actual router call to update the page
+ */
+
+ const filterSearch = (filtersFromSearchFilter) => {
+ console.log('ProgramPage: Applying filters from SearchFilter:', filtersFromSearchFilter);
+
+ // Build query params by rejecting any filter: null or ""
+ const cleanedFilters = extractFilterValues(filtersFromSearchFilter || filters.value)
+ debugger
+ // MAke search request and says which prop to update
+ router.get(props.tabBaseUrl, cleanedFilters, {
+ preserveScroll: true,
+ only: propsToUpdate
+ })
+ };
+
+ /**
+ * Called when user clicks a filter tag to remove it
+ * This updates the filters state and makes a router call
+ */
+
+ const removeQuery = (filterToRemove) => {
+ debugger
+ // Clear the specific filter
+ // TODO: REFAC TIP ADD FILTER_KEY FROM CONTROLLER
+ // IT SHOULD MAKE AGNOSTIC
+ // Map filter labels to filter keys (could be improved with a filter_key from controller)
+ const filterKeyMap = {
+ 'Time': 'sessao',
+ 'Sessão': 'sessao',
+ 'Showcase': 'mostra',
+ 'Mostra': 'mostra',
+ 'Cinema': 'cinema',
+ 'Genre': 'genero',
+ 'Genero': 'genero',
+ 'Country': 'pais',
+ 'País': 'pais',
+ 'Director': 'direcao',
+ 'Direção': 'direcao',
+ 'Cast': 'elenco',
+ 'Elenco': 'elenco',
+ "Caderno": "caderno",
+ "Category": "caderno",
+ "Data de publicação": "data",
+ "Publication date": "data"
+ };
+ const filterKey = filterKeyMap[filterToRemove.filter_label];
+ if (filterKey) {
+ // Updated local filter state
+ filters.value[filterKey] = null;
+
+ // Build new URL params from remaining filters
+ const newParams = new URLSearchParams();
+ Object.entries(filters.value).forEach(([key, value]) => {
+ if (value !== null && value !== undefined && value !== "" && value?.filter_value) {
+ newParams.set(key, value.filter_value);
+ }
+ });
+
+ router.get(props.tabBaseUrl, newParams, {
+ preserveScroll: true,
+ only: propsToUpdate
+ })
+ } else {
+ console.warn('ProgramPage: Unknown filter label:', filterToRemove.filter_label);
+ }
+ }
+
+ /**
+ * Called when SearchFilter emits filtersCleared
+ * This clears all filters and optionally makes a router call
+ */
+ const clearSearchQuery = () => {
+ console.log('ProgramPage: Clearing all filters');
+
+ const clearedFilters = initializeFilters()
+ filters.value = clearedFilters
+ // Only make router call if there were actually filters applied
+ const hasFiltersApplied = Object.entries(props.current_filters).some(([key, value]) => value != null)
+ if (hasFiltersApplied) {
+ router.get(props.tabBaseUrl, {}, {
+ preserveState: true,
+ preserveScroll: true,
+ only: propsToUpdate
+ });
+ }
+ };
+
+ /**
+ * Called when user clears search bar directly (if needed)
+ */
+ const handleClear = () => {
+ console.log('ProgramPage: Clearing search query');
+ filters.value.query = null;
+ filterSearch(filters.value);
+ };
+
+ return {
+ initializeFilters,
+ overrideFiltersValues,
+ filters,
+ filterSearch,
+ removeQuery,
+ clearSearchQuery,
+ handleClear
+ }
+}
diff --git a/app/frontend/components/ui/DatePickerComponent.vue b/app/frontend/components/ui/DatePickerComponent.vue
new file mode 100644
index 0000000..5bb32b3
--- /dev/null
+++ b/app/frontend/components/ui/DatePickerComponent.vue
@@ -0,0 +1,69 @@
+<script setup>
+import {
+ DateFormatter,
+ getLocalTimeZone,
+ parseDate,
+} from "@internationalized/date"
+import { CalendarIcon } from "lucide-vue-next"
+
+import { ref, watch } from "vue"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Calendar } from "@/components/ui/calendar"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+
+const props = defineProps({
+ modelValue: String, // ISO format date, e.g., "2025-10-09"
+})
+const emit = defineEmits(["update:modelValue"])
+
+// Create local DateValue from string
+const value = ref(props.modelValue ? parseDate(props.modelValue) : null)
+
+// Sync from external modelValue -> internal DateValue
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ value.value = newVal ? parseDate(newVal) : null
+ }
+)
+
+// Emit ISO string when user picks a date
+watch(value, (newVal) => {
+ if (newVal) {
+ const iso = newVal.toDate(getLocalTimeZone()).toISOString().split("T")[0]
+ emit("update:modelValue", iso)
+ } else {
+ emit("update:modelValue", null)
+ }
+})
+
+// For display in the button
+const df = new DateFormatter("en-US", {
+ dateStyle: "long",
+})
+</script>
+
+<template>
+ <Popover>
+ <PopoverTrigger as-child>
+ <Button
+ variant="outline"
+ :class="cn(
+ 'w-full justify-start text-left font-normal',
+ !value && 'text-muted-foreground',
+ )"
+ >
+ <CalendarIcon class="mr-2 h-4 w-4" />
+ {{
+ value
+ ? df.format(value.toDate(getLocalTimeZone()))
+ : "Pick a date"
+ }}
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent class="w-auto p-0">
+ <Calendar v-model="value" initial-focus />
+ </PopoverContent>
+ </Popover>
+</template>
diff --git a/app/frontend/components/ui/calendar/Calendar.vue b/app/frontend/components/ui/calendar/Calendar.vue
new file mode 100644
index 0000000..9b92333
--- /dev/null
+++ b/app/frontend/components/ui/calendar/Calendar.vue
@@ -0,0 +1,98 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarRoot, useForwardPropsEmits } from "reka-ui";
+import { cn } from "@/lib/utils";
+import {
+ CalendarCell,
+ CalendarCellTrigger,
+ CalendarGrid,
+ CalendarGridBody,
+ CalendarGridHead,
+ CalendarGridRow,
+ CalendarHeadCell,
+ CalendarHeader,
+ CalendarHeading,
+ CalendarNextButton,
+ CalendarPrevButton,
+} from ".";
+
+const props = defineProps({
+ defaultValue: { type: null, required: false },
+ defaultPlaceholder: { type: null, required: false },
+ placeholder: { type: null, required: false },
+ pagedNavigation: { type: Boolean, required: false },
+ preventDeselect: { type: Boolean, required: false },
+ weekStartsOn: { type: Number, required: false },
+ weekdayFormat: { type: String, required: false },
+ calendarLabel: { type: String, required: false },
+ fixedWeeks: { type: Boolean, required: false },
+ maxValue: { type: null, required: false },
+ minValue: { type: null, required: false },
+ locale: { type: String, required: false },
+ numberOfMonths: { type: Number, required: false },
+ disabled: { type: Boolean, required: false },
+ readonly: { type: Boolean, required: false },
+ initialFocus: { type: Boolean, required: false },
+ isDateDisabled: { type: Function, required: false },
+ isDateUnavailable: { type: Function, required: false },
+ dir: { type: String, required: false },
+ nextPage: { type: Function, required: false },
+ prevPage: { type: Function, required: false },
+ modelValue: { type: null, required: false },
+ multiple: { type: Boolean, required: false },
+ disableDaysOutsideCurrentView: { type: Boolean, required: false },
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+const emits = defineEmits(["update:modelValue", "update:placeholder"]);
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwarded = useForwardPropsEmits(delegatedProps, emits);
+</script>
+
+<template>
+ <CalendarRoot
+ v-slot="{ grid, weekDays }"
+ data-slot="calendar"
+ :class="cn('p-3', props.class)"
+ v-bind="forwarded"
+ >
+ <CalendarHeader>
+ <CalendarHeading />
+
+ <div class="flex items-center gap-1">
+ <CalendarPrevButton />
+ <CalendarNextButton />
+ </div>
+ </CalendarHeader>
+
+ <div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
+ <CalendarGrid v-for="month in grid" :key="month.value.toString()">
+ <CalendarGridHead>
+ <CalendarGridRow>
+ <CalendarHeadCell v-for="day in weekDays" :key="day">
+ {{ day }}
+ </CalendarHeadCell>
+ </CalendarGridRow>
+ </CalendarGridHead>
+ <CalendarGridBody>
+ <CalendarGridRow
+ v-for="(weekDates, index) in month.rows"
+ :key="`weekDate-${index}`"
+ class="mt-2 w-full"
+ >
+ <CalendarCell
+ v-for="weekDate in weekDates"
+ :key="weekDate.toString()"
+ :date="weekDate"
+ >
+ <CalendarCellTrigger :day="weekDate" :month="month.value" />
+ </CalendarCell>
+ </CalendarGridRow>
+ </CalendarGridBody>
+ </CalendarGrid>
+ </div>
+ </CalendarRoot>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarCell.vue b/app/frontend/components/ui/calendar/CalendarCell.vue
new file mode 100644
index 0000000..7a720dd
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarCell.vue
@@ -0,0 +1,31 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarCell, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+
+const props = defineProps({
+ date: { type: null, required: true },
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarCell
+ data-slot="calendar-cell"
+ :class="
+ cn(
+ 'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent',
+ props.class,
+ )
+ "
+ v-bind="forwardedProps"
+ >
+ <slot />
+ </CalendarCell>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarCellTrigger.vue b/app/frontend/components/ui/calendar/CalendarCellTrigger.vue
new file mode 100644
index 0000000..b8f2462
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarCellTrigger.vue
@@ -0,0 +1,43 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarCellTrigger, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from '@/components/ui/button';
+
+const props = defineProps({
+ day: { type: null, required: true },
+ month: { type: null, required: true },
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false, default: "button" },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarCellTrigger
+ data-slot="calendar-cell-trigger"
+ :class="
+ cn(
+ buttonVariants({ variant: 'ghost' }),
+ 'size-8 p-0 font-normal aria-selected:opacity-100 cursor-default',
+ '[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
+ // Selected
+ 'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
+ // Disabled
+ 'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
+ // Unavailable
+ 'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
+ // Outside months
+ 'data-[outside-view]:text-muted-foreground',
+ props.class,
+ )
+ "
+ v-bind="forwardedProps"
+ >
+ <slot />
+ </CalendarCellTrigger>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarGrid.vue b/app/frontend/components/ui/calendar/CalendarGrid.vue
new file mode 100644
index 0000000..52bef72
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarGrid.vue
@@ -0,0 +1,25 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarGrid, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+
+const props = defineProps({
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarGrid
+ data-slot="calendar-grid"
+ :class="cn('w-full border-collapse space-x-1', props.class)"
+ v-bind="forwardedProps"
+ >
+ <slot />
+ </CalendarGrid>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarGridBody.vue b/app/frontend/components/ui/calendar/CalendarGridBody.vue
new file mode 100644
index 0000000..131bc5a
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarGridBody.vue
@@ -0,0 +1,14 @@
+<script setup>
+import { CalendarGridBody } from "reka-ui";
+
+const props = defineProps({
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+});
+</script>
+
+<template>
+ <CalendarGridBody data-slot="calendar-grid-body" v-bind="props">
+ <slot />
+ </CalendarGridBody>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarGridHead.vue b/app/frontend/components/ui/calendar/CalendarGridHead.vue
new file mode 100644
index 0000000..1d8e684
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarGridHead.vue
@@ -0,0 +1,15 @@
+<script setup>
+import { CalendarGridHead } from "reka-ui";
+
+const props = defineProps({
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+</script>
+
+<template>
+ <CalendarGridHead data-slot="calendar-grid-head" v-bind="props">
+ <slot />
+ </CalendarGridHead>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarGridRow.vue b/app/frontend/components/ui/calendar/CalendarGridRow.vue
new file mode 100644
index 0000000..9ad8c5b
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarGridRow.vue
@@ -0,0 +1,25 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarGridRow, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+
+const props = defineProps({
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarGridRow
+ data-slot="calendar-grid-row"
+ :class="cn('flex', props.class)"
+ v-bind="forwardedProps"
+ >
+ <slot />
+ </CalendarGridRow>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarHeadCell.vue b/app/frontend/components/ui/calendar/CalendarHeadCell.vue
new file mode 100644
index 0000000..451bfa2
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarHeadCell.vue
@@ -0,0 +1,30 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarHeadCell, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+
+const props = defineProps({
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarHeadCell
+ data-slot="calendar-head-cell"
+ :class="
+ cn(
+ 'text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]',
+ props.class,
+ )
+ "
+ v-bind="forwardedProps"
+ >
+ <slot />
+ </CalendarHeadCell>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarHeader.vue b/app/frontend/components/ui/calendar/CalendarHeader.vue
new file mode 100644
index 0000000..8f2fd1c
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarHeader.vue
@@ -0,0 +1,27 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarHeader, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+
+const props = defineProps({
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarHeader
+ data-slot="calendar-header"
+ :class="
+ cn('flex justify-center pt-1 relative items-center w-full', props.class)
+ "
+ v-bind="forwardedProps"
+ >
+ <slot />
+ </CalendarHeader>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarHeading.vue b/app/frontend/components/ui/calendar/CalendarHeading.vue
new file mode 100644
index 0000000..7e6ef9a
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarHeading.vue
@@ -0,0 +1,30 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { CalendarHeading, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+
+const props = defineProps({
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+defineSlots();
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarHeading
+ v-slot="{ headingValue }"
+ data-slot="calendar-heading"
+ :class="cn('text-sm font-medium', props.class)"
+ v-bind="forwardedProps"
+ >
+ <slot :heading-value>
+ {{ headingValue }}
+ </slot>
+ </CalendarHeading>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarNextButton.vue b/app/frontend/components/ui/calendar/CalendarNextButton.vue
new file mode 100644
index 0000000..fa6d04e
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarNextButton.vue
@@ -0,0 +1,37 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { ChevronRight } from "lucide-vue-next";
+import { CalendarNext, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from '@/components/ui/button';
+
+const props = defineProps({
+ nextPage: { type: Function, required: false },
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarNext
+ data-slot="calendar-next-button"
+ :class="
+ cn(
+ buttonVariants({ variant: 'outline' }),
+ 'absolute right-1',
+ 'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
+ props.class,
+ )
+ "
+ v-bind="forwardedProps"
+ >
+ <slot>
+ <ChevronRight class="size-4" />
+ </slot>
+ </CalendarNext>
+</template>
diff --git a/app/frontend/components/ui/calendar/CalendarPrevButton.vue b/app/frontend/components/ui/calendar/CalendarPrevButton.vue
new file mode 100644
index 0000000..9c115fb
--- /dev/null
+++ b/app/frontend/components/ui/calendar/CalendarPrevButton.vue
@@ -0,0 +1,37 @@
+<script setup>
+import { reactiveOmit } from "@vueuse/core";
+import { ChevronLeft } from "lucide-vue-next";
+import { CalendarPrev, useForwardProps } from "reka-ui";
+import { cn } from "@/lib/utils";
+import { buttonVariants } from '@/components/ui/button';
+
+const props = defineProps({
+ prevPage: { type: Function, required: false },
+ asChild: { type: Boolean, required: false },
+ as: { type: null, required: false },
+ class: { type: null, required: false },
+});
+
+const delegatedProps = reactiveOmit(props, "class");
+
+const forwardedProps = useForwardProps(delegatedProps);
+</script>
+
+<template>
+ <CalendarPrev
+ data-slot="calendar-prev-button"
+ :class="
+ cn(
+ buttonVariants({ variant: 'outline' }),
+ 'absolute left-1',
+ 'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
+ props.class,
+ )
+ "
+ v-bind="forwardedProps"
+ >
+ <slot>
+ <ChevronLeft class="size-4" />
+ </slot>
+ </CalendarPrev>
+</template>
diff --git a/app/frontend/components/ui/calendar/index.js b/app/frontend/components/ui/calendar/index.js
new file mode 100644
index 0000000..f5e9b7e
--- /dev/null
+++ b/app/frontend/components/ui/calendar/index.js
@@ -0,0 +1,12 @@
+export { default as Calendar } from "./Calendar.vue";
+export { default as CalendarCell } from "./CalendarCell.vue";
+export { default as CalendarCellTrigger } from "./CalendarCellTrigger.vue";
+export { default as CalendarGrid } from "./CalendarGrid.vue";
+export { default as CalendarGridBody } from "./CalendarGridBody.vue";
+export { default as CalendarGridHead } from "./CalendarGridHead.vue";
+export { default as CalendarGridRow } from "./CalendarGridRow.vue";
+export { default as CalendarHeadCell } from "./CalendarHeadCell.vue";
+export { default as CalendarHeader } from "./CalendarHeader.vue";
+export { default as CalendarHeading } from "./CalendarHeading.vue";
+export { default as CalendarNextButton } from "./CalendarNextButton.vue";
+export { default as CalendarPrevButton } from "./CalendarPrevButton.vue";
diff --git a/app/frontend/pages/Noticias/Index.vue b/app/frontend/pages/Noticias/Index.vue
index 959e3a0..184abab 100644
--- a/app/frontend/pages/Noticias/Index.vue
+++ b/app/frontend/pages/Noticias/Index.vue
@@ -4,9 +4,8 @@
// 1. 📦 Node.js built-ins (if used)
// 2. 🔌 External packages (npm, libraries)
-import { Head, router } from "@inertiajs/vue3";
+import { Head } from "@inertiajs/vue3";
// 3. 🧠 Internal libs/helpers/utilities
-import { applyFiltersToQuery } from "@/lib/applyFiltersToQuery";
// 4. 🧩 Global components / shared UI
import TwContainer from "@/components/layout/TwContainer.vue";
@@ -15,156 +14,111 @@ import TagFilter from "@/components/common/tags/TagFilter.vue";
// 5. 🧱 Feature-specific components
import MobileTrigger from "@/components/features/filters/MobileTrigger.vue";
-import MobileFilterMenu from "@/components/features/filters/MobileFilterMenu.vue";
+import ResponsiveFilterMenu from "@/components/features/filters/ResponsiveFilterMenu.vue";
import NoticiasFilterForm from "@/components/features/filters/NoticiasFilterForm.vue";
-import PagyPagination from "../PagyPagination.vue"; // relative path (usually for siblings only)
+import InfiniteScrollLayout from "@/components/layout/InfiniteScrollLayout.vue";
+import IndexArticleCard from "@/components/common/cards/IndexArticleCard.vue";
import { useMobileTrigger } from "@/components/features/filters/composables/useMobileTrigger";
+import { useSearchFilter } from "@/components/features/filters/composables/usaSearchFilter";
const props = defineProps({
breadcrumbs: { type: Array, default: () => []},
- noticias: { type: Array, default: () => []},
+ tabBaseUrl: { type: String, default: "MISSING" },
+ dataLabel: { type: String, required: true },
+ elements: { type: Array, default: () => []},
cadernos: { type: Array, required: true },
- selectedFilters: { type: Object, default: () => {} },
+ current_filters: { type: Object, default: () => {} },
pagy: { type: Object, required: true }
})
const { isFilterMenuOpen, openMenu, closeMenu } = useMobileTrigger();
+const { filters, filterSearch, removeQuery } = useSearchFilter(props)
+console.log(filters);
-const filterSearch = (filtersFromChild) => {
- // console.log("Filters from MobileFilterMenu:", filtersFromChild);
-
- const params = applyFiltersToQuery(filtersFromChild)
- const query = params.toString()
- router.visit(`/noticias?${query}`, {
- preserveScroll: true,
- preserveState: true,
- only: ["noticias", "selectedFilters"],
- });
-};
-
-const removeQuery = (item) => {
- const newFilters = { ...props.selectedFilters };
-
- for (const [key, value] of Object.entries(newFilters)) {
- if (value?.permalink_pt === item) {
- newFilters[key] = null;
- }
- }
-
- const params = applyFiltersToQuery(newFilters)
- const query = params.toString()
-
- router.visit(`/noticias?${query}`, {
- preserveScroll: true,
- preserveState: false, // full reset
- only: ["noticias", "selectedFilters"],
- });
-}
-
-const clearSearchQuery = () => {
- const currentUrl = new URL(window.location.href);
- const hasUrlParams = currentUrl.searchParams.toString() !== "";
-
- if (!hasUrlParams) {
- // console.log("Already clean - no request needed");
- return;
- }
-
- router.visit(`/noticias`, {
- preserveScroll: true,
- preserveState: true,
- only: [ "noticias", "selectedFilters" ]
- })
-}
</script>
<template>
+ <!-- DEBUGGER -->
+ <div v-if="false" class="bg-amarelo-200 p-2 mb-4 text-md text-neutrals-900">
+ <p><strong>Noticias.Index current_filters:</strong> {{ current_filters }}</p>
+ </div>
<Head>
<title>Noticias - Festival do Rio</title>
<!-- TODO: Add metatags into all pages! -->
</Head>
+ <!-- <Breadcrumb :crumbs="props.breadcrumbs"/> -->
<TwContainer>
- <Breadcrumb :crumbs="props.breadcrumbs"/>
- <!-- search & ordering -->
- <div
- class="filter flex items-center justify-between md:gap-800 lg:gap-1200 py-300"
- >
+ <Breadcrumb :crumbs="props.breadcrumbs" />
+ </TwContainer>
+ <TwContainer class="relative">
+ <div class="filter flex lg:hidden items-center justify-end py-300 bg-white">
<MobileTrigger @open-menu="openMenu" />
-
- <!-- Ordering -->
- <div class="flex items-center gap-300">
- <span class="text-body-strong-sm uppercase text-secondary-gray"
- >A - Z</span
- >
- <img
- src="@assets/icons/divisor.svg"
- alt="divisor"
- height="16px"
- width="1px"
- />
- <span class="text-body-strong-sm uppercase text-neutrals-900">
- <!-- {{$t("filter_by.date")}} -->
- por Data
- </span>
- </div>
- <!-- Ordering -->
</div>
- <!-- search & ordering -->
-
- <!-- mobile filter content -->
- <transition name="slide-left">
- <MobileFilterMenu
- :is-open="isFilterMenuOpen"
- :initialFilters="props.selectedFilters"
- @filtersApplied="filterSearch"
- @filtersCleared="clearSearchQuery"
- @close-filter-menu="closeMenu"
- >
- <template #filters="{ modelValue, updateField }">
- <!-- class="py-600" -->
- <NoticiasFilterForm
- :model-value="modelValue"
- :update-field="updateField"
- :cadernos="props.cadernos"
- />
- </template>
- </MobileFilterMenu>
- <!-- input -->
- </transition>
- <!-- mobile filter content -->
- <!-- filtered tag -->
+ <!-- MOBILE TAG FILTER -->
<div
- class="flex gap-300 pt-200 pb-300"
- v-show="Object.values(props.selectedFilters).some((item) => item !== null)"
+ 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)"
>
- <TagFilter
- v-for="(value, key) in props.selectedFilters"
- :key="key"
- :filter="{label: value.nome_pt, value: value.permalink_pt }"
- :text="value.nome_pt"
- @remove-filter="removeQuery"
- />
+ <TagFilter
+ v-for="[key, value] in Object.entries(props.current_filters).filter(([k, v]) => v !== null)"
+ :key="key"
+ :filter="value"
+ :text="value.filter_display"
+ @remove-filter="removeQuery"
+ />
</div>
- <!-- filtered tag -->
-
- <!-- list -->
<div class="grid grid-cols-12">
- <div class="col-span-12">
- <PagyPagination
- :noticias="props.noticias"
+ <div class="col-span-12 md:col-span-6">
+ <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="value.filter_value"
+ :filter="value"
+ :text="value.filter_display"
+ @remove-filter="removeQuery"
+ />
+ </div>
+ <InfiniteScrollLayout #content="{ allElements }"
+ :elements="props.elements"
:pagy="props.pagy"
- >
- <!-- :filters="props.filters" -->
- </PagyPagination>
+ >
+ <IndexArticleCard
+ v-for="article in allElements"
+ :key="article.id"
+ :title="article.titulo"
+ :permalink="article.permalink"
+ :chamada="article.chamada"
+ :imagem="article.imagem"
+ :category="article.caderno_nome"
+ :date="article.display_date"
+ />
+ </InfiniteScrollLayout>
</div>
- <div class="hidden col-span-5 col-start-8">
- <p>Flamengo</p>
+ <div class="col-start-8 col-span-6 sticky top-0 z-50">
+ <div ref="sentinel" class="h-1"></div>
+ <ResponsiveFilterMenu
+ v-model="filters"
+ :is-open="isFilterMenuOpen"
+ @filtersApplied="filterSearch"
+ @close-filter-menu="closeMenu"
+ >
+ <template #filters="{ modelValue, updateField }">
+ <NoticiasFilterForm
+ :model-value="modelValue"
+ :update-field="updateField"
+ :data-label="props.dataLabel"
+ :cadernos="props.cadernos"
+ />
+ </template>
+ </ResponsiveFilterMenu>
</div>
</div>
- <!-- list -->
</TwContainer>
</template>
diff --git a/app/frontend/pages/Peliculas/Show.vue b/app/frontend/pages/Peliculas/Show.vue
index 0e869d5..0169874 100644
--- a/app/frontend/pages/Peliculas/Show.vue
+++ b/app/frontend/pages/Peliculas/Show.vue
@@ -83,7 +83,7 @@ const isDesktop = useUpdateWindowWidth();
aria-hidden="true"
class="h-[16px] select-none"
/>
- <p class="text-overline">{{ props.pelicula.duracao_coord_int }}</p>
+ <p class="text-overline">{{ props.pelicula.duracao_coord_int }}'</p>
<img
src="@assets/icons/divisor_black.svg"
alt=""
diff --git a/app/frontend/pages/ProgramPage.vue b/app/frontend/pages/ProgramPage.vue
index c79eeda..d9628a0 100644
--- a/app/frontend/pages/ProgramPage.vue
+++ b/app/frontend/pages/ProgramPage.vue
@@ -13,8 +13,6 @@
// but maybe this is not mandatory
-import { ref, watch } from "vue";
-import { router } from "@inertiajs/vue3"
import TwContainer from "@/components/layout/TwContainer.vue";
import InfiniteScrollLayout from "@/components/layout/InfiniteScrollLayout.vue";
@@ -30,8 +28,8 @@ import { useStickyMenuTabs } from "@/components/layout/navbar/composables/useSti
import ResponsiveFilterMenu from "@/components/features/filters/ResponsiveFilterMenu.vue";
import Breadcrumb from "@/components/common/Breadcrumb.vue";
-import { extractFilterValues } from "@/lib/filterUtils";
-import { slugify } from "@/lib/utils";
+// import { slugify } from "@/lib/utils";
+import { useSearchFilter } from "@/components/features/filters/composables/usaSearchFilter";
const { isFilterMenuOpen, openMenu, closeMenu } = useMobileTrigger();
@@ -54,139 +52,26 @@ const props = defineProps({
,crumbs: { type: Array, required: true }
})
-// ============================================================================
-// FILTER STATE MANAGEMENT - SINGLE SOURCE OF TRUTH
-// ============================================================================
-
-/**
- * Initialize empty filter structure
- */
-const initializeFilters = () => ({
- query: null,
- sessao: null,
- mostra: null,
- cinema: null,
- genero: null,
- pais: null,
- direcao: null,
- elenco: null,
-})
-
-/**
- * Override empty filters with current filter values from controller
- */
-const overrideFiltersValues = () => {
- return { ...initializeFilters(), ...props.current_filters }
-}
-
-
-// Main filter state - this is passed to SearchFilter via ResponsiveFilterMenu
-const filters = ref(overrideFiltersValues())
-
-// Watch for prop changes from server (shouldn't happen often but good to have)
-watch(() => props.current_filters, (newFilters) => {
- console.log('ProgramPage: current_filters changed from server:', newFilters);
- filters.value = overrideFiltersValues()
-}, { immediate: true, deep: true })
-
-// ============================================================================
-// FILTER OPERATIONS - CALLED BY SEARCHFILTER VIA EVENTS
-// ============================================================================
-
-/**
- * Called when SearchFilter emits filtersApplied
- * This makes the actual router call to update the page
- */
-
-const filterSearch = (filtersFromSearchFilter) => {
- console.log('ProgramPage: Applying filters from SearchFilter:', filtersFromSearchFilter);
-
- // Build query params by rejecting any filter: null or ""
- const cleanedFilters = extractFilterValues(filtersFromSearchFilter || filters.value)
-
- // MAke search request and says which prop to update
- router.get(props.tabBaseUrl, cleanedFilters, {
- preserveScroll: true,
- only: ['elements', 'pagy', 'current_filters', 'has_active_filters', 'menuTabs']
- })
-};
-
-/**
- * Called when user clicks a filter tag to remove it
- * This updates the filters state and makes a router call
- */
-
-const removeQuery = (filterToRemove) => {
- const newParams = new URLSearchParams()
- // Clear the specific filter
- // TODO: REFAC TIP ADD FILTER_KEY FROM CONTROLLER
- // IT SHOULD MAKE AGNOSTIC
- // Map filter labels to filter keys (could be improved with a filter_key from controller)
- const filterKeyMap = {
- 'Time': 'sessao',
- 'Sessão': 'sessao',
- 'Showcase': 'mostra',
- 'Mostra': 'mostra',
- 'Cinema': 'cinema',
- 'Genre': 'genero',
- 'Genero': 'genero',
- 'Country': 'pais',
- 'Pais': 'pais',
- 'Director': 'direcao',
- 'Direção': 'direcao',
- 'Cast': 'elenco',
- 'Elenco': 'elenco'
- };
- const filterKey = filterKeyMap[filterToRemove.filter_label];
- if (filterKey) {
- // Updated local filter state
- filters.value[filterKey] = null;
-
- // Build new URL params from remaining filters
- const newParams = new URLSearchParams();
- Object.entries(filters.value).forEach(([key, value]) => {
- if (value !== null && value !== undefined && value !== "" && value?.filter_value) {
- newParams.set(key, value.filter_value);
- }
- });
-
- router.get(props.tabBaseUrl, newParams, {
- preserveScroll: true,
- only: ['elements', 'pagy', 'current_filters', 'has_active_filters', 'menuTabs']
- })
- } else {
- console.warn('ProgramPage: Unknown filter label:', filterToRemove.filter_label);
- }
+const controllerParamsKeys = {
+ // query: null,
+ // sessao: null,
+ // mostra: null,
+ // cinema: null,
+ // genero: null,
+ // pais: null,
+ // // direcao: null,
+ // elenco: null,
}
-/**
- * Called when SearchFilter emits filtersCleared
- * This clears all filters and optionally makes a router call
- */
-const clearSearchQuery = () => {
- console.log('ProgramPage: Clearing all filters');
-
- const clearedFilters = initializeFilters()
- filters.value = clearedFilters
- // Only make router call if there were actually filters applied
- const hasFiltersApplied = Object.entries(props.current_filters).some(([key, value]) => value != null)
- if (hasFiltersApplied) {
- router.get(props.tabBaseUrl, {}, {
- preserveState: true,
- preserveScroll: true,
- only: ['elements', 'pagy', 'current_filters', 'has_active_filters', 'menuTabs']
- });
- }
-};
-
-/**
- * Called when user clears search bar directly (if needed)
- */
-const handleClear = () => {
- console.log('ProgramPage: Clearing search query');
- filters.value.query = null;
- filterSearch(filters.value);
-};
+const {
+ initializeFilters,
+ overrideFiltersValues,
+ filters,
+ filterSearch,
+ removeQuery,
+ clearSearchQuery,
+ handleClear
+} = useSearchFilter(props)
// ============================================================================
// UI UTILITIES
diff --git a/app/models/caderno.rb b/app/models/caderno.rb
index 8736d11..e97c3ba 100644
--- a/app/models/caderno.rb
+++ b/app/models/caderno.rb
@@ -1,4 +1,5 @@
class Caderno < ApplicationRecord
+ include Filterable
has_many :noticias
def self.for_filters
diff --git a/app/models/concerns/Filterable.rb b/app/models/concerns/Filterable.rb
index dfb037a..35ee98b 100644
--- a/app/models/concerns/Filterable.rb
+++ b/app/models/concerns/Filterable.rb
@@ -2,7 +2,9 @@ module Filterable
extend ActiveSupport::Concern
class_methods do
- def collection_for(field_name, filter_params_key)
+ def collection_for(field_name, field_display = nil, filter_params_key)
+ field_display ||= field_name
+
full_field_collection = filter_scope(field_name)
return unless block_given?
@@ -14,8 +16,16 @@ module Filterable
end
def build_filter_json(value, key)
+ logger.debug(value)
+ if value.is_a?(Array)
+ display = value.second
+ value = value.first
+ else
+ # binding.b
+ display = value
+ end
{
- "filter_display" => value,
+ "filter_display" => display,
"filter_value" => value,
"filter_label" => I18n.t("filter.#{key}")
}
@@ -26,5 +36,23 @@ module Filterable
.where.not(field_name => [ nil, "" ])
.pluck(field_name)
end
+
+ # HORRIVEL MAS EH ISSO
+ def collection_without_edition_for(field_name, field_display = nil, filter_params_key)
+ field_display ||= field_name
+ full_field_collection = no_edition_filter_scope(field_name, field_display)
+ # return unless block_given?
+
+ full_field_collection = yield(full_field_collection) if block_given?
+ full_field_collection.compact_blank
+ .uniq
+ .sort
+ .map { |item| build_filter_json(item, filter_params_key) }
+ end
+
+ def no_edition_filter_scope(field_name, field_display)
+ where.not(field_name => [ nil, "" ])
+ .pluck(field_name, field_display)
+ end
end
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index b23ad61..391dc7a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -31,7 +31,7 @@ en:
filter:
title: "Filters"
- date: "Date"
+ date: "Publication date"
time: "Time"
submostra: "Showcase"
cinema: "Theater"
@@ -43,7 +43,7 @@ en:
festivais: "Festivals"
premios: "Awards"
palavras_chaves: "Keywords"
-
+ caderno: "Category"
filter_by:
date: "by date"
time: "by time"
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 9b575fa..1f5ef66 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -31,7 +31,7 @@ pt:
filter:
title: "Filtros"
- date: "Data"
+ date: "Data de publicação"
time: "Sessão"
submostra: "Mostra"
cinema: "Cinema"
@@ -43,6 +43,7 @@ pt:
festivais: "Festivais"
premios: "Prêmios"
palavras_chaves: "Palavras chaves"
+ caderno: "Caderno"
filtro: "Filtro | Filtros"
diff --git a/package-lock.json b/package-lock.json
index 4f81d37..6dcd11d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,7 @@
"": {
"dependencies": {
"@inertiajs/vue3": "^2.1.3",
+ "@internationalized/date": "^3.9.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.12",
@@ -1647,9 +1648,9 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
- "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
diff --git a/package.json b/package.json
index 140639c..44d7f8a 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@inertiajs/vue3": "^2.1.3",
+ "@internationalized/date": "^3.9.0",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.12",
diff --git a/test/controllers/noticias_controller_test.rb b/test/controllers/noticias_controller_test.rb
index 7f62ab9..4bc043e 100644
--- a/test/controllers/noticias_controller_test.rb
+++ b/test/controllers/noticias_controller_test.rb
@@ -1,7 +1,30 @@
require "test_helper"
class NoticiasControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
+ test "filters by caderno in PT - Premio Felix" do
+ get noticias_url, params: { caderno: "premio-felix" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes [ "TEST FELIX TWO titulo" ], element["titulo"]
+ end
+ end
+ test "filters by caderno in EN - Felix Award" do
+ get noticias_url(locale: :en), params: { caderno: "felix-award" }
+
+ assert_response :success
+ props = inertia_props
+
+ elements = props["elements"]
+ assert_equal 1, elements.length
+
+ elements.each do |element|
+ assert_includes [ "TEST FELIX TWO titulo" ], element["titulo"]
+ end
+ end
end
diff --git a/test/fixtures/cadernos.yml b/test/fixtures/cadernos.yml
new file mode 100644
index 0000000..90eed8b
--- /dev/null
+++ b/test/fixtures/cadernos.yml
@@ -0,0 +1,13 @@
+talents:
+ nome_en: "Talents Rio"
+ nome_pt: "Talents Rio"
+ permalink_en: "talents-rio"
+ permalink_pt: "talents-rio"
+ created: "2016-10-03 11:17:54.000000000 +0000"
+
+felix:
+ nome_en: "Felix Award"
+ permalink_en: "felix-award"
+ nome_pt: "Prêmio Felix"
+ permalink_pt: "premio-felix"
+ created: "2015-10-10 12:43:00.000000000 +0000"
diff --git a/test/fixtures/idiomas.yml b/test/fixtures/idiomas.yml
new file mode 100644
index 0000000..6468f1f
--- /dev/null
+++ b/test/fixtures/idiomas.yml
@@ -0,0 +1,7 @@
+port:
+ nome: "Português"
+ locale: "pt-br"
+
+ing:
+ nome: "Inglês"
+ locale: "en"
diff --git a/test/fixtures/noticias.yml b/test/fixtures/noticias.yml
new file mode 100644
index 0000000..0c344f5
--- /dev/null
+++ b/test/fixtures/noticias.yml
@@ -0,0 +1,27 @@
+one_talents:
+ idioma: port
+ caderno: talents
+ data: "2025-08-10"
+ hora: "2000-01-01 10:00:00.000000000 +0000"
+ titulo: "TEST TALENTS ONE titulo"
+ permalink: "test-talents-one-titulo"
+ imagem: "3281cf9b6dd1fc05aa42e9d0c47ba471.jpg"
+ chamada: "test TALENTS ONE chamada"
+ conteudo: "<p>test TALENTS ONE conteudo</p>"
+ ativo: 1
+ created: "2025-08-08 18:54:10.000000000 +0000"
+ updated: "2025-08-11 19:36:02.000000000 +0000"
+
+one_felix:
+ idioma: port
+ caderno: felix
+ data: "2025-08-12"
+ hora: "2000-01-01 12:00:00.000000000 +0000"
+ titulo: "TEST FELIX TWO titulo"
+ permalink: "test-felix-two-titulo"
+ imagem: "3281cf9b6dd1fc05aa42e9d0c47ba471.jpg"
+ chamada: "test FELIX TWO chamada"
+ conteudo: "<p>test FELIX TWO conteudo</p>"
+ ativo: 1
+ created: "2025-08-08 18:54:10.000000000 +0000"
+ updated: "2025-08-12 19:36:02.000000000 +0000"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment