trails/apps/planner/app/components/ParticipantList.tsx
Ullrich Schäfer 650fa0f36a
Fix mobile header layout: hide secondary items on small screens
- Participant list: hidden on mobile (sm:block)
- Undo/redo buttons: hidden on mobile (sm:flex)
- Connection status: hidden on mobile (sm:inline)
- Header: no-wrap, tighter padding on mobile
- Username input: text-base on mobile to prevent iOS auto-zoom

Keeps profile selector and export button visible on all sizes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:39:19 +02:00

136 lines
4 KiB
TypeScript

import { useEffect, useState, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import type { YjsState } from "~/lib/use-yjs";
import { electHost } from "~/lib/host-election";
interface Participant {
clientId: number;
name: string;
color: string;
isHost: boolean;
isLocal: boolean;
}
interface ParticipantListProps {
yjs: YjsState;
}
export function ParticipantList({ yjs }: ParticipantListProps) {
const { t } = useTranslation("planner");
const [participants, setParticipants] = useState<Participant[]>([]);
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const updateParticipants = useCallback(() => {
const states = yjs.awareness.getStates() as Map<number, Record<string, unknown>>;
const localId = yjs.awareness.clientID;
const list: Participant[] = [];
states.forEach((state, clientId) => {
const user = state.user as { color: string; name: string } | undefined;
if (!user) return;
const { isHost } = electHost(states, clientId);
list.push({
clientId,
name: user.name,
color: user.color,
isHost,
isLocal: clientId === localId,
});
});
// Sort: local user first, then host, then alphabetical
list.sort((a, b) => {
if (a.isLocal !== b.isLocal) return a.isLocal ? -1 : 1;
if (a.isHost !== b.isHost) return a.isHost ? -1 : 1;
return a.name.localeCompare(b.name);
});
setParticipants(list);
}, [yjs.awareness]);
useEffect(() => {
yjs.awareness.on("change", updateParticipants);
updateParticipants();
return () => {
yjs.awareness.off("change", updateParticipants);
};
}, [yjs.awareness, updateParticipants]);
const startEditing = () => {
const localParticipant = participants.find((p) => p.isLocal);
if (localParticipant) {
setEditValue(localParticipant.name);
setEditing(true);
}
};
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
const commitEdit = () => {
const trimmed = editValue.trim();
if (trimmed && trimmed.length <= 20) {
yjs.setUserName(trimmed);
}
setEditing(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
commitEdit();
} else if (e.key === "Escape") {
setEditing(false);
}
};
return (
<div className="flex items-center gap-1.5">
{participants.map((p) => (
<div key={p.clientId} className="flex items-center gap-1">
<span
className="inline-block h-2.5 w-2.5 rounded-full flex-shrink-0"
style={{ backgroundColor: p.color }}
/>
{p.isLocal && editing ? (
<input
ref={inputRef}
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onBlur={commitEdit}
onKeyDown={handleKeyDown}
maxLength={20}
className="w-20 rounded border border-gray-300 px-1 py-0 text-base sm:text-xs text-gray-700 focus:border-blue-500 focus:outline-none"
/>
) : (
<button
type="button"
onClick={p.isLocal ? startEditing : undefined}
className={`text-xs text-gray-700 ${p.isLocal ? "cursor-pointer hover:underline" : "cursor-default"}`}
title={p.isLocal ? t("participants.editName") : p.name}
>
{p.name}
{p.isLocal && (
<span className="ml-0.5 text-gray-400">{t("participants.you")}</span>
)}
</button>
)}
{p.isHost && (
<span className="rounded-full bg-green-100 px-1.5 py-0 text-[10px] text-green-700">
{t("participants.host")}
</span>
)}
</div>
))}
</div>
);
}