mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
adding added ad slot support for adddss
This commit is contained in:
@@ -52,6 +52,7 @@ import NeatoCard from './components/NeatoCard/index.jsx';
|
||||
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
|
||||
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
|
||||
import DuplicateIdentityOverlay from './components/DuplicateIdentityOverlay/index.jsx';
|
||||
import DriverAdCard from './components/DriverAdCard/index.jsx';
|
||||
import {
|
||||
DEFAULT_PAGE_THEME_KEY,
|
||||
getPageThemeClass,
|
||||
@@ -66,6 +67,13 @@ function DesktopLayout({ layout, onOpenHelpOverlay }) {
|
||||
<div className={`flex min-w-0 flex-[1.22] flex-col ${themeGapClass} overflow-y-auto pr-0`}>
|
||||
<DriverVideo />
|
||||
<PiHostStatsCard />
|
||||
{/*
|
||||
The ad owns its empty-state gate and uses auto top margin to consume
|
||||
any spare column height. Keeping it as the final desktop-only child
|
||||
pins it to the bottom without introducing layout measurement code or
|
||||
exposing it on mobile and alternate application routes.
|
||||
*/}
|
||||
<DriverAdCard className="mt-auto" />
|
||||
{/* <TelemetryPanel /> */}
|
||||
</div>
|
||||
<div className={`flex min-w-0 flex-1 flex-col ${themeGapClass} overflow-y-auto`}>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Driver Ad Card
|
||||
// Purpose: Renders the optional server-configured advertisement in the desktop
|
||||
// driver column. Scope: This component owns presentation and self-gating only;
|
||||
// the server remains the source of both the title and trusted HTML markup.
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
|
||||
export default function DriverAdCard({ className = '' }) {
|
||||
const driverAd = useSessionSelector((state) => state.session?.driverAd || null);
|
||||
const title = typeof driverAd?.title === 'string' ? driverAd.title : '';
|
||||
const html = typeof driverAd?.html === 'string' ? driverAd.html.trim() : '';
|
||||
|
||||
/*
|
||||
The HTML value is the feature gate. A configured title without content
|
||||
should not reserve desktop space or display an empty CardFrame.
|
||||
*/
|
||||
if (!html) return null;
|
||||
|
||||
return (
|
||||
<CardFrame
|
||||
title={title}
|
||||
/*
|
||||
The card owns its full-width media surface, while each layout owns
|
||||
placement such as desktop auto-spacing or mode-gate centering. This
|
||||
keeps one ad renderer reusable without baking either location into it.
|
||||
*/
|
||||
className={`w-full shrink-0 ${className}`}
|
||||
bodyClassName="w-full overflow-hidden p-0"
|
||||
>
|
||||
{/*
|
||||
This markup is deliberately not sanitized: the requested contract is
|
||||
trusted HTML authored by the server operator. Anyone allowed to edit
|
||||
server configuration must therefore be treated as having control over
|
||||
content rendered in connected driver browsers.
|
||||
|
||||
React's inner-HTML insertion renders markup such as links, images, and
|
||||
iframes, but browsers do not normally execute script elements inserted
|
||||
this way. Providers that require a script loader need a separate,
|
||||
explicit integration rather than silently changing this contract.
|
||||
*/}
|
||||
{/*
|
||||
The slot deliberately imposes no rules on the operator-authored child
|
||||
markup. Images, iframes, or more complex embeds keep their own sizing
|
||||
contract instead of inheriting assumptions from one ad provider.
|
||||
*/}
|
||||
<div className="w-full" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
@@ -195,10 +195,15 @@ export function InterInstancePopup({ onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
function InterInstanceCards({ instances, centered = false }) {
|
||||
function InterInstanceCards({ instances, centered = false, singleColumn = false }) {
|
||||
return (
|
||||
<div className={classNames(
|
||||
'flex flex-wrap justify-center gap-0.5',
|
||||
/*
|
||||
The full browser wraps cards to use available popup space. Restricted
|
||||
mode instead requests one stable vertical column so queue overflow can
|
||||
only add vertical scrolling and can never widen the gate overlay.
|
||||
*/
|
||||
singleColumn ? 'flex flex-col items-center gap-0.5' : 'flex flex-wrap justify-center gap-0.5',
|
||||
centered && 'mx-auto w-full max-w-3xl',
|
||||
)}>
|
||||
{instances.map((remote) => (
|
||||
@@ -235,6 +240,7 @@ export function InterInstanceBrowserFrame({
|
||||
bodyClassName = 'p-0.5',
|
||||
centered = false,
|
||||
scaledOverlay = false,
|
||||
singleColumn = false,
|
||||
}) {
|
||||
const enabled = useInterInstanceEnabled();
|
||||
const instances = useRemoteInstances();
|
||||
@@ -259,7 +265,7 @@ export function InterInstanceBrowserFrame({
|
||||
clipOverflow={false}
|
||||
>
|
||||
{instances.length ? (
|
||||
<InterInstanceCards instances={instances} centered={centered} />
|
||||
<InterInstanceCards instances={instances} centered={centered} singleColumn={singleColumn} />
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">No external instances discovered.</p>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import SocialButton from '../SocialButton/index.jsx';
|
||||
import ChatPanel from '../ChatPanel/index.jsx';
|
||||
import { InterInstanceBrowserFrame } from '../InterInstancePanel/index.jsx';
|
||||
import { isFeatureEnabled } from '../../lib/features.js';
|
||||
import DriverAdCard from '../DriverAdCard/index.jsx';
|
||||
|
||||
const PRIVILEGED_ROLES = new Set(['admin', 'lockdown']);
|
||||
const LOCKDOWN_ROLES = new Set(['lockdown']);
|
||||
@@ -29,22 +30,19 @@ function getModeDetails(mode = 'admin') {
|
||||
};
|
||||
}
|
||||
|
||||
export default function ModeGateOverlay() {
|
||||
const mode = useSessionSelector((state) => state.session?.mode || null);
|
||||
const role = useSessionSelector((state) => state.session?.role || null);
|
||||
const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
|
||||
const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
|
||||
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
|
||||
const restricted = RESTRICTED_MODES.has(mode);
|
||||
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
|
||||
function ServerTime({ timezone }) {
|
||||
/*
|
||||
The overlay is mounted for the whole app, but the server-time display is
|
||||
only visible while access is actually blocked. Gating the shared clock here
|
||||
prevents the hidden overlay from registering a permanent interval.
|
||||
*/
|
||||
const nowMs = useSharedClock(1000, restricted && !privileged);
|
||||
Keep the one-second clock subscription in this leaf component. If the mode
|
||||
gate itself owns the changing timestamp, React rerenders the authentication
|
||||
card, chat, external-instance browser, and raw ad iframe every second even
|
||||
though only this short label changed.
|
||||
|
||||
const serverTime = useMemo(() => {
|
||||
ServerTime is only mounted while the gate is visible, so useSharedClock
|
||||
automatically removes its listener when access is restored and no hidden
|
||||
overlay interval remains active.
|
||||
*/
|
||||
const nowMs = useSharedClock(1000);
|
||||
const formattedTime = useMemo(() => {
|
||||
const now = new Date(nowMs);
|
||||
try {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
@@ -58,6 +56,18 @@ export default function ModeGateOverlay() {
|
||||
}
|
||||
}, [nowMs, timezone]);
|
||||
|
||||
return <p className="text-center text-sm text-slate-300">Server time: {formattedTime}</p>;
|
||||
}
|
||||
|
||||
export default function ModeGateOverlay() {
|
||||
const mode = useSessionSelector((state) => state.session?.mode || null);
|
||||
const role = useSessionSelector((state) => state.session?.role || null);
|
||||
const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
|
||||
const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
|
||||
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
|
||||
const restricted = RESTRICTED_MODES.has(mode);
|
||||
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
|
||||
|
||||
if (!restricted || privileged) {
|
||||
return null;
|
||||
}
|
||||
@@ -66,47 +76,61 @@ export default function ModeGateOverlay() {
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto fixed inset-0 z-50 overflow-y-auto bg-black px-0.5 py-0.5">
|
||||
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col items-center justify-center gap-0.5 lg:flex-row lg:items-center">
|
||||
<div className="surface w-full max-w-md shrink-0 space-y-0.5 text-slate-100 shadow-2xl">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-lg font-semibold">{details.title}</p>
|
||||
<p className="text-sm text-slate-300">{details.description}</p>
|
||||
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-0.5">
|
||||
{/*
|
||||
The access and external-instance cards live in a flexible center
|
||||
region. The ad remains a separate final row, so it stays centered at
|
||||
the bottom of the overlay rather than becoming a third column or
|
||||
shifting the access controls away from the visual center.
|
||||
*/}
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center">
|
||||
<div className="flex w-full min-w-0 flex-col items-center justify-center gap-0.5 lg:flex-row lg:items-center">
|
||||
<div className="surface w-full max-w-md shrink-0 space-y-0.5 text-slate-100 shadow-2xl">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-lg font-semibold">{details.title}</p>
|
||||
<p className="text-sm text-slate-300">{details.description}</p>
|
||||
</div>
|
||||
<div className="surface-muted space-y-0.5">
|
||||
<p className="text-[0.7rem] tracking-wide text-slate-400">Reason for locking:</p>
|
||||
<p className="text-lg font-semibold text-slate-100">
|
||||
{reason ? reason : 'No reason set.'}
|
||||
</p>
|
||||
<ServerTime timezone={timezone} />
|
||||
</div>
|
||||
<div className="surface-muted">
|
||||
<AuthPanel />
|
||||
</div>
|
||||
<SocialButton id="discord" label="Join our Discord server for updates!" />
|
||||
You can still use the chat while the server is locked:
|
||||
{/* set max height of this box */}
|
||||
<div className='max-h-80 overflow-y-auto'>
|
||||
<ChatPanel nicknameLayout="stacked" />
|
||||
</div>
|
||||
</div>
|
||||
{interInstanceEnabled ? (
|
||||
/*
|
||||
Give the mode-gate copy of the shared browser a stable width
|
||||
and allow it to shrink below that width. Scrollable queue
|
||||
descendants must not expand this flex item to their intrinsic
|
||||
content width when the body gains a vertical scrollbar.
|
||||
*/
|
||||
<InterInstanceBrowserFrame
|
||||
hideWhenEmpty
|
||||
scaledOverlay
|
||||
singleColumn
|
||||
className="inter-instance-overlay-frame w-[20.5rem] min-w-0 max-w-full"
|
||||
bodyClassName="inter-instance-overlay-body min-w-0 overflow-x-hidden overflow-y-auto p-0.5"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="surface-muted space-y-0.5">
|
||||
<p className="text-[0.7rem] tracking-wide text-slate-400">Reason for locking:</p>
|
||||
<p className="text-lg font-semibold text-slate-100">
|
||||
{reason ? reason : 'No reason set.'}
|
||||
</p>
|
||||
<p className="text-center text-sm text-slate-300">Server time: {serverTime}</p>
|
||||
</div>
|
||||
<div className="surface-muted">
|
||||
<AuthPanel />
|
||||
</div>
|
||||
<SocialButton id="discord" label="Join our Discord server for updates!" />
|
||||
You can still use the chat while the server is locked:
|
||||
{/* set max height of this box */}
|
||||
<div className='max-h-80 overflow-y-auto'>
|
||||
<ChatPanel nicknameLayout="stacked" />
|
||||
</div>
|
||||
|
||||
{/* <p className="text-xs text-slate-500">
|
||||
Your controls are paused until access is granted. You will automatically regain the interface once the mode
|
||||
changes or after a successful login.
|
||||
</p> */}
|
||||
</div>
|
||||
{interInstanceEnabled ? (
|
||||
/*
|
||||
The external browser is a sibling of the login card, not content
|
||||
inside it. hideWhenEmpty lets the login card remain centered when
|
||||
the directory has no other servers to offer.
|
||||
*/
|
||||
<InterInstanceBrowserFrame
|
||||
hideWhenEmpty
|
||||
scaledOverlay
|
||||
className="inter-instance-overlay-frame"
|
||||
bodyClassName="inter-instance-overlay-body overflow-y-auto p-0.5"
|
||||
/>
|
||||
) : null}
|
||||
{/*
|
||||
DriverAdCard self-gates on configured HTML. When disabled it returns
|
||||
null and consumes no footer space; when enabled this responsive width
|
||||
keeps the card centered without tying the overlay to one provider's
|
||||
creative dimensions.
|
||||
*/}
|
||||
<DriverAdCard className="mx-auto max-w-3xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user