From 4ec604be49ecc19f640eca48a13964d170325b87 Mon Sep 17 00:00:00 2001 From: victor-134 Date: Sun, 28 Jun 2026 16:55:05 +0100 Subject: [PATCH 1/4] feat: Add poll creation UI and backend integration --- src/app/api/polls/route.ts | 60 +++++++ src/components/CommandPalette.tsx | 153 ++++++++++++++++++ .../collaboration/server/webSocketServer.ts | 5 + src/features/collaboration/types.ts | 11 ++ 4 files changed, 229 insertions(+) create mode 100644 src/app/api/polls/route.ts diff --git a/src/app/api/polls/route.ts b/src/app/api/polls/route.ts new file mode 100644 index 00000000..ed647303 --- /dev/null +++ b/src/app/api/polls/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from 'next/server'; +import { query } from '@/lib/db/pool'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const courseId = searchParams.get('course_id'); + + let polls; + if (courseId) { + const result = await query('SELECT * FROM polls WHERE course_id = $1 ORDER BY created_at DESC', [courseId]); + polls = result.rows; + } else { + const result = await query('SELECT * FROM polls ORDER BY created_at DESC LIMIT 100'); + polls = result.rows; + } + + return NextResponse.json({ data: polls }); + } catch (error) { + console.error('Failed to fetch polls:', error); + return NextResponse.json({ error: 'Failed to fetch polls' }, { status: 500 }); + } +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { id, question, options, course_id, created_by } = body; + + // Create table if it doesn't exist + await query(` + CREATE TABLE IF NOT EXISTS polls ( + id VARCHAR(255) PRIMARY KEY, + question TEXT NOT NULL, + options JSONB NOT NULL, + course_id VARCHAR(255), + created_by VARCHAR(255), + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + ) + `); + + const result = await query( + \`INSERT INTO polls (id, question, options, course_id, created_by) + VALUES ($1, $2, $3, $4, $5) + RETURNING *\`, + [ + id || crypto.randomUUID(), + question, + JSON.stringify(options || []), + course_id || null, + created_by || 'anonymous' + ] + ); + + return NextResponse.json({ data: result.rows[0] }, { status: 201 }); + } catch (error) { + console.error('Failed to create poll:', error); + return NextResponse.json({ error: 'Failed to create poll' }, { status: 500 }); + } +} diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 57eec3c0..affedf56 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import { useTheme } from '@/lib/theme-provider'; +import { wsManager } from '@/lib/websocketManager'; import { type ShortcutActionId, type ShortcutCommand, @@ -82,6 +83,11 @@ function ShortcutRow({ export function CommandPalette() { const [open, setOpen] = useState(false); const [showHelp, setShowHelp] = useState(false); + const [showPollUI, setShowPollUI] = useState(false); + const [pollQuestion, setPollQuestion] = useState(''); + const [pollOptions, setPollOptions] = useState(['', '']); + const [isSubmittingPoll, setIsSubmittingPoll] = useState(false); + const [query, setQuery] = useState(''); const { theme, setTheme } = useTheme(); @@ -135,6 +141,12 @@ export function CommandPalette() { description: 'Open shortcuts help and customization panel', run: () => setShowHelp(true), }, + { + id: 'createPoll', + title: 'Create Poll', + description: 'Create a new poll for the session', + run: () => setShowPollUI(true), + }, ]; }, [setTheme, theme]); @@ -295,6 +307,147 @@ export function CommandPalette() { ) : null} + + {showPollUI ? ( + <> +
setShowPollUI(false)} + aria-hidden="true" + /> +
+
+

+ Create Poll +

+ +
+ +
+
+ + setPollQuestion(e.target.value)} + placeholder="Ask a question..." + className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100" + /> +
+ +
+ +
+ {pollOptions.map((opt, idx) => ( +
+ { + const newOpts = [...pollOptions]; + newOpts[idx] = e.target.value; + setPollOptions(newOpts); + }} + placeholder={\`Option \${idx + 1}\`} + className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100" + /> + {pollOptions.length > 2 && ( + + )} +
+ ))} +
+ {pollOptions.length < 5 && ( + + )} +
+ +
+ + +
+
+
+ + ) : null} ); } diff --git a/src/features/collaboration/server/webSocketServer.ts b/src/features/collaboration/server/webSocketServer.ts index 3c1e0d90..84eb2336 100644 --- a/src/features/collaboration/server/webSocketServer.ts +++ b/src/features/collaboration/server/webSocketServer.ts @@ -83,6 +83,11 @@ export const setupCollaborationWebSocketServer = (httpServer: HttpServer): Socke return; } + if (message.type === 'poll:created' || message.type === 'poll:vote') { + socket.to(message.roomId).emit('collaboration:message', message); + return; + } + if (message.type === 'operation') { const roomState = getRoomState(message.roomId); diff --git a/src/features/collaboration/types.ts b/src/features/collaboration/types.ts index ed4a346f..87eedef4 100644 --- a/src/features/collaboration/types.ts +++ b/src/features/collaboration/types.ts @@ -66,4 +66,15 @@ export type CollaborationMessage = type: 'error'; roomId: string; message: string; + } + | { + type: 'poll:created'; + roomId: string; + poll: any; + } + | { + type: 'poll:vote'; + roomId: string; + pollId: string; + optionIndex: number; }; From 47cb02c8aa2614a871ebc570ae4f563c9c44f8d3 Mon Sep 17 00:00:00 2001 From: victor-134 Date: Sun, 28 Jun 2026 18:55:22 +0100 Subject: [PATCH 2/4] Fix typescript and lint errors in CommandPalette --- src/components/CommandPalette.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 76587e39..ae51bbe8 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -318,7 +318,7 @@ export function CommandPalette() { if (isSubmittingPoll) return; setIsSubmittingPoll(true); try { - const options = draft.options.filter(o => o.text.trim()).map(o => o.text); + const options = draft.options.filter(o => o.trim() !== ''); const res = await fetch('/api/polls', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -340,7 +340,7 @@ export function CommandPalette() { } setPollModalOpen(false); } - } catch (err) { + } catch (err: any) { console.error('Failed to submit poll', err); } finally { setIsSubmittingPoll(false); From 78fe06a7b4c475a26d7fb045ef38fba8c4a42d62 Mon Sep 17 00:00:00 2001 From: victor-134 Date: Sun, 28 Jun 2026 18:59:33 +0100 Subject: [PATCH 3/4] Fix syntax error in route.ts --- src/app/api/polls/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/polls/route.ts b/src/app/api/polls/route.ts index ed647303..2df4f411 100644 --- a/src/app/api/polls/route.ts +++ b/src/app/api/polls/route.ts @@ -40,9 +40,9 @@ export async function POST(request: Request) { `); const result = await query( - \`INSERT INTO polls (id, question, options, course_id, created_by) + `INSERT INTO polls (id, question, options, course_id, created_by) VALUES ($1, $2, $3, $4, $5) - RETURNING *\`, + RETURNING *`, [ id || crypto.randomUUID(), question, From 701bb958da53807a6c8e3962b5b7038c421b5325 Mon Sep 17 00:00:00 2001 From: victor-134 Date: Sun, 28 Jun 2026 19:06:16 +0100 Subject: [PATCH 4/4] fix: rewrite polls route.ts to eliminate hidden characters causing TS1127 --- src/app/api/polls/route.ts | 45 ++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/app/api/polls/route.ts b/src/app/api/polls/route.ts index 2df4f411..c5ec48a1 100644 --- a/src/app/api/polls/route.ts +++ b/src/app/api/polls/route.ts @@ -5,16 +5,19 @@ export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const courseId = searchParams.get('course_id'); - + let polls; if (courseId) { - const result = await query('SELECT * FROM polls WHERE course_id = $1 ORDER BY created_at DESC', [courseId]); + const result = await query( + 'SELECT * FROM polls WHERE course_id = $1 ORDER BY created_at DESC', + [courseId], + ); polls = result.rows; } else { const result = await query('SELECT * FROM polls ORDER BY created_at DESC LIMIT 100'); polls = result.rows; } - + return NextResponse.json({ data: polls }); } catch (error) { console.error('Failed to fetch polls:', error); @@ -26,32 +29,32 @@ export async function POST(request: Request) { try { const body = await request.json(); const { id, question, options, course_id, created_by } = body; - + // Create table if it doesn't exist - await query(` - CREATE TABLE IF NOT EXISTS polls ( - id VARCHAR(255) PRIMARY KEY, - question TEXT NOT NULL, - options JSONB NOT NULL, - course_id VARCHAR(255), - created_by VARCHAR(255), - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP - ) - `); - + await query( + 'CREATE TABLE IF NOT EXISTS polls (' + + 'id VARCHAR(255) PRIMARY KEY, ' + + 'question TEXT NOT NULL, ' + + 'options JSONB NOT NULL, ' + + 'course_id VARCHAR(255), ' + + 'created_by VARCHAR(255), ' + + 'created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP' + + ')', + ); + const result = await query( - `INSERT INTO polls (id, question, options, course_id, created_by) - VALUES ($1, $2, $3, $4, $5) - RETURNING *`, + 'INSERT INTO polls (id, question, options, course_id, created_by) ' + + 'VALUES ($1, $2, $3, $4, $5) ' + + 'RETURNING *', [ id || crypto.randomUUID(), question, JSON.stringify(options || []), course_id || null, - created_by || 'anonymous' - ] + created_by || 'anonymous', + ], ); - + return NextResponse.json({ data: result.rows[0] }, { status: 201 }); } catch (error) { console.error('Failed to create poll:', error);