mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
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:
@@ -34,19 +34,28 @@ 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">
|
||||||
<AdminBackButton />
|
{/* Row 1: Back + Title + (desktop: search + filter) + (mobile: filter icon) */}
|
||||||
<div className="mr-2 min-w-0">
|
<div className="flex items-center gap-3 px-3 py-2 sm:px-4 sm:py-3">
|
||||||
<h1 className="truncate text-sm font-bold text-dark-100 sm:text-lg">
|
<AdminBackButton />
|
||||||
|
<h1 className="min-w-0 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>
|
<NetworkSearch className="max-w-md flex-1" />
|
||||||
|
{networkData && <NetworkFilters data={networkData} />}
|
||||||
|
</div>
|
||||||
|
{/* Mobile: filter icon only */}
|
||||||
|
{networkData && (
|
||||||
|
<div className="ml-auto sm:hidden">
|
||||||
|
<NetworkFilters data={networkData} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 items-center gap-2">
|
{/* Row 2 (mobile only): full-width search */}
|
||||||
<NetworkSearch className="max-w-md flex-1" />
|
<div className="px-3 pb-2 sm:hidden">
|
||||||
{networkData && <NetworkFilters data={networkData} />}
|
<NetworkSearch className="w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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,96 +62,136 @@ 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="space-y-4">
|
||||||
>
|
{/* Campaigns */}
|
||||||
<div className="mb-3 flex items-center justify-between">
|
{data.campaigns.length > 0 && (
|
||||||
<h3 className="text-sm font-semibold text-dark-100">
|
<div>
|
||||||
{t('admin.referralNetwork.filters.title')}
|
<label className="mb-1.5 block text-xs font-medium text-dark-400">
|
||||||
</h3>
|
{t('admin.referralNetwork.filters.campaigns')}
|
||||||
<button
|
</label>
|
||||||
onClick={() => setIsOpen(false)}
|
<div className="max-h-32 space-y-1 overflow-y-auto">
|
||||||
aria-label={t('common.close')}
|
{data.campaigns.map((campaign) => (
|
||||||
className="text-dark-500 transition-colors hover:text-dark-300"
|
<label
|
||||||
>
|
key={campaign.id}
|
||||||
<svg
|
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-sm transition-colors hover:bg-dark-800/50"
|
||||||
className="h-4 w-4"
|
>
|
||||||
fill="none"
|
<input
|
||||||
viewBox="0 0 24 24"
|
type="checkbox"
|
||||||
stroke="currentColor"
|
checked={filters.campaigns.includes(campaign.id)}
|
||||||
strokeWidth={2}
|
onChange={() => toggleCampaign(campaign.id)}
|
||||||
>
|
className="h-3.5 w-3.5 rounded border-dark-600 bg-dark-800 text-accent-500 focus:ring-accent-500/30"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
/>
|
||||||
</svg>
|
<span className="truncate text-dark-200">{campaign.name}</span>
|
||||||
</button>
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Partners only */}
|
||||||
|
<label className="flex cursor-pointer items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filters.partnersOnly}
|
||||||
|
onChange={(e) => updateFilters({ partnersOnly: e.target.checked })}
|
||||||
|
className="h-3.5 w-3.5 rounded border-dark-600 bg-dark-800 text-accent-500 focus:ring-accent-500/30"
|
||||||
|
/>
|
||||||
|
<span className="text-dark-200">{t('admin.referralNetwork.filters.partnersOnly')}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Min referrals */}
|
||||||
|
<div>
|
||||||
|
<label className="mb-1.5 flex items-center justify-between text-xs font-medium text-dark-400">
|
||||||
|
<span>{t('admin.referralNetwork.filters.minReferrals')}</span>
|
||||||
|
<span className="font-mono text-dark-300">{filters.minReferrals}</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={50}
|
||||||
|
value={filters.minReferrals}
|
||||||
|
onChange={(e) => updateFilters({ minReferrals: Number(e.target.value) })}
|
||||||
|
className="w-full accent-accent-500"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* Reset */}
|
||||||
{/* Campaigns */}
|
<button
|
||||||
{data.campaigns.length > 0 && (
|
onClick={resetFilters}
|
||||||
<div>
|
className="w-full rounded-lg border border-dark-700/50 py-1.5 text-xs font-medium text-dark-400 transition-colors hover:border-dark-600 hover:text-dark-200"
|
||||||
<label className="mb-1.5 block text-xs font-medium text-dark-400">
|
>
|
||||||
{t('admin.referralNetwork.filters.campaigns')}
|
{t('admin.referralNetwork.filters.reset')}
|
||||||
</label>
|
</button>
|
||||||
<div className="max-h-32 space-y-1 overflow-y-auto">
|
</div>
|
||||||
{data.campaigns.map((campaign) => (
|
);
|
||||||
<label
|
|
||||||
key={campaign.id}
|
return (
|
||||||
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-sm transition-colors hover:bg-dark-800/50"
|
<div ref={panelRef} className={className}>
|
||||||
>
|
{/* Desktop: inline dropdown */}
|
||||||
<input
|
<div className="hidden sm:block">
|
||||||
type="checkbox"
|
<div className="w-64 rounded-xl border border-dark-700/50 bg-dark-900/95 p-4 backdrop-blur-md">
|
||||||
checked={filters.campaigns.includes(campaign.id)}
|
<div className="mb-3 flex items-center justify-between">
|
||||||
onChange={() => toggleCampaign(campaign.id)}
|
<h3 className="text-sm font-semibold text-dark-100">
|
||||||
className="h-3.5 w-3.5 rounded border-dark-600 bg-dark-800 text-accent-500 focus:ring-accent-500/30"
|
{t('admin.referralNetwork.filters.title')}
|
||||||
/>
|
</h3>
|
||||||
<span className="truncate text-dark-200">{campaign.name}</span>
|
<button
|
||||||
</label>
|
onClick={() => setIsOpen(false)}
|
||||||
))}
|
aria-label={t('common.close')}
|
||||||
</div>
|
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>
|
||||||
)}
|
{panelContent}
|
||||||
|
|
||||||
{/* Partners only */}
|
|
||||||
<label className="flex cursor-pointer items-center gap-2 text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.partnersOnly}
|
|
||||||
onChange={(e) => updateFilters({ partnersOnly: e.target.checked })}
|
|
||||||
className="h-3.5 w-3.5 rounded border-dark-600 bg-dark-800 text-accent-500 focus:ring-accent-500/30"
|
|
||||||
/>
|
|
||||||
<span className="text-dark-200">{t('admin.referralNetwork.filters.partnersOnly')}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Min referrals */}
|
|
||||||
<div>
|
|
||||||
<label className="mb-1.5 flex items-center justify-between text-xs font-medium text-dark-400">
|
|
||||||
<span>{t('admin.referralNetwork.filters.minReferrals')}</span>
|
|
||||||
<span className="font-mono text-dark-300">{filters.minReferrals}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={50}
|
|
||||||
value={filters.minReferrals}
|
|
||||||
onChange={(e) => updateFilters({ minReferrals: Number(e.target.value) })}
|
|
||||||
className="w-full accent-accent-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Reset */}
|
{/* Mobile: full-width overlay below top bar */}
|
||||||
<button
|
<div className="fixed inset-x-0 top-0 z-50 sm:hidden">
|
||||||
onClick={resetFilters}
|
{/* Backdrop */}
|
||||||
className="w-full rounded-lg border border-dark-700/50 py-1.5 text-xs font-medium text-dark-400 transition-colors hover:border-dark-600 hover:text-dark-200"
|
<div className="fixed inset-0 bg-black/60" onClick={() => setIsOpen(false)} />
|
||||||
>
|
{/* Panel */}
|
||||||
{t('admin.referralNetwork.filters.reset')}
|
<div className="relative mx-3 mt-3 rounded-xl border border-dark-700/50 bg-dark-900/95 p-4 backdrop-blur-md">
|
||||||
</button>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user