mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
new socials buttons and stuffses
This commit is contained in:
@@ -47,3 +47,17 @@ discord:
|
||||
roles:
|
||||
announcementPing: "123456789012345678"
|
||||
adminPing: "123456789012345678"
|
||||
|
||||
socials:
|
||||
- id: "discord"
|
||||
label: "Discord"
|
||||
url: "https://discord.gg/your-invite"
|
||||
- id: "kofi"
|
||||
label: "Ko-fi"
|
||||
url: "https://ko-fi.com/your-handle"
|
||||
- id: "wiki"
|
||||
label: "Wiki"
|
||||
url: "https://wiki.example.com"
|
||||
- id: "throne"
|
||||
label: "Throne"
|
||||
url: "https://throne.me/yourname"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-BL7iGbVu.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-be89mHSW.css">
|
||||
<script type="module" crossorigin src="/assets/index-COzMxBLj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DKzpxr2C.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -22,8 +22,10 @@ const config = loadConfig();
|
||||
const discordInvite = config.discord?.invite || null;
|
||||
const kofiLink = config.kofi?.link || null;
|
||||
const serverTimezone = config.timezone || null;
|
||||
const configuredSocials = Array.isArray(config.socials) ? config.socials : null;
|
||||
logger.info('Discord invite loaded:', discordInvite ? 'present' : 'not configured');
|
||||
logger.info('Ko-fi link loaded:', kofiLink ? 'present' : 'not configured');
|
||||
logger.info('Socials config loaded:', configuredSocials?.length ? `${configuredSocials.length} entries` : 'not configured');
|
||||
|
||||
const ACTIVITY_SYNC_COOLDOWN_MS = 3000;
|
||||
const NIGHT_VISION_SYNC_COOLDOWN_MS = 1000;
|
||||
@@ -49,6 +51,13 @@ function buildSession(socket) {
|
||||
const users = Array.from(io.sockets.sockets.values())
|
||||
.map((sock) => buildUserEntry(sock))
|
||||
.filter(Boolean);
|
||||
const socials =
|
||||
configuredSocials?.length
|
||||
? configuredSocials
|
||||
: [
|
||||
...(discordInvite ? [{ id: 'discord', label: 'Discord', url: discordInvite }] : []),
|
||||
...(kofiLink ? [{ id: 'kofi', label: 'Ko-fi', url: kofiLink }] : []),
|
||||
];
|
||||
return {
|
||||
socketId: socket?.id || null,
|
||||
role: getRole(socket),
|
||||
@@ -66,6 +75,7 @@ function buildSession(socket) {
|
||||
communityGoal: getCommunityGoal(),
|
||||
adminReason: getAdminReason(),
|
||||
users,
|
||||
socials,
|
||||
discord: {
|
||||
invite: discordInvite,
|
||||
},
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useSession } from "../context/SessionContext";
|
||||
import { FaDiscord } from "react-icons/fa";
|
||||
|
||||
export default function DiscordInviteButton({ text = "Join our Discord!", className = "" }) {
|
||||
const { session } = useSession();
|
||||
const discordInvite = session?.discord?.invite || null;
|
||||
|
||||
if (!discordInvite) return null;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={discordInvite}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white rainbow-animate-bg transition justify-center gap-1 rounded-md ${className}`}
|
||||
// animated rainbow backgound
|
||||
// className="inline-flex items-center px-3 py-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white rounded hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 transition"
|
||||
>
|
||||
<FaDiscord className="mr-0" />
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useSession } from "../context/SessionContext";
|
||||
import { FaCoffee } from "react-icons/fa";
|
||||
|
||||
export default function KoFiButton({ text = "Support me on Ko-fi!", className = "" }) {
|
||||
const { session } = useSession();
|
||||
const kofiLink = session?.kofi?.link || null;
|
||||
|
||||
if (!kofiLink) return null;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={kofiLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`inline-flex items-center w-full px-0.5 py-0.5 text-sm font-medium text-white kofi-animate-bg transition justify-center gap-1 rounded-md ${className}`}
|
||||
>
|
||||
<FaCoffee className="mr-0" />
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import AuthPanel from './AuthPanel.jsx';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
||||
import SocialButton from './SocialButton.jsx';
|
||||
import ChatPanel from './ChatPanel.jsx';
|
||||
import NicknameForm from './NicknameForm.jsx';
|
||||
|
||||
@@ -31,6 +31,13 @@ export default function ModeGateOverlay() {
|
||||
const reason = session?.adminReason?.text || '';
|
||||
const reasonUpdatedAt = session?.adminReason?.updatedAt || null;
|
||||
const timezone = session?.timezone || 'UTC';
|
||||
const discordUrl =
|
||||
session?.socials?.find((entry) => {
|
||||
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
||||
return key === 'discord';
|
||||
})?.url ||
|
||||
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());
|
||||
@@ -81,8 +88,12 @@ export default function ModeGateOverlay() {
|
||||
<div className="surface-muted">
|
||||
<AuthPanel />
|
||||
</div>
|
||||
<div className='w-full justify-center items-center'>
|
||||
<DiscordInviteButton text='Join our Discord server for updates!'/>
|
||||
<div className="w-full justify-center items-center">
|
||||
<SocialButton
|
||||
id="discord"
|
||||
label="Join our Discord server for updates!"
|
||||
url={discordUrl}
|
||||
/>
|
||||
</div>
|
||||
You can still use the chat while the server is locked:
|
||||
{/* set max height of this box */}
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import NicknameForm from './NicknameForm.jsx';
|
||||
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
||||
import KoFiButton from './KoFiButton.jsx';
|
||||
import SocialButtonsGrid from './SocialButtonsGrid.jsx';
|
||||
|
||||
function roleColors(role) {
|
||||
switch (role) {
|
||||
@@ -97,10 +96,7 @@ export default function RawUserPilePanel({
|
||||
<NicknameForm compact={compact} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-0.5 sm:grid-cols-2 md:grid-cols-1">
|
||||
<DiscordInviteButton />
|
||||
<KoFiButton />
|
||||
</div>
|
||||
<SocialButtonsGrid />
|
||||
</div>
|
||||
{!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { FaBook, FaCoffee, FaCrown, FaDiscord, FaLink } from 'react-icons/fa';
|
||||
|
||||
const ICONS_BY_ID = {
|
||||
discord: FaDiscord,
|
||||
kofi: FaCoffee,
|
||||
'ko-fi': FaCoffee,
|
||||
wiki: FaBook,
|
||||
throne: FaCrown,
|
||||
};
|
||||
|
||||
const VARIANT_BY_ID = {
|
||||
discord: 'rainbow-animate-bg',
|
||||
kofi: 'kofi-animate-bg',
|
||||
'ko-fi': 'kofi-animate-bg',
|
||||
wiki: 'wiki-animate-bg',
|
||||
throne: 'throne-animate-bg',
|
||||
};
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
export default function SocialButton({ id, label, url, className = '' }) {
|
||||
if (!url) return null;
|
||||
const key = normalizeId(id, label);
|
||||
const Icon = ICONS_BY_ID[key] || FaLink;
|
||||
const variantClass = VARIANT_BY_ID[key] || 'bg-slate-700 hover:bg-slate-600';
|
||||
const text = label || id || 'Link';
|
||||
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
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 ${variantClass} ${className}`}
|
||||
>
|
||||
<Icon className="mr-0" />
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import SocialButton from './SocialButton.jsx';
|
||||
|
||||
function normalizeSocials(session) {
|
||||
const configured = Array.isArray(session?.socials) ? session.socials : null;
|
||||
if (configured && configured.length) {
|
||||
return configured;
|
||||
}
|
||||
const fallback = [];
|
||||
if (session?.discord?.invite) {
|
||||
fallback.push({ id: 'discord', label: 'Discord', url: session.discord.invite });
|
||||
}
|
||||
if (session?.kofi?.link) {
|
||||
fallback.push({ id: 'kofi', label: 'Ko-fi', url: session.kofi.link });
|
||||
}
|
||||
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 { session } = useSession();
|
||||
const socials = normalizeSocials(session)
|
||||
.map(normalizeEntry)
|
||||
.filter(Boolean)
|
||||
.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} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import { useSocket } from '../context/SocketContext.jsx';
|
||||
import NicknameForm from './NicknameForm.jsx';
|
||||
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
||||
import KoFiButton from './KoFiButton.jsx';
|
||||
import SocialButtonsGrid from './SocialButtonsGrid.jsx';
|
||||
|
||||
export function NicknameEntryPanel({ compact = false }) {
|
||||
return (
|
||||
@@ -19,10 +18,7 @@ export function NicknameEntryPanel({ compact = false }) {
|
||||
export function LinkButtonsPanel() {
|
||||
return (
|
||||
<section className="panel-section flex h-full min-h-0 flex-col gap-0.5 text-base">
|
||||
<div className="grid flex-1 min-h-0 gap-0.5 grid-rows-2">
|
||||
<DiscordInviteButton className="h-full" />
|
||||
<KoFiButton className="h-full" />
|
||||
</div>
|
||||
<SocialButtonsGrid className="flex-1 min-h-0" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -185,10 +181,7 @@ export default function UserListPanel({
|
||||
<NicknameForm compact={compact} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-0.5 sm:grid-cols-2 md:grid-cols-1">
|
||||
<DiscordInviteButton />
|
||||
<KoFiButton />
|
||||
</div>
|
||||
<SocialButtonsGrid />
|
||||
</div>
|
||||
{!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
|
||||
import { useChat } from '../context/ChatContext.jsx';
|
||||
import { useSession } from '../context/SessionContext.jsx';
|
||||
import { useSettingsNamespace } from '../settings/index.js';
|
||||
import DiscordInviteButton from './DiscordInviteButton.jsx';
|
||||
import SocialButton from './SocialButton.jsx';
|
||||
|
||||
const RESTART_DELAY_MS = 2000;
|
||||
const UNMUTE_RETRY_MS = 3000;
|
||||
@@ -61,6 +61,7 @@ export default function VideoTile({
|
||||
isActiveDriver = false,
|
||||
idleSkipSeconds = null,
|
||||
}) {
|
||||
const { session } = useSession();
|
||||
const videoRef = useRef(null);
|
||||
const audioRef = useRef(null);
|
||||
const restartTimer = useRef(null);
|
||||
@@ -110,6 +111,13 @@ export default function VideoTile({
|
||||
const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
|
||||
const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
|
||||
const overlayVisible = Boolean(overlayMotors.length);
|
||||
const discordUrl =
|
||||
session?.socials?.find((entry) => {
|
||||
const key = String(entry?.id || entry?.label || '').toLowerCase();
|
||||
return key === 'discord';
|
||||
})?.url ||
|
||||
session?.discord?.invite ||
|
||||
null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!debugHud) return;
|
||||
@@ -471,7 +479,11 @@ export default function VideoTile({
|
||||
>
|
||||
<div className="text-center">{qualityNotice}</div>
|
||||
<div className="pointer-events-auto mt-0">
|
||||
<DiscordInviteButton text={'Join our Discord server while you wait!'} />
|
||||
<SocialButton
|
||||
id="discord"
|
||||
label="Join our Discord server while you wait!"
|
||||
url={discordUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,6 +127,30 @@ body {
|
||||
background-size: 800% 800%;
|
||||
animation: rainbowBG 240s ease infinite;
|
||||
}
|
||||
|
||||
.wiki-animate-bg {
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
#14b8a6,
|
||||
#38bdf8,
|
||||
#22d3ee,
|
||||
#14b8a6
|
||||
);
|
||||
background-size: 700% 700%;
|
||||
animation: rainbowBG 240s ease infinite;
|
||||
}
|
||||
|
||||
.throne-animate-bg {
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
#f59e0b,
|
||||
#fbbf24,
|
||||
#fde68a,
|
||||
#f59e0b
|
||||
);
|
||||
background-size: 700% 700%;
|
||||
animation: rainbowBG 240s ease infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
Reference in New Issue
Block a user