mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +00:00
feat: add referral network graph visualization page
Interactive Sigma.js + graphology graph with ForceAtlas2 web worker layout, campaign/user node coloring, filters via nodeReducer pattern, search highlighting, user/campaign detail side panels, and 4-locale translations. Dependencies: sigma v3, graphology, graphology-layout-forceatlas2.
This commit is contained in:
163
src/pages/ReferralNetwork/components/NetworkControls.tsx
Normal file
163
src/pages/ReferralNetwork/components/NetworkControls.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getSigmaInstance } from '../sigmaGlobals';
|
||||
|
||||
interface NetworkControlsProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function NetworkControls({ className }: NetworkControlsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
|
||||
// Sync fullscreen state when user exits via Escape key or browser UI
|
||||
const handleFullscreenChange = useCallback(() => {
|
||||
setIsFullscreen(!!document.fullscreenElement);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||
}, [handleFullscreenChange]);
|
||||
|
||||
function handleZoomIn() {
|
||||
const sigma = getSigmaInstance();
|
||||
if (!sigma) return;
|
||||
const camera = sigma.getCamera();
|
||||
camera.animate({ ratio: camera.ratio / 1.5 }, { duration: 200 });
|
||||
}
|
||||
|
||||
function handleZoomOut() {
|
||||
const sigma = getSigmaInstance();
|
||||
if (!sigma) return;
|
||||
const camera = sigma.getCamera();
|
||||
camera.animate({ ratio: camera.ratio * 1.5 }, { duration: 200 });
|
||||
}
|
||||
|
||||
function handleResetZoom() {
|
||||
const sigma = getSigmaInstance();
|
||||
if (!sigma) return;
|
||||
const camera = sigma.getCamera();
|
||||
camera.animate({ x: 0.5, y: 0.5, ratio: 1 }, { duration: 400 });
|
||||
}
|
||||
|
||||
function handleFullscreen() {
|
||||
const container = document.getElementById('referral-network-container');
|
||||
if (!container) return;
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
container
|
||||
.requestFullscreen()
|
||||
.then(() => setIsFullscreen(true))
|
||||
.catch(() => {});
|
||||
} else {
|
||||
document
|
||||
.exitFullscreen()
|
||||
.then(() => setIsFullscreen(false))
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: t('admin.referralNetwork.controls.zoomIn'),
|
||||
onClick: handleZoomIn,
|
||||
icon: (
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: t('admin.referralNetwork.controls.zoomOut'),
|
||||
onClick: handleZoomOut,
|
||||
icon: (
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 12h-15" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: t('admin.referralNetwork.controls.resetZoom'),
|
||||
onClick: handleResetZoom,
|
||||
icon: (
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: t('admin.referralNetwork.controls.fullscreen'),
|
||||
onClick: handleFullscreen,
|
||||
icon: isFullscreen ? (
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex gap-1 rounded-xl border border-dark-700/50 bg-dark-900/80 p-1.5 backdrop-blur-md ${className ?? ''}`}
|
||||
>
|
||||
{buttons.map((btn) => (
|
||||
<button
|
||||
key={btn.label}
|
||||
onClick={btn.onClick}
|
||||
title={btn.label}
|
||||
aria-label={btn.label}
|
||||
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-800 hover:text-dark-200"
|
||||
>
|
||||
{btn.icon}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user