diff --git a/src/components/OrbitParticipantsPopover.tsx b/src/components/OrbitParticipantsPopover.tsx index e5a58a9a..b7d4457b 100644 --- a/src/components/OrbitParticipantsPopover.tsx +++ b/src/components/OrbitParticipantsPopover.tsx @@ -1,10 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { Crown, UserMinus, ShieldOff, Copy, Check } from 'lucide-react'; +import { Crown, User, UserMinus, ShieldOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useOrbitStore } from '../store/orbitStore'; -import { useAuthStore } from '../store/authStore'; -import { kickOrbitParticipant, removeOrbitParticipant, buildOrbitShareLink } from '../utils/orbit'; +import { kickOrbitParticipant, removeOrbitParticipant } from '../utils/orbit'; import ConfirmModal from './ConfirmModal'; interface Props { @@ -27,25 +26,10 @@ export default function OrbitParticipantsPopover({ anchorRef, onClose }: Props) const { t } = useTranslation(); const state = useOrbitStore(s => s.state); const role = useOrbitStore(s => s.role); - const sessionId = useOrbitStore(s => s.sessionId); const popRef = useRef(null); - const [copied, setCopied] = useState(false); const [confirm, setConfirm] = useState<{ user: string; mode: 'remove' | 'ban' } | null>(null); const nowMs = Date.now(); - const shareLink = role === 'host' && sessionId - ? buildOrbitShareLink(useAuthStore.getState().getActiveServer()?.url ?? '', sessionId) - : null; - - const onCopy = async () => { - if (!shareLink) return; - try { - await navigator.clipboard.writeText(shareLink); - setCopied(true); - window.setTimeout(() => setCopied(false), 1500); - } catch { /* silent */ } - }; - // Close on outside click / Escape — unless a confirm dialog is open // (otherwise outside-clicking the modal would dismiss the popover too, // and re-opening would lose the in-flight confirm context). @@ -92,24 +76,6 @@ export default function OrbitParticipantsPopover({ anchorRef, onClose }: Props) return createPortal( <>
- {shareLink && ( -
-
{t('orbit.participantsInviteLabel')}
-
- {shareLink} - -
-
- )} -
{t('orbit.participantsCountLabel', { count: state.participants.length + 1 })}
@@ -126,6 +92,7 @@ export default function OrbitParticipantsPopover({ anchorRef, onClose }: Props) {state.participants.map(p => (
+ {p.user} {joinedFor(p.joinedAt, nowMs)} {role === 'host' && ( diff --git a/src/components/OrbitSessionBar.tsx b/src/components/OrbitSessionBar.tsx index d33ed06f..5c18f648 100644 --- a/src/components/OrbitSessionBar.tsx +++ b/src/components/OrbitSessionBar.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { X, RefreshCw, Shuffle, Settings2 } from 'lucide-react'; +import { X, RefreshCw, Shuffle, Settings2, Share2 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useOrbitStore } from '../store/orbitStore'; import { usePlayerStore, songToTrack } from '../store/playerStore'; @@ -14,6 +14,7 @@ import { estimateLivePosition } from '../api/orbit'; import OrbitParticipantsPopover from './OrbitParticipantsPopover'; import OrbitExitModal from './OrbitExitModal'; import OrbitSettingsPopover from './OrbitSettingsPopover'; +import OrbitSharePopover from './OrbitSharePopover'; import ConfirmModal from './ConfirmModal'; /** @@ -46,9 +47,11 @@ export default function OrbitSessionBar() { const [nowMs, setNowMs] = useState(() => Date.now()); const [peopleOpen, setPeopleOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); + const [shareOpen, setShareOpen] = useState(false); const [confirmLeave, setConfirmLeave] = useState(false); const peopleBtnRef = useRef(null); const settingsBtnRef = useRef(null); + const shareBtnRef = useRef(null); // Second-level tick just for the shuffle countdown + drift readout — // the store itself only ticks at 2.5 s which is too coarse for a smooth @@ -179,6 +182,20 @@ export default function OrbitSessionBar() { )} + {role === 'host' && ( + + )} {showCatchUp && ( +
+
, + document.body, + ); +} diff --git a/src/locales/de.ts b/src/locales/de.ts index 437d4a5b..22d6e51b 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1480,6 +1480,7 @@ export const deTranslation = { hostLabel: 'Gastgeber: {{name}}', participantsTooltip: 'Teilnehmer', settingsTooltip: 'Session-Einstellungen', + shareTooltip: 'Einladungslink teilen', shuffleLabel: 'Warteschlange wird neu gemischt in', catchUpLabel: 'aufholen', catchUpTooltip: 'Zur aktuellen Host-Position springen', diff --git a/src/locales/en.ts b/src/locales/en.ts index 5fc0dc7d..6bac73bf 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1483,6 +1483,7 @@ export const enTranslation = { hostLabel: 'Host: {{name}}', participantsTooltip: 'Participants', settingsTooltip: 'Session settings', + shareTooltip: 'Share invite link', shuffleLabel: 'Queue reshuffles in', catchUpLabel: 'catch up', catchUpTooltip: "Jump to the host's current position", diff --git a/src/styles/components.css b/src/styles/components.css index 0575b834..c197d3aa 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -11689,22 +11689,27 @@ html[data-psy-native-hidden="true"] *::after { box-shadow: 0 10px 30px rgba(0,0,0,0.5); } -.orbit-participants-pop__invite { - padding: 8px 8px 10px; - margin-bottom: 4px; - border-bottom: 1px solid color-mix(in srgb, var(--text-primary) 8%, transparent); +/* Share popover (host-only) — standalone counterpart to the participants popover. */ +.orbit-share-pop { + min-width: 320px; + max-width: 380px; + padding: 12px 14px 14px; + background: var(--ctp-base, #1e1e2e); + border: 1px solid color-mix(in srgb, var(--accent) 22%, rgba(255,255,255,0.1)); + border-radius: var(--radius-md); + box-shadow: 0 10px 30px rgba(0,0,0,0.5); } -.orbit-participants-pop__invite-label { +.orbit-share-pop__label { font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); - margin-bottom: 6px; + margin-bottom: 8px; } -.orbit-participants-pop__invite-row { +.orbit-share-pop__row { display: flex; align-items: center; gap: 6px; @@ -11714,7 +11719,7 @@ html[data-psy-native-hidden="true"] *::after { padding: 6px 6px 6px 8px; } -.orbit-participants-pop__invite-link { +.orbit-share-pop__link { flex: 1; min-width: 0; overflow: hidden; @@ -11729,7 +11734,7 @@ html[data-psy-native-hidden="true"] *::after { user-select: all; } -.orbit-participants-pop__invite-copy { +.orbit-share-pop__copy { flex: 0 0 auto; display: inline-flex; align-items: center; @@ -11744,7 +11749,7 @@ html[data-psy-native-hidden="true"] *::after { cursor: pointer; transition: filter 0.15s ease; } -.orbit-participants-pop__invite-copy:hover { filter: brightness(1.1); } +.orbit-share-pop__copy:hover { filter: brightness(1.1); } .orbit-participants-pop__head { font-size: 10px; @@ -11768,6 +11773,12 @@ html[data-psy-native-hidden="true"] *::after { .orbit-participants-pop__row--host { color: var(--accent); } .orbit-participants-pop__row--host svg { color: var(--accent); } +/* Guest rows — give the user icon a quieter tone so the host's crown stays + the accent focal point. */ +.orbit-participants-pop__row:not(.orbit-participants-pop__row--host) > svg { + color: var(--text-muted); + flex-shrink: 0; +} .orbit-participants-pop__name { flex: 1;