diff --git a/dashboards/admin-web/src/app/(dashboard)/surveys/[id]/page.tsx b/dashboards/admin-web/src/app/(dashboard)/surveys/[id]/page.tsx new file mode 100644 index 00000000..bc1061f5 --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/surveys/[id]/page.tsx @@ -0,0 +1,487 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import { + apiCreateSurvey, + apiGetSurvey, + apiUpdateSurvey, + type ApiSurvey, +} from '@/lib/api'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { useToast } from '@/components/ui/toast'; +import { ArrowLeft, Plus, Trash2, GripVertical, Loader2 } from 'lucide-react'; +import Link from 'next/link'; + +const QUESTION_TYPES = [ + { id: 'single_choice', label: 'Single Choice', icon: '○' }, + { id: 'multiple_choice', label: 'Multiple Choice', icon: '☐' }, + { id: 'rating', label: 'Rating (1-10)', icon: '★' }, + { id: 'nps', label: 'NPS (0-10)', icon: '📊' }, + { id: 'text_short', label: 'Short Text', icon: '📝' }, + { id: 'text_long', label: 'Long Text', icon: '📄' }, + { id: 'dropdown', label: 'Dropdown', icon: '▼' }, + { id: 'scale', label: 'Scale', icon: '↔' }, + { id: 'ranking', label: 'Ranking', icon: '⇅' }, +] as const; + +const PLATFORMS = ['ios', 'android', 'web', 'macos', 'windows'] as const; +const SEGMENTS = ['free', 'pro', 'enterprise', 'churned', 'active'] as const; +const TRIGGERS = ['immediate', 'delay_seconds', 'event', 'page_view'] as const; + +interface Question { + id: string; + type: typeof QUESTION_TYPES[number]['id']; + text: string; + description: string; + required: boolean; + options: { id: string; text: string }[]; +} + +export default function SurveyEditorPage() { + const params = useParams(); + const router = useRouter(); + const { toast } = useToast(); + const isEdit = params.id && params.id !== 'new'; + const surveyId = isEdit ? (params.id as string) : null; + + const [loading, setLoading] = useState(isEdit); + const [saving, setSaving] = useState(false); + + const [form, setForm] = useState({ + title: '', + description: '', + questions: [] as Question[], + target: { + platforms: [] as string[], + userSegments: [] as string[], + }, + displayTrigger: { type: 'immediate' as typeof TRIGGERS[number] }, + incentive: null as { type: 'pro_days' | 'credits'; amount: number } | null, + }); + + useEffect(() => { + if (surveyId) { + loadSurvey(); + } + }, [surveyId]); + + async function loadSurvey() { + try { + const { data, error } = await apiGetSurvey(surveyId!); + if (error) throw new Error(error); + if (data) { + setForm({ + title: data.title, + description: data.description || '', + questions: data.questions as Question[], + target: { + platforms: data.target.platforms || [], + userSegments: data.target.userSegments || [], + }, + displayTrigger: data.displayTrigger, + incentive: data.incentive || null, + }); + } + } catch (err) { + toast({ + title: 'Error', + description: err instanceof Error ? err.message : 'Failed to load survey', + variant: 'error', + }); + } finally { + setLoading(false); + } + } + + function addQuestion(type: Question['type']) { + const newQuestion: Question = { + id: `q_${Date.now()}`, + type, + text: '', + description: '', + required: true, + options: ['single_choice', 'multiple_choice', 'dropdown', 'ranking'].includes(type) + ? [ + { id: `opt_${Date.now()}_1`, text: 'Option 1' }, + { id: `opt_${Date.now()}_2`, text: 'Option 2' }, + ] + : [], + }; + setForm({ ...form, questions: [...form.questions, newQuestion] }); + } + + function updateQuestion(index: number, updates: Partial) { + const updated = [...form.questions]; + updated[index] = { ...updated[index], ...updates }; + setForm({ ...form, questions: updated }); + } + + function removeQuestion(index: number) { + const updated = [...form.questions]; + updated.splice(index, 1); + setForm({ ...form, questions: updated }); + } + + function addOption(questionIndex: number) { + const question = form.questions[questionIndex]; + const newOption = { + id: `opt_${Date.now()}_${question.options.length + 1}`, + text: `Option ${question.options.length + 1}`, + }; + updateQuestion(questionIndex, { + options: [...question.options, newOption], + }); + } + + function updateOption(questionIndex: number, optionIndex: number, text: string) { + const question = form.questions[questionIndex]; + const updatedOptions = [...question.options]; + updatedOptions[optionIndex] = { ...updatedOptions[optionIndex], text }; + updateQuestion(questionIndex, { options: updatedOptions }); + } + + function removeOption(questionIndex: number, optionIndex: number) { + const question = form.questions[questionIndex]; + const updatedOptions = [...question.options]; + updatedOptions.splice(optionIndex, 1); + updateQuestion(questionIndex, { options: updatedOptions }); + } + + async function handleSave(asDraft = false) { + setSaving(true); + try { + const payload = { + ...form, + status: asDraft ? 'draft' : 'active', + }; + + if (isEdit) { + const { error } = await apiUpdateSurvey(surveyId!, payload as unknown as Partial); + if (error) throw new Error(error); + toast({ title: 'Success', description: 'Survey updated', variant: 'success' }); + } else { + const { error } = await apiCreateSurvey(payload as unknown as Omit); + if (error) throw new Error(error); + toast({ title: 'Success', description: 'Survey created', variant: 'success' }); + } + router.push('/surveys'); + } catch (err) { + toast({ + title: 'Error', + description: err instanceof Error ? err.message : 'Failed to save', + variant: 'error', + }); + } finally { + setSaving(false); + } + } + + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + return ( +
+
+ + + +
+

{isEdit ? 'Edit Survey' : 'New Survey'}

+

+ {isEdit ? 'Update your survey questions' : 'Create an in-app survey with conditional logic'} +

+
+
+ + + + Questions + Settings + Targeting + + + + + + Survey Details + + +
+ + setForm({ ...form, title: e.target.value })} + placeholder="Enter survey title" + /> +
+
+ +