style: unify filter UI, add custom to-from date picker, and ensure all buttons use pointer cursors
This commit is contained in:
@@ -38,13 +38,41 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange,
|
|||||||
return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
return date.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDateForInput = (date: Date) => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseLocalDate = (value: string) => {
|
||||||
|
if (!value) return null;
|
||||||
|
const [year, month, day] = value.split('-');
|
||||||
|
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newStart = parseLocalDate(e.target.value);
|
||||||
|
if (newStart && !isNaN(newStart.getTime())) {
|
||||||
|
onChange({ ...dateRange, start: newStart });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newEnd = parseLocalDate(e.target.value);
|
||||||
|
if (newEnd && !isNaN(newEnd.getTime())) {
|
||||||
|
newEnd.setHours(23, 59, 59, 999);
|
||||||
|
onChange({ ...dateRange, end: newEnd });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{/* Date Presets Dropdown */}
|
{/* Date Presets Dropdown */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsPresetOpen(!isPresetOpen)}
|
onClick={() => setIsPresetOpen(!isPresetOpen)}
|
||||||
className="flex items-center gap-2 bg-dark-card border border-dark-border px-4 py-2.5 rounded-xl shadow-sm hover:border-brand-primary transition-colors text-sm font-medium text-dark-text"
|
className="flex items-center gap-2 bg-dark-card border border-dark-border px-4 py-2.5 rounded-xl shadow-sm hover:border-brand-primary transition-colors text-sm font-medium text-dark-text cursor-pointer"
|
||||||
>
|
>
|
||||||
<Calendar size={16} className="text-dark-muted shrink-0" />
|
<Calendar size={16} className="text-dark-muted shrink-0" />
|
||||||
<span>{formatShortDate(dateRange.start)} - {formatShortDate(dateRange.end)}</span>
|
<span>{formatShortDate(dateRange.start)} - {formatShortDate(dateRange.end)}</span>
|
||||||
@@ -54,7 +82,31 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange,
|
|||||||
{isPresetOpen && (
|
{isPresetOpen && (
|
||||||
<>
|
<>
|
||||||
<div className="fixed inset-0 z-10" onClick={() => setIsPresetOpen(false)}></div>
|
<div className="fixed inset-0 z-10" onClick={() => setIsPresetOpen(false)}></div>
|
||||||
<div className="absolute right-0 mt-2 w-48 bg-dark-card border border-dark-border rounded-xl shadow-xl z-20 py-2 max-h-64 overflow-y-auto">
|
<div className="absolute right-0 mt-2 w-56 bg-dark-card border border-dark-border rounded-xl shadow-xl z-20 py-2 max-h-[32rem] overflow-y-auto">
|
||||||
|
<div className="px-4 pb-3 pt-1 border-b border-dark-border mb-2 flex flex-col gap-2">
|
||||||
|
<span className="text-xs font-bold text-dark-muted uppercase tracking-widest">Período Customizado</span>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-medium text-dark-text w-6">De:</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={formatDateForInput(dateRange.start)}
|
||||||
|
onChange={handleStartChange}
|
||||||
|
className="bg-dark-input border border-dark-border text-dark-text text-xs rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary w-full cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs font-medium text-dark-text w-6">Até:</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={formatDateForInput(dateRange.end)}
|
||||||
|
onChange={handleEndChange}
|
||||||
|
className="bg-dark-input border border-dark-border text-dark-text text-xs rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary w-full cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="px-4 text-xs font-bold text-dark-muted uppercase tracking-widest mb-1 block">Atalhos</span>
|
||||||
{PRESETS.map((preset) => (
|
{PRESETS.map((preset) => (
|
||||||
<button
|
<button
|
||||||
key={preset.label}
|
key={preset.label}
|
||||||
@@ -62,7 +114,7 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange,
|
|||||||
onChange(preset.getRange());
|
onChange(preset.getRange());
|
||||||
setIsPresetOpen(false);
|
setIsPresetOpen(false);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-dark-muted hover:text-dark-text hover:bg-dark-input transition-colors"
|
className="w-full text-left px-4 py-2 text-sm text-dark-muted hover:text-dark-text hover:bg-dark-input transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
{preset.label}
|
{preset.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -77,7 +129,7 @@ const DateRangePicker: React.FC<DateRangePickerProps> = ({ dateRange, onChange,
|
|||||||
<div className="flex items-center bg-dark-card border border-dark-border rounded-xl shadow-sm overflow-hidden">
|
<div className="flex items-center bg-dark-card border border-dark-border rounded-xl shadow-sm overflow-hidden">
|
||||||
<button
|
<button
|
||||||
onClick={onManualRefresh}
|
onClick={onManualRefresh}
|
||||||
className="flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium text-dark-text hover:text-brand-primary hover:bg-dark-input transition-colors border-r border-dark-border"
|
className="flex items-center gap-1.5 px-4 py-2.5 text-sm font-medium text-dark-text hover:text-brand-primary hover:bg-dark-input transition-colors border-r border-dark-border cursor-pointer"
|
||||||
title="Atualizar Agora"
|
title="Atualizar Agora"
|
||||||
>
|
>
|
||||||
<RefreshCw size={16} className="text-brand-primary" />
|
<RefreshCw size={16} className="text-brand-primary" />
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const Layout = () => {
|
|||||||
<div className="p-4 border-t border-dark-border">
|
<div className="p-4 border-t border-dark-border">
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className={`w-full flex items-center text-red-500 hover:bg-red-500/10 px-4 py-3 rounded-xl transition-all ${isSidebarCollapsed ? 'justify-center' : 'space-x-3'}`}
|
className={`w-full flex items-center text-red-500 hover:bg-red-500/10 px-4 py-3 rounded-xl transition-all cursor-pointer ${isSidebarCollapsed ? 'justify-center' : 'space-x-3'}`}
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5 shrink-0" />
|
<LogOut className="w-5 h-5 shrink-0" />
|
||||||
{!isSidebarCollapsed && <span className="font-medium">Sair</span>}
|
{!isSidebarCollapsed && <span className="font-medium">Sair</span>}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const Clients = () => {
|
|||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value as SortOption)}
|
onChange={(e) => setSortBy(e.target.value as SortOption)}
|
||||||
className="appearance-none bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border text-zinc-700 dark:text-dark-text text-sm rounded-xl pl-9 pr-8 py-2.5 focus:outline-none focus:border-brand-primary transition-all shadow-sm cursor-pointer"
|
className="appearance-none bg-dark-card border border-dark-border text-dark-text text-sm rounded-xl pl-9 pr-8 py-2.5 focus:outline-none focus:border-brand-primary transition-colors shadow-sm cursor-pointer"
|
||||||
>
|
>
|
||||||
<option value="recent">Mais Recentes</option>
|
<option value="recent">Mais Recentes</option>
|
||||||
<option value="spent_desc">Maior Gasto</option>
|
<option value="spent_desc">Maior Gasto</option>
|
||||||
@@ -107,7 +107,7 @@ const Clients = () => {
|
|||||||
placeholder="Buscar cliente..."
|
placeholder="Buscar cliente..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full md:w-64 bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border text-zinc-900 dark:text-dark-text rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20 transition-all shadow-sm"
|
className="w-full md:w-64 bg-dark-card border border-dark-border text-dark-text rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:border-brand-primary transition-colors shadow-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +141,7 @@ const Clients = () => {
|
|||||||
<td className="px-6 py-2.5 text-right">
|
<td className="px-6 py-2.5 text-right">
|
||||||
<Link
|
<Link
|
||||||
to={`/clients/${encodeURIComponent(client.name)}`}
|
to={`/clients/${encodeURIComponent(client.name)}`}
|
||||||
className="inline-flex items-center text-xs font-bold text-brand-primary hover:opacity-80 transition-opacity"
|
className="inline-flex items-center text-xs font-bold text-brand-primary hover:opacity-80 transition-opacity cursor-pointer"
|
||||||
>
|
>
|
||||||
Ver detalhes
|
Ver detalhes
|
||||||
<ChevronRight className="w-3.5 h-3.5 ml-1" />
|
<ChevronRight className="w-3.5 h-3.5 ml-1" />
|
||||||
@@ -163,7 +163,7 @@ const Clients = () => {
|
|||||||
setItemsPerPage(Number(e.target.value));
|
setItemsPerPage(Number(e.target.value));
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="bg-zinc-50 dark:bg-dark-input border border-zinc-200 dark:border-dark-border rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary cursor-pointer"
|
className="bg-dark-card border border-dark-border rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary cursor-pointer text-dark-text"
|
||||||
>
|
>
|
||||||
<option value={10}>10</option>
|
<option value={10}>10</option>
|
||||||
<option value={20}>20</option>
|
<option value={20}>20</option>
|
||||||
@@ -181,14 +181,14 @@ const Clients = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
className="p-1 rounded-lg border border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:border-brand-primary transition-colors text-dark-muted hover:text-dark-text cursor-pointer bg-dark-card"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
disabled={currentPage === totalPages || totalPages === 0}
|
disabled={currentPage === totalPages || totalPages === 0}
|
||||||
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
className="p-1 rounded-lg border border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:border-brand-primary transition-colors text-dark-muted hover:text-dark-text cursor-pointer bg-dark-card"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const Products = () => {
|
|||||||
placeholder="Buscar por nome ou ID..."
|
placeholder="Buscar por nome ou ID..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full md:w-64 bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border text-zinc-900 dark:text-dark-text rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20 transition-all shadow-sm"
|
className="w-full md:w-64 bg-dark-card border border-dark-border text-dark-text rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:border-brand-primary transition-colors shadow-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +122,7 @@ const Products = () => {
|
|||||||
<td className="px-6 py-2.5 text-right">
|
<td className="px-6 py-2.5 text-right">
|
||||||
<Link
|
<Link
|
||||||
to={`/products/${product.id}`}
|
to={`/products/${product.id}`}
|
||||||
className="inline-flex items-center text-xs font-bold text-brand-primary hover:opacity-80 transition-opacity bg-brand-primary/10 px-3 py-1.5 rounded-lg"
|
className="inline-flex items-center text-xs font-bold text-brand-primary hover:opacity-80 transition-opacity bg-brand-primary/10 px-3 py-1.5 rounded-lg cursor-pointer"
|
||||||
>
|
>
|
||||||
<TrendingUp className="w-3.5 h-3.5 mr-1.5" />
|
<TrendingUp className="w-3.5 h-3.5 mr-1.5" />
|
||||||
Ver Gráfico
|
Ver Gráfico
|
||||||
@@ -144,7 +144,7 @@ const Products = () => {
|
|||||||
setItemsPerPage(Number(e.target.value));
|
setItemsPerPage(Number(e.target.value));
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
}}
|
}}
|
||||||
className="bg-zinc-50 dark:bg-dark-input border border-zinc-200 dark:border-dark-border rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary cursor-pointer"
|
className="bg-dark-card border border-dark-border rounded-lg px-2 py-1 focus:outline-none focus:border-brand-primary cursor-pointer text-dark-text"
|
||||||
>
|
>
|
||||||
<option value={10}>10</option>
|
<option value={10}>10</option>
|
||||||
<option value={20}>20</option>
|
<option value={20}>20</option>
|
||||||
@@ -162,14 +162,14 @@ const Products = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
className="p-1 rounded-lg border border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:border-brand-primary transition-colors text-dark-muted hover:text-dark-text cursor-pointer bg-dark-card"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
<ChevronLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
disabled={currentPage === totalPages || totalPages === 0}
|
disabled={currentPage === totalPages || totalPages === 0}
|
||||||
className="p-1 rounded-lg border border-zinc-200 dark:border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:bg-zinc-50 dark:hover:bg-dark-input transition-colors text-zinc-600 dark:text-dark-text"
|
className="p-1 rounded-lg border border-dark-border disabled:opacity-50 disabled:cursor-not-allowed hover:border-brand-primary transition-colors text-dark-muted hover:text-dark-text cursor-pointer bg-dark-card"
|
||||||
>
|
>
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user