base con autenticacion, registro, modulo encuestas

This commit is contained in:
2025-06-16 12:02:22 -04:00
commit 475e0754df
411 changed files with 26265 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
// Modal para configurar cada pregunta individual
// Funcionalidades:
// - Configuración específica según el tipo de pregunta
// - Para títulos: solo contenido
// - Para preguntas simples: texto de la pregunta
// - Para preguntas con opciones: texto y lista de opciones
// - Switch para hacer la pregunta obligatoria/opcional
'use client';
import { Button } from '@repo/shadcn/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@repo/shadcn/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@repo/shadcn/form';
import { Input } from '@repo/shadcn/input';
import { Switch } from '@repo/shadcn/switch';
import { useEffect } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { QuestionType } from '../../schemas/survey';
import { Plus, Trash2 } from 'lucide-react';
interface QuestionConfigModalProps {
isOpen: boolean;
onClose: () => void;
question: any;
onSave: (config: any) => void;
}
export function QuestionConfigModal({
isOpen,
onClose,
question,
onSave,
}: QuestionConfigModalProps) {
const form = useForm({
defaultValues: {
content: '',
question: '',
required: false,
options: [{ id: '1', text: '' }],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'options',
});
useEffect(() => {
if (question) {
form.reset({
content: question.content || '',
question: question.question || '',
required: question.required || false,
options: question.options || [{ id: '1', text: '' }],
});
}
}, [question, form]);
const handleSubmit = (data: any) => {
const config = {
...question,
...data,
};
// Remove options if not needed
if (![
QuestionType.MULTIPLE_CHOICE,
QuestionType.SINGLE_CHOICE,
QuestionType.SELECT
].includes(question.type)) {
delete config.options;
}
// Remove content if not a title
if (question.type !== QuestionType.TITLE) {
delete config.content;
}
onSave(config);
};
const renderFields = () => {
switch (question?.type) {
case QuestionType.TITLE:
return (
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Contenido del Título</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
case QuestionType.SIMPLE:
return (
<FormField
control={form.control}
name="question"
render={({ field }) => (
<FormItem>
<FormLabel>Pregunta</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
case QuestionType.MULTIPLE_CHOICE:
case QuestionType.SINGLE_CHOICE:
case QuestionType.SELECT:
return (
<>
<FormField
control={form.control}
name="question"
render={({ field }) => (
<FormItem>
<FormLabel>Pregunta</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<div className="flex justify-between items-center">
<FormLabel>Opciones</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => append({ id: `${fields.length + 1}`, text: '' })}
>
<Plus className="h-4 w-4 mr-2" />
Agregar Opción
</Button>
</div>
<div className="max-h-[200px] overflow-y-auto pr-2 space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<FormField
control={form.control}
name={`options.${index}.text`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input {...field} placeholder={`Opción ${index + 1}`} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
))}
</div>
</div>
</>
);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent
className="max-w-2xl"
aria-describedby="question-config-description"
>
<div id="question-config-description" className="sr-only">
Configuración de la pregunta de la encuesta
</div>
<DialogHeader>
<DialogTitle>Configurar Pregunta</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
{renderFields()}
{question?.type !== QuestionType.TITLE && (
<FormField
control={form.control}
name="required"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>Respuesta Obligatoria</FormLabel>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancelar
</Button>
<Button type="submit">Guardar</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,88 @@
// Caja de herramientas con tipos de preguntas disponibles
// Funcionalidades:
// - Lista de elementos arrastrables
// - Tipos disponibles: Título, Pregunta Simple, Opción Múltiple, Opción Única, Selección
// - Cada elemento es arrastrable al área de construcción
'use client';
import { Card, CardContent } from '@repo/shadcn/card';
import { QuestionType } from '../../schemas/survey';
import { useDraggable } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
const questionTypes = [
{
type: QuestionType.TITLE,
label: 'Título',
icon: '📝',
},
{
type: QuestionType.SIMPLE,
label: 'Pregunta Simple',
icon: '✏️',
},
{
type: QuestionType.MULTIPLE_CHOICE,
label: 'Opción Múltiple',
icon: '☑️',
},
{
type: QuestionType.SINGLE_CHOICE,
label: 'Opción Única',
icon: '⭕',
},
{
type: QuestionType.SELECT,
label: 'Selección',
icon: '📋',
},
];
function DraggableItem({ type, label, icon }: { type: string; label: string; icon: string }) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: type,
data: {
type,
isTemplate: true,
},
});
const style = transform ? {
transform: CSS.Translate.toString(transform),
} : undefined;
return (
<div
ref={setNodeRef}
style={style}
{...listeners}
{...attributes}
className="p-3 bg-background border rounded-lg cursor-move hover:bg-accent touch-none"
>
<div className="flex items-center gap-2">
<span>{icon}</span>
<span>{label}</span>
</div>
</div>
);
}
export function QuestionToolbox() {
return (
<Card>
<CardContent className="p-4">
<h3 className="font-semibold mb-4">Elementos Disponibles</h3>
<div className="space-y-2">
{questionTypes.map((item) => (
<DraggableItem
key={item.type}
type={item.type}
label={item.label}
icon={item.icon}
/>
))}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,454 @@
// Componente principal para crear/editar encuestas
// Funcionalidades:
// - Formulario para datos básicos (título, descripción, fecha de cierre)
// - Sistema de drag & drop para agregar preguntas
// - Reordenamiento de preguntas existentes
// - Guardado como borrador o publicación directa'use client';
'use client';
import { Button } from '@repo/shadcn/button';
import { Card, CardContent } from '@repo/shadcn/card';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@repo/shadcn/form';
import { Input } from '@repo/shadcn/input';
import { Textarea } from '@repo/shadcn/textarea';
import { CalendarIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { QuestionType, Survey } from '../../schemas/survey';
import { QuestionConfigModal } from './question-config-modal';
import { QuestionToolbox } from './question-toolbox';
import { cn } from '@repo/shadcn/lib/utils';
import { Calendar } from '@repo/shadcn/calendar';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@repo/shadcn/popover';
import { format } from 'date-fns';
import { DndContext, DragEndEvent, useSensor, useSensors, PointerSensor } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useDroppable } from '@dnd-kit/core';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@repo/shadcn/select";
import { useParams, useRouter } from 'next/navigation';
// Añade el import de Trash2
import { Trash2 } from 'lucide-react';
import { useSurveysByIdQuery } from '../../hooks/use-query-surveys';
import { useSurveyMutation } from '../../hooks/use-mutation-surveys';
function SortableQuestion({
question,
index,
onDelete,
onEdit
}: {
question: any;
index: number;
onDelete: (id: string) => void;
onEdit: (question: any) => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: question.id,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<Card className="mb-4">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div
className="flex-1 cursor-pointer"
onClick={() => onEdit(question)}
>
<span>{question.question || question.content}</span>
</div>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation();
onDelete(question.id);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
function DroppableArea({ children }: { children: React.ReactNode }) {
const { setNodeRef } = useDroppable({
id: 'questions-container',
});
return (
<div
ref={setNodeRef}
className="min-h-[200px] border-2 border-dashed rounded-lg p-4 mt-6"
>
{children}
</div>
);
}
export function SurveyBuilder() {
const [questions, setQuestions] = useState<any[]>([]);
const [selectedQuestion, setSelectedQuestion] = useState<any>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const params = useParams();
const router = useRouter();
const surveyId = params?.id as string;
const isEditing = Boolean(surveyId);
const form = useForm({
defaultValues: {
title: '',
description: '',
closingDate: undefined as Date | undefined,
targetAudience: '', // Nuevo campo
},
});
const {
mutate: MutateSurvey,
} = useSurveyMutation()
// Remove the loadSurvey function and use the query hook at component level
if (isEditing) {
const { data: surveyById, isLoading } = useSurveysByIdQuery(parseInt(surveyId))
// Use useEffect to handle the form reset when data is available
useEffect(() => {
// console.log(isEditing ? parseInt(surveyId) : 0);
if (surveyById?.data && !isLoading) {
form.reset({
title: surveyById.data.title,
description: surveyById.data.description,
closingDate: surveyById.data.closingDate || undefined,
targetAudience: surveyById.data.targetAudience,
});
// Fix: Set the questions directly without wrapping in array
setQuestions(surveyById.data.questions || []);
}
}, [surveyById, isLoading, form]);
}
// Remove the loadSurvey() call from the component body
// Procesa la configuración de una pregunta después de cerrar el modal
// Actualiza o agrega la pregunta al listado
const handleQuestionConfig = (questionConfig: any) => {
if (selectedQuestion) {
const updatedQuestions = [...questions];
const index = updatedQuestions.findIndex(q => q.id === selectedQuestion.id);
if (index === -1) {
updatedQuestions.push({
...selectedQuestion,
...questionConfig,
});
} else {
updatedQuestions[index] = {
...selectedQuestion,
...questionConfig,
};
}
setQuestions(updatedQuestions);
}
setIsModalOpen(false);
};
// Maneja el guardado de la encuesta
// Valida campos requeridos y guarda como borrador o publicada
const handleSave = async (status: 'draft' | 'published') => {
const formData = form.getValues();
// validar que los campos no esten vacíos
if (!formData.title) return toast.error('El título es obligatorio')
if (!formData.description) return toast.error('La descripción es obligatorio')
if (!formData.targetAudience) return toast.error('El público objetivo es obligatorio')
if (!formData.closingDate) return toast.error('La fecha de cierre es obligatorio')
if (questions.length === 0) return toast.error('Debe agregar al menos una pregunta');
const surveyData: Omit<Survey, 'created_at'> = {
title: formData.title,
description: formData.description,
closingDate: formData.closingDate,
targetAudience: formData.targetAudience,
published: status === 'published',
questions: questions.map((q, index) => ({ ...q, position: index })),
};
try {
await MutateSurvey({
...surveyData,
id: isEditing ? parseInt(surveyId) : undefined,
}, {
onSuccess: () => {
toast.success(
isEditing
? 'Encuesta actualizada exitosamente'
: status === 'published'
? 'Encuesta publicada'
: 'Encuesta guardada como borrador'
);
router.push('/dashboard/administracion/encuestas');
},
onError: (e) => {
toast.error(e.message)
}
});
} catch (error) {
toast.error( `Error al ${isEditing ? 'actualizar' : 'guardar'} la encuesta`)
}
};
// Configuración de los sensores para el drag and drop
// Define la distancia mínima para activar el arrastre
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
// Manejador del evento cuando se termina de arrastrar un elemento
// Gestiona tanto nuevas preguntas como reordenamiento
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
if (active.data.current?.isTemplate) {
// Handle new question from toolbox
const questionType = active.data.current.type;
const newQuestion = {
id: `q-${questions.length + 1}`,
type: questionType as QuestionType,
position: questions.length,
required: false,
};
setSelectedQuestion(newQuestion);
setIsModalOpen(true);
} else {
// Handle reordering of existing questions
const oldIndex = questions.findIndex(q => q.id === active.id);
const newIndex = questions.findIndex(q => q.id === over.id);
if (oldIndex !== newIndex) {
const updatedQuestions = [...questions];
const [movedQuestion] = updatedQuestions.splice(oldIndex, 1);
updatedQuestions.splice(newIndex, 0, movedQuestion);
setQuestions(updatedQuestions);
}
}
};
// Añade estas funciones manejadoras
const handleDeleteQuestion = (id: string) => {
setQuestions(questions.filter(q => q.id !== id));
};
const handleEditQuestion = (question: any) => {
setSelectedQuestion(question);
setIsModalOpen(true);
};
return (
<DndContext
sensors={sensors}
onDragEnd={handleDragEnd}
>
<div className="flex gap-6">
<div className="w-64">
<QuestionToolbox />
</div>
<div className="flex-1">
<Card>
<CardContent className="p-6">
<Form {...form}>
<form className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Título de la Encuesta</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Descripción</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="closingDate"
render={({ field }) => (
<FormItem>
<FormLabel>Fecha de Cierre</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full pl-3 text-left font-normal",
!field.value && "text-muted-foreground"
)}
>
{field.value ? (
format(field.value, "PPP")
) : (
<span>Seleccione una fecha</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value}
onSelect={field.onChange}
disabled={(date) =>
date < new Date()
}
initialFocus
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="targetAudience"
render={({ field }) => (
<FormItem>
<FormLabel>Dirigido a</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger className="w-full">
<SelectValue placeholder="Seleccione el público objetivo" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="producers">Productores</SelectItem>
<SelectItem value="organization">Organización</SelectItem>
<SelectItem value="all">Todos</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
</Form>
<DroppableArea>
<SortableContext
items={questions.map(q => q.id)}
strategy={verticalListSortingStrategy}
>
{questions.map((question, index) => (
<SortableQuestion
key={question.id}
question={question}
index={index}
onDelete={handleDeleteQuestion}
onEdit={handleEditQuestion}
/>
))}
{questions.length === 0 && (
<div className="text-center text-muted-foreground p-4">
Arrastra elementos aquí para crear la encuesta
</div>
)}
</SortableContext>
</DroppableArea>
<div className="flex justify-end gap-4 mt-6">
<Button
variant="ghost"
onClick={() => router.push('/dashboard/administracion/encuestas')}
>
Cancelar
</Button>
<Button variant="outline" onClick={() => handleSave('draft')}>
Guardar como Borrador
</Button>
<Button onClick={() => handleSave('published')}>
Publicar
</Button>
</div>
</CardContent>
</Card>
</div>
<QuestionConfigModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
question={selectedQuestion}
onSave={handleQuestionConfig}
/>
</div>
</DndContext>
);
}

View File

@@ -0,0 +1,42 @@
'use client';
import { DataTable } from '@repo/shadcn/table/data-table';
import { DataTableSkeleton } from '@repo/shadcn/table/data-table-skeleton';
import { columns } from './surveys-tables/columns';
import { useSurveysQuery } from '../../hooks/use-query-surveys';
interface SurveysAdminListProps {
initialPage: number;
initialSearch?: string | null;
initialLimit: number;
initialType?: string | null;
}
export default function SurveysAdminList({
initialPage,
initialSearch,
initialLimit,
initialType,
}: SurveysAdminListProps) {
const filters = {
page: initialPage,
limit: initialLimit,
...(initialSearch && { search: initialSearch }),
...(initialType && { type: initialType }),
};
const {data, isLoading} = useSurveysQuery(filters)
if (isLoading) {
return <DataTableSkeleton columnCount={6} rowCount={initialLimit} />;
}
return (
<DataTable
columns={columns}
data={data?.data || []}
totalItems={data?.meta.totalCount || 0}
pageSizeOptions={[10, 20, 30, 40, 50]}
/>
);
}

View File

@@ -0,0 +1,24 @@
'use client';
import { useRouter } from 'next/navigation';
import { Button } from '@repo/shadcn/button';
import { Heading } from '@repo/shadcn/heading';
import { Plus } from 'lucide-react';
export function SurveysHeader() {
const router = useRouter();
return (
<>
<div className="flex items-start justify-between">
<Heading
title="Administración de Encuestas"
description="Gestiona las encuestas disponibles en la plataforma"
/>
<Button onClick={() => router.push(`/dashboard/administracion/encuestas/crear`)} size="sm">
<Plus className="mr-2 h-4 w-4" /> Agregar Encuesta
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,88 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { AlertModal } from '@/components/modal/alert-modal';
import { Button } from '@repo/shadcn/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@repo/shadcn/tooltip';
import { Edit, Trash } from 'lucide-react';
import { SurveyTable } from '@/feactures/surveys/schemas/survey';
import { useDeleteSurvey } from '@/feactures/surveys/hooks/use-mutation-surveys';
interface CellActionProps {
data: SurveyTable;
}
export const CellAction: React.FC<CellActionProps> = ({ data }) => {
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const { mutate: deleteSurvey } = useDeleteSurvey();
const router = useRouter();
const onConfirm = async () => {
try {
setLoading(true);
deleteSurvey(data.id!);
setOpen(false);
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
}
};
return (
<>
<AlertModal
isOpen={open}
onClose={() => setOpen(false)}
onConfirm={onConfirm}
loading={loading}
title="¿Estás seguro que desea eliminar la encuesta?"
description="Esta acción no se puede deshacer."
/>
<div className="flex gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => router.push(`/dashboard/administracion/encuestas/editar/${data.id!}`)}
>
<Edit className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Editar</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={() => setOpen(true)}
>
<Trash className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Eliminar</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</>
);
};

View File

@@ -0,0 +1,29 @@
import { Badge } from "@repo/shadcn/badge";
import { ColumnDef } from '@tanstack/react-table';
import { CellAction } from './cell-action';
import { SurveyTable } from '@/feactures/surveys/schemas/survey';
export const columns: ColumnDef<SurveyTable>[] = [
{
accessorKey: 'title',
header: 'Título',
},
{
accessorKey: "published",
header: "Estado",
cell: ({ row }) => {
const published = row.getValue("published");
return (
<Badge variant={published == 'Publicada' ? "default" : "secondary"}>
{published == 'Publicada' ? 'Publicada' : 'Borrador'}
</Badge>
)
},
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => <CellAction data={row.original} />,
},
];

View File

@@ -0,0 +1,36 @@
'use client';
import { DataTableFilterBox } from '@repo/shadcn/table/data-table-filter-box';
import { DataTableSearch } from '@repo/shadcn/table/data-table-search';
import {
TYPE_OPTIONS,
useSurveyTableFilters,
} from './use-survey-table-filters';
export default function SurveysTableAction() {
const {
typeFilter,
searchQuery,
setPage,
setTypeFilter,
setSearchQuery,
} = useSurveyTableFilters();
return (
<div className="flex flex-wrap items-center gap-4 pt-2">
<DataTableSearch
searchKey={searchQuery}
searchQuery={searchQuery || ''}
setSearchQuery={setSearchQuery}
setPage={setPage}
/>
<DataTableFilterBox
filterKey="type"
title="Estado"
options={TYPE_OPTIONS}
setFilterValue={setTypeFilter}
filterValue={typeFilter}
/>
</div>
);
}

View File

@@ -0,0 +1,59 @@
'use client';
import { PUBLISHED_TYPES } from '@/feactures/surveys/schemas/surveys-options';
import { searchParams } from '@repo/shadcn/lib/searchparams';
import { useQueryState } from 'nuqs';
import { useCallback, useMemo } from 'react';
export const TYPE_OPTIONS = Object.entries(PUBLISHED_TYPES).map(
([value, label]) => ({
value,
label,
}),
);
export function useSurveyTableFilters() {
const [searchQuery, setSearchQuery] = useQueryState(
'q',
searchParams.q
.withOptions({
shallow: false,
throttleMs: 500, // Add 500ms delay
// Removed dedupingInterval as it's not a valid option
})
.withDefault(''),
);
const [typeFilter, setTypeFilter] = useQueryState(
'published',
searchParams.q.withOptions({ shallow: false }).withDefault(''),
);
const [page, setPage] = useQueryState(
'page',
searchParams.page.withDefault(1),
);
const resetFilters = useCallback(() => {
setSearchQuery(null);
setTypeFilter(null);
setPage(1);
}, [setSearchQuery, setPage]);
const isAnyFilterActive = useMemo(() => {
return !!searchQuery || !!typeFilter;
}, [searchQuery]);
return {
searchQuery,
setSearchQuery,
page,
setPage,
resetFilters,
isAnyFilterActive,
typeFilter,
setTypeFilter
};
}