Merge components

This commit is contained in:
Andras Schmelczer 2026-02-07 13:44:40 +00:00
parent 5cbb180c57
commit 80fc203226
9 changed files with 11 additions and 11 deletions

View file

@ -0,0 +1,44 @@
import type { FeatureMeta } from '../../types';
import { EyeIcon, InfoIcon, PlusIcon, CloseIcon } from './icons';
import { IconButton } from './IconButton';
interface FeatureActionsProps {
feature: FeatureMeta;
isPinned: boolean;
onTogglePin: (name: string) => void;
onShowInfo?: (feature: FeatureMeta) => void;
onRemove?: (name: string) => void;
onAdd?: (name: string) => void;
}
export function FeatureActions({
feature,
isPinned,
onTogglePin,
onShowInfo,
onRemove,
onAdd,
}: FeatureActionsProps) {
return (
<div className="flex items-center gap-0.5 shrink-0">
{feature.detail && onShowInfo && (
<IconButton onClick={() => onShowInfo(feature)} title="Feature info">
<InfoIcon />
</IconButton>
)}
<IconButton onClick={() => onTogglePin(feature.name)} title={isPinned ? 'Unpin color view' : 'Color map by this feature'} active={isPinned}>
<EyeIcon filled={isPinned} />
</IconButton>
{onAdd && (
<IconButton onClick={() => onAdd(feature.name)} title="Add filter">
<PlusIcon />
</IconButton>
)}
{onRemove && (
<IconButton onClick={() => onRemove(feature.name)} title="Remove filter">
<CloseIcon className="w-3.5 h-3.5" />
</IconButton>
)}
</div>
);
}

View file

@ -0,0 +1,37 @@
import type { FeatureMeta } from '../../types';
import InfoPopup from './InfoPopup';
interface FeatureInfoPopupProps {
feature: FeatureMeta;
onClose: () => void;
onNavigateToSource?: (slug: string, featureName: string) => void;
}
export function FeatureInfoPopup({ feature, onClose, onNavigateToSource }: FeatureInfoPopupProps) {
return (
<InfoPopup
title={feature.name}
onClose={onClose}
sourceLink={
feature.source && onNavigateToSource
? {
label: 'View data source',
onClick: () => {
onNavigateToSource(feature.source!, feature.name);
onClose();
},
}
: undefined
}
>
{feature.description && (
<p className="text-xs text-warm-500 dark:text-warm-400 mb-2">{feature.description}</p>
)}
{feature.detail && (
<p className="text-sm text-warm-700 dark:text-warm-300 mb-4 leading-relaxed">
{feature.detail}
</p>
)}
</InfoPopup>
);
}

View file

@ -0,0 +1,143 @@
import { useState, useCallback } from 'react';
export type Page = 'home' | 'dashboard' | 'data-sources' | 'faq';
export default function Header({
activePage,
onPageChange,
theme,
onToggleTheme,
}: {
activePage: Page;
onPageChange: (page: Page) => void;
theme: 'light' | 'dark';
onToggleTheme: () => void;
}) {
const [copied, setCopied] = useState(false);
const handleShare = useCallback(() => {
navigator.clipboard.writeText(window.location.href).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, []);
const tabClass = (page: Page) =>
`px-3 py-1.5 rounded text-sm font-medium transition-colors ${
activePage === page
? 'bg-navy-700 text-white'
: 'text-warm-300 hover:bg-navy-800 hover:text-white'
}`;
return (
<header className="h-12 bg-navy-900 text-white flex items-center justify-between px-4 shrink-0">
<div className="flex items-center gap-4">
<button
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
onClick={() => onPageChange('home')}
>
<svg
className="w-5 h-5 text-teal-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span className="font-semibold text-lg">Narrowit</span>
</button>
<nav className="flex items-center gap-2">
<button className={tabClass('dashboard')} onClick={() => onPageChange('dashboard')}>
Dashboard
</button>
<button className={tabClass('data-sources')} onClick={() => onPageChange('data-sources')}>
Data Sources
</button>
<button className={tabClass('faq')} onClick={() => onPageChange('faq')}>
FAQ
</button>
</nav>
</div>
<div className="flex items-center gap-2">
<button
onClick={onToggleTheme}
className="flex items-center justify-center w-8 h-8 rounded bg-navy-800 hover:bg-navy-700 transition-colors"
title={`Theme: ${theme}`}
>
{theme === 'light' ? (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
) : (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
)}
</button>
{activePage === 'dashboard' && (
<button
onClick={handleShare}
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm"
>
{copied ? (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
Copied!
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
/>
</svg>
Share
</>
)}
</button>
)}
</div>
</header>
);
}

View file

@ -0,0 +1,46 @@
import { useRef, useCallback, type ReactNode } from 'react';
import { useClickOutside } from '../../hooks/useClickOutside';
import { CloseIcon } from './icons';
import { IconButton } from './IconButton';
interface InfoPopupProps {
title: string;
children: ReactNode;
onClose: () => void;
sourceLink?: { label: string; onClick: () => void };
}
export default function InfoPopup({ title, children, onClose, sourceLink }: InfoPopupProps) {
const popupRef = useRef<HTMLDivElement>(null);
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
useClickOutside(popupRef, handleClose);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
<div
ref={popupRef}
className="bg-white dark:bg-navy-800 border border-warm-200 dark:border-navy-700 rounded-lg shadow-xl max-w-md w-full mx-4 p-5"
>
<div className="flex items-start justify-between mb-3">
<h3 className="text-sm font-semibold text-warm-900 dark:text-warm-100 pr-4">{title}</h3>
<IconButton onClick={onClose} className="shrink-0">
<CloseIcon />
</IconButton>
</div>
{children}
{sourceLink && (
<button
onClick={sourceLink.onClick}
className="text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300 hover:underline"
>
{sourceLink.label}
</button>
)}
</div>
</div>
);
}