Good changes
This commit is contained in:
parent
d39d1b15fd
commit
1f68ca0512
23 changed files with 670 additions and 289 deletions
|
|
@ -32,11 +32,11 @@ export function FeatureActions({
|
|||
active={isPinned}
|
||||
size="md"
|
||||
>
|
||||
<EyeIcon filled={isPinned} />
|
||||
<EyeIcon filled={isPinned} className="w-7 h-7 md:w-3.5 md:h-3.5" />
|
||||
</IconButton>
|
||||
{onAdd && (
|
||||
<IconButton onClick={() => onAdd(feature.name)} title="Add filter" size="md">
|
||||
<PlusIcon />
|
||||
<PlusIcon className="w-7 h-7 md:w-3.5 md:h-3.5" />
|
||||
</IconButton>
|
||||
)}
|
||||
{onRemove && (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useCallback, useEffect } from 'react';
|
||||
import type { AuthUser } from '../../hooks/useAuth';
|
||||
import { shortenUrl } from '../../lib/api';
|
||||
import { DownloadIcon } from './icons/DownloadIcon';
|
||||
import { BookmarkIcon } from './icons/BookmarkIcon';
|
||||
import { LogoIcon } from './icons/LogoIcon';
|
||||
|
|
@ -12,7 +13,7 @@ import { SpinnerIcon } from './icons/SpinnerIcon';
|
|||
import UserMenu from './UserMenu';
|
||||
import MobileMenu from './MobileMenu';
|
||||
|
||||
export type Page = 'home' | 'dashboard' | 'data-sources' | 'faq' | 'saved-searches' | 'pricing';
|
||||
export type Page = 'home' | 'dashboard' | 'saved-searches' | 'pricing';
|
||||
|
||||
export default function Header({
|
||||
activePage,
|
||||
|
|
@ -44,6 +45,7 @@ export default function Header({
|
|||
isMobile: boolean;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [sharing, setSharing] = useState(false);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
// Close menu on Escape
|
||||
|
|
@ -61,17 +63,16 @@ export default function Header({
|
|||
if (!isMobile) setMenuOpen(false);
|
||||
}, [isMobile]);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
const url = window.location.href;
|
||||
const copyToClipboard = useCallback((text: string) => {
|
||||
const onSuccess = () => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
if (navigator.clipboard?.writeText) {
|
||||
navigator.clipboard.writeText(url).then(onSuccess);
|
||||
navigator.clipboard.writeText(text).then(onSuccess);
|
||||
} else {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = url;
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
document.body.appendChild(ta);
|
||||
|
|
@ -82,6 +83,23 @@ export default function Header({
|
|||
}
|
||||
}, []);
|
||||
|
||||
const handleShare = useCallback(async () => {
|
||||
const params = window.location.search.replace(/^\?/, '');
|
||||
if (!params) {
|
||||
copyToClipboard(window.location.href);
|
||||
return;
|
||||
}
|
||||
setSharing(true);
|
||||
try {
|
||||
const shortUrl = await shortenUrl(params);
|
||||
copyToClipboard(shortUrl);
|
||||
} catch {
|
||||
copyToClipboard(window.location.href);
|
||||
} finally {
|
||||
setSharing(false);
|
||||
}
|
||||
}, [copyToClipboard]);
|
||||
|
||||
const tabClass = (page: Page) =>
|
||||
`px-3 py-1.5 rounded text-sm font-medium transition-colors ${
|
||||
activePage === page
|
||||
|
|
@ -98,7 +116,7 @@ export default function Header({
|
|||
onClick={() => onPageChange('home')}
|
||||
>
|
||||
<LogoIcon className="w-5 h-5 text-teal-400" />
|
||||
<span className="font-semibold text-lg">Perfect Postcodes</span>
|
||||
<span className="font-semibold text-lg">Perfect Postcode</span>
|
||||
</button>
|
||||
|
||||
{/* Desktop nav */}
|
||||
|
|
@ -115,15 +133,6 @@ export default function Header({
|
|||
Saved
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={tabClass('data-sources')}
|
||||
onClick={() => onPageChange('data-sources')}
|
||||
>
|
||||
Data Sources
|
||||
</button>
|
||||
<button className={tabClass('faq')} onClick={() => onPageChange('faq')}>
|
||||
FAQ
|
||||
</button>
|
||||
<button className={tabClass('pricing')} onClick={() => onPageChange('pricing')}>
|
||||
Pricing
|
||||
</button>
|
||||
|
|
@ -152,9 +161,15 @@ export default function Header({
|
|||
)}
|
||||
<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"
|
||||
disabled={sharing}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-navy-800 hover:bg-navy-700 transition-colors text-sm disabled:opacity-50 disabled:cursor-wait"
|
||||
>
|
||||
{copied ? (
|
||||
{sharing ? (
|
||||
<>
|
||||
<SpinnerIcon className="w-4 h-4 animate-spin" />
|
||||
Sharing...
|
||||
</>
|
||||
) : copied ? (
|
||||
<>
|
||||
<CheckIcon className="w-4 h-4" />
|
||||
Copied!
|
||||
|
|
@ -255,6 +270,13 @@ export default function Header({
|
|||
copied={copied}
|
||||
/>
|
||||
)}
|
||||
{/* Mobile "Copied" toast */}
|
||||
{isMobile && copied && (
|
||||
<div className="fixed top-14 left-1/2 -translate-x-1/2 z-[60] flex items-center gap-2 px-4 py-2 rounded-lg bg-navy-900 text-white text-sm shadow-lg animate-fade-in">
|
||||
<CheckIcon className="w-4 h-4 text-teal-400" />
|
||||
Copied to clipboard
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,10 +78,9 @@ export default function MobileMenu({
|
|||
</button>
|
||||
</div>
|
||||
<nav className="flex-1 flex flex-col gap-1 p-3 overflow-y-auto">
|
||||
{mobileNavItem('home', 'Home')}
|
||||
{mobileNavItem('dashboard', 'Dashboard')}
|
||||
{user && mobileNavItem('saved-searches', 'Saved')}
|
||||
{mobileNavItem('data-sources', 'Data Sources')}
|
||||
{mobileNavItem('faq', 'FAQ')}
|
||||
{mobileNavItem('pricing', 'Pricing')}
|
||||
|
||||
{/* Dashboard actions */}
|
||||
|
|
|
|||
16
frontend/src/components/ui/PillGroup.tsx
Normal file
16
frontend/src/components/ui/PillGroup.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { ReactNode } from 'react';
|
||||
|
||||
interface PillGroupProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PillGroup({ children, className = '' }: PillGroupProps) {
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-nowrap overflow-x-auto gap-1.5 md:flex-wrap md:overflow-x-visible scrollbar-hide ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
frontend/src/components/ui/PillToggle.tsx
Normal file
34
frontend/src/components/ui/PillToggle.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
interface PillToggleProps {
|
||||
label: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
/** Visual hint for partial selection (e.g. some children selected) */
|
||||
indeterminate?: boolean;
|
||||
size?: 'sm' | 'xs';
|
||||
}
|
||||
|
||||
export function PillToggle({
|
||||
label,
|
||||
active,
|
||||
onClick,
|
||||
indeterminate,
|
||||
size = 'sm',
|
||||
}: PillToggleProps) {
|
||||
const sizeClasses = size === 'xs' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm';
|
||||
|
||||
const colorClasses = active
|
||||
? 'bg-teal-600 text-white dark:bg-teal-500'
|
||||
: indeterminate
|
||||
? 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300'
|
||||
: 'bg-warm-100 text-warm-600 hover:bg-warm-200 dark:bg-warm-700 dark:text-warm-300 dark:hover:bg-warm-600';
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`${sizeClasses} ${colorClasses} rounded-full font-medium whitespace-nowrap cursor-pointer`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue