Create a poll in seconds β Share the link β Watch votes roll in live
No sign-up. No forms. Just instant feedback.
ItsMyScreen is a full-stack real-time polling application. Users create polls with questions and options, share a link, and anyone with the link can vote. Results update live for all viewersβno refresh needed.
Built as a modern, production-ready demo of Next.js 16, Supabase, and Tailwind CSS.
| Create | Share | Vote | Results |
|---|---|---|---|
| Question + 2β10 options in under 10 seconds | One-click copy link or QR code | Anyone with the link can vote (single-choice) | Live updates for everyone via Realtime + polling |
| Feature | Details |
|---|---|
| Question & options | Question (max 200 chars), optional description (300 chars), 2β10 options (100 chars each) |
| 6 templates | Yes/No, 1β5 Scale, Simple Choice, Feedback, Meeting Time, Topic Vote |
| Live preview | See your poll as you type, with sticky sidebar on desktop |
| Validation | Duplicate option detection, character counters, required fields |
| URL templates | /create?template=yes-no pre-fills the form |
| Feature | Details |
|---|---|
| Single-choice | One vote per person per poll |
| Change vote | Switch your choice before the poll ends |
| Real-time sync | Supabase Realtime subscriptions + payload-based updates (no full refetch) |
| Polling fallback | 5-second polling when Realtime doesnβt deliver |
| Optimistic UI | Instant feedback when you vote; state updates before server response |
| Animated bars | Progress bars and percentages animate when votes change |
| Feature | Details |
|---|---|
| Copy link | One-click copy with clipboard API + execCommand fallback |
| QR code | Generate scannable QR for in-person voting |
| CSV export | Download results (Option, Votes, Percentage) |
| Print-friendly layout; hides nav, footer, non-essential UI |
| Feature | Details |
|---|---|
| Browse | Search by question, sort by most recent or most votes |
| Quick-create | Template buttons on Browse page β jump to Create with form pre-filled |
| Recent polls | Home page shows 6 most recent community polls |
| Feature | Details |
|---|---|
| Theme | Light, warm off-white with orange accent (#c2410c) |
| Typography | DM Sans |
| Confetti | CSS confetti on first vote |
| Toasts | Non-intrusive feedback for actions (vote, copy, errors) |
| Live badge | Pulsing green dot for real-time connection |
| Loading | Skeleton loaders (home, browse, poll), spinners |
| Animations | Staggered fade-in, scale-in, smooth transitions |
| Responsive | Mobile-first; safe-area support for notched devices |
| Error states | Poll not found, 404, error boundary with retry |
| Layer | Technology | Notes |
|---|---|---|
| Framework | Next.js 16.1.6 | App Router, Turbopack, React 19 |
| Language | TypeScript 5 | Strict mode |
| Styling | Tailwind CSS 4 | Custom theme, animations |
| Backend | Supabase | PostgreSQL, Realtime, RLS |
| Icons | Lucide React | |
| QR | qrcode.react | |
| Deployment | Vercel | Edge, serverless |
| Table | Purpose |
|---|---|
polls |
id (UUID), question, description, created_at, created_by |
options |
id, poll_id (FK), text, vote_count |
votes |
id, poll_id, option_id, voter_token, created_at; unique on (poll_id, voter_token) |
vote(p_poll_id, p_option_id, p_voter_token)β Inserts vote, increments optionvote_count. Validates option belongs to poll.change_vote(p_poll_id, p_old_option_id, p_new_option_id, p_voter_token)β Deletes old vote, inserts new one; updates both option counts.
Tables polls, options, votes are in supabase_realtime publication.
All tables have RLS enabled. Policies allow anonymous select/insert; delete on polls restricted to created_by.
ItsMyScreen/
βββ app/
β βββ components/
β β βββ Confetti.tsx # CSS confetti on vote
β β βββ Footer.tsx # Branded footer
β β βββ Navbar.tsx # Minimal navbar (logo)
β βββ create/
β β βββ page.tsx # Poll creation + templates + live preview
β βββ poll/
β β βββ [id]/
β β βββ layout.tsx # Metadata
β β βββ loading.tsx # Skeleton loading
β β βββ opengraph-image.tsx # Dynamic OG image for sharing
β β βββ page.tsx # Vote, results, share, QR, CSV, print
β βββ polls/
β β βββ page.tsx # Browse, search, sort, quick-create
β βββ globals.css # Theme, components, keyframes
β βββ layout.tsx # Root layout, viewport, metadata
β βββ page.tsx # Home: hero, features, how it works, recent polls
β βββ not-found.tsx # 404 page
β βββ error.tsx # Error boundary
β βββ global-error.tsx # Root error boundary
βββ utils/
β βββ pollTemplates.ts # 6 templates + getTemplateById
β βββ sanitize.ts # sanitizeText (trim + length limit)
β βββ supabase.ts # Supabase client
β βββ timeAgo.ts # Relative time ("5m ago")
βββ supabase/
β βββ schema.sql # Base schema
β βββ migrations/ # Incremental migrations
βββ scripts/
β βββ apply-schema.js # Apply schema to Supabase (optional)
βββ .env.local # NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY
- Create β User goes to
/create, picks a template or writes from scratch, adds options, clicks Create. - Redirect β App creates poll + options in DB, redirects to
/poll/[id]. - Share β User copies link or shows QR code.
- Vote β Visitors open link, vote (single choice), can change vote.
- Results β All viewers see live updates via Realtime; 5s polling as fallback.
- Node.js 18+
- Supabase account (free tier)
git clone https://github.com/SriramDivi1/ItsMyScreen.git
cd ItsMyScreen
npm install- Create a project at supabase.com.
- Apply schema:
- Option A: Run
npm run db:sql, copy output, paste into Supabase β SQL Editor, run. - Option B:
SUPABASE_PROJECT_REF=xxx SUPABASE_DB_PASSWORD=xxx npm run db:apply
- Option A: Run
- Copy Project URL and Anon Key from Settings β API.
Create .env.local:
NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_keynpm run devOpen http://localhost:3000.
| Route | Description |
|---|---|
/ |
Home β hero, features, how it works, 6 recent polls |
/create |
Create poll; ?template=id pre-fills form |
/polls |
Browse β search, sort, quick-create templates |
/poll/[id] |
Poll view β vote, results, share, QR, CSV, print |
- Push to GitHub.
- Import repo at vercel.com.
- Add env vars:
NEXT_PUBLIC_SUPABASE_URL,NEXT_PUBLIC_SUPABASE_ANON_KEY. - Deploy.
npm run build # Test locally| Script | Description |
|---|---|
npm run dev |
Start dev server |
npm run build |
Production build |
npm run start |
Start production server |
npm run db:sql |
Print schema + migrations for SQL Editor |
npm run db:apply |
Apply schema via pg (needs DB credentials) |
- What it does: Each browser gets a unique token (
crypto.randomUUID) inlocalStorage. DB enforcesunique(poll_id, voter_token). - What it prevents: Same user voting multiple times from the same browser on the same poll.
- Limitations: Clearing storage, incognito, or another device/browser = new token = another vote. Acceptable for no-sign-up.
- What it does: 2s cooldown between vote attempts (including vote changes). Blocked attempts show a toast.
- What it prevents: Double-clicks, rapid automated clicking, simple bot abuse.
- Limitations: Determined attackers can space out votes; cooldown improves UX and slows basic abuse.
Additional: RPCs vote and change_vote validate that the option belongs to the poll before counting.
- Invalid poll ID β "Poll not found" + link home
- Duplicate options β Client validation + highlight
- Option validation β RPC rejects invalid options
- Clipboard fallback β
execCommand('copy')ifnavigator.clipboardunavailable - Voter token fallback β Session token if
localStoragefails - Realtime fallback β 5s polling
- Input limits β 200 / 300 / 100 chars; sanitized before DB
- Invalid dates β
timeAgoreturns empty string - Empty options β Fallback message
- Orphan voted option β "You voted for" only when option exists
- No poll expiry or closure
- Device-based token (multiple devices = multiple votes)
- No auth β no poll ownership
- No API rate limiting
- Future: Poll expiry, CAPTCHA, optional sign-in, email verification
Built with β€οΈ By Sriram using Next.js & Supabase