feat: conditionally enable recording studio and feedback tool via env vars
This commit is contained in:
3
.env
3
.env
@@ -4,7 +4,8 @@ NEXT_PUBLIC_BASE_URL=https://klz-cables.com
|
|||||||
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me
|
||||||
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
SENTRY_DSN=https://c10957d0182245b1a2a806ac2d34a197@errors.infra.mintel.me/1
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
NEXT_PUBLIC_FEEDBACK_ENABLED=true
|
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
||||||
|
NEXT_PUBLIC_RECORD_MODE_ENABLED=false
|
||||||
|
|
||||||
# SMTP Configuration
|
# SMTP Configuration
|
||||||
MAIL_HOST=smtp.eu.mailgun.org
|
MAIL_HOST=smtp.eu.mailgun.org
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ DIRECTUS_PORT=8055
|
|||||||
# NEXT_PUBLIC_TARGET makes this information available to the frontend
|
# NEXT_PUBLIC_TARGET makes this information available to the frontend
|
||||||
TARGET=development
|
TARGET=development
|
||||||
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
NEXT_PUBLIC_FEEDBACK_ENABLED=false
|
||||||
|
NEXT_PUBLIC_RECORD_MODE_ENABLED=true
|
||||||
|
|
||||||
# ────────────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────────────
|
||||||
# Analytics (Umami)
|
# Analytics (Umami)
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export default async function LocaleLayout({
|
|||||||
}) {
|
}) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
|
||||||
// Ensure locale is a valid string, fallback to 'en'
|
|
||||||
const supportedLocales = ['en', 'de'];
|
const supportedLocales = ['en', 'de'];
|
||||||
const localeStr = (typeof locale === 'string' ? locale : '').trim();
|
const localeStr = (typeof locale === 'string' ? locale : '').trim();
|
||||||
const safeLocale = supportedLocales.includes(localeStr) ? localeStr : 'en';
|
const safeLocale = supportedLocales.includes(localeStr) ? localeStr : 'en';
|
||||||
@@ -66,12 +65,9 @@ export default async function LocaleLayout({
|
|||||||
messages = {};
|
messages = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track pageview on the server with high-fidelity header context
|
|
||||||
const { getServerAppServices } = await import('@/lib/services/create-services.server');
|
const { getServerAppServices } = await import('@/lib/services/create-services.server');
|
||||||
const serverServices = getServerAppServices();
|
const serverServices = getServerAppServices();
|
||||||
|
|
||||||
// We wrap this in a try-catch to allow static rendering during build
|
|
||||||
// headers() and cookies() force dynamic rendering in Next.js
|
|
||||||
try {
|
try {
|
||||||
const { headers } = await import('next/headers');
|
const { headers } = await import('next/headers');
|
||||||
const requestHeaders = await headers();
|
const requestHeaders = await headers();
|
||||||
@@ -85,10 +81,8 @@ export default async function LocaleLayout({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track initial server-side pageview
|
|
||||||
serverServices.analytics.trackPageview();
|
serverServices.analytics.trackPageview();
|
||||||
} catch {
|
} catch {
|
||||||
// Falls back to noop or client-side only during static generation
|
|
||||||
if (process.env.NODE_ENV !== 'production' || !process.env.CI) {
|
if (process.env.NODE_ENV !== 'production' || !process.env.CI) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[Layout] Static generation detected or headers unavailable, skipping server-side analytics context',
|
'[Layout] Static generation detected or headers unavailable, skipping server-side analytics context',
|
||||||
@@ -96,38 +90,29 @@ export default async function LocaleLayout({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read directly from process.env — bypasses all abstraction to guarantee correctness
|
||||||
|
const recordModeEnabled = process.env.NEXT_PUBLIC_RECORD_MODE_ENABLED === 'true';
|
||||||
|
const feedbackEnabled = process.env.NEXT_PUBLIC_FEEDBACK_ENABLED === 'true';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={safeLocale} className={`scroll-smooth overflow-x-hidden ${inter.variable}`}>
|
<html lang={safeLocale} className={`scroll-smooth overflow-x-hidden ${inter.variable}`}>
|
||||||
<head>
|
<head>
|
||||||
<style dangerouslySetInnerHTML={{
|
<style
|
||||||
__html: `
|
dangerouslySetInnerHTML={{
|
||||||
/* Effectively Invisible Scrollbar */
|
__html: `
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar { width: 2px; height: 2px; }
|
||||||
width: 2px;
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
height: 2px;
|
::-webkit-scrollbar-thumb { background: transparent; border-radius: 10px; }
|
||||||
}
|
::-webkit-scrollbar-thumb:hover { background: rgba(130, 237, 32, 0.4); }
|
||||||
::-webkit-scrollbar-track {
|
* { scrollbar-width: none; }
|
||||||
background: transparent;
|
*:hover { scrollbar-width: thin; scrollbar-color: rgba(130, 237, 32, 0.2) transparent; }
|
||||||
}
|
`,
|
||||||
::-webkit-scrollbar-thumb {
|
}}
|
||||||
background: transparent;
|
/>
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(130, 237, 32, 0.4);
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
*:hover {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(130, 237, 32, 0.2) transparent;
|
|
||||||
}
|
|
||||||
`}} />
|
|
||||||
</head>
|
</head>
|
||||||
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
<body className="flex flex-col min-h-screen font-sans selection:bg-accent selection:text-primary-dark antialiased overflow-x-hidden">
|
||||||
<NextIntlClientProvider messages={messages} locale={safeLocale}>
|
<NextIntlClientProvider messages={messages} locale={safeLocale}>
|
||||||
<RecordModeProvider>
|
<RecordModeProvider isEnabled={recordModeEnabled}>
|
||||||
<RecordModeVisuals>
|
<RecordModeVisuals>
|
||||||
<JsonLd />
|
<JsonLd />
|
||||||
<Header />
|
<Header />
|
||||||
@@ -140,7 +125,7 @@ export default async function LocaleLayout({
|
|||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<AnalyticsProvider />
|
<AnalyticsProvider />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<ToolCoordinator />
|
<ToolCoordinator feedbackEnabled={feedbackEnabled} />
|
||||||
</RecordModeProvider>
|
</RecordModeProvider>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ interface RecordModeContextType {
|
|||||||
hoveredEventId: string | null;
|
hoveredEventId: string | null;
|
||||||
setHoveredEventId: (id: string | null) => void;
|
setHoveredEventId: (id: string | null) => void;
|
||||||
isClicking: boolean;
|
isClicking: boolean;
|
||||||
|
isEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
const RecordModeContext = createContext<RecordModeContextType | null>(null);
|
||||||
@@ -56,12 +57,19 @@ export function useRecordMode(): RecordModeContextType {
|
|||||||
setHoveredEventId: () => {},
|
setHoveredEventId: () => {},
|
||||||
setEvents: () => {},
|
setEvents: () => {},
|
||||||
isClicking: false,
|
isClicking: false,
|
||||||
|
isEnabled: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RecordModeProvider({ children }: { children: React.ReactNode }) {
|
export function RecordModeProvider({
|
||||||
|
children,
|
||||||
|
isEnabled = false,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isEnabled?: boolean;
|
||||||
|
}) {
|
||||||
const [isActive, setIsActiveState] = useState(false);
|
const [isActive, setIsActiveState] = useState(false);
|
||||||
const [events, setEvents] = useState<RecordEvent[]>([]);
|
const [events, setEvents] = useState<RecordEvent[]>([]);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@@ -74,45 +82,54 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
const [isEmbedded, setIsEmbedded] = useState(false);
|
const [isEmbedded, setIsEmbedded] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('[RecordModeProvider] Mounted with isEnabled:', isEnabled);
|
||||||
|
}, [isEnabled]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEnabled) return;
|
||||||
const embedded =
|
const embedded =
|
||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
(window.location.search.includes('embedded=true') ||
|
(window.location.search.includes('embedded=true') ||
|
||||||
window.name === 'record-mode-iframe' ||
|
window.name === 'record-mode-iframe' ||
|
||||||
window.self !== window.top);
|
window.self !== window.top);
|
||||||
setIsEmbedded(embedded);
|
setIsEmbedded(embedded);
|
||||||
}, []);
|
}, [isEnabled]);
|
||||||
|
|
||||||
const setIsActive = (active: boolean) => {
|
const setIsActive = (active: boolean) => {
|
||||||
|
if (!isEnabled) return;
|
||||||
setIsActiveState(active);
|
setIsActiveState(active);
|
||||||
if (active) setIsFeedbackActiveState(false);
|
if (active) setIsFeedbackActiveState(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setIsFeedbackActive = (active: boolean) => {
|
const setIsFeedbackActive = (active: boolean) => {
|
||||||
setIsFeedbackActiveState(active);
|
setIsFeedbackActiveState(active);
|
||||||
if (active) setIsActiveState(false);
|
if (active && isEnabled) setIsActiveState(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPlayingRef = useRef(false);
|
const isPlayingRef = useRef(false);
|
||||||
const isLoadedRef = useRef(false);
|
const isLoadedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isEnabled) return;
|
||||||
const savedEvents = localStorage.getItem('klz-record-events');
|
const savedEvents = localStorage.getItem('klz-record-events');
|
||||||
const savedActive = localStorage.getItem('klz-record-active');
|
const savedActive = localStorage.getItem('klz-record-active');
|
||||||
if (savedEvents) setEvents(JSON.parse(savedEvents));
|
if (savedEvents) setEvents(JSON.parse(savedEvents));
|
||||||
if (savedActive) setIsActive(JSON.parse(savedActive));
|
if (savedActive) setIsActive(JSON.parse(savedActive));
|
||||||
isLoadedRef.current = true;
|
isLoadedRef.current = true;
|
||||||
}, []);
|
}, [isEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoadedRef.current) return;
|
if (!isEnabled || !isLoadedRef.current) return;
|
||||||
localStorage.setItem('klz-record-events', JSON.stringify(events));
|
localStorage.setItem('klz-record-events', JSON.stringify(events));
|
||||||
}, [events]);
|
}, [events, isEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isEnabled) return;
|
||||||
localStorage.setItem('klz-record-active', JSON.stringify(isActive));
|
localStorage.setItem('klz-record-active', JSON.stringify(isActive));
|
||||||
}, [isActive]);
|
}, [isActive, isEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isEnabled) return;
|
||||||
if (isEmbedded) {
|
if (isEmbedded) {
|
||||||
const handlePlaybackMessage = (e: MessageEvent) => {
|
const handlePlaybackMessage = (e: MessageEvent) => {
|
||||||
if (e.data.type === 'PLAY_EVENT') {
|
if (e.data.type === 'PLAY_EVENT') {
|
||||||
@@ -177,10 +194,10 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
window.addEventListener('message', handlePlaybackMessage);
|
window.addEventListener('message', handlePlaybackMessage);
|
||||||
return () => window.removeEventListener('message', handlePlaybackMessage);
|
return () => window.removeEventListener('message', handlePlaybackMessage);
|
||||||
}
|
}
|
||||||
}, [isEmbedded]);
|
}, [isEmbedded, isEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEmbedded || !isActive) return;
|
if (!isEnabled || isEmbedded || !isActive) return;
|
||||||
const event = events.find((e) => e.id === hoveredEventId);
|
const event = events.find((e) => e.id === hoveredEventId);
|
||||||
const iframe = document.querySelector('iframe[name="record-mode-iframe"]') as HTMLIFrameElement;
|
const iframe = document.querySelector('iframe[name="record-mode-iframe"]') as HTMLIFrameElement;
|
||||||
if (iframe?.contentWindow) {
|
if (iframe?.contentWindow) {
|
||||||
@@ -189,9 +206,10 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
'*',
|
'*',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [hoveredEventId, events, isActive, isEmbedded]);
|
}, [hoveredEventId, events, isActive, isEmbedded, isEnabled]);
|
||||||
|
|
||||||
const addEvent = (event: Omit<RecordEvent, 'id' | 'timestamp'>) => {
|
const addEvent = (event: Omit<RecordEvent, 'id' | 'timestamp'>) => {
|
||||||
|
if (!isEnabled) return;
|
||||||
const newEvent: RecordEvent = {
|
const newEvent: RecordEvent = {
|
||||||
realClick: false,
|
realClick: false,
|
||||||
...event,
|
...event,
|
||||||
@@ -202,12 +220,14 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateEvent = (id: string, updatedFields: Partial<RecordEvent>) => {
|
const updateEvent = (id: string, updatedFields: Partial<RecordEvent>) => {
|
||||||
|
if (!isEnabled) return;
|
||||||
setEvents((prev) =>
|
setEvents((prev) =>
|
||||||
prev.map((event) => (event.id === id ? { ...event, ...updatedFields } : event)),
|
prev.map((event) => (event.id === id ? { ...event, ...updatedFields } : event)),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reorderEvents = (startIndex: number, endIndex: number) => {
|
const reorderEvents = (startIndex: number, endIndex: number) => {
|
||||||
|
if (!isEnabled) return;
|
||||||
const result = Array.from(events);
|
const result = Array.from(events);
|
||||||
const [removed] = result.splice(startIndex, 1);
|
const [removed] = result.splice(startIndex, 1);
|
||||||
result.splice(endIndex, 0, removed);
|
result.splice(endIndex, 0, removed);
|
||||||
@@ -215,10 +235,12 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeEvent = (id: string) => {
|
const removeEvent = (id: string) => {
|
||||||
|
if (!isEnabled) return;
|
||||||
setEvents((prev) => prev.filter((event) => event.id !== id));
|
setEvents((prev) => prev.filter((event) => event.id !== id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearEvents = () => {
|
const clearEvents = () => {
|
||||||
|
if (!isEnabled) return;
|
||||||
if (confirm('Clear all recorded events?')) setEvents([]);
|
if (confirm('Clear all recorded events?')) setEvents([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,11 +255,12 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const saveSession = (name: string) => {
|
const saveSession = (name: string) => {
|
||||||
|
if (!isEnabled) return;
|
||||||
console.log('Saving session:', name, events);
|
console.log('Saving session:', name, events);
|
||||||
};
|
};
|
||||||
|
|
||||||
const playEvents = async () => {
|
const playEvents = async () => {
|
||||||
if (events.length === 0 || isPlayingRef.current) return;
|
if (!isEnabled || events.length === 0 || isPlayingRef.current) return;
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
isPlayingRef.current = true;
|
isPlayingRef.current = true;
|
||||||
const sortedEvents = [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
const sortedEvents = [...events].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
||||||
@@ -263,7 +286,6 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
if (iframe?.contentWindow)
|
if (iframe?.contentWindow)
|
||||||
iframe.contentWindow.postMessage({ type: 'PLAY_EVENT', event }, '*');
|
iframe.contentWindow.postMessage({ type: 'PLAY_EVENT', event }, '*');
|
||||||
} else {
|
} else {
|
||||||
// Self-execution logic for guest
|
|
||||||
const el = document.querySelector(event.selector) as HTMLElement;
|
const el = document.querySelector(event.selector) as HTMLElement;
|
||||||
if (el) {
|
if (el) {
|
||||||
if (event.type === 'scroll') el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
if (event.type === 'scroll') el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
@@ -361,6 +383,7 @@ export function RecordModeProvider({ children }: { children: React.ReactNode })
|
|||||||
hoveredEventId,
|
hoveredEventId,
|
||||||
setHoveredEventId,
|
setHoveredEventId,
|
||||||
isClicking,
|
isClicking,
|
||||||
|
isEnabled,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -2,62 +2,65 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRecordMode } from './RecordModeContext';
|
import { useRecordMode } from './RecordModeContext';
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import { FeedbackOverlay } from '@mintel/next-feedback';
|
import { FeedbackOverlay } from '@mintel/next-feedback';
|
||||||
import { RecordModeOverlay } from './RecordModeOverlay';
|
import { RecordModeOverlay } from './RecordModeOverlay';
|
||||||
import { PickingHelper } from './PickingHelper';
|
import { PickingHelper } from './PickingHelper';
|
||||||
import { config } from '@/lib/config';
|
|
||||||
|
|
||||||
export function ToolCoordinator({ isEmbedded: isEmbeddedProp }: { isEmbedded?: boolean }) {
|
interface ToolCoordinatorProps {
|
||||||
const { isActive, setIsActive, isFeedbackActive, setIsFeedbackActive } = useRecordMode();
|
isEmbedded?: boolean;
|
||||||
const [isEmbedded, setIsEmbedded] = useState(false);
|
feedbackEnabled?: boolean;
|
||||||
const [mounted, setMounted] = useState(false);
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
export function ToolCoordinator({
|
||||||
setMounted(true);
|
isEmbedded: isEmbeddedProp,
|
||||||
const embedded =
|
feedbackEnabled = false,
|
||||||
isEmbeddedProp ||
|
}: ToolCoordinatorProps) {
|
||||||
window.location.search.includes('embedded=true') ||
|
const { isActive, setIsActive, isFeedbackActive, setIsFeedbackActive, isEnabled } =
|
||||||
window.name === 'record-mode-iframe' ||
|
useRecordMode();
|
||||||
(window.self !== window.top);
|
const [isEmbedded, setIsEmbedded] = useState(false);
|
||||||
setIsEmbedded(embedded);
|
const [mounted, setMounted] = useState(false);
|
||||||
}, [isEmbeddedProp]);
|
|
||||||
|
useEffect(() => {
|
||||||
if (!mounted) return null;
|
setMounted(true);
|
||||||
|
const embedded =
|
||||||
// ABSOLUTE Priority 1: Inside Iframe - ONLY Rendering PickingHelper
|
isEmbeddedProp ||
|
||||||
if (isEmbedded) {
|
window.location.search.includes('embedded=true') ||
|
||||||
return <PickingHelper />;
|
window.name === 'record-mode-iframe' ||
|
||||||
}
|
window.self !== window.top;
|
||||||
|
setIsEmbedded(embedded);
|
||||||
// ABSOLUTE Priority 2: Record Mode Studio Active - NO OTHER TOOLS ALLOWED
|
}, [isEmbeddedProp]);
|
||||||
if (isActive) {
|
|
||||||
return <RecordModeOverlay />;
|
if (!mounted) return null;
|
||||||
}
|
|
||||||
|
// Nothing enabled → render nothing
|
||||||
// Priority 3: Feedback Tool Active - NO OTHER TOOLS ALLOWED
|
if (!feedbackEnabled && !isEnabled) return null;
|
||||||
if (isFeedbackActive) {
|
|
||||||
return (
|
// Iframe → only PickingHelper
|
||||||
<FeedbackOverlay
|
if (isEmbedded) return <PickingHelper />;
|
||||||
isActive={isFeedbackActive}
|
|
||||||
onActiveChange={(active) => setIsFeedbackActive(active)}
|
// Record Mode active and enabled
|
||||||
/>
|
if (isActive && isEnabled) return <RecordModeOverlay />;
|
||||||
);
|
|
||||||
}
|
// Feedback active and enabled
|
||||||
|
if (isFeedbackActive && feedbackEnabled) {
|
||||||
// Baseline: Both toggle buttons (inactive state)
|
return (
|
||||||
// Only render if neither is active to prevent any overlapping residues
|
<FeedbackOverlay
|
||||||
// IMPORTANT: FeedbackOverlay must be rendered with isActive={false} to provide the toggle button,
|
isActive={isFeedbackActive}
|
||||||
// but only if Record Mode is not active.
|
onActiveChange={(active) => setIsFeedbackActive(active)}
|
||||||
return (
|
/>
|
||||||
<div className="feedback-ui-ignore">
|
);
|
||||||
{config.feedbackEnabled && (
|
}
|
||||||
<FeedbackOverlay
|
|
||||||
isActive={false}
|
// Baseline: toggle buttons
|
||||||
onActiveChange={(active) => setIsFeedbackActive(active)}
|
return (
|
||||||
/>
|
<div className="feedback-ui-ignore">
|
||||||
)}
|
{feedbackEnabled && (
|
||||||
<RecordModeOverlay />
|
<FeedbackOverlay
|
||||||
</div>
|
isActive={false}
|
||||||
);
|
onActiveChange={(active) => setIsFeedbackActive(active)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isEnabled && <RecordModeOverlay />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ function createConfig() {
|
|||||||
|
|
||||||
const target = env.NEXT_PUBLIC_TARGET || env.TARGET;
|
const target = env.NEXT_PUBLIC_TARGET || env.TARGET;
|
||||||
|
|
||||||
|
console.log('[Config] Initializing Toggles:', {
|
||||||
|
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
||||||
|
recordModeEnabled: env.NEXT_PUBLIC_RECORD_MODE_ENABLED,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
env: env.NODE_ENV,
|
env: env.NODE_ENV,
|
||||||
target,
|
target,
|
||||||
@@ -23,6 +28,7 @@ function createConfig() {
|
|||||||
isTesting: target === 'testing',
|
isTesting: target === 'testing',
|
||||||
isDevelopment: target === 'development',
|
isDevelopment: target === 'development',
|
||||||
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
feedbackEnabled: env.NEXT_PUBLIC_FEEDBACK_ENABLED,
|
||||||
|
recordModeEnabled: env.NEXT_PUBLIC_RECORD_MODE_ENABLED,
|
||||||
gatekeeperUrl: env.GATEKEEPER_URL,
|
gatekeeperUrl: env.GATEKEEPER_URL,
|
||||||
|
|
||||||
baseUrl: env.NEXT_PUBLIC_BASE_URL,
|
baseUrl: env.NEXT_PUBLIC_BASE_URL,
|
||||||
@@ -144,6 +150,9 @@ export const config = {
|
|||||||
get feedbackEnabled() {
|
get feedbackEnabled() {
|
||||||
return getConfig().feedbackEnabled;
|
return getConfig().feedbackEnabled;
|
||||||
},
|
},
|
||||||
|
get recordModeEnabled() {
|
||||||
|
return getConfig().recordModeEnabled;
|
||||||
|
},
|
||||||
get infraCMS() {
|
get infraCMS() {
|
||||||
return getConfig().infraCMS;
|
return getConfig().infraCMS;
|
||||||
},
|
},
|
||||||
|
|||||||
23
lib/env.ts
23
lib/env.ts
@@ -1,6 +1,18 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { validateMintelEnv, mintelEnvSchema, withMintelRefinements } from '@mintel/next-utils';
|
import { validateMintelEnv, mintelEnvSchema, withMintelRefinements } from '@mintel/next-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Robust boolean preprocessor for environment variables.
|
||||||
|
* Handles strings 'true'/'false' and actual booleans.
|
||||||
|
*/
|
||||||
|
const booleanSchema = z.preprocess((val) => {
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
if (val.toLowerCase() === 'true') return true;
|
||||||
|
if (val.toLowerCase() === 'false') return false;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}, z.boolean());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment variable schema.
|
* Environment variable schema.
|
||||||
* Extends the default Mintel environment schema.
|
* Extends the default Mintel environment schema.
|
||||||
@@ -11,14 +23,9 @@ const envExtension = {
|
|||||||
|
|
||||||
// Gatekeeper specifics not in base
|
// Gatekeeper specifics not in base
|
||||||
GATEKEEPER_URL: z.string().url().default('http://gatekeeper:3000'),
|
GATEKEEPER_URL: z.string().url().default('http://gatekeeper:3000'),
|
||||||
GATEKEEPER_BYPASS_ENABLED: z.preprocess(
|
GATEKEEPER_BYPASS_ENABLED: booleanSchema.default(false),
|
||||||
(val) => val === 'true' || val === true,
|
NEXT_PUBLIC_FEEDBACK_ENABLED: booleanSchema.default(false),
|
||||||
z.boolean().default(false),
|
NEXT_PUBLIC_RECORD_MODE_ENABLED: booleanSchema.default(false),
|
||||||
),
|
|
||||||
NEXT_PUBLIC_FEEDBACK_ENABLED: z.preprocess(
|
|
||||||
(val) => val === 'true' || val === true,
|
|
||||||
z.boolean().default(false),
|
|
||||||
),
|
|
||||||
|
|
||||||
INFRA_DIRECTUS_URL: z.string().url().optional(),
|
INFRA_DIRECTUS_URL: z.string().url().optional(),
|
||||||
INFRA_DIRECTUS_TOKEN: z.string().optional(),
|
INFRA_DIRECTUS_TOKEN: z.string().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user