diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index eb155e3..fb0fca2 100644
--- a/frontend/src/components/NavBarItem.tsx
+++ b/frontend/src/components/NavBarItem.tsx
@@ -1,6 +1,5 @@
-import React, {useEffect, useRef} from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
-import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
diff --git a/frontend/src/helpers/modularInteractionHub.ts b/frontend/src/helpers/modularInteractionHub.ts
new file mode 100644
index 0000000..85ee5d1
--- /dev/null
+++ b/frontend/src/helpers/modularInteractionHub.ts
@@ -0,0 +1,179 @@
+export type HubLinkType = 'agent' | 'demo' | 'form' | 'social' | 'media';
+
+export type HubLink = {
+ id: string;
+ title: string;
+ url: string;
+ type: HubLinkType;
+ summary: string;
+ eyebrow: string;
+ accent: string;
+};
+
+export type MediaPresetType = 'radio' | 'tv';
+export type MediaPresetMode = 'audio' | 'video' | 'embed';
+
+export type MediaPreset = {
+ id: string;
+ type: MediaPresetType;
+ title: string;
+ url: string;
+ notes: string;
+ mode: MediaPresetMode;
+ isSample?: boolean;
+};
+
+export type AdminShortcut = {
+ title: string;
+ href: string;
+ summary: string;
+};
+
+export const modularInteractionLinks: HubLink[] = [
+ {
+ id: 'agent-prof-aliyo',
+ title: 'Prof Aliyo Momot AI Agent',
+ url: 'https://prof-aliyo-momot-ai-agent.onhercules.app/agent',
+ type: 'agent',
+ eyebrow: 'AI agent',
+ summary: 'Primary conversational entry point for visitors who want guided AI interaction.',
+ accent: 'from-fuchsia-500/20 via-violet-500/10 to-cyan-400/20',
+ },
+ {
+ id: 'agent-frame-smith',
+ title: 'Frame Smith AI',
+ url: 'https://frame-smith-ai.lovable.app',
+ type: 'agent',
+ eyebrow: 'Creative workflow',
+ summary: 'A second AI experience positioned for making, framing, and ideation workflows.',
+ accent: 'from-sky-500/20 via-cyan-500/10 to-emerald-400/20',
+ },
+ {
+ id: 'demo-aliyo-media',
+ title: 'Aliyo Momot Media Demo',
+ url: 'https://aliyo-momot-media-fb6f.dev.flatlogic.app/?stream=d5ade7f8-7b12-4b2b-997e-4f1de9180e9b',
+ type: 'media',
+ eyebrow: 'Media experience',
+ summary: 'Existing demo destination for media-first storytelling and streaming discovery.',
+ accent: 'from-amber-500/20 via-orange-500/10 to-rose-400/20',
+ },
+ {
+ id: 'demo-copyright-revealer',
+ title: 'Copyright Revealer',
+ url: 'https://copyright-revealer-cc67.dev.flatlogic.app',
+ type: 'demo',
+ eyebrow: 'Specialized demo',
+ summary: 'A supporting utility demo that expands the modular toolset around your concept.',
+ accent: 'from-indigo-500/20 via-blue-500/10 to-cyan-400/20',
+ },
+ {
+ id: 'form-1',
+ title: 'Fillout Intake 01',
+ url: 'https://build.fillout.com/use/fwxccezugw',
+ type: 'form',
+ eyebrow: 'Fillout form',
+ summary: 'Capture structured visitor input without forcing them to leave your ecosystem.',
+ accent: 'from-lime-500/20 via-emerald-500/10 to-cyan-400/20',
+ },
+ {
+ id: 'form-2',
+ title: 'Fillout Intake 02',
+ url: 'https://build.fillout.com/use/fouwuywqjg',
+ type: 'form',
+ eyebrow: 'Fillout form',
+ summary: 'Useful for onboarding, submissions, or modular requests from the public.',
+ accent: 'from-purple-500/20 via-pink-500/10 to-rose-400/20',
+ },
+ {
+ id: 'form-3',
+ title: 'Fillout Intake 03',
+ url: 'https://build.fillout.com/use/1e7hvg7j95',
+ type: 'form',
+ eyebrow: 'Fillout form',
+ summary: 'A third intake surface that keeps lead capture visible inside the link dashboard.',
+ accent: 'from-cyan-500/20 via-blue-500/10 to-violet-400/20',
+ },
+ {
+ id: 'form-4',
+ title: 'Fillout Intake 04',
+ url: 'https://build.fillout.com/use/j1yzoiedhq',
+ type: 'form',
+ eyebrow: 'Fillout form',
+ summary: 'Additional form endpoint for follow-ups, media submissions, or campaign flows.',
+ accent: 'from-pink-500/20 via-rose-500/10 to-orange-400/20',
+ },
+ {
+ id: 'social-facebook',
+ title: 'Facebook Community Profile',
+ url: 'https://www.facebook.com/profile.php?id=61567817647812',
+ type: 'social',
+ eyebrow: 'Social presence',
+ summary: 'A community touchpoint for followers arriving from Facebook and public promotion.',
+ accent: 'from-blue-500/20 via-indigo-500/10 to-violet-400/20',
+ },
+];
+
+export const starterMediaPresets: MediaPreset[] = [
+ {
+ id: 'sample-radio',
+ type: 'radio',
+ title: 'Sample Radio Placeholder',
+ url: 'https://samplelib.com/lib/preview/mp3/sample-12s.mp3',
+ notes: 'Replace this with your live radio stream URL when you are ready.',
+ mode: 'audio',
+ isSample: true,
+ },
+ {
+ id: 'sample-tv',
+ type: 'tv',
+ title: 'Sample TV Placeholder',
+ url: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
+ notes: 'This placeholder shows the TV widget layout until you paste a real embed or video stream.',
+ mode: 'video',
+ isSample: true,
+ },
+];
+
+export const adminShortcuts: AdminShortcut[] = [
+ {
+ title: 'Admin interface',
+ href: '/login',
+ summary: 'Sign in to manage all generated CRUD screens and protected content modules.',
+ },
+ {
+ title: 'External links manager',
+ href: '/external_links/external_links-list',
+ summary: 'Use the existing CRUD to curate agents, demos, forms, and social links.',
+ },
+ {
+ title: 'Media streams manager',
+ href: '/media_streams/media_streams-list',
+ summary: 'Manage stream records for radio and television sources inside the admin area.',
+ },
+ {
+ title: 'Widgets manager',
+ href: '/widgets/widgets-list',
+ summary: 'Configure reusable radio, TV, AI, and embed widgets for future authenticated workflows.',
+ },
+];
+
+export const hubHighlights = [
+ {
+ title: 'Modular by design',
+ description: 'Visitors can move between AI, media, and forms without losing the overall story.',
+ },
+ {
+ title: 'Public-friendly',
+ description: 'The front door is open, branded, and focused on exploration instead of admin jargon.',
+ },
+ {
+ title: 'Admin-ready',
+ description: 'Existing CRUD screens stay in place so you can manage content once you log in.',
+ },
+];
+
+export const experienceSteps = [
+ 'Open the public landing page and understand the Modular Artificial Interaction concept at a glance.',
+ 'Jump into the interaction hub to browse AI agents, demos, forms, and community links.',
+ 'Preview radio or TV in-page, or paste your own stream URL and save it as a preset in this browser.',
+];
diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx
index 1b9907d..73d8391 100644
--- a/frontend/src/layouts/Authenticated.tsx
+++ b/frontend/src/layouts/Authenticated.tsx
@@ -1,5 +1,4 @@
-import React, { ReactNode, useEffect } from 'react'
-import { useState } from 'react'
+import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 3d7301f..b8c2ff0 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -7,6 +7,13 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
+ {
+ href: '/interaction-hub',
+ label: 'Interaction hub',
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ icon: 'mdiAtomVariant' in icon ? icon['mdiAtomVariant' as keyof typeof icon] : icon.mdiBroadcast ?? icon.mdiViewDashboardOutline,
+ },
{
href: '/users/users-list',
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index ffe2474..9ecc6cb 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,348 @@
-
-import React, { useEffect, useState } from 'react';
+import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
+import {
+ mdiArrowRight,
+ mdiBroadcast,
+ mdiCubeOutline,
+ mdiLogin,
+ mdiOpenInNew,
+ mdiPlayCircleOutline,
+ mdiRobotOutline,
+ mdiTelevisionPlay,
+ mdiViewDashboardOutline,
+} from '@mdi/js';
import BaseButton from '../components/BaseButton';
-import CardBox from '../components/CardBox';
-import SectionFullScreen from '../components/SectionFullScreen';
+import BaseIcon from '../components/BaseIcon';
import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
-import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
+import {
+ adminShortcuts,
+ experienceSteps,
+ hubHighlights,
+ modularInteractionLinks,
+} from '../helpers/modularInteractionHub';
+const moduleCards = [
+ {
+ title: 'AI entry points',
+ description: 'Launch your existing agents from one branded, public-facing destination.',
+ icon: mdiRobotOutline,
+ },
+ {
+ title: 'Link switchboard',
+ description: 'Show demos, Fillout flows, and social destinations without the page feeling scattered.',
+ icon: mdiCubeOutline,
+ },
+ {
+ title: 'Radio + TV widgets',
+ description: 'Give visitors in-page media playback with presets and future room for live streams.',
+ icon: mdiBroadcast,
+ },
+ {
+ title: 'Admin-ready modules',
+ description: 'Keep the generated admin, CRUD, and permissions setup intact for future growth.',
+ icon: mdiViewDashboardOutline,
+ },
+];
-export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('video');
- const [contentPosition, setContentPosition] = useState('right');
- const textColor = useAppSelector((state) => state.style.linkColor);
+const featuredLinks = modularInteractionLinks.slice(0, 6);
- const title = 'Modular Interaction Hub'
-
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
-
- const imageBlock = (image) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
- };
+const statCards = [
+ { value: '2', label: 'AI experiences' },
+ { value: '4', label: 'Fillout entry forms' },
+ { value: '2', label: 'Media widgets' },
+];
+export default function HomePage() {
return (
-
+ <>
-
{getPageTitle('Starter Page')}
+
{getPageTitle('Modular Artificial Interaction')}
+
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
+
+
+
+
+
+ Beautiful public front door for AI, links, and media
+
-
+
+
+ Present Modular Artificial Interaction as one living experience.
+
+
+ This first slice turns the starter app into a polished public hub: your AI agents,
+ demos, Fillout links, and radio/TV widgets now feel connected, intentional, and ready
+ for visitors coming from social or direct links.
+
+
+
+
+
+
+
+
+
+ {statCards.map((item) => (
+
+
{item.value}
+
+ {item.label}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
Live concept board
+
How the experience feels
+
+
+ Public ready
+
+
+
+
+ {hubHighlights.map((item, index) => (
+
+
+
+ 0{index + 1}
+
+
{item.title}
+
+
{item.description}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
Concept
+
+ A modular stack for interaction, intake, and media.
+
+
+ Instead of sending people to scattered tools, the app now frames everything as a
+ coherent digital venue: AI assistance, form-based capture, media playback, and
+ curated launch points are all organized under one calm, futuristic visual system.
+
+
+
+
+ {experienceSteps.map((step, index) => (
+
+
+ Step {index + 1}
+
+
{step}
+
+ ))}
+
+
+
+
+
+
+
+
Modules
+
+ The first branded slice is more than a landing page.
+
+
+
+
+ {moduleCards.map((item) => (
+
+
+
+
+
{item.title}
+
{item.description}
+
+ ))}
+
+
+
+
+
+
+
Switchboard preview
+
+ Curated launch cards for the tools you already have.
+
+
+
+
+
+
+
+
+
+
+
+
+
In-page listening for your audience
+
+ The hub includes a radio player area with browser-saved presets, so you can start with
+ placeholders now and swap in real stream URLs later.
+
+
+
+
+
Flexible embed or video preview
+
+ Visitors can stay on your site while watching a sample video or a future live embed,
+ giving the experience a richer broadcast identity.
+
+
+
+
+
+
+
+
+
Admin continuity
+
+ Keep the admin power you already have.
+
+
+ The new public experience works as the front door, while the generated admin remains the
+ control room for managing links, streams, widgets, and future content.
+
+
+
+
+ {adminShortcuts.map((item) => (
+
+
{item.title}
+
{item.summary}
+
+ ))}
+
+
+
+
+
+
+
+ >
);
}
-Starter.getLayout = function getLayout(page: ReactElement) {
+HomePage.getLayout = function getLayout(page: ReactElement) {
return {page} ;
};
-
diff --git a/frontend/src/pages/interaction-hub.tsx b/frontend/src/pages/interaction-hub.tsx
new file mode 100644
index 0000000..b0a3e54
--- /dev/null
+++ b/frontend/src/pages/interaction-hub.tsx
@@ -0,0 +1,682 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import type { ReactElement } from 'react';
+import Head from 'next/head';
+import Link from 'next/link';
+import {
+ mdiArrowLeft,
+ mdiBroadcast,
+ mdiDeleteOutline,
+ mdiOpenInNew,
+ mdiPlus,
+ mdiRadioTower,
+ mdiRobotOutline,
+ mdiTelevisionPlay,
+ mdiViewDashboardOutline,
+} from '@mdi/js';
+import BaseButton from '../components/BaseButton';
+import BaseIcon from '../components/BaseIcon';
+import CardBox from '../components/CardBox';
+import LayoutGuest from '../layouts/Guest';
+import { getPageTitle } from '../config';
+import {
+ adminShortcuts,
+ MediaPreset,
+ MediaPresetMode,
+ MediaPresetType,
+ modularInteractionLinks,
+ starterMediaPresets,
+} from '../helpers/modularInteractionHub';
+
+const MEDIA_PRESETS_STORAGE_KEY = 'modular-interaction-media-presets';
+
+type FormState = {
+ type: MediaPresetType;
+ title: string;
+ url: string;
+ notes: string;
+ mode: MediaPresetMode;
+};
+
+type FormErrors = Partial>;
+
+const defaultFormState: FormState = {
+ type: 'radio',
+ title: '',
+ url: '',
+ notes: '',
+ mode: 'audio',
+};
+
+const sectionMeta: Record = {
+ agent: {
+ title: 'AI agents',
+ subtitle: 'Conversation and guidance entry points',
+ icon: mdiRobotOutline,
+ },
+ media: {
+ title: 'Media demos',
+ subtitle: 'Existing media-led experiences',
+ icon: mdiBroadcast,
+ },
+ demo: {
+ title: 'Supporting demos',
+ subtitle: 'Specialized utility and proof-of-concept pages',
+ icon: mdiViewDashboardOutline,
+ },
+ form: {
+ title: 'Forms & intake',
+ subtitle: 'Submission endpoints for structured visitor data',
+ icon: mdiPlus,
+ },
+ social: {
+ title: 'Community',
+ subtitle: 'Social touchpoints and audience handoff',
+ icon: mdiOpenInNew,
+ },
+};
+
+const createId = () => `preset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+
+const isValidUrl = (value: string) => {
+ try {
+ const parsed = new URL(value);
+ return parsed.protocol === 'https:' || parsed.protocol === 'http:';
+ } catch {
+ return false;
+ }
+};
+
+const inferMode = (type: MediaPresetType, url: string, fallback: MediaPresetMode) => {
+ if (type === 'radio') {
+ return 'audio';
+ }
+
+ const lowered = url.toLowerCase();
+
+ if (
+ lowered.endsWith('.mp4') ||
+ lowered.endsWith('.webm') ||
+ lowered.endsWith('.ogg') ||
+ lowered.includes('.mp4?')
+ ) {
+ return 'video';
+ }
+
+ if (lowered.includes('youtube.com/watch?v=')) {
+ return 'embed';
+ }
+
+ return fallback;
+};
+
+const normalizeTvUrl = (url: string, mode: MediaPresetMode) => {
+ if (mode !== 'embed') {
+ return url;
+ }
+
+ if (url.includes('youtube.com/watch?v=')) {
+ const parsed = new URL(url);
+ const videoId = parsed.searchParams.get('v');
+
+ if (videoId) {
+ return `https://www.youtube.com/embed/${videoId}`;
+ }
+ }
+
+ return url;
+};
+
+const getPreviewLabel = (preset: MediaPreset) => {
+ if (preset.type === 'radio') {
+ return 'Radio preview';
+ }
+
+ return preset.mode === 'embed' ? 'TV embed preview' : 'TV video preview';
+};
+
+export default function InteractionHubPage() {
+ const [presets, setPresets] = useState(starterMediaPresets);
+ const [selectedPresetId, setSelectedPresetId] = useState(starterMediaPresets[0].id);
+ const [formState, setFormState] = useState(defaultFormState);
+ const [formErrors, setFormErrors] = useState({});
+ const [feedbackMessage, setFeedbackMessage] = useState('');
+
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ const storedPresets = window.localStorage.getItem(MEDIA_PRESETS_STORAGE_KEY);
+
+ if (!storedPresets) {
+ return;
+ }
+
+ try {
+ const parsedPresets = JSON.parse(storedPresets) as MediaPreset[];
+
+ if (Array.isArray(parsedPresets) && parsedPresets.length > 0) {
+ setPresets(parsedPresets);
+ setSelectedPresetId(parsedPresets[0].id);
+ }
+ } catch (error) {
+ console.error('Failed to load media presets from localStorage:', error);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ window.localStorage.setItem(MEDIA_PRESETS_STORAGE_KEY, JSON.stringify(presets));
+ }, [presets]);
+
+ const groupedLinks = useMemo(() => {
+ return modularInteractionLinks.reduce>((acc, item) => {
+ const key = item.type;
+
+ if (!acc[key]) {
+ acc[key] = [];
+ }
+
+ acc[key].push(item);
+ return acc;
+ }, {});
+ }, []);
+
+ const selectedPreset = presets.find((preset) => preset.id === selectedPresetId) ?? presets[0];
+
+ const radioPresets = presets.filter((preset) => preset.type === 'radio');
+ const tvPresets = presets.filter((preset) => preset.type === 'tv');
+
+ useEffect(() => {
+ if (!selectedPreset && presets[0]) {
+ setSelectedPresetId(presets[0].id);
+ }
+ }, [presets, selectedPreset]);
+
+ const validateForm = (values: FormState) => {
+ const errors: FormErrors = {};
+
+ if (!values.title.trim()) {
+ errors.title = 'Give this preset a short title.';
+ }
+
+ if (!values.url.trim()) {
+ errors.url = 'Paste a stream, media, or embed URL.';
+ } else if (!isValidUrl(values.url.trim())) {
+ errors.url = 'Use a valid http:// or https:// URL.';
+ }
+
+ if (values.type === 'tv' && values.mode === 'embed' && !values.url.includes('embed') && !values.url.includes('youtube.com/watch?v=')) {
+ errors.mode = 'Use an embeddable URL or a YouTube watch link for TV embeds.';
+ }
+
+ return errors;
+ };
+
+ const handleFieldChange = (field: K, value: FormState[K]) => {
+ setFeedbackMessage('');
+ setFormErrors((current) => ({ ...current, [field]: undefined }));
+ setFormState((current) => {
+ const nextState = { ...current, [field]: value };
+
+ if (field === 'type' && value === 'radio') {
+ nextState.mode = 'audio';
+ }
+
+ return nextState;
+ });
+ };
+
+ const handleSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+
+ const errors = validateForm(formState);
+ if (Object.keys(errors).length > 0) {
+ setFormErrors(errors);
+ return;
+ }
+
+ const nextMode = inferMode(formState.type, formState.url.trim(), formState.mode);
+ const nextPreset: MediaPreset = {
+ id: createId(),
+ type: formState.type,
+ title: formState.title.trim(),
+ url: normalizeTvUrl(formState.url.trim(), nextMode),
+ notes: formState.notes.trim(),
+ mode: nextMode,
+ };
+
+ const nextPresets = [nextPreset, ...presets];
+ setPresets(nextPresets);
+ setSelectedPresetId(nextPreset.id);
+ setFeedbackMessage(`${nextPreset.title} is now saved in this browser and loaded into the preview.`);
+ setFormErrors({});
+ setFormState({
+ ...defaultFormState,
+ type: formState.type,
+ mode: formState.type === 'radio' ? 'audio' : 'video',
+ });
+ };
+
+ const handleDeletePreset = (presetId: string) => {
+ const nextPresets = presets.filter((preset) => preset.id !== presetId);
+ setPresets(nextPresets.length > 0 ? nextPresets : []);
+ setFeedbackMessage('Preset removed from this browser.');
+
+ if (selectedPresetId === presetId) {
+ setSelectedPresetId(nextPresets[0]?.id ?? '');
+ }
+ };
+
+ return (
+ <>
+
+ {getPageTitle('Interaction Hub')}
+
+
+
+
+
+
+
+
+
+
+
+ Back to landing page
+
+
Interaction Hub
+
+ Browse the public tool switchboard, launch your external destinations, and test radio or TV presets without leaving the page.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Public switchboard
+
Launch your existing modules
+
+
+ {modularInteractionLinks.length} live destinations
+
+
+
+
+ {Object.entries(groupedLinks).map(([key, items]) => {
+ const meta = sectionMeta[key];
+
+ return (
+
+
+
+
+
+
+
{meta.title}
+
{meta.subtitle}
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
Preset studio
+
Save a radio or TV source locally and push it straight into the preview.
+
+
+
+
+
+
+
+
+
+
+
+
Live preview
+
Media deck
+
+
+ {selectedPreset ? getPreviewLabel(selectedPreset) : 'Awaiting selection'}
+
+
+
+
+
+
+
+
+
+
+
Radio widget
+
Audio presets and placeholder radio streams
+
+
+
+ {selectedPreset?.type === 'radio' ? (
+
+
+
Now loaded
+
{selectedPreset.title}
+ {selectedPreset.notes ?
{selectedPreset.notes}
: null}
+
+
+
+ Your browser does not support the audio element.
+
+
+ ) : (
+
+ Select a radio preset from the list below, or add a new one in the preset studio.
+
+ )}
+
+
+
+
+
+
+
+
+
TV widget
+
Video or embed presets displayed in-page
+
+
+
+ {selectedPreset?.type === 'tv' ? (
+
+
+
Now loaded
+
{selectedPreset.title}
+ {selectedPreset.notes ?
{selectedPreset.notes}
: null}
+
+
+ {selectedPreset.mode === 'embed' ? (
+
+
+
+ ) : (
+
+
+ Your browser does not support the video tag.
+
+ )}
+
+ ) : (
+
+ Select a TV preset from the list below, or add a new one in the preset studio.
+
+ )}
+
+
+
+
+
+
+
+
Saved presets
+
Click any card to load it into the radio or TV preview above.
+
+
+ {radioPresets.length} radio · {tvPresets.length} TV
+
+
+
+
+ {presets.length === 0 ? (
+
+ No presets saved yet. Start by adding a radio or TV source in the preset studio.
+
+ ) : (
+ presets.map((preset) => {
+ const isActive = preset.id === selectedPreset?.id;
+
+ return (
+
+
+
setSelectedPresetId(preset.id)}
+ className="text-left"
+ >
+
+
+ {preset.type}
+
+ {preset.isSample ? (
+
+ sample
+
+ ) : null}
+
+ {preset.title}
+ {preset.url}
+ {preset.notes ? {preset.notes}
: null}
+
+
+
handleDeletePreset(preset.id)}
+ className="inline-flex items-center gap-2 self-start rounded-full border border-white/10 px-4 py-2 text-sm text-slate-300 transition hover:border-rose-300/40 hover:text-rose-200"
+ >
+
+ Remove
+
+
+
+ );
+ })
+ )}
+
+
+
+
+
+
+
+
+
+
Admin follow-through
+
When you are ready, continue from the public experience into the protected control room.
+
+
+
+
+ {adminShortcuts.map((item) => (
+
+
{item.title}
+
{item.summary}
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
+
+InteractionHubPage.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};