Build a customizable survey builder with real-time analytics, QR code sharing, and response management
* Preview is for reference only. Actual results may vary depending on the AI model, variable values, and tools used.
You are an expert full-stack developer building a Survey & Feedback Collection Tool for {company_name}, a Malaysian SME. This tool replaces Google Forms, SurveyMonkey, and Typeform with a fully owned, self-hosted solution.
The business needs to run: {survey_types}. Each survey supports up to {max_questions} questions using these question types: {question_types}.
## Tech Stack
- Next.js 14 (App Router) with TypeScript
- Tailwind CSS + shadcn/ui (new-york style, slate base, yellow #FCD34D accent)
- Supabase (Postgres + Auth + RLS + Realtime)
- @dnd-kit/core for drag-and-drop question reordering
- recharts for analytics charts
- qrcode.react for QR code generation
- react-hook-form + zod for form validation
- xlsx (SheetJS) for Excel export
- Vercel hosting
## Database Schema
Create the following Supabase tables:
sql
-- Surveys table
create table surveys (
id uuid primary key default gen_random_uuid(),
created_by uuid references auth.users(id),
title text not null,
description text,
status text not null default 'draft' check (status in ('draft','active','closed','scheduled')),
theme jsonb default '{"primary_color": "#FCD34D", "logo_url": null, "bg_color": "#FFFFFF"}'::jsonb,
settings jsonb default '{"one_response_per_device": true, "show_progress_bar": true, "randomize_questions": false, "response_limit": null}'::jsonb,
opens_at timestamptz,
closes_at timestamptz,
thank_you_message text default 'Thank you for your feedback!',
thank_you_redirect_url text,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index idx_surveys_status on surveys(status);
create index idx_surveys_created_by on surveys(created_by);
-- Questions table
create table questions (
id uuid primary key default gen_random_uuid(),
survey_id uuid not null references surveys(id) on delete cascade,
type text not null check (type in ('single_choice','multiple_choice','rating','text','nps','matrix','date','email','phone')),
text text not null,
description text,
options jsonb,
matrix_rows jsonb,
matrix_cols jsonb,
required boolean default false,
display_order integer not null default 0,
conditional_logic jsonb,
created_at timestamptz default now()
);
create index idx_questions_survey_id on questions(survey_id);
create index idx_questions_order on questions(survey_id, display_order);
-- Responses table
create table responses (
id uuid primary key default gen_random_uuid(),
survey_id uuid not null references surveys(id) on delete cascade,
respondent_info jsonb,
device_fingerprint text,
ip_address text,
user_agent text,
share_method text,
completed boolean default false,
submitted_at timestamptz default now()
);
create index idx_responses_survey_id on responses(survey_id);
create index idx_responses_submitted_at on responses(survey_id, submitted_at);
create index idx_responses_fingerprint on responses(survey_id, device_fingerprint);
-- Answers table
create table answers (
id uuid primary key default gen_random_uuid(),
response_id uuid not null references responses(id) on delete cascade,
question_id uuid not null references questions(id) on delete cascade,
value text,
value_array jsonb,
value_number numeric,
created_at timestamptz default now()
);
create index idx_answers_response_id on answers(response_id);
create index idx_answers_question_id on answers(question_id);
-- Survey shares table
create table survey_shares (
id uuid primary key default gen_random_uuid(),
survey_id uuid not null references surveys(id) on delete cascade,
method text check (method in ('link','qr_code','email','embed')),
url text,
qr_code_data text,
click_count integer default 0,
created_at timestamptz default now()
);
create index idx_survey_shares_survey_id on survey_shares(survey_id);
Enable RLS. Public (anon) can SELECT active surveys, INSERT responses and answers. Authenticated admins can do full CRUD. Block duplicate responses using device_fingerprint check in a Postgres function.
## Application Structure
src/app/
(public)/
s/[survey-id]/page.tsx # Public response form
s/[survey-id]/thank-you/page.tsx
(admin)/
admin/
layout.tsx # Auth guard
page.tsx # Survey list dashboard
surveys/new/page.tsx # Create survey
surveys/[id]/builder/page.tsx # Drag-and-drop builder
surveys/[id]/results/page.tsx # Analytics dashboard
surveys/[id]/responses/page.tsx # Individual responses
src/components/
builder/
SurveyBuilder.tsx # Main builder (Client Component)
QuestionList.tsx # DnD sortable list
QuestionCard.tsx # Individual question editor
QuestionTypeSelector.tsx # Type picker modal
SurveyPreview.tsx # Live preview panel
response-form/
SurveyForm.tsx # Public respondent form
QuestionRenderer.tsx # Renders each question type
NPSQuestion.tsx # 0-10 NPS scale
RatingQuestion.tsx # Star rating
MatrixQuestion.tsx # Grid question
analytics/
ResponseOverTimeChart.tsx # Line chart
NPSGauge.tsx # NPS score gauge
QuestionBreakdown.tsx # Per-question stats
SentimentBadge.tsx # Text sentiment label
SurveySharePanel.tsx # QR + link + embed sharing
## Core Features to Implement
### 1. Drag-and-Drop Survey Builder
Use @dnd-kit/core with vertical sortable list. Each question card shows: question text, type badge, required toggle, edit/delete icons, drag handle. Clicking a question opens an inline editor panel on the right. Support reordering via drag; persist new display_order to Supabase on drop.
### 2. Question Types
Implement all of: Single Choice (radio buttons + add option), Multiple Choice (checkboxes), Rating (1-5 stars with label customization), Text (short/long toggle), NPS (0-10 horizontal scale with Detractor/Passive/Promoter labels), Matrix (row/column grid), Email, Phone. Each type has its own editor UI and preview renderer.
### 3. Live Preview Panel
Split-screen layout: left 55% is the editor, right 45% is a live preview frame. Preview updates instantly as the user edits. Preview shows the survey exactly as respondents will see it, using the configured theme colors. Add a mobile/desktop toggle button to simulate different viewports.
### 4. Public Response Form
Route: /s/[survey-id]. No authentication required. Before showing the form, check: survey status is 'active', current time is within opens_at/closes_at window, device has not already submitted (check cookie + device fingerprint). Show a branded header with company logo and survey title. Render questions in order, applying conditional logic. Show progress bar. On submit, write response + answers to Supabase in a single transaction using a Server Action. Redirect to /s/[survey-id]/thank-you.
### 5. QR Code Generation
In the Share panel, generate a QR code using qrcode.react pointing to the public survey URL. Provide a Download PNG button. Also show an embed code snippet (<iframe>) and a copy-link button. Track share method in survey_shares table.
### 6. Analytics Dashboard
For each active/closed survey show:
- Summary cards: Total Responses, Completion Rate, Avg. Time to Complete, Response Rate vs. Target
- Line chart: responses per day over time (recharts LineChart)
- Per-question breakdown: for choice questions show a horizontal bar chart with counts and percentages; for rating questions show average score + star distribution; for NPS show gauge with Promoters/Passives/Detractors breakdown and calculated NPS score (% Promoters - % Detractors × 100); for text responses show response list with basic keyword frequency
- NPS formula: NPS = (Promoters% - Detractors%) where Promoters = scores 9-10, Detractors = scores 0-6
### 7. Response Export
Export button downloads all responses as an Excel file using SheetJS. Sheet 1: summary stats. Sheet 2: raw response data with one row per response and one column per question. Include respondent_info fields and submitted_at timestamp.
### 8. Survey Templates
Pre-load 5 templates in the new survey flow: Customer Satisfaction (CSAT), Net Promoter Score (NPS), Employee Satisfaction (eNPS), Event Feedback, Product Feedback. Each template pre-populates questions. User selects a template card then customizes.
### 9. Conditional Logic
For each question, allow adding a condition: show this question only if [previous question] [equals / contains / is greater than] [value]. Store as JSON in questions.conditional_logic: { depends_on_question_id, operator, value }. Evaluate in QuestionRenderer on the client side.
### 10. Survey Scheduling
In survey settings, allow setting opens_at and closes_at datetime fields. Show a status badge: Draft, Scheduled, Active, Closed. The public form respects these dates and shows appropriate messages ("This survey has not opened yet" / "This survey is now closed").
## UI/UX Guidelines
- Admin nav: dark navy (#0F172A) sidebar with survey list, white content area
- Yellow (#FCD34D) for primary CTA buttons (Publish Survey, Copy Link, Export)
- shadcn/ui Card for question cards, Table for response list, Badge for question types and status
- Mobile-first public form: max-w-2xl centered, generous padding, large touch targets for choice options
- Progress bar at top of public form showing % questions answered
- Skeleton loaders for analytics charts while data loads
- Toast notifications (sonner) for save confirmations, publish success, export complete
## Business Rules
- One response per device: set a cookie `survey_{id}_submitted=true` after submit; also check device_fingerprint server-side
- Required questions block form progression if unanswered
- Response limit: if settings.response_limit is set and responses >= limit, close survey automatically via Supabase Edge Function
- Admins can re-open closed surveys and reset individual responses
- Survey URLs are permanent even after closing
## Deliverables
Build the complete application. Start with: (1) database schema and RLS policies, (2) the survey builder page, (3) the public response form, (4) the analytics dashboard. Make sure each is fully functional before moving to the next.