diff --git a/package.json b/package.json index 95c7d5d1..bc40fed2 100644 --- a/package.json +++ b/package.json @@ -137,22 +137,5 @@ "typescript": "^5.8.3", "vite": "^6.4.3", "vitest": "^3.2.6" - }, - "pnpm": { - "auditConfig": { - "ignoreCves": [], - "ignoreGhsas": [] - }, - "overrides": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "next": "15.5.19", - "vite": ">=6.4.3", - "vitest": ">=3.2.6", - "ws": ">=8.21.0", - "typescript": "^5.8.3", - "eslint": "^9", - "prettier": "^2.8.8" - } } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d9368ad8..1ec5a9ea 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,18 @@ packages: - . - packages/* + +auditConfig: + ignoreCves: [] + ignoreGhsas: [] + +overrides: + react: ^18.3.1 + react-dom: ^18.3.1 + next: 15.5.19 + vite: '>=6.4.3' + vitest: '>=3.2.6' + ws: '>=8.21.0' + typescript: ^5.8.3 + eslint: ^9 + prettier: ^2.8.8 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..7b7d3b74 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, @@ -85,6 +86,7 @@ function ShortcutRow({ export function CommandPalette() { const [open, setOpen] = useState(false); const [showHelp, setShowHelp] = useState(false); + const [isSubmittingPoll, setIsSubmittingPoll] = useState(false); const [query, setQuery] = useState(''); const { theme, setTheme } = useTheme(); const [pollModalOpen, setPollModalOpen] = useState(false); @@ -318,10 +320,42 @@ 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 res = await fetch('/api/polls', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + question: draft.question, + options: draft.options.filter((option) => option.trim()), + durationDays: draft.durationDays, + allowAnonymous: draft.allowAnonymous, + resultsVisibility: draft.resultsVisibility, + }), + }); + + 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); + socket?.emit('collaboration:message', { + type: 'poll:created', + roomId: 'global', + poll: data, + }); + } + } + } catch { + toastInfo('Failed to submit poll.'); + } 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; };