Skip to content

Commit 44b4d53

Browse files
committed
feat: add responses in realtime
1 parent ea1867e commit 44b4d53

File tree

22 files changed

+229
-79
lines changed

22 files changed

+229
-79
lines changed

src/app/dashboard/[id]/page.tsx

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import { notFound } from 'next/navigation';
22
import Link from 'next/link';
3-
import { ArrowLeft, Edit, BarChart3, Table } from 'lucide-react';
3+
import { ArrowLeft, Edit } from 'lucide-react';
44

55
import { getForm, getResponses } from '@/app/actions/forms';
6-
import {
7-
ResponseTable,
8-
ResponseCharts,
9-
CopyLinkButton,
10-
} from '@/components/dashboard';
6+
import { CopyLinkButton, ResponsesTabs } from '@/components/dashboard';
117
import { Button } from '@/components/ui/button';
128
import { Badge } from '@/components/ui/badge';
13-
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
149

1510
interface ResponseDetailPageProps {
1611
params: Promise<{ id: string }>;
@@ -59,10 +54,6 @@ export default async function ResponseDetailPage({
5954
{form.published ? 'Publicado' : 'Borrador'}
6055
</Badge>
6156
</div>
62-
<p className="text-muted-foreground text-sm">
63-
{responses.length}{' '}
64-
{responses.length === 1 ? 'respuesta' : 'respuestas'} recibidas
65-
</p>
6657
</div>
6758
</div>
6859
<div className="flex shrink-0 items-center gap-2">
@@ -77,26 +68,11 @@ export default async function ResponseDetailPage({
7768
</div>
7869

7970
{/* Content */}
80-
<Tabs defaultValue="table" className="space-y-6">
81-
<TabsList className="bg-muted">
82-
<TabsTrigger value="table" className="text-sm">
83-
<Table className="mr-1.5 h-4 w-4" />
84-
Respuestas
85-
</TabsTrigger>
86-
<TabsTrigger value="charts" className="text-sm">
87-
<BarChart3 className="mr-1.5 h-4 w-4" />
88-
Gráficas
89-
</TabsTrigger>
90-
</TabsList>
91-
92-
<TabsContent value="table">
93-
<ResponseTable fields={form.fields} responses={responses} />
94-
</TabsContent>
95-
96-
<TabsContent value="charts">
97-
<ResponseCharts fields={form.fields} responses={responses} />
98-
</TabsContent>
99-
</Tabs>
71+
<ResponsesTabs
72+
formId={form.id}
73+
fields={form.fields}
74+
initialResponses={responses}
75+
/>
10076
</div>
10177
);
10278
}

src/components/builder/constants.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/components/builder/fields/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export { FieldTypeDragOverlay } from './field-type-drag-overlay';
55
export { FormBuilderDragOverlay } from './form-builder-drag-overlay';
66
export { SortableFieldItem } from './sortable-field-item';
77
export { FIELD_TYPE_ICON_MAP } from './field-type-icons';
8+
export { InsertionSpacer } from './insertion-spacer';

src/components/builder/layout/insertion-spacer.tsx renamed to src/components/builder/fields/insertion-spacer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { type FC } from 'react';
44

5-
import { FIELD_ROW_MIN_HEIGHT } from '../constants';
5+
/** Min height of a field row; used for insertion spacer during drag. */
6+
const FIELD_ROW_MIN_HEIGHT = 52;
67

78
/** Invisible spacer shown during drag to shift the list and indicate insertion point. */
89
export const InsertionSpacer: FC = () => (

src/components/builder/fields/sortable-field-item.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { FormField } from '@/core/domain/entities/form';
1111
import { FIELD_TYPE_LABELS } from '@/core/domain/value-objects/field-types';
1212
import { cn } from '@/utils/cn';
1313

14-
import { SORTABLE_TRANSITION } from '../constants';
1514
import { useFormBuilderContext } from '../form-builder-context';
1615

1716
interface SortableFieldItemProps {
@@ -35,7 +34,10 @@ export const SortableFieldItem: FC<SortableFieldItemProps> = ({ field }) => {
3534
isDragging,
3635
} = useSortable({
3736
id: field.id,
38-
transition: SORTABLE_TRANSITION,
37+
transition: {
38+
duration: 280,
39+
easing: 'cubic-bezier(0.33, 1, 0.68, 1)',
40+
},
3941
});
4042

4143
const style = {

src/components/builder/form-builder.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import {
1313
FormBuilderSidebar,
1414
FormBuilderFieldsColumn,
1515
type FormBuilderTab,
16+
FormPreview,
1617
} from './layout';
1718
import { FormBuilderDragOverlay } from './fields';
18-
import { FormPreview } from './preview';
1919

2020
export interface FormBuilderProps {
2121
formId?: string;

src/components/builder/hooks/use-form-builder-dnd.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
resolveNewItemInsertionIndex,
2020
} from '@/utils/dnd/insertion-index';
2121

22-
import { DND_POINTER_ACTIVATION_DISTANCE } from '../constants';
2322
import { useFormBuilderContext } from '../form-builder-context';
2423
import { DROP_ZONE_ID } from '../fields';
2524

@@ -38,9 +37,7 @@ export function useFormBuilderDnD() {
3837
const [dragWidth, setDragWidth] = useState<number | null>(null);
3938

4039
const sensors = useSensors(
41-
useSensor(PointerSensor, {
42-
activationConstraint: { distance: DND_POINTER_ACTIVATION_DISTANCE },
43-
}),
40+
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
4441
useSensor(KeyboardSensor, {
4542
coordinateGetter: sortableKeyboardCoordinates,
4643
}),

src/components/builder/hooks/use-form-builder.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,20 @@ export const useFormBuilder = (initialData?: FormBuilderData) => {
3838
name: 'fields',
3939
});
4040

41-
const fields = useWatch({ control: form.control, name: 'fields' });
41+
const fields = useWatch({ control: form.control, name: 'fields' }) ?? [];
4242
const selectedField = fields.find((f) => f.id === selectedFieldId) ?? null;
4343

44+
const setFieldPathValue = (
45+
index: number,
46+
path: string,
47+
value: unknown,
48+
): void => {
49+
form.setValue(`fields.${index}.${path}` as never, value as never, {
50+
shouldDirty: true,
51+
shouldTouch: true,
52+
});
53+
};
54+
4455
/** Create a default field of the given type */
4556
const createDefaultField = (type: FieldType): FormField => {
4657
const base: FormField = {
@@ -82,7 +93,10 @@ export const useFormBuilder = (initialData?: FormBuilderData) => {
8293
const updateField = (id: string, updates: Partial<FormField>) => {
8394
const index = fields.findIndex((f) => f.id === id);
8495
if (index === -1) return;
85-
fieldArray.update(index, { ...fields[index], ...updates });
96+
97+
for (const [key, value] of Object.entries(updates)) {
98+
setFieldPathValue(index, key, value);
99+
}
86100
};
87101

88102
/** Remove a field */
@@ -121,37 +135,34 @@ export const useFormBuilder = (initialData?: FormBuilderData) => {
121135
if (index === -1) return;
122136
const field = fields[index];
123137
const options = field.options ?? [];
124-
fieldArray.update(index, {
125-
...field,
126-
options: [
127-
...options,
128-
{ id: uuidv4(), label: `Opción ${options.length + 1}` },
129-
],
130-
});
138+
setFieldPathValue(index, 'options', [
139+
...options,
140+
{ id: uuidv4(), label: `Opción ${options.length + 1}` },
141+
]);
131142
};
132143

133144
/** Update an option */
134145
const updateOption = (fieldId: string, optionId: string, label: string) => {
135146
const index = fields.findIndex((f) => f.id === fieldId);
136147
if (index === -1) return;
137148
const field = fields[index];
138-
fieldArray.update(index, {
139-
...field,
140-
options: field.options?.map((o) =>
141-
o.id === optionId ? { ...o, label } : o,
142-
),
143-
});
149+
setFieldPathValue(
150+
index,
151+
'options',
152+
field.options?.map((o) => (o.id === optionId ? { ...o, label } : o)),
153+
);
144154
};
145155

146156
/** Remove an option */
147157
const removeOption = (fieldId: string, optionId: string) => {
148158
const index = fields.findIndex((f) => f.id === fieldId);
149159
if (index === -1) return;
150160
const field = fields[index];
151-
fieldArray.update(index, {
152-
...field,
153-
options: field.options?.filter((o) => o.id !== optionId),
154-
});
161+
setFieldPathValue(
162+
index,
163+
'options',
164+
field.options?.filter((o) => o.id !== optionId),
165+
);
155166
};
156167

157168
return {

src/components/builder/layout/form-builder-fields-column.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import {
88
} from '@dnd-kit/sortable';
99

1010
import { useFormBuilderContext } from '../form-builder-context';
11-
import { FieldsDropZone, SortableFieldItem } from '../fields';
11+
import { FieldsDropZone, SortableFieldItem, InsertionSpacer } from '../fields';
1212
import type { FormBuilderTab } from './form-builder-tabs';
13-
import { InsertionSpacer } from './insertion-spacer';
1413

1514
interface FormBuilderFieldsColumnProps {
1615
activeTab: FormBuilderTab;
File renamed without changes.

0 commit comments

Comments
 (0)