Skip to content

Commit 6f4a261

Browse files
committed
feat: allow role switching
1 parent ce81bf7 commit 6f4a261

File tree

6 files changed

+111
-0
lines changed

6 files changed

+111
-0
lines changed

client/src/Session.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,18 @@ export const Session = () => {
407407
localStorage.setItem(STORAGE_KEY_DARK_MODE, String(newMode));
408408
};
409409

410+
const toggleRole = () => {
411+
const currentUser = users.find(u => u.id === currentUserId);
412+
if (!currentUser) return;
413+
414+
const newRole = currentUser.role === 'observer' ? 'participant' : 'observer';
415+
sendMessage({
416+
type: MessageType.CHANGE_ROLE,
417+
role: newRole,
418+
});
419+
setNotification(`Switched to ${newRole} role`);
420+
};
421+
410422
const colors = darkMode ? {
411423
background: '#1a1a1a',
412424
surface: '#2d2d2d',
@@ -465,6 +477,9 @@ export const Session = () => {
465477
<button onClick={toggleDarkMode} style={styles.darkModeButton}>
466478
{darkMode ? '☀️' : '🌙'}
467479
</button>
480+
<button onClick={toggleRole} style={styles.roleButton}>
481+
{users.find(u => u.id === currentUserId)?.role === 'observer' ? '👤 Join as Participant' : '👁️ Switch to Observer'}
482+
</button>
468483
<button onClick={copySessionId} style={styles.copyButton}>
469484
Copy Session ID
470485
</button>
@@ -630,6 +645,15 @@ const getStyles = (colors: any): { [key: string]: React.CSSProperties } => ({
630645
borderRadius: '4px',
631646
cursor: 'pointer',
632647
},
648+
roleButton: {
649+
padding: '8px 16px',
650+
fontSize: '14px',
651+
backgroundColor: colors.surface,
652+
color: colors.text,
653+
border: `2px solid ${colors.border}`,
654+
borderRadius: '4px',
655+
cursor: 'pointer',
656+
},
633657
copyButton: {
634658
padding: '8px 16px',
635659
fontSize: '14px',

client/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export enum MessageType {
1919
STORY_FOCUSED = 'story_focused',
2020
UNFOCUS_STORY = 'unfocus_story',
2121
STORY_UNFOCUSED = 'story_unfocused',
22+
CHANGE_ROLE = 'change_role',
23+
ROLE_CHANGED = 'role_changed',
2224
AI_ANALYSIS_STARTED = 'ai_analysis_started',
2325
AI_RECOMMENDATION = 'ai_recommendation',
2426
LOAD_MORE_STORIES = 'load_more_stories',
@@ -108,6 +110,11 @@ export interface UnfocusStoryMessage {
108110
type: MessageType.UNFOCUS_STORY;
109111
}
110112

113+
export interface ChangeRoleMessage {
114+
type: MessageType.CHANGE_ROLE;
115+
role: string;
116+
}
117+
111118
export interface LoadMoreStoriesMessage {
112119
type: MessageType.LOAD_MORE_STORIES;
113120
offset: number;
@@ -125,6 +132,7 @@ export type ClientMessage =
125132
| ResetVotesMessage
126133
| SetFocusedStoryMessage
127134
| UnfocusStoryMessage
135+
| ChangeRoleMessage
128136
| LoadMoreStoriesMessage;
129137

130138
export interface UserJoinedMessage {
@@ -181,6 +189,12 @@ export interface StoryUnfocusedMessage {
181189
type: MessageType.STORY_UNFOCUSED;
182190
}
183191

192+
export interface RoleChangedMessage {
193+
type: MessageType.ROLE_CHANGED;
194+
userId: string;
195+
role: string;
196+
}
197+
184198
export interface AIAnalysisStartedMessage {
185199
type: MessageType.AI_ANALYSIS_STARTED;
186200
storyId: string;
@@ -236,6 +250,7 @@ export type ServerMessage =
236250
| VotesResetMessage
237251
| StoryFocusedMessage
238252
| StoryUnfocusedMessage
253+
| RoleChangedMessage
239254
| AIAnalysisStartedMessage
240255
| AIRecommendationMessage
241256
| PastStoriesLoadedMessage

client/src/useWebSocket.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,19 @@ export const useWebSocket = (
255255
})));
256256
break;
257257

258+
case MessageType.ROLE_CHANGED:
259+
setUsers((prev) => prev.map(user =>
260+
user.id === message.userId
261+
? { ...user, role: message.role }
262+
: user
263+
));
264+
// Update localStorage if it's the current user
265+
const roleChangedUser = users.find(u => u.name === userName);
266+
if (roleChangedUser?.id === message.userId) {
267+
localStorage.setItem('planning_poker_user_role', message.role);
268+
}
269+
break;
270+
258271
case MessageType.AI_ANALYSIS_STARTED:
259272
setAiLoadingMap((prev) => {
260273
const newMap = new Map(prev);

server/src/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
VotesResetMessage,
2323
StoryFocusedMessage,
2424
StoryUnfocusedMessage,
25+
RoleChangedMessage,
2526
AIAnalysisStartedMessage,
2627
AIRecommendationMessage,
2728
PastStoriesLoadedMessage,
@@ -513,6 +514,36 @@ wss.on('connection', (ws: WebSocket) => {
513514
break;
514515
}
515516

517+
case MessageType.CHANGE_ROLE: {
518+
const sessionInfo = userToSession.get(ws);
519+
if (!sessionInfo) return;
520+
521+
const { role } = message;
522+
523+
// Validate role
524+
if (role !== 'participant' && role !== 'observer') {
525+
const error: ErrorMessage = {
526+
type: MessageType.ERROR,
527+
message: 'Invalid role. Must be "participant" or "observer"',
528+
};
529+
ws.send(JSON.stringify(error));
530+
break;
531+
}
532+
533+
const success = await sessionManager.changeUserRole(sessionInfo.userId, role);
534+
535+
if (success) {
536+
const roleChanged: RoleChangedMessage = {
537+
type: MessageType.ROLE_CHANGED,
538+
userId: sessionInfo.userId,
539+
role,
540+
};
541+
await sessionManager.broadcast(sessionInfo.sessionId, roleChanged);
542+
console.log(`User ${sessionInfo.userId} changed role to ${role} in session ${sessionInfo.sessionId}`);
543+
}
544+
break;
545+
}
546+
516547
case MessageType.DELETE_STORY: {
517548
const sessionInfo = userToSession.get(ws);
518549
if (!sessionInfo) return;

server/src/sessionManager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,19 @@ export class SessionManager {
747747
}
748748
}
749749

750+
async changeUserRole(userId: string, role: string): Promise<boolean> {
751+
try {
752+
await this.prisma.user.update({
753+
where: { id: userId },
754+
data: { role },
755+
});
756+
return true;
757+
} catch (error) {
758+
console.error('Error changing user role:', error);
759+
return false;
760+
}
761+
}
762+
750763
async getUserName(sessionId: string, userId: string): Promise<string | null> {
751764
try {
752765
const user = await this.prisma.user.findUnique({

server/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export enum MessageType {
4949
STORY_FOCUSED = 'story_focused',
5050
UNFOCUS_STORY = 'unfocus_story',
5151
STORY_UNFOCUSED = 'story_unfocused',
52+
CHANGE_ROLE = 'change_role',
53+
ROLE_CHANGED = 'role_changed',
5254
AI_ANALYSIS_STARTED = 'ai_analysis_started',
5355
AI_RECOMMENDATION = 'ai_recommendation',
5456
LOAD_MORE_STORIES = 'load_more_stories',
@@ -110,6 +112,11 @@ export interface UnfocusStoryMessage {
110112
type: MessageType.UNFOCUS_STORY;
111113
}
112114

115+
export interface ChangeRoleMessage {
116+
type: MessageType.CHANGE_ROLE;
117+
role: string;
118+
}
119+
113120
export interface DeleteStoryMessage {
114121
type: MessageType.DELETE_STORY;
115122
storyId: string;
@@ -132,6 +139,7 @@ export type ClientMessage =
132139
| ResetVotesMessage
133140
| SetFocusedStoryMessage
134141
| UnfocusStoryMessage
142+
| ChangeRoleMessage
135143
| LoadMoreStoriesMessage;
136144

137145
export interface UserJoinedMessage {
@@ -196,6 +204,12 @@ export interface StoryUnfocusedMessage {
196204
type: MessageType.STORY_UNFOCUSED;
197205
}
198206

207+
export interface RoleChangedMessage {
208+
type: MessageType.ROLE_CHANGED;
209+
userId: string;
210+
role: string;
211+
}
212+
199213
export interface StoryDeletedMessage {
200214
type: MessageType.STORY_DELETED;
201215
storyId: string;
@@ -264,6 +278,7 @@ export type ServerMessage =
264278
| VotesResetMessage
265279
| StoryFocusedMessage
266280
| StoryUnfocusedMessage
281+
| RoleChangedMessage
267282
| AIAnalysisStartedMessage
268283
| AIRecommendationMessage
269284
| PastStoriesLoadedMessage

0 commit comments

Comments
 (0)