feat: full mobile responsive optimization for Android/iPhone

- Add bottom navigation bar (6 tabs) for mobile, hidden on desktop
- Compact header with smaller logo and reduced padding on mobile
- Collapsible filter bar with active filter count badge on mobile
- Bottom-sheet style dropdowns with overlay and larger touch targets
- Fullscreen AI chat on mobile, floating window on desktop
- Responsive dashboard cards with always-visible expand button
- Smaller table text and touch-friendly scroll on all data grids
- iPhone safe-area support and thinner scrollbars on mobile

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Christian Vidal Wolf
2026-02-16 09:27:46 +01:00
co-authored by Claude Opus 4.6
parent fecc7264c8
commit 9895545910
11 changed files with 172 additions and 72 deletions
+6 -6
View File
@@ -93,21 +93,21 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
return (
<>
{/* Trigger Button */}
{/* Trigger Button - above bottom nav on mobile */}
<button
onClick={() => setIsOpen(!isOpen)}
className={`fixed bottom-6 right-6 z-50 p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-110
className={`fixed bottom-20 right-4 md:bottom-6 md:right-6 z-50 p-3 md:p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-110
${isOpen ? 'bg-slate-700 rotate-90 opacity-0 pointer-events-none' : 'bg-primary text-white rotate-0 opacity-100'}`}
>
<ChatIcon />
</button>
{/* Chat Window */}
{/* Chat Window - fullscreen on mobile, floating on desktop */}
<div
className={`fixed z-50 bg-slate-900 border border-border shadow-2xl transition-all duration-300 flex flex-col overflow-hidden
className={`fixed z-[60] bg-slate-900 border border-border shadow-2xl transition-all duration-300 flex flex-col overflow-hidden
${isOpen
? 'bottom-6 right-6 w-96 h-[600px] rounded-2xl opacity-100 translate-y-0'
: 'bottom-6 right-6 w-96 h-0 opacity-0 translate-y-10 pointer-events-none'}`}
? 'inset-0 rounded-none md:inset-auto md:bottom-6 md:right-6 md:w-96 md:h-[600px] md:rounded-2xl opacity-100 translate-y-0'
: 'bottom-0 right-0 w-full h-0 md:bottom-6 md:right-6 md:w-96 opacity-0 translate-y-10 pointer-events-none'}`}
>
{/* Header */}
<div className="bg-primary/10 p-4 border-b border-border flex justify-between items-center backdrop-blur">
+6 -6
View File
@@ -363,10 +363,10 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
}, [filteredAndSorted]);
return (
<div className="flex flex-col gap-6 animate-fade-in p-6 bg-[#0B0E14] min-h-screen text-slate-300">
<div className="flex flex-col gap-4 md:gap-6 animate-fade-in p-3 md:p-6 bg-[#0B0E14] min-h-screen text-slate-300">
<div className="flex justify-between items-end">
<div>
<h1 className="text-2xl font-bold text-white mb-1">Performance Overview</h1>
<h1 className="text-lg md:text-2xl font-bold text-white mb-1">Performance Overview</h1>
<div className="flex items-center gap-2 text-xs text-slate-400">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span>
<span>
@@ -376,7 +376,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 xl:grid-cols-13 gap-4 bg-[#141820] p-4 rounded-xl border border-white/5 shadow-xl">
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 xl:grid-cols-13 gap-2 md:gap-4 bg-[#141820] p-2 md:p-4 rounded-xl border border-white/5 shadow-xl">
<SummaryCard label="TOTAL SALES" value={totals.curr.salesTotal} prevValue={totals.prev?.salesTotal} format="currency" color="text-emerald-400" />
<SummaryCard label="ADS SALES" value={totals.curr.salesAds} prevValue={totals.prev?.salesAds} format="currency" color="text-orange-400" />
<SummaryCard label="ORGANIC SALES" value={totals.curr.salesOrganic} prevValue={totals.prev?.salesOrganic} format="currency" color="text-blue-400" />
@@ -395,7 +395,7 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
</div>
<div className="bg-[#141820] rounded-xl border border-white/10 overflow-hidden shadow-2xl flex-1 flex flex-col">
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-[#1A1F29]">
<div className="p-2 md:p-4 border-b border-white/5 flex flex-wrap justify-between items-center gap-2 bg-[#1A1F29]">
<div>
<h2 className="text-sm font-bold text-white uppercase tracking-wider">Product Inventory</h2>
<p className="text-[10px] text-slate-400 mt-0.5">
@@ -428,8 +428,8 @@ const AdsPerformance: React.FC<AdsPerformanceProps> = ({
</div>
</div>
<div className="overflow-x-auto flex-1 custom-scrollbar">
<table className="w-full text-left border-collapse text-[11px]">
<div className="overflow-x-auto flex-1 custom-scrollbar scroll-touch">
<table className="w-full text-left border-collapse text-[10px] md:text-[11px]">
<thead className="sticky top-0 z-20 bg-[#1A1F29]">
<tr className="border-b border-white/5">
<th className="p-4 font-black text-slate-400 uppercase tracking-widest min-w-[250px]">
+14 -14
View File
@@ -34,12 +34,12 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
if (isExpanded) {
return (
<div className="fixed inset-0 z-50 bg-slate-950 px-6 pb-6 pt-32 flex flex-col animate-fade-in overflow-hidden">
<div className="fixed inset-0 z-50 bg-slate-950 px-3 pb-16 pt-16 md:px-6 md:pb-6 md:pt-32 flex flex-col animate-fade-in overflow-hidden">
{/* Fixed Close Button - Positioned TOP RIGHT ON TOP OF FILTER BAR with z-[100] */}
{/* Fixed Close Button */}
<button
onClick={toggleExpand}
className="fixed top-3 right-3 z-[100] p-2 bg-red-600/90 hover:bg-red-500 border border-red-400 rounded-full text-white transition-all shadow-2xl hover:scale-110 flex items-center gap-2 group"
className="fixed top-3 right-3 z-[100] p-2 bg-red-600/90 hover:bg-red-500 active:bg-red-500 border border-red-400 rounded-full text-white transition-all shadow-2xl hover:scale-110 flex items-center gap-2 group"
title="Exit Fullscreen"
>
<span className="text-xs font-bold hidden group-hover:inline pr-1">CLOSE</span>
@@ -48,10 +48,10 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
</svg>
</button>
<div className="flex justify-between items-center mb-4 border-b border-slate-800 pb-4 shrink-0">
<h3 className="text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
<div className="flex justify-between items-center mb-3 md:mb-4 border-b border-slate-800 pb-3 md:pb-4 shrink-0">
<h3 className="text-base md:text-xl font-bold text-slate-100 uppercase tracking-wide">{title}</h3>
</div>
<div className="flex-1 overflow-auto bg-slate-900 rounded-xl p-6 border border-border custom-scrollbar">
<div className="flex-1 overflow-auto bg-slate-900 rounded-xl p-3 md:p-6 border border-border custom-scrollbar scroll-touch">
{children}
</div>
</div>
@@ -60,13 +60,13 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
return (
<div
className={`bg-surface border border-border rounded-xl p-6 shadow-sm flex flex-col group relative transition-all duration-300 hover:shadow-primary/5 ${className}`}
className={`bg-surface border border-border rounded-xl p-3 md:p-6 shadow-sm flex flex-col group relative transition-all duration-300 hover:shadow-primary/5 ${className}`}
>
<div className="flex justify-between items-start mb-4">
<h3 className="text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
<div className="flex justify-between items-start mb-3 md:mb-4">
<h3 className="text-xs md:text-sm font-semibold text-slate-400 uppercase tracking-wide">{title}</h3>
<button
onClick={toggleExpand}
className="opacity-0 group-hover:opacity-100 text-slate-500 hover:text-primary transition-opacity"
className="opacity-100 md:opacity-0 md:group-hover:opacity-100 text-slate-500 hover:text-primary active:text-primary transition-opacity"
title="Expand to Fullscreen"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5">
@@ -74,7 +74,7 @@ const ExpandableCard: React.FC<{ title: string; children: React.ReactNode; class
</svg>
</button>
</div>
<div className="flex-1 min-h-[250px] cursor-pointer" onClick={toggleExpand}>{children}</div>
<div className="flex-1 min-h-[200px] md:min-h-[250px] cursor-pointer" onClick={toggleExpand}>{children}</div>
</div>
);
};
@@ -102,8 +102,8 @@ const MultiYearKPICard: React.FC<{
const displayTitle = contextData ? `${title} (Selected Item)` : title;
return (
<div className="bg-gradient-to-br from-surface to-slate-900 border border-border rounded-xl p-6 flex flex-col justify-center">
<h3 className="text-sm font-medium text-slate-400 mb-3">{displayTitle}</h3>
<div className="bg-gradient-to-br from-surface to-slate-900 border border-border rounded-xl p-3 md:p-6 flex flex-col justify-center">
<h3 className="text-xs md:text-sm font-medium text-slate-400 mb-2 md:mb-3">{displayTitle}</h3>
{sortedYears.length === 0 && <p className="text-3xl font-bold text-white">0</p>}
@@ -508,7 +508,7 @@ const Dashboard: React.FC<DashboardProps> = ({ data, contextData, adsData = [] }
const chartHeight = Math.max(displayData.topLinesSplit.length * 60, 300);
return (
<div className="p-6 space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
<div className="p-3 md:p-6 space-y-4 md:space-y-6 max-w-7xl mx-auto animate-fade-in pb-24">
{/* Ads Performance Section - Condensed Layout */}
{adsKPIsByYear && availableAdsYears.length > 0 && (
+3 -3
View File
@@ -738,7 +738,7 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
}, [rowFilters, data, effectiveDimensions, searchTerm]);
return (
<div className="space-y-6 max-w-[95vw] mx-auto animate-fade-in pb-24">
<div className="space-y-4 md:space-y-6 max-w-[98vw] md:max-w-[95vw] mx-auto animate-fade-in px-1 md:px-0 pb-24">
{/* 1. Time Series Chart Section */}
{showChart && chartData.length > 0 && (
@@ -1173,8 +1173,8 @@ const DataGrid: React.FC<DataGridProps> = ({ data, filters, hasCustomerFilter, a
)}
{/* Data Table */}
<div className="overflow-x-auto min-h-[400px]">
<table className="w-full text-left text-sm border-collapse">
<div className="overflow-x-auto min-h-[400px] scroll-touch">
<table className="w-full text-left text-xs md:text-sm border-collapse">
<thead className="bg-slate-950 text-slate-400 uppercase text-xs font-semibold tracking-wider sticky top-0 z-20 shadow-sm border-b border-white/10">
{/* NEW: Filtered Totals Header Row */}
<tr className="bg-indigo-950/20 border-b border-indigo-500/30">
+41 -12
View File
@@ -1,7 +1,8 @@
import React from 'react';
import React, { useState, useMemo } from 'react';
import { FilterState } from '../types';
import MultiSelectDropdown from './MultiSelectDropdown';
import { FunnelIcon } from './Icons';
interface FilterBarProps {
filters: FilterState;
@@ -20,36 +21,64 @@ interface FilterBarProps {
}
const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options }) => {
const [isExpanded, setIsExpanded] = useState(false);
const activeFilterCount = useMemo(() => {
return filters.customer.length + filters.year.length + filters.month.length +
filters.week.length + filters.line.length + filters.sku.length +
filters.title.length + filters.asin.length + filters.stock.length;
}, [filters]);
return (
<div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-4 shadow-xl">
<div className="max-w-7xl mx-auto flex flex-wrap gap-4 items-end">
<div className="bg-slate-950 border-b border-border sticky top-0 z-[60] p-2 md:p-4 shadow-xl">
{/* Mobile toggle button */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="md:hidden w-full flex items-center justify-between px-3 py-2 bg-surface border border-border rounded-lg text-sm text-slate-300 active:bg-slate-800 transition-colors"
>
<div className="flex items-center gap-2">
<FunnelIcon />
<span className="font-medium">Filters</span>
{activeFilterCount > 0 && (
<span className="bg-primary text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[20px] text-center">
{activeFilterCount}
</span>
)}
</div>
<svg className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Filter dropdowns - always visible on desktop, toggle on mobile */}
<div className={`max-w-7xl mx-auto flex-wrap gap-2 md:gap-4 items-end ${isExpanded ? 'flex flex-col md:flex-row mt-3' : 'hidden md:flex md:flex-row'}`}>
<MultiSelectDropdown
label="Customer"
selected={filters.customer}
options={options.customer}
onChange={(v) => onFilterChange('customer', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Year"
selected={filters.year}
options={options.year}
onChange={(v) => onFilterChange('year', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Month"
selected={filters.month}
options={options.month}
onChange={(v) => onFilterChange('month', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Week"
selected={filters.week}
options={options.week}
onChange={(v) => onFilterChange('week', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
enableRangeSelect={true}
/>
<MultiSelectDropdown
@@ -57,35 +86,35 @@ const FilterBar: React.FC<FilterBarProps> = ({ filters, onFilterChange, options
selected={filters.line}
options={options.line}
onChange={(v) => onFilterChange('line', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="SKU"
selected={filters.sku}
options={options.sku}
onChange={(v) => onFilterChange('sku', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Title"
selected={filters.title}
options={options.title}
onChange={(v) => onFilterChange('title', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="ASIN"
selected={filters.asin}
options={options.asin}
onChange={(v) => onFilterChange('asin', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
/>
<MultiSelectDropdown
label="Stock"
selected={filters.stock}
options={options.stock}
onChange={(v) => onFilterChange('stock', v)}
className="flex-1"
className="w-full md:w-auto md:flex-1"
enableRangeSelect={true}
/>
</div>
+6 -6
View File
@@ -414,8 +414,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
};
return (
<div className="flex flex-col gap-6 animate-fade-in p-6 h-full overflow-hidden">
<div className="flex flex-col xl:flex-row gap-6 h-[400px] shrink-0">
<div className="flex flex-col gap-4 md:gap-6 animate-fade-in p-3 md:p-6 h-full overflow-hidden">
<div className="flex flex-col xl:flex-row gap-4 md:gap-6 h-auto xl:h-[400px] shrink-0">
<div className="flex flex-col gap-4 min-w-[300px] xl:w-[25%]">
<div className="bg-slate-900 border border-white/5 p-4 rounded-xl shadow-lg flex-1 flex flex-col justify-center">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Annual Forecast Total</span>
@@ -478,8 +478,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
</div>
<div className="bg-slate-900 border border-white/10 rounded-2xl overflow-hidden shadow-2xl flex-1 flex flex-col min-h-0 relative">
<div className="p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-4 bg-slate-800/20 shrink-0">
<h3 className="text-lg font-bold text-white uppercase tracking-tight">
<div className="p-2 md:p-4 border-b border-white/5 flex flex-col md:flex-row justify-between gap-2 md:gap-4 bg-slate-800/20 shrink-0">
<h3 className="text-sm md:text-lg font-bold text-white uppercase tracking-tight">
Product Performance Comparison <span className="text-xs text-slate-500 font-normal normal-case ml-2">(v2.5 optimized)</span>
</h3>
<div className="flex items-center gap-4">
@@ -536,8 +536,8 @@ const ForecastView: React.FC<ForecastViewProps> = ({
</div>
</div>
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar relative">
<table className="w-full text-left border-collapse relative">
<div className="flex-1 overflow-auto min-h-0 custom-scrollbar scroll-touch relative">
<table className="w-full text-left border-collapse relative text-[10px] md:text-sm">
<thead className="sticky top-0 z-20 bg-slate-950 shadow-sm text-[10px] font-black text-slate-500 uppercase tracking-wider">
<tr>
<th className="px-6 py-4 text-left w-[30%]">
+27 -4
View File
@@ -38,6 +38,14 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
}
}, [isOpen]);
// Prevent body scroll when dropdown is open on mobile
useEffect(() => {
if (isOpen && window.innerWidth < 768) {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}
}, [isOpen]);
const filteredOptions = useMemo(() => {
if (!searchTerm) return options;
return options.filter(opt => opt.toLowerCase().includes(searchTerm.toLowerCase()));
@@ -139,8 +147,23 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
</svg>
</button>
{/* Mobile overlay backdrop */}
{isOpen && (
<div className="absolute top-[calc(100%+4px)] left-0 w-64 max-h-96 overflow-hidden bg-slate-900 border border-slate-700 rounded-xl shadow-2xl z-[100] animate-fade-in flex flex-col">
<div className="fixed inset-0 bg-black/50 z-[99] md:hidden" onClick={() => setIsOpen(false)} />
)}
{isOpen && (
<div className="fixed inset-x-3 bottom-3 max-h-[75vh] md:absolute md:inset-auto md:top-[calc(100%+4px)] md:left-0 md:bottom-auto md:w-64 md:max-h-96 overflow-hidden bg-slate-900 border border-slate-700 rounded-xl shadow-2xl z-[100] animate-fade-in flex flex-col">
{/* Mobile header with close */}
<div className="flex items-center justify-between p-3 border-b border-slate-800 md:hidden">
<h3 className="text-sm font-bold text-white">{label}</h3>
<button onClick={() => setIsOpen(false)} className="text-slate-400 active:text-white p-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Search Bar */}
<div className="p-2 border-b border-slate-800 sticky top-0 bg-slate-900 z-10">
@@ -209,14 +232,14 @@ const MultiSelectDropdown: React.FC<MultiSelectDropdownProps> = ({ label, select
</button>
</div>
<div className="space-y-1 overflow-y-auto custom-scrollbar p-2 max-h-60">
<div className="space-y-1 overflow-y-auto custom-scrollbar scroll-touch p-2 max-h-60 md:max-h-60">
{filteredOptions.map((opt) => (
<label key={opt} className="flex items-center space-x-3 p-2 rounded hover:bg-slate-800 cursor-pointer group">
<label key={opt} className="flex items-center space-x-3 p-2.5 md:p-2 rounded hover:bg-slate-800 active:bg-slate-700 cursor-pointer group">
<input
type="checkbox"
checked={selected.includes(opt)}
onChange={() => toggleOption(opt)}
className="form-checkbox h-4 w-4 text-primary rounded border-slate-600 bg-slate-800 focus:ring-primary focus:ring-offset-slate-900 transition duration-150 ease-in-out"
className="form-checkbox h-5 w-5 md:h-4 md:w-4 text-primary rounded border-slate-600 bg-slate-800 focus:ring-primary focus:ring-offset-slate-900 transition duration-150 ease-in-out"
/>
<span className={`text-sm break-all group-hover:text-white ${selected.includes(opt) ? 'text-white' : 'text-slate-400'}`}>
{opt}
+1 -1
View File
@@ -241,7 +241,7 @@ const TopMovers: React.FC<TopMoversProps> = ({ data }) => {
}
return (
<div className="space-y-6 max-w-7xl mx-auto pb-24 animate-fade-in">
<div className="space-y-4 md:space-y-6 max-w-7xl mx-auto px-1 md:px-0 pb-24 animate-fade-in">
{/* Controls Header */}
<div className="bg-surface border border-border rounded-xl p-6 shadow-sm flex flex-col md:flex-row justify-between items-center gap-4">
+4 -4
View File
@@ -632,9 +632,9 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
}, [sortedRows, allWeeks, weekTotals]);
return (
<div className="flex flex-col gap-4 animate-fade-in">
<div className="flex flex-col gap-3 md:gap-4 animate-fade-in px-1 md:px-0">
{/* Toolbar: Search & Pagination */}
<div className="flex flex-col lg:flex-row justify-between items-center gap-4 bg-slate-900 border border-white/10 p-4 rounded-xl shadow-lg">
<div className="flex flex-col lg:flex-row justify-between items-center gap-3 md:gap-4 bg-slate-900 border border-white/10 p-2 md:p-4 rounded-xl shadow-lg">
<div className="flex flex-wrap items-center gap-4 w-full lg:w-auto">
{/* Search Input */}
<div className="relative w-full md:w-80">
@@ -739,8 +739,8 @@ const WeeklyGrid: React.FC<WeeklyGridProps & { top50Mode: 'eu' | 'uk' }> = ({
</div>
<div className="bg-slate-900 border border-white/10 rounded-xl overflow-hidden shadow-2xl flex-1 min-h-0">
<div className="overflow-x-auto overflow-y-auto h-full max-h-[calc(100vh-220px)] custom-scrollbar">
<table className="w-full text-left border-collapse min-w-[max-content]">
<div className="overflow-x-auto overflow-y-auto h-full max-h-[calc(100vh-220px)] custom-scrollbar scroll-touch">
<table className="w-full text-left border-collapse min-w-[max-content] text-[10px] md:text-sm">
<thead className="sticky top-0 z-20 bg-slate-900">
<tr className="bg-slate-900 border-b border-white/10 relative z-30">
<th