mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
overseer improvements and new social buttones
This commit is contained in:
@@ -6,7 +6,6 @@ import SocialButton from '../../SocialButton/index.jsx';
|
||||
function TurnsOverlay({
|
||||
roverId = null,
|
||||
mobileHud = false,
|
||||
discordUrl: discordUrlProp = null,
|
||||
}) {
|
||||
const assignedRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
||||
const effectiveRoverId = roverId ?? assignedRoverId;
|
||||
@@ -16,19 +15,9 @@ function TurnsOverlay({
|
||||
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
|
||||
const socketId = useSessionSelector((state) => state.session?.socketId || null);
|
||||
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
|
||||
const discordUrl = useSessionSelector((state) => {
|
||||
const socials = state.session?.socials || [];
|
||||
const socialUrl =
|
||||
socials.find((entry) => {
|
||||
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
||||
return key === 'discord';
|
||||
})?.url || null;
|
||||
return socialUrl || state.session?.discord?.invite || null;
|
||||
});
|
||||
const {
|
||||
state: { lastControlIntentAt },
|
||||
} = useControlSystem();
|
||||
const effectiveDiscordUrl = discordUrlProp || discordUrl;
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const [showTurnCue, setShowTurnCue] = useState(false);
|
||||
const [turnCueStartAt, setTurnCueStartAt] = useState(null);
|
||||
@@ -214,11 +203,7 @@ function TurnsOverlay({
|
||||
</div>
|
||||
) : null}
|
||||
<div className="pointer-events-auto mt-0.5">
|
||||
<SocialButton
|
||||
id="discord"
|
||||
label="Join our Discord while you wait!"
|
||||
url={effectiveDiscordUrl}
|
||||
/>
|
||||
<SocialButton id="discord" label="Join our Discord while you wait!" layout='inline'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,14 +32,6 @@ export default function ModeGateOverlay() {
|
||||
const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
|
||||
const reasonUpdatedAt = useSessionSelector((state) => state.session?.adminReason?.updatedAt || null);
|
||||
const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
|
||||
const discordUrl = useSessionSelector((state) => {
|
||||
const socials = state.session?.socials || [];
|
||||
const fromSocials = socials.find((entry) => {
|
||||
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
||||
return key === 'discord';
|
||||
})?.url;
|
||||
return fromSocials || state.session?.discord?.invite || null;
|
||||
});
|
||||
const restricted = RESTRICTED_MODES.has(mode);
|
||||
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
@@ -91,11 +83,7 @@ export default function ModeGateOverlay() {
|
||||
<AuthPanel />
|
||||
</div>
|
||||
<div className="w-full justify-center items-center">
|
||||
<SocialButton
|
||||
id="discord"
|
||||
label="Join our Discord server for updates!"
|
||||
url={discordUrl}
|
||||
/>
|
||||
<SocialButton id="discord" label="Join our Discord server for updates!"/>
|
||||
</div>
|
||||
You can still use the chat while the server is locked:
|
||||
{/* set max height of this box */}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { formatKeyLabel } from '../../controls/keymapUtils.js';
|
||||
import NicknameForm from '../NicknameForm/index.jsx';
|
||||
import SocialButton from '../SocialButton/index.jsx';
|
||||
import KeyPill from '../vip/VipAudioUploadCard/KeyPill.jsx';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
|
||||
function ControlRow({ label, keyLabel }) {
|
||||
return (
|
||||
@@ -56,15 +55,6 @@ export default function QuickstartOverlay({
|
||||
}) {
|
||||
const { state } = useControlSystem();
|
||||
const isDesktop = layout === 'desktop';
|
||||
const discordUrl = useSessionSelector((sessionState) => {
|
||||
const socials = sessionState.session?.socials || [];
|
||||
const entry = socials.find((item) => {
|
||||
const key = String(item?.id || item?.label || '').toLowerCase();
|
||||
return key === 'discord';
|
||||
});
|
||||
return entry?.url || sessionState.session?.discord?.invite || null;
|
||||
});
|
||||
|
||||
const keymap = useMemo(() => state?.keymap || {}, [state?.keymap]);
|
||||
|
||||
if (!visible) return null;
|
||||
@@ -98,7 +88,7 @@ export default function QuickstartOverlay({
|
||||
<div className="surface p-0.5">
|
||||
<p className="text-xl font-semibold text-slate-200">Join our Discord server!</p>
|
||||
<p className="text-sm font-semibold text-slate-200">We have an active and welcoming community :3</p>
|
||||
<SocialButton id="discord" label="Join Discord" url={discordUrl} />
|
||||
<SocialButton id="discord" label="Join Discord" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,97 +1,59 @@
|
||||
// Social Button
|
||||
// Purpose: Defines the Social Button module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
import { useEffect } from 'react';
|
||||
import { FaBook, FaCoffee, FaCrown, FaDiscord, FaLink } from 'react-icons/fa';
|
||||
import * as FaIcons from 'react-icons/fa';
|
||||
import { FaLink } from 'react-icons/fa';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { getSocialById } from '../../lib/socials.js';
|
||||
|
||||
const ICONS_BY_ID = {
|
||||
discord: FaDiscord,
|
||||
kofi: FaCoffee,
|
||||
'ko-fi': FaCoffee,
|
||||
wiki: FaBook,
|
||||
throne: FaCrown,
|
||||
};
|
||||
|
||||
const GRADIENT_STYLE_BY_ID = {
|
||||
discord: {
|
||||
backgroundImage:
|
||||
'linear-gradient(270deg,#5865F2,#404EED,#5865F2)',
|
||||
backgroundSize: '600% 600%',
|
||||
animation: 'socialGradient 180s ease infinite',
|
||||
},
|
||||
kofi: {
|
||||
backgroundImage: 'linear-gradient(270deg,#FF6433,#E04822,#FF6433)',
|
||||
backgroundSize: '600% 600%',
|
||||
animation: 'socialGradient 170s ease infinite',
|
||||
},
|
||||
'ko-fi': {
|
||||
backgroundImage: 'linear-gradient(270deg,#FF6433,#E04822,#FF6433)',
|
||||
backgroundSize: '600% 600%',
|
||||
animation: 'socialGradient 170s ease infinite',
|
||||
},
|
||||
wiki: {
|
||||
backgroundImage: 'linear-gradient(270deg,#EE8019,#C65C08,#EE8019)',
|
||||
backgroundSize: '600% 600%',
|
||||
animation: 'socialGradient 180s ease infinite',
|
||||
},
|
||||
throne: {
|
||||
backgroundImage: 'linear-gradient(270deg,#7C3AED,#5B21B6,#7C3AED)',
|
||||
backgroundSize: '600% 600%',
|
||||
animation: 'socialGradient 180s ease infinite',
|
||||
},
|
||||
};
|
||||
|
||||
function useSocialButtonStyles() {
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
const styleId = 'social-button-keyframes';
|
||||
if (document.getElementById(styleId)) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
@keyframes socialGradient {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}, []);
|
||||
function sanitizeCssColor(value) {
|
||||
if (typeof value !== 'string') return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return trimmed;
|
||||
if (/^(rgb|rgba|hsl|hsla)\([^)]+\)$/.test(trimmed)) return trimmed;
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeId(id, label) {
|
||||
if (typeof id === 'string' && id.trim()) return id.trim().toLowerCase();
|
||||
if (typeof label === 'string' && label.trim()) {
|
||||
return label
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
return '';
|
||||
function resolveIcon(iconName) {
|
||||
if (typeof iconName !== 'string' || !iconName.trim()) return FaLink;
|
||||
const icon = FaIcons[iconName.trim()];
|
||||
return typeof icon === 'function' ? icon : FaLink;
|
||||
}
|
||||
|
||||
export default function SocialButton({ id, label, url, className = '' }) {
|
||||
if (!url) return null;
|
||||
useSocialButtonStyles();
|
||||
const key = normalizeId(id, label);
|
||||
const Icon = ICONS_BY_ID[key] || FaLink;
|
||||
const gradientStyle = GRADIENT_STYLE_BY_ID[key] || null;
|
||||
const text = label || id || 'Link';
|
||||
export default function SocialButton({ id = null, label, url, icon, color, layout = 'stacked', className = '' }) {
|
||||
const socialFromId = useSessionSelector((state) => (id ? getSocialById(state, id) : null));
|
||||
const resolvedUrl = url || socialFromId?.url || null;
|
||||
if (!resolvedUrl) return null;
|
||||
const resolvedIcon = icon || socialFromId?.icon || null;
|
||||
const resolvedColor = color || socialFromId?.color || null;
|
||||
const Icon = resolveIcon(resolvedIcon);
|
||||
const bgColor = sanitizeCssColor(resolvedColor);
|
||||
const text = label || socialFromId?.label || 'Link';
|
||||
const isInline = layout === 'inline';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
href={resolvedUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={text}
|
||||
className={`inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white transition justify-center gap-1 rounded-md ${
|
||||
gradientStyle ? '' : 'bg-slate-700 hover:bg-slate-600'
|
||||
} ${className}`}
|
||||
style={gradientStyle || undefined}
|
||||
className={`grid h-full min-h-0 w-full place-items-center rounded-md bg-slate-700 px-0.5 py-0.5 text-center text-sm font-medium text-white transition hover:opacity-90 ${className}`}
|
||||
style={bgColor ? { backgroundColor: bgColor } : undefined}
|
||||
>
|
||||
<Icon className="mr-0" />
|
||||
{text}
|
||||
{isInline ? (
|
||||
<span className="inline-flex max-w-full items-center justify-center gap-1 text-center leading-tight">
|
||||
<Icon className="shrink-0" style={{ fontSize: '1.1em' }} />
|
||||
<span className="break-words">{text}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex h-full w-full max-w-full flex-col items-center justify-between text-center leading-tight">
|
||||
<span className="flex min-h-0 flex-1 items-center justify-center">
|
||||
<Icon className="shrink-0" style={{ fontSize: 'clamp(1rem, 2.2vh + 1.2vw, 2rem)' }} />
|
||||
</span>
|
||||
<span className="w-full break-words leading-tight">{text}</span>
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,46 +4,18 @@
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import SocialButton from '../SocialButton/index.jsx';
|
||||
|
||||
function normalizeSocials({ socials, discordInvite, kofiLink }) {
|
||||
const configured = Array.isArray(socials) ? socials : null;
|
||||
if (configured && configured.length) {
|
||||
return configured;
|
||||
}
|
||||
const fallback = [];
|
||||
if (discordInvite) {
|
||||
fallback.push({ id: 'discord', label: 'Discord', url: discordInvite });
|
||||
}
|
||||
if (kofiLink) {
|
||||
fallback.push({ id: 'kofi', label: 'Ko-fi', url: kofiLink });
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeEntry(entry) {
|
||||
if (!entry || typeof entry !== 'object') return null;
|
||||
const id = entry.id || entry.service || entry.key || entry.name || null;
|
||||
const label = entry.label || entry.title || entry.name || id || 'Link';
|
||||
const url = entry.url || entry.link || entry.href || null;
|
||||
return url ? { id, label, url } : null;
|
||||
}
|
||||
|
||||
export default function SocialButtonsGrid({ className = '' }) {
|
||||
const socialsInput = useSessionSelector((state) => ({
|
||||
socials: state.session?.socials ?? [],
|
||||
discordInvite: state.session?.discord?.invite || null,
|
||||
kofiLink: state.session?.kofi?.link || null,
|
||||
}));
|
||||
const socials = normalizeSocials(socialsInput)
|
||||
.map(normalizeEntry)
|
||||
.filter(Boolean)
|
||||
.slice(0, 4);
|
||||
const socials = useSessionSelector((state) => state.session?.socials ?? []).slice(0, 4);
|
||||
|
||||
if (!socials.length) return null;
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-2 grid-rows-2 gap-0.5 ${className}`}>
|
||||
{socials.map((entry) => (
|
||||
<SocialButton key={`${entry.id || entry.label}-${entry.url}`} {...entry} />
|
||||
<SocialButton
|
||||
key={`${entry?.id || entry?.label || 'social'}-${entry?.url || ''}`}
|
||||
id={entry?.id || entry?.key || entry?.service || null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function getSessionSocials(state) {
|
||||
const socials = state?.session?.socials;
|
||||
return Array.isArray(socials) ? socials : [];
|
||||
}
|
||||
|
||||
export function getSocialById(state, socialId) {
|
||||
const key = String(socialId || '').trim().toLowerCase();
|
||||
if (!key) return null;
|
||||
return (
|
||||
getSessionSocials(state).find((entry) => {
|
||||
const entryKey = String(entry?.id || entry?.label || '').trim().toLowerCase();
|
||||
return entryKey === key;
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user