diff --git a/src/app/api/polls/route.ts b/src/app/api/polls/route.ts new file mode 100644 index 00000000..c5ec48a1 --- /dev/null +++ b/src/app/api/polls/route.ts @@ -0,0 +1,63 @@ +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 7bf93d12..ae51bbe8 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -5,6 +5,7 @@ import { PollCreationModal, type PollDraft } from '@/components/polls/PollCreati import { useSettingsStore } from '@/lib/settings/store'; import { useToast } from '@/context/ToastContext'; import { useTheme } from '@/lib/theme-provider'; +import { wsManager } from '@/lib/websocketManager'; import { type ShortcutActionId, type ShortcutCommand, @@ -88,6 +89,7 @@ export function CommandPalette() { const [query, setQuery] = useState(''); const { theme, setTheme } = useTheme(); const [pollModalOpen, setPollModalOpen] = useState(false); + const [isSubmittingPoll, setIsSubmittingPoll] = useState(false); const settings = useSettingsStore((s) => s.settings); const { info: toastInfo } = useToast(); @@ -148,12 +150,6 @@ export function CommandPalette() { setPollModalOpen(true); }, }, - { - id: 'openShortcutHelp', - title: 'Show keyboard shortcuts', - description: 'Open shortcuts help and customization panel', - run: () => setShowHelp(true), - }, ]; }, [setTheme, theme, settings.pollCreationEnabled, toastInfo]); @@ -318,10 +314,37 @@ export function CommandPalette() { setPollModalOpen(false)} - onCreate={(draft: PollDraft) => { - // TODO: integrate with poll creation backend/GraphQL. - // For now, keep placeholder to satisfy typing and modal behavior. - console.log('Create poll draft', draft); + onCreate={async (draft: PollDraft) => { + if (isSubmittingPoll) return; + setIsSubmittingPoll(true); + try { + const options = draft.options.filter(o => o.trim() !== ''); + const res = await fetch('/api/polls', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ question: draft.question, options }), + }); + if (res.ok) { + const { data } = await res.json(); + const statuses = wsManager.getAllStatuses(); + const activeKey = Object.keys(statuses).find(key => statuses[key].isConnected); + if (activeKey) { + const socket = wsManager.getSocket(activeKey); + if (socket) { + socket.emit('collaboration:message', { + type: 'poll:created', + roomId: 'global', + poll: data + }); + } + } + setPollModalOpen(false); + } + } catch (err: any) { + console.error('Failed to submit poll', err); + } finally { + setIsSubmittingPoll(false); + } }} /> 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; };