rmaps-online/src/stores/room.ts

347 lines
9.1 KiB
TypeScript

import { create } from 'zustand';
import { nanoid } from 'nanoid';
import type {
Room,
Participant,
ParticipantLocation,
ParticipantStatus,
Waypoint,
RoomSettings,
PrecisionLevel,
Route,
RouteSegment,
} from '@/types';
// Route state for navigation
interface ActiveRoute {
id: string;
from: {
type: 'participant' | 'waypoint' | 'current';
id?: string;
name: string;
};
to: {
type: 'participant' | 'waypoint';
id: string;
name: string;
};
segments: RouteSegment[];
totalDistance: number;
estimatedTime: number;
summary: string;
isLoading: boolean;
error?: string;
}
// Color palette for participants
const COLORS = [
'#10b981', // emerald
'#6366f1', // indigo
'#f59e0b', // amber
'#ef4444', // red
'#8b5cf6', // violet
'#ec4899', // pink
'#14b8a6', // teal
'#f97316', // orange
'#84cc16', // lime
'#06b6d4', // cyan
];
interface RoomState {
room: Room | null;
participants: Participant[];
currentParticipantId: string | null;
isConnected: boolean;
error: string | null;
activeRoute: ActiveRoute | null;
// Actions
joinRoom: (slug: string, name: string, emoji: string) => void;
leaveRoom: () => void;
updateParticipant: (updates: Partial<Participant>) => void;
updateLocation: (location: ParticipantLocation) => void;
setStatus: (status: ParticipantStatus) => void;
addWaypoint: (waypoint: Omit<Waypoint, 'id' | 'createdAt' | 'createdBy'>) => void;
removeWaypoint: (waypointId: string) => void;
// Route actions
navigateTo: (target: { type: 'participant' | 'waypoint'; id: string }) => Promise<void>;
clearRoute: () => void;
// Internal
_syncFromDocument: (doc: unknown) => void;
}
export const useRoomStore = create<RoomState>((set, get) => ({
room: null,
participants: [],
currentParticipantId: null,
isConnected: false,
error: null,
activeRoute: null,
joinRoom: (slug: string, name: string, emoji: string) => {
const participantId = nanoid();
const colorIndex = Math.floor(Math.random() * COLORS.length);
const participant: Participant = {
id: participantId,
name,
emoji,
color: COLORS[colorIndex],
joinedAt: new Date(),
lastSeen: new Date(),
status: 'online',
privacySettings: {
sharingEnabled: true,
defaultPrecision: 'exact' as PrecisionLevel,
showIndoorFloor: true,
ghostMode: false,
},
};
// Create or join room
const room: Room = {
id: nanoid(),
slug,
name: slug,
createdAt: new Date(),
createdBy: participantId,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
settings: {
maxParticipants: 10,
defaultPrecision: 'exact' as PrecisionLevel,
allowGuestJoin: true,
showC3NavIndoor: true,
},
participants: new Map([[participantId, participant]]),
waypoints: [],
};
set({
room,
participants: [participant],
currentParticipantId: participantId,
isConnected: true,
error: null,
});
// TODO: Connect to Automerge sync server
console.log(`Joined room: ${slug} as ${name} (${emoji})`);
},
leaveRoom: () => {
const { room, currentParticipantId } = get();
if (room && currentParticipantId) {
room.participants.delete(currentParticipantId);
}
set({
room: null,
participants: [],
currentParticipantId: null,
isConnected: false,
});
},
updateParticipant: (updates: Partial<Participant>) => {
const { room, currentParticipantId, participants } = get();
if (!room || !currentParticipantId) return;
const current = room.participants.get(currentParticipantId);
if (!current) return;
const updated = { ...current, ...updates, lastSeen: new Date() };
room.participants.set(currentParticipantId, updated);
set({
participants: participants.map((p) =>
p.id === currentParticipantId ? updated : p
),
});
},
updateLocation: (location: ParticipantLocation) => {
get().updateParticipant({ location });
},
setStatus: (status: ParticipantStatus) => {
get().updateParticipant({ status });
},
addWaypoint: (waypoint) => {
const { room, currentParticipantId } = get();
if (!room || !currentParticipantId) return;
const newWaypoint: Waypoint = {
...waypoint,
id: nanoid(),
createdAt: new Date(),
createdBy: currentParticipantId,
};
room.waypoints.push(newWaypoint);
set({ room: { ...room } });
},
removeWaypoint: (waypointId: string) => {
const { room } = get();
if (!room) return;
room.waypoints = room.waypoints.filter((w) => w.id !== waypointId);
set({ room: { ...room } });
},
navigateTo: async (target: { type: 'participant' | 'waypoint'; id: string }) => {
const { participants, room, currentParticipantId } = get();
// Get current user's location
const currentUser = participants.find((p) => p.id === currentParticipantId);
if (!currentUser?.location) {
set({
activeRoute: {
id: nanoid(),
from: { type: 'current', name: 'You' },
to: { type: target.type, id: target.id, name: 'Target' },
segments: [],
totalDistance: 0,
estimatedTime: 0,
summary: '',
isLoading: false,
error: 'Enable location sharing to get directions',
},
});
return;
}
// Get destination
let destLocation: { latitude: number; longitude: number; indoor?: { level: number; x: number; y: number } } | null = null;
let destName = '';
if (target.type === 'participant') {
const participant = participants.find((p) => p.id === target.id);
if (participant?.location) {
destLocation = {
latitude: participant.location.latitude,
longitude: participant.location.longitude,
indoor: participant.location.indoor,
};
destName = participant.name;
}
} else if (target.type === 'waypoint') {
const waypoint = room?.waypoints.find((w) => w.id === target.id);
if (waypoint) {
destLocation = {
latitude: waypoint.location.latitude,
longitude: waypoint.location.longitude,
indoor: waypoint.location.indoor,
};
destName = waypoint.name;
}
}
if (!destLocation) {
set({
activeRoute: {
id: nanoid(),
from: { type: 'current', name: currentUser.name },
to: { type: target.type, id: target.id, name: destName || 'Unknown' },
segments: [],
totalDistance: 0,
estimatedTime: 0,
summary: '',
isLoading: false,
error: 'Destination location not available',
},
});
return;
}
// Set loading state
set({
activeRoute: {
id: nanoid(),
from: { type: 'current', name: currentUser.name },
to: { type: target.type, id: target.id, name: destName },
segments: [],
totalDistance: 0,
estimatedTime: 0,
summary: '',
isLoading: true,
},
});
try {
const response = await fetch('/api/routing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
origin: {
latitude: currentUser.location.latitude,
longitude: currentUser.location.longitude,
indoor: currentUser.location.indoor,
},
destination: destLocation,
mode: 'walking',
eventId: room?.settings.eventId || '39c3',
}),
});
const data = await response.json();
if (data.success && data.route) {
set({
activeRoute: {
id: nanoid(),
from: { type: 'current', name: currentUser.name },
to: { type: target.type, id: target.id, name: destName },
segments: data.route.segments,
totalDistance: data.route.totalDistance,
estimatedTime: data.route.estimatedTime,
summary: data.route.summary,
isLoading: false,
},
});
} else {
set({
activeRoute: {
id: nanoid(),
from: { type: 'current', name: currentUser.name },
to: { type: target.type, id: target.id, name: destName },
segments: [],
totalDistance: 0,
estimatedTime: 0,
summary: '',
isLoading: false,
error: data.error || 'Could not calculate route',
},
});
}
} catch (error) {
console.error('Navigation error:', error);
set({
activeRoute: {
id: nanoid(),
from: { type: 'current', name: currentUser.name },
to: { type: target.type, id: target.id, name: destName },
segments: [],
totalDistance: 0,
estimatedTime: 0,
summary: '',
isLoading: false,
error: 'Failed to calculate route',
},
});
}
},
clearRoute: () => {
set({ activeRoute: null });
},
_syncFromDocument: (doc: unknown) => {
// TODO: Implement Automerge document sync
console.log('Sync from document:', doc);
},
}));