Skip to content
36 changes: 25 additions & 11 deletions app/[locale]/(user)/usecases/[useCaseSlug]/Dashboards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Text } from 'opub-ui';
import { GraphQL } from '@/lib/api';
import { getSafeEmbedUrl } from '@/lib/dashboardEmbed';
import { Loading } from '@/components/loading';
import JusticeHubEmbed, { isJusticeHubEmbed } from './JusticeHubEmbed';

const DashboardsList = graphql(`
query usecaseDashboards($usecaseId: Int!) {
Expand Down Expand Up @@ -41,7 +42,10 @@ const Dashboards = () => {
...dashboard,
embedUrl: getSafeEmbedUrl(dashboard.link),
}))
.filter((dashboard) => dashboard.embedUrl);
.filter(
(dashboard): dashboard is typeof dashboard & { embedUrl: string } =>
Boolean(dashboard.embedUrl)
);

if (!isValidId || (!isLoading && dashboards.length === 0)) {
return null;
Expand All @@ -65,18 +69,28 @@ const Dashboards = () => {
<Text variant="headingLg" className="text-primaryText">
{dashboard.name}
</Text>
<div className="mt-4 overflow-hidden rounded-2 border border-baseGraySlateSolid9 bg-surfaceDefault">
<iframe
title={dashboard.name || 'Dashboard'}
src={dashboard.embedUrl || undefined}
className="min-h-[640px] w-full border-0"
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
<div className="mt-4">
{isJusticeHubEmbed(dashboard.embedUrl) ? (
<JusticeHubEmbed
key={dashboard.embedUrl}
src={dashboard.embedUrl}
title={dashboard.name || 'Dashboard'}
/>
) : (
<div className="overflow-hidden rounded-2 border border-baseGraySlateSolid9 bg-surfaceDefault">
<iframe
title={dashboard.name || 'Dashboard'}
src={dashboard.embedUrl}
className="min-h-[640px] w-full border-0"
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
/>
</div>
)}
</div>
<a
href={dashboard.embedUrl || undefined}
href={dashboard.embedUrl}
target="_blank"
rel="noreferrer"
className="mt-4 inline-block text-primaryBlue underline"
Expand Down
104 changes: 104 additions & 0 deletions app/[locale]/(user)/usecases/[useCaseSlug]/JusticeHubEmbed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import { Spinner } from 'opub-ui';

const JUSTICEHUB_ORIGIN = 'https://justicehub.in';
const THEME_CSS = `${JUSTICEHUB_ORIGIN}/jh_home_new1.css`;
const THEME_LOGO = `${JUSTICEHUB_ORIGIN}/assets/jh_logo.png`;

const RETRY_MS = 2000;
const REVEAL_MS = 800;
const FALLBACK_MS = 8000;

export const isJusticeHubEmbed = (src: string) => {
try {
const { hostname } = new URL(src);
return hostname === 'justicehub.in' || hostname === 'www.justicehub.in';
} catch {
return false;
}
};

const JusticeHubEmbed = ({ src, title }: { src: string; title: string }) => {
const [attempt, setAttempt] = useState(0);
const [isReady, setIsReady] = useState(false);
const hasRetried = useRef(false);
const revealed = useRef(false);
const timers = useRef<ReturnType<typeof setTimeout>[]>([]);

const queue = (fn: () => void, delay: number) => {
timers.current.push(setTimeout(fn, delay));
};

const reveal = useCallback(() => {
if (revealed.current) return;
revealed.current = true;
setIsReady(true);
}, []);

useEffect(() => {
revealed.current = false;
hasRetried.current = false;

const fallback = setTimeout(reveal, FALLBACK_MS);
timers.current.push(fallback);

return () => {
timers.current.forEach(clearTimeout);
timers.current = [];
};
}, [src, reveal]);

const handleLoad = () => {
if (revealed.current) return;

if (!hasRetried.current) {
hasRetried.current = true;
// First response means a reload is coming. Cancel the give-up timer
// so it cannot drop the spinner and then wipe the frame.
timers.current.forEach(clearTimeout);
timers.current = [];
queue(() => {
if (revealed.current) return;
setAttempt((current) => current + 1);
}, RETRY_MS);
queue(reveal, FALLBACK_MS);
return;
}

queue(reveal, REVEAL_MS);
};

return (
<>
<link rel="preconnect" href={JUSTICEHUB_ORIGIN} />
<link rel="preload" href={THEME_CSS} as="style" />
<link rel="preload" href={THEME_LOGO} as="image" />
<div
className="relative min-h-[640px] overflow-hidden rounded-2 border border-baseGraySlateSolid9 bg-surfaceDefault"
aria-busy={!isReady}
>
{!isReady ? (
<div
className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-4 bg-surfaceDefault"
role="status"
>
<Spinner />
<span className="text-600">Loading dashboard</span>
</div>
) : null}
<iframe
key={attempt}
title={title}
src={src}
onLoad={handleLoad}
className={`min-h-[640px] w-full border-0 ${isReady ? 'visible' : 'invisible'}`}
allowFullScreen
/>
</div>
</>
);
};

export default JusticeHubEmbed;
5 changes: 3 additions & 2 deletions app/sitemap/[entityPage]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
} from '@/lib/sitemap-utils';

interface EntityItem {
id: string;
// AIModel ids are GraphQL Int; every other entity's are strings.
id: string | number;
slug?: string;
name?: string;
fullName?: string;
Expand All @@ -25,7 +26,7 @@ function isEntityItem(item: unknown): item is EntityItem {
typeof item === 'object' &&
item !== null &&
'id' in item &&
typeof (item as { id: unknown }).id === 'string'
['string', 'number'].includes(typeof (item as { id: unknown }).id)
);
}

Expand Down
5 changes: 1 addition & 4 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,7 @@ export type ENTITY_CONFIG_TYPE = Record<

// Check if sitemap is enabled
export const isSitemapEnabled = () => {
return (
process.env.FEATURE_SITEMAPS === 'true' ||
process.env.NODE_ENV === 'production'
);
return process.env.FEATURE_SITEMAPS === 'true';
};

// Entity Config
Expand Down
16 changes: 15 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,21 @@ const nextConfig = withNextIntl({
'next-intl/config': './i18n.ts',
},
},


// Non-public environments (dev) must not be indexed by search engines.
// X-Git-Sha: the commit this build came from (GITHUB_SHA in CI), so tests can
// tell which code is live, like the backend's /health/ git_sha.
async headers() {
const headers = [];
if (process.env.GITHUB_SHA) {
headers.push({ key: 'X-Git-Sha', value: process.env.GITHUB_SHA });
}
if (process.env.FEATURE_SITEMAPS !== 'true') {
headers.push({ key: 'X-Robots-Tag', value: 'noindex, nofollow' });
}
return headers.length ? [{ source: '/:path*', headers }] : [];
},

// Performance optimizations
experimental: {
optimizePackageImports: ['opub-ui', 'echarts', 'lucide-react', '@tabler/icons-react'],
Expand Down
Loading