Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions web/components/posts/QuestionBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { FormQuestion } from '~/data/mock-pg-announcements';
import { cn } from '~/lib/utils';

export const MAX_QUESTIONS = 5;
export const MAX_QUESTION_TEXT_LENGTH = 120;
export const MAX_QUESTION_DESCRIPTION_LENGTH = 250;
const MIN_MCQ_OPTIONS = 2;
const MAX_MCQ_OPTIONS = 6;

Expand Down Expand Up @@ -40,31 +42,43 @@ function QuestionBuilder({ questions, dispatch }: QuestionBuilderProps) {
<div key={question.id} className="space-y-3 rounded-xl border p-4">
<div className="flex items-start gap-2">
<div className="flex-1 space-y-2">
<Input
placeholder={`Question ${index + 1}`}
value={question.text}
onChange={(e) =>
dispatch({
type: 'UPDATE_QUESTION',
id: question.id,
payload: { text: e.target.value },
})
}
/>
<div className="space-y-1">
<Input
placeholder={`Question ${index + 1}`}
value={question.text}
maxLength={MAX_QUESTION_TEXT_LENGTH}
onChange={(e) =>
dispatch({
type: 'UPDATE_QUESTION',
id: question.id,
payload: { text: e.target.value },
})
}
/>
<span className="block text-right text-xs text-muted-foreground tabular-nums">
{question.text.length}/{MAX_QUESTION_TEXT_LENGTH}
</span>
</div>

<Textarea
placeholder="Helper text (optional)"
value={question.description ?? ''}
rows={2}
className="resize-none text-sm"
onChange={(e) =>
dispatch({
type: 'UPDATE_QUESTION',
id: question.id,
payload: { description: e.target.value || undefined },
})
}
/>
<div className="space-y-1">
<Textarea
placeholder="Helper text (optional)"
value={question.description ?? ''}
rows={2}
maxLength={MAX_QUESTION_DESCRIPTION_LENGTH}
className="resize-none text-sm"
onChange={(e) =>
dispatch({
type: 'UPDATE_QUESTION',
id: question.id,
payload: { description: e.target.value || undefined },
})
}
/>
<span className="block text-right text-xs text-muted-foreground tabular-nums">
{(question.description ?? '').length}/{MAX_QUESTION_DESCRIPTION_LENGTH}
</span>
</div>

<div className="flex items-center gap-2">
<Button
Expand Down
38 changes: 37 additions & 1 deletion web/containers/CreatePostView.validation.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';

import type { SelectedEntity } from '~/components/comms/entity-selector';
import type { ReminderConfig } from '~/data/mock-pg-announcements';
import type { FormQuestion, ReminderConfig } from '~/data/mock-pg-announcements';

import { isCreatePostFormValid } from './createPostValidation';
import type { PostFormState } from './CreatePostView';
Expand Down Expand Up @@ -88,4 +88,40 @@ describe('isCreatePostFormValid — post-with-response (form)', () => {
const reminder: ReminderConfig = { type: 'NONE' };
expect(isCreatePostFormValid({ ...formBase, reminder }, 'post-with-response')).toBe(true);
});

const question: FormQuestion = { id: '1', text: 'Favourite colour?', type: 'free-text' };

it('passes with valid custom questions', () => {
expect(
isCreatePostFormValid({ ...formBase, questions: [question] }, 'post-with-response'),
).toBe(true);
});

it('fails when question text exceeds 120 characters', () => {
const long: FormQuestion = { ...question, text: 'a'.repeat(121) };
expect(isCreatePostFormValid({ ...formBase, questions: [long] }, 'post-with-response')).toBe(
false,
);
});

it('passes when question text is exactly 120 characters', () => {
const atLimit: FormQuestion = { ...question, text: 'a'.repeat(120) };
expect(isCreatePostFormValid({ ...formBase, questions: [atLimit] }, 'post-with-response')).toBe(
true,
);
});

it('fails when question description exceeds 250 characters', () => {
const long: FormQuestion = { ...question, description: 'a'.repeat(251) };
expect(isCreatePostFormValid({ ...formBase, questions: [long] }, 'post-with-response')).toBe(
false,
);
});

it('passes when question description is exactly 250 characters', () => {
const atLimit: FormQuestion = { ...question, description: 'a'.repeat(250) };
expect(isCreatePostFormValid({ ...formBase, questions: [atLimit] }, 'post-with-response')).toBe(
true,
);
});
});
10 changes: 10 additions & 0 deletions web/containers/createPostValidation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { PostKind } from '~/components/posts/PostTypePicker';
import {
MAX_QUESTION_DESCRIPTION_LENGTH,
MAX_QUESTION_TEXT_LENGTH,
} from '~/components/posts/QuestionBuilder';

import type { PostFormState } from './CreatePostView';

Expand Down Expand Up @@ -57,6 +61,12 @@ export function isCreatePostFormValid(
if (r < min || r > max) return false;
}

// Gate 4: custom-question character limits (PG enforces 120 / 250).
for (const q of state.questions) {
if (q.text.length > MAX_QUESTION_TEXT_LENGTH) return false;
if ((q.description ?? '').length > MAX_QUESTION_DESCRIPTION_LENGTH) return false;
}

return true;
}

Expand Down