fix: redesign top bar and filter panel for mobile

- Split top bar into two rows on mobile: title + filter icon, then
  full-width search below
- Desktop keeps single-row layout with search and filter inline
- Filter button: icon-only on mobile with active indicator dot
- Filter panel: full-screen overlay with backdrop on mobile,
  inline dropdown on desktop
- Outside-click closes filter panel
- Removed subtitle (only on desktop, saves space)
This commit is contained in:
Fringg
2026-03-19 08:30:32 +03:00
parent 818557f57d
commit ea143fdbc9
2 changed files with 159 additions and 92 deletions

View File

@@ -34,20 +34,29 @@ export function ReferralNetwork() {
className="relative flex h-[calc(100dvh-64px)] flex-col overflow-hidden bg-[#0a0a0f]" className="relative flex h-[calc(100dvh-64px)] flex-col overflow-hidden bg-[#0a0a0f]"
> >
{/* Top bar */} {/* Top bar */}
<div className="relative z-20 flex shrink-0 items-center gap-3 border-b border-dark-700/50 bg-dark-900/90 px-4 py-3 backdrop-blur-md"> <div className="relative z-20 shrink-0 border-b border-dark-700/50 bg-dark-900/90 backdrop-blur-md">
{/* Row 1: Back + Title + (desktop: search + filter) + (mobile: filter icon) */}
<div className="flex items-center gap-3 px-3 py-2 sm:px-4 sm:py-3">
<AdminBackButton /> <AdminBackButton />
<div className="mr-2 min-w-0"> <h1 className="min-w-0 truncate text-sm font-bold text-dark-100 sm:text-lg">
<h1 className="truncate text-sm font-bold text-dark-100 sm:text-lg">
{t('admin.referralNetwork.title')} {t('admin.referralNetwork.title')}
</h1> </h1>
<p className="hidden text-xs text-dark-500 sm:block"> {/* Desktop: search + filter inline */}
{t('admin.referralNetwork.subtitle')} <div className="hidden flex-1 items-center gap-2 sm:flex">
</p>
</div>
<div className="flex flex-1 items-center gap-2">
<NetworkSearch className="max-w-md flex-1" /> <NetworkSearch className="max-w-md flex-1" />
{networkData && <NetworkFilters data={networkData} />} {networkData && <NetworkFilters data={networkData} />}
</div> </div>
{/* Mobile: filter icon only */}
{networkData && (
<div className="ml-auto sm:hidden">
<NetworkFilters data={networkData} />
</div>
)}
</div>
{/* Row 2 (mobile only): full-width search */}
<div className="px-3 pb-2 sm:hidden">
<NetworkSearch className="w-full" />
</div>
</div> </div>
{/* Main content area */} {/* Main content area */}

View File

@@ -1,3 +1,4 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useReferralNetworkStore } from '@/store/referralNetwork'; import { useReferralNetworkStore } from '@/store/referralNetwork';
import type { NetworkGraphData } from '@/types/referralNetwork'; import type { NetworkGraphData } from '@/types/referralNetwork';
@@ -9,6 +10,7 @@ interface NetworkFiltersProps {
export function NetworkFilters({ data, className }: NetworkFiltersProps) { export function NetworkFilters({ data, className }: NetworkFiltersProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const panelRef = useRef<HTMLDivElement>(null);
const filters = useReferralNetworkStore((s) => s.filters); const filters = useReferralNetworkStore((s) => s.filters);
const updateFilters = useReferralNetworkStore((s) => s.updateFilters); const updateFilters = useReferralNetworkStore((s) => s.updateFilters);
@@ -16,6 +18,21 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
const isOpen = useReferralNetworkStore((s) => s.isFiltersOpen); const isOpen = useReferralNetworkStore((s) => s.isFiltersOpen);
const setIsOpen = useReferralNetworkStore((s) => s.setIsFiltersOpen); const setIsOpen = useReferralNetworkStore((s) => s.setIsFiltersOpen);
const hasActiveFilters =
filters.campaigns.length > 0 || filters.partnersOnly || filters.minReferrals > 0;
// Close on outside click
useEffect(() => {
if (!isOpen) return;
function handleClick(e: MouseEvent) {
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [isOpen, setIsOpen]);
function toggleCampaign(campaignId: number) { function toggleCampaign(campaignId: number) {
const current = filters.campaigns; const current = filters.campaigns;
const next = current.includes(campaignId) const next = current.includes(campaignId)
@@ -24,12 +41,13 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
updateFilters({ campaigns: next }); updateFilters({ campaigns: next });
} }
// Trigger button
if (!isOpen) { if (!isOpen) {
return ( return (
<button <button
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
aria-label={t('admin.referralNetwork.filters.title')} aria-label={t('admin.referralNetwork.filters.title')}
className={`flex items-center gap-2 rounded-lg border border-dark-700/50 bg-dark-800/80 px-3 py-2 text-sm text-dark-300 transition-colors hover:border-dark-600 hover:text-dark-100 ${className ?? ''}`} className={`relative flex shrink-0 items-center gap-2 rounded-lg border border-dark-700/50 bg-dark-800/80 px-2.5 py-2 text-sm text-dark-300 transition-colors hover:border-dark-600 hover:text-dark-100 ${className ?? ''}`}
> >
<svg <svg
className="h-4 w-4" className="h-4 w-4"
@@ -44,36 +62,16 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"
/> />
</svg> </svg>
{t('admin.referralNetwork.filters.title')} <span className="hidden sm:inline">{t('admin.referralNetwork.filters.title')}</span>
{hasActiveFilters && (
<span className="absolute -right-1 -top-1 h-2 w-2 rounded-full bg-accent-500" />
)}
</button> </button>
); );
} }
return ( // Filter panel content (shared between mobile/desktop)
<div const panelContent = (
className={`rounded-xl border border-dark-700/50 bg-dark-900/90 p-4 backdrop-blur-md ${className ?? ''}`}
>
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-dark-100">
{t('admin.referralNetwork.filters.title')}
</h3>
<button
onClick={() => setIsOpen(false)}
aria-label={t('common.close')}
className="text-dark-500 transition-colors hover:text-dark-300"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-4"> <div className="space-y-4">
{/* Campaigns */} {/* Campaigns */}
{data.campaigns.length > 0 && ( {data.campaigns.length > 0 && (
@@ -135,6 +133,66 @@ export function NetworkFilters({ data, className }: NetworkFiltersProps) {
{t('admin.referralNetwork.filters.reset')} {t('admin.referralNetwork.filters.reset')}
</button> </button>
</div> </div>
);
return (
<div ref={panelRef} className={className}>
{/* Desktop: inline dropdown */}
<div className="hidden sm:block">
<div className="w-64 rounded-xl border border-dark-700/50 bg-dark-900/95 p-4 backdrop-blur-md">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-dark-100">
{t('admin.referralNetwork.filters.title')}
</h3>
<button
onClick={() => setIsOpen(false)}
aria-label={t('common.close')}
className="text-dark-500 transition-colors hover:text-dark-300"
>
<svg
className="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{panelContent}
</div>
</div>
{/* Mobile: full-width overlay below top bar */}
<div className="fixed inset-x-0 top-0 z-50 sm:hidden">
{/* Backdrop */}
<div className="fixed inset-0 bg-black/60" onClick={() => setIsOpen(false)} />
{/* Panel */}
<div className="relative mx-3 mt-3 rounded-xl border border-dark-700/50 bg-dark-900/95 p-4 backdrop-blur-md">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-dark-100">
{t('admin.referralNetwork.filters.title')}
</h3>
<button
onClick={() => setIsOpen(false)}
aria-label={t('common.close')}
className="rounded-lg p-1 text-dark-500 transition-colors hover:bg-dark-800 hover:text-dark-300"
>
<svg
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{panelContent}
</div>
</div>
</div> </div>
); );
} }