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:
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
+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-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>
+10
View File
@@ -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>
);
}
-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 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 */}
+2 -6
View File
@@ -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>
+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 { 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>
+14 -2
View File
@@ -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>
+24
View File
@@ -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 {