Skip to content

Commit ceebc3a

Browse files
author
github-actions
committed
Merge develop into main
2 parents 187ac5a + f558f27 commit ceebc3a

File tree

74 files changed

+6605
-1881
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+6605
-1881
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ await this.formModel.updateOne(
186186
5. Explore insights through 13 analytics cards
187187

188188
## Documentation
189-
- **Architecture**: See `docs/ARCHITECTURE.md` for detailed system design
189+
- **Architecture**: See `docs/architecture.md` for detailed system design
190190
- **API Docs**: http://localhost:3001/api/docs (Swagger)
191191
- **README**: Project overview and business value
192192
- **TECHNICAL_STACK**: Deployment and infrastructure guide

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Layered architecture:
3737
4. **Queue Layer**: Bull queues for async analytics (orchestration, response processing, topic clustering, aggregation, AI generation)
3838
5. **Data Layer**: MongoDB with Mongoose ODM
3939

40-
See `docs/ARCHITECTURE.md` for details.
40+
See `docs/architecture.md` for details.
4141

4242

4343
## 🚀 Quick Start

client/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
"type": "module",
66
"dependencies": {
77
"@hookform/resolvers": "^5.2.2",
8+
"@types/html2pdf.js": "^0.10.0",
89
"axios": "^1.13.1",
10+
"html2pdf.js": "^0.14.0",
11+
"jspdf": "^4.1.0",
12+
"jspdf-autotable": "^5.0.7",
913
"lucide-react": "^0.552.0",
1014
"react": "^19.2.0",
1115
"react-dnd": "^16.0.1",

client/src/App.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import LandingPage from './pages/LandingPage';
66
import Dashboard from './pages/Dashboard';
77
import FormEditor from './pages/FormEditor';
88
import FormAnalytics from './pages/FormAnalytics';
9+
import PrintableAnalytics from './pages/PrintableAnalytics';
910
import PublicFormView from './pages/PublicFormView';
1011
import EmailConfirmation from './pages/EmailConfirmation';
12+
import ResetPassword from './pages/ResetPassword';
1113
import AdminSettings from './pages/AdminSettings';
14+
import AdminDashboardPage from './pages/AdminDashboardPage';
1215
import './App.css';
1316

1417
// Protected Route Component
@@ -88,6 +91,14 @@ function App() {
8891
</ProtectedRoute>
8992
}
9093
/>
94+
<Route
95+
path="/forms/:formId/analytics/print"
96+
element={
97+
<ProtectedRoute>
98+
<PrintableAnalytics />
99+
</ProtectedRoute>
100+
}
101+
/>
91102
{/* Public form route - accessible without authentication */}
92103
<Route
93104
path="/form/:formId"
@@ -97,6 +108,10 @@ function App() {
97108
path="/confirm-email"
98109
element={<EmailConfirmation />}
99110
/>
111+
<Route
112+
path="/reset-password"
113+
element={<ResetPassword />}
114+
/>
100115
<Route
101116
path="/admin/settings"
102117
element={
@@ -105,6 +120,14 @@ function App() {
105120
</ProtectedRoute>
106121
}
107122
/>
123+
<Route
124+
path="/admin"
125+
element={
126+
<ProtectedRoute>
127+
<AdminDashboardPage />
128+
</ProtectedRoute>
129+
}
130+
/>
108131
{/* Redirect any unknown routes to home */}
109132
<Route path="*" element={<Navigate to="/" replace />} />
110133
</Routes>

client/src/components/FormEditor/FormCanvas.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const FormCanvas: React.FC<FormCanvasProps> = ({
6161
required: false,
6262
order: dropIndex,
6363
options: needsOptions(fieldType) ? ['Option 1'] : undefined,
64+
canBeOther: false,
6465
};
6566

6667
const updatedQuestions = [...form.questions];

client/src/components/FormEditor/FormPreview.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,13 @@ const FormPreview: React.FC<FormPreviewProps> = ({ form }) => {
153153
);
154154

155155
case QuestionType.RATING:
156+
const minRating = Number(question.validation?.min?.value) || 1;
157+
const maxRating = Number(question.validation?.max?.value) || 5;
158+
const ratingRange = Array.from({ length: maxRating - minRating + 1 }, (_, i) => minRating + i);
159+
156160
return (
157161
<div className="flex items-center space-x-1">
158-
{[1, 2, 3, 4, 5].map((rating) => (
162+
{ratingRange.map((rating) => (
159163
<button
160164
key={rating}
161165
type="button"

client/src/components/FormEditor/QuestionEditor.tsx

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -224,22 +224,74 @@ const QuestionEditor: React.FC<QuestionEditorProps> = ({
224224
);
225225

226226
case QuestionType.RATING:
227+
const minRatingEdit = Number(question.validation?.min?.value) || 1;
228+
const maxRatingEdit = Number(question.validation?.max?.value) || 5;
229+
const ratingRangeEdit = Array.from({ length: maxRatingEdit - minRatingEdit + 1 }, (_, i) => minRatingEdit + i);
230+
227231
return (
228-
<div className="flex items-center space-x-1">
229-
{[1, 2, 3, 4, 5].map((star) => (
230-
<svg
231-
key={star}
232-
className="w-6 h-6 text-gray-300"
233-
fill="currentColor"
234-
viewBox="0 0 24 24"
235-
>
236-
<path
237-
fillRule="evenodd"
238-
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z"
239-
clipRule="evenodd"
240-
/>
241-
</svg>
242-
))}
232+
<div className="space-y-3">
233+
{/* Rating preview */}
234+
<div className="flex items-center space-x-1">
235+
{ratingRangeEdit.map((star) => (
236+
<svg
237+
key={star}
238+
className="w-6 h-6 text-gray-300"
239+
fill="currentColor"
240+
viewBox="0 0 24 24"
241+
>
242+
<path
243+
fillRule="evenodd"
244+
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z"
245+
clipRule="evenodd"
246+
/>
247+
</svg>
248+
))}
249+
</div>
250+
251+
{/* Rating configuration */}
252+
{isSelected && (
253+
<div className="flex items-center space-x-4 text-sm">
254+
<div className="flex items-center space-x-2">
255+
<label className="text-gray-600">Min:</label>
256+
<input
257+
type="number"
258+
min="0"
259+
max={maxRatingEdit - 1}
260+
value={minRatingEdit}
261+
onChange={(e) => {
262+
const newMin = Math.max(0, parseInt(e.target.value) || 1);
263+
onUpdate({
264+
validation: {
265+
...question.validation,
266+
min: { type: 'min', value: newMin }
267+
}
268+
});
269+
}}
270+
className="w-16 px-2 py-1 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
271+
/>
272+
</div>
273+
<div className="flex items-center space-x-2">
274+
<label className="text-gray-600">Max:</label>
275+
<input
276+
type="number"
277+
min={minRatingEdit + 1}
278+
max="10"
279+
value={maxRatingEdit}
280+
onChange={(e) => {
281+
const newMax = Math.min(10, Math.max(minRatingEdit + 1, parseInt(e.target.value) || 5));
282+
onUpdate({
283+
validation: {
284+
...question.validation,
285+
max: { type: 'max', value: newMax }
286+
}
287+
});
288+
}}
289+
className="w-16 px-2 py-1 border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
290+
/>
291+
</div>
292+
<span className="text-gray-500">({ratingRangeEdit.length} stars)</span>
293+
</div>
294+
)}
243295
</div>
244296
);
245297

client/src/components/analytics/ClosedQuestionTopicsCard.tsx

Lines changed: 80 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
2-
import { BarChart, Filter, Users, Info } from 'lucide-react';
1+
import React, { useState } from 'react';
2+
import { BarChart, Filter, Users, Info, ChevronDown, ChevronUp } from 'lucide-react';
33
import { AnalyticsData } from '../../types/analytics';
44

55
interface ClosedQuestionTopicsCardProps {
@@ -13,7 +13,19 @@ export const ClosedQuestionTopicsCard: React.FC<ClosedQuestionTopicsCardProps> =
1313
selectedTopics = [],
1414
hasActiveFilters = false
1515
}) => {
16+
const [expandedAnswers, setExpandedAnswers] = useState<Set<string>>(new Set());
1617
const closedQuestionCorrelations = analytics?.correlations?.closedQuestionTopics || [];
18+
19+
const toggleAnswer = (questionIndex: number, answerIndex: number) => {
20+
const key = `${questionIndex}-${answerIndex}`;
21+
const newExpanded = new Set(expandedAnswers);
22+
if (newExpanded.has(key)) {
23+
newExpanded.delete(key);
24+
} else {
25+
newExpanded.add(key);
26+
}
27+
setExpandedAnswers(newExpanded);
28+
};
1729

1830
// Filter by selected topics if any
1931
const filteredCorrelations = selectedTopics.length > 0
@@ -101,52 +113,75 @@ export const ClosedQuestionTopicsCard: React.FC<ClosedQuestionTopicsCardProps> =
101113

102114
{/* Answer values in 2-column grid */}
103115
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
104-
{question.correlations.map((answer, aIndex) => (
105-
<div key={aIndex} className="bg-gray-50 rounded-lg p-3 border border-gray-200">
106-
<div className="flex items-center justify-between mb-2">
107-
<span className="text-sm font-medium text-gray-900">{answer.answerValue}</span>
108-
<span className="flex items-center gap-1 text-xs text-gray-600">
109-
<Users className="w-3 h-3" />
110-
{answer.responseCount} {answer.responseCount === 1 ? 'response' : 'responses'}
111-
</span>
112-
</div>
116+
{question.correlations.map((answer, aIndex) => {
117+
const answerKey = `${qIndex}-${aIndex}`;
118+
const isExpanded = expandedAnswers.has(answerKey);
119+
const displayedTopics = isExpanded
120+
? answer.topicDistribution
121+
: answer.topicDistribution.slice(0, 5);
122+
123+
return (
124+
<div key={aIndex} className="bg-gray-50 rounded-lg p-3 border border-gray-200">
125+
<div className="flex items-center justify-between mb-2">
126+
<span className="text-sm font-medium text-gray-900">{answer.answerValue}</span>
127+
<span className="flex items-center gap-1 text-xs text-gray-600">
128+
<Users className="w-3 h-3" />
129+
{answer.responseCount} {answer.responseCount === 1 ? 'response' : 'responses'}
130+
</span>
131+
</div>
113132

114-
{/* Topic distribution */}
115-
{answer.topicDistribution.length > 0 ? (
116-
<div className="space-y-1.5">
117-
{answer.topicDistribution.slice(0, 5).map((topic, tIndex) => (
118-
<div key={tIndex} className="flex items-center gap-2">
119-
<div className="flex-1">
120-
<div className="flex items-center justify-between text-xs mb-1">
121-
<span className={`inline-flex px-2 py-0.5 rounded font-medium ${getTopicColor(tIndex)}`}>
122-
{topic.topic}
123-
</span>
124-
<span className="text-gray-600 font-medium">{topic.percentage}%</span>
125-
</div>
126-
{/* Progress bar */}
127-
<div className="h-1.5 bg-gray-200 rounded-full overflow-hidden">
128-
<div
129-
className={`h-full ${getTopicColor(tIndex).split(' ')[0]}`}
130-
style={{ width: `${topic.percentage}%` }}
131-
/>
133+
{/* Topic distribution */}
134+
{answer.topicDistribution.length > 0 ? (
135+
<div className="space-y-1.5">
136+
{displayedTopics.map((topic, tIndex) => (
137+
<div key={tIndex} className="flex items-center gap-2">
138+
<div className="flex-1">
139+
<div className="flex items-center justify-between text-xs mb-1">
140+
<span className={`inline-flex px-2 py-0.5 rounded font-medium ${getTopicColor(tIndex)}`}>
141+
{topic.topic}
142+
</span>
143+
<span className="text-gray-600 font-medium">{topic.percentage}%</span>
144+
</div>
145+
{/* Progress bar */}
146+
<div className="h-1.5 bg-gray-200 rounded-full overflow-hidden">
147+
<div
148+
className={`h-full ${getTopicColor(tIndex).split(' ')[0]}`}
149+
style={{ width: `${topic.percentage}%` }}
150+
/>
151+
</div>
132152
</div>
153+
<span className="text-xs text-gray-500 w-8 text-right">
154+
{topic.count}
155+
</span>
133156
</div>
134-
<span className="text-xs text-gray-500 w-8 text-right">
135-
{topic.count}
136-
</span>
137-
</div>
138-
))}
139-
{answer.topicDistribution.length > 5 && (
140-
<p className="text-xs text-gray-500 text-center mt-2">
141-
+{answer.topicDistribution.length - 5} more topics
142-
</p>
143-
)}
144-
</div>
145-
) : (
146-
<p className="text-xs text-gray-500 italic">No topics discussed</p>
147-
)}
148-
</div>
149-
))}
157+
))}
158+
{answer.topicDistribution.length > 5 && (
159+
<div className="text-center mt-2">
160+
<button
161+
onClick={() => toggleAnswer(qIndex, aIndex)}
162+
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded transition-colors"
163+
>
164+
{isExpanded ? (
165+
<>
166+
<ChevronUp className="w-3 h-3" />
167+
Show Less
168+
</>
169+
) : (
170+
<>
171+
<ChevronDown className="w-3 h-3" />
172+
Show All {answer.topicDistribution.length} Topics
173+
</>
174+
)}
175+
</button>
176+
</div>
177+
)}
178+
</div>
179+
) : (
180+
<p className="text-xs text-gray-500 italic">No topics discussed</p>
181+
)}
182+
</div>
183+
);
184+
})}
150185
</div>
151186
</div>
152187
))}

0 commit comments

Comments
 (0)