Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,5 @@
"typescript": "^5.8.3",
"vite": "^5.4.19",
"vitest": "^2.1.9"
},
"pnpm": {
"overrides": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"next": "15.3.1",
"typescript": "^5.8.3",
"eslint": "^9",
"prettier": "^2.8.8"
}
}
}
8 changes: 8 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
packages:
- .
- packages/*

overrides:
react: ^18.3.1
react-dom: ^18.3.1
next: 15.3.1
typescript: ^5.8.3
eslint: ^9
prettier: ^2.8.8
63 changes: 63 additions & 0 deletions src/app/api/polls/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
43 changes: 33 additions & 10 deletions src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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]);

Expand Down Expand Up @@ -318,10 +314,37 @@ export function CommandPalette() {
<PollCreationModal
isOpen={pollModalOpen}
onClose={() => 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);
}
}}
/>
</>
Expand Down
5 changes: 5 additions & 0 deletions src/features/collaboration/server/webSocketServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 11 additions & 0 deletions src/features/collaboration/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};