new socials buttons and stuffses

This commit is contained in:
legop3
2026-02-10 20:02:55 -05:00
parent 1ac46f4a03
commit 42c99410f4
16 changed files with 296 additions and 187 deletions
+14
View File
@@ -47,3 +47,17 @@ discord:
roles: roles:
announcementPing: "123456789012345678" announcementPing: "123456789012345678"
adminPing: "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
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-BL7iGbVu.js"></script> <script type="module" crossorigin src="/assets/index-COzMxBLj.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-be89mHSW.css"> <link rel="stylesheet" crossorigin href="/assets/index-DKzpxr2C.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+10
View File
@@ -22,8 +22,10 @@ const config = loadConfig();
const discordInvite = config.discord?.invite || null; const discordInvite = config.discord?.invite || null;
const kofiLink = config.kofi?.link || null; const kofiLink = config.kofi?.link || null;
const serverTimezone = config.timezone || 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('Discord invite loaded:', discordInvite ? 'present' : 'not configured');
logger.info('Ko-fi link loaded:', kofiLink ? '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 ACTIVITY_SYNC_COOLDOWN_MS = 3000;
const NIGHT_VISION_SYNC_COOLDOWN_MS = 1000; const NIGHT_VISION_SYNC_COOLDOWN_MS = 1000;
@@ -49,6 +51,13 @@ function buildSession(socket) {
const users = Array.from(io.sockets.sockets.values()) const users = Array.from(io.sockets.sockets.values())
.map((sock) => buildUserEntry(sock)) .map((sock) => buildUserEntry(sock))
.filter(Boolean); .filter(Boolean);
const socials =
configuredSocials?.length
? configuredSocials
: [
...(discordInvite ? [{ id: 'discord', label: 'Discord', url: discordInvite }] : []),
...(kofiLink ? [{ id: 'kofi', label: 'Ko-fi', url: kofiLink }] : []),
];
return { return {
socketId: socket?.id || null, socketId: socket?.id || null,
role: getRole(socket), role: getRole(socket),
@@ -66,6 +75,7 @@ function buildSession(socket) {
communityGoal: getCommunityGoal(), communityGoal: getCommunityGoal(),
adminReason: getAdminReason(), adminReason: getAdminReason(),
users, users,
socials,
discord: { discord: {
invite: discordInvite, 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>
);
}
-21
View File
@@ -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>
);
}
+14 -3
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import AuthPanel from './AuthPanel.jsx'; import AuthPanel from './AuthPanel.jsx';
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
import DiscordInviteButton from './DiscordInviteButton.jsx'; import SocialButton from './SocialButton.jsx';
import ChatPanel from './ChatPanel.jsx'; import ChatPanel from './ChatPanel.jsx';
import NicknameForm from './NicknameForm.jsx'; import NicknameForm from './NicknameForm.jsx';
@@ -31,6 +31,13 @@ export default function ModeGateOverlay() {
const reason = session?.adminReason?.text || ''; const reason = session?.adminReason?.text || '';
const reasonUpdatedAt = session?.adminReason?.updatedAt || null; const reasonUpdatedAt = session?.adminReason?.updatedAt || null;
const timezone = session?.timezone || 'UTC'; 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 restricted = RESTRICTED_MODES.has(mode);
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role); const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
const [now, setNow] = useState(() => new Date()); const [now, setNow] = useState(() => new Date());
@@ -81,8 +88,12 @@ export default function ModeGateOverlay() {
<div className="surface-muted"> <div className="surface-muted">
<AuthPanel /> <AuthPanel />
</div> </div>
<div className='w-full justify-center items-center'> <div className="w-full justify-center items-center">
<DiscordInviteButton text='Join our Discord server for updates!'/> <SocialButton
id="discord"
label="Join our Discord server for updates!"
url={discordUrl}
/>
</div> </div>
You can still use the chat while the server is locked: You can still use the chat while the server is locked:
{/* set max height of this box */} {/* set max height of this box */}
+2 -6
View File
@@ -3,8 +3,7 @@ import { useSession } from '../context/SessionContext.jsx';
import { useSettingsNamespace } from '../settings/index.js'; import { useSettingsNamespace } from '../settings/index.js';
import { useSocket } from '../context/SocketContext.jsx'; import { useSocket } from '../context/SocketContext.jsx';
import NicknameForm from './NicknameForm.jsx'; import NicknameForm from './NicknameForm.jsx';
import DiscordInviteButton from './DiscordInviteButton.jsx'; import SocialButtonsGrid from './SocialButtonsGrid.jsx';
import KoFiButton from './KoFiButton.jsx';
function roleColors(role) { function roleColors(role) {
switch (role) { switch (role) {
@@ -97,10 +96,7 @@ export default function RawUserPilePanel({
<NicknameForm compact={compact} /> <NicknameForm compact={compact} />
</div> </div>
</div> </div>
<div className="grid gap-0.5 sm:grid-cols-2 md:grid-cols-1"> <SocialButtonsGrid />
<DiscordInviteButton />
<KoFiButton />
</div>
</div> </div>
{!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>} {!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>}
</div> </div>
+50
View File
@@ -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 -10
View File
@@ -3,8 +3,7 @@ import { useSession } from '../context/SessionContext.jsx';
import { useSettingsNamespace } from '../settings/index.js'; import { useSettingsNamespace } from '../settings/index.js';
import { useSocket } from '../context/SocketContext.jsx'; import { useSocket } from '../context/SocketContext.jsx';
import NicknameForm from './NicknameForm.jsx'; import NicknameForm from './NicknameForm.jsx';
import DiscordInviteButton from './DiscordInviteButton.jsx'; import SocialButtonsGrid from './SocialButtonsGrid.jsx';
import KoFiButton from './KoFiButton.jsx';
export function NicknameEntryPanel({ compact = false }) { export function NicknameEntryPanel({ compact = false }) {
return ( return (
@@ -19,10 +18,7 @@ export function NicknameEntryPanel({ compact = false }) {
export function LinkButtonsPanel() { export function LinkButtonsPanel() {
return ( return (
<section className="panel-section flex h-full min-h-0 flex-col gap-0.5 text-base"> <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"> <SocialButtonsGrid className="flex-1 min-h-0" />
<DiscordInviteButton className="h-full" />
<KoFiButton className="h-full" />
</div>
</section> </section>
); );
} }
@@ -185,10 +181,7 @@ export default function UserListPanel({
<NicknameForm compact={compact} /> <NicknameForm compact={compact} />
</div> </div>
</div> </div>
<div className="grid gap-0.5 sm:grid-cols-2 md:grid-cols-1"> <SocialButtonsGrid />
<DiscordInviteButton />
<KoFiButton />
</div>
</div> </div>
{!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>} {!canSetNickname && <p className="text-xs text-slate-500">Spectators cannot set nicknames.</p>}
</div> </div>
+14 -2
View File
@@ -5,7 +5,7 @@ import { useHudMapSetting } from '../hooks/useHudMapSetting.js';
import { useChat } from '../context/ChatContext.jsx'; import { useChat } from '../context/ChatContext.jsx';
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
import { useSettingsNamespace } from '../settings/index.js'; import { useSettingsNamespace } from '../settings/index.js';
import DiscordInviteButton from './DiscordInviteButton.jsx'; import SocialButton from './SocialButton.jsx';
const RESTART_DELAY_MS = 2000; const RESTART_DELAY_MS = 2000;
const UNMUTE_RETRY_MS = 3000; const UNMUTE_RETRY_MS = 3000;
@@ -61,6 +61,7 @@ export default function VideoTile({
isActiveDriver = false, isActiveDriver = false,
idleSkipSeconds = null, idleSkipSeconds = null,
}) { }) {
const { session } = useSession();
const videoRef = useRef(null); const videoRef = useRef(null);
const audioRef = useRef(null); const audioRef = useRef(null);
const restartTimer = useRef(null); const restartTimer = useRef(null);
@@ -110,6 +111,13 @@ export default function VideoTile({
const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : []; const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0); const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
const overlayVisible = Boolean(overlayMotors.length); 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(() => { useEffect(() => {
if (!debugHud) return; if (!debugHud) return;
@@ -471,7 +479,11 @@ export default function VideoTile({
> >
<div className="text-center">{qualityNotice}</div> <div className="text-center">{qualityNotice}</div>
<div className="pointer-events-auto mt-0"> <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> </div>
</div> </div>
+24
View File
@@ -127,6 +127,30 @@ body {
background-size: 800% 800%; background-size: 800% 800%;
animation: rainbowBG 240s ease infinite; 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 { @layer utilities {