Add off-white theme mode
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en" class="dark" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
||||
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield } from 'lucide-react';
|
||||
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, Moon, Sun } from 'lucide-react';
|
||||
import type { DateRange, OrderData } from '../types';
|
||||
import { isSuperAdmin, logout } from '../dataService';
|
||||
import { rangeForLastDays } from '../dateRanges';
|
||||
|
||||
const emptyOrdersData: OrderData[] = [];
|
||||
type ThemeMode = 'dark' | 'offwhite';
|
||||
|
||||
const Layout = () => {
|
||||
const location = useLocation();
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
||||
return localStorage.getItem('graph_sidebar_collapsed') === 'true';
|
||||
});
|
||||
const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
|
||||
return localStorage.getItem('nexstar_theme') === 'offwhite' ? 'offwhite' : 'dark';
|
||||
});
|
||||
|
||||
const [dateRange, setDateRange] = useState<DateRange>(() => {
|
||||
const saved = localStorage.getItem('nexstar_date_range');
|
||||
@@ -29,6 +33,12 @@ const Layout = () => {
|
||||
return saved ? Number(saved) : 0;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.dataset.theme = themeMode;
|
||||
localStorage.setItem('nexstar_theme', themeMode);
|
||||
}, [themeMode]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('nexstar_refresh_interval', refreshInterval.toString());
|
||||
}, [refreshInterval]);
|
||||
@@ -45,6 +55,9 @@ const Layout = () => {
|
||||
setIsSidebarCollapsed(newState);
|
||||
localStorage.setItem('graph_sidebar_collapsed', String(newState));
|
||||
};
|
||||
const toggleTheme = () => {
|
||||
setThemeMode(current => current === 'dark' ? 'offwhite' : 'dark');
|
||||
};
|
||||
|
||||
const appNavigation = [
|
||||
{ name: 'Dashboard', href: '/graph', icon: LayoutDashboard },
|
||||
@@ -133,13 +146,22 @@ const Layout = () => {
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col h-screen overflow-hidden">
|
||||
{/* Header */}
|
||||
<header className="h-20 bg-dark-header border-b border-dark-border flex items-center px-8 shrink-0">
|
||||
<header className="h-20 bg-dark-header border-b border-dark-border flex items-center justify-between px-8 shrink-0">
|
||||
<h2 className="text-xl font-bold text-dark-text">Painel de Análise</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-transparent bg-transparent text-dark-text transition-colors hover:border-dark-border hover:bg-dark-input hover:text-brand-primary cursor-pointer"
|
||||
title={themeMode === 'dark' ? 'Usar modo off-white' : 'Usar modo escuro'}
|
||||
aria-label={themeMode === 'dark' ? 'Usar modo off-white' : 'Usar modo escuro'}
|
||||
>
|
||||
{themeMode === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="app-content flex-1 overflow-y-auto p-8 relative">
|
||||
<Outlet context={{ dateRange, setDateRange, ordersData: emptyOrdersData, isDataLoading: false, refreshInterval, setRefreshInterval }} />
|
||||
<Outlet context={{ dateRange, setDateRange, ordersData: emptyOrdersData, isDataLoading: false, refreshInterval, setRefreshInterval, themeMode }} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
@theme {
|
||||
--color-brand-primary: #b8c7d1;
|
||||
--color-brand-contrast: #070707;
|
||||
--color-dark-bg: #070707;
|
||||
--color-dark-card: #141414;
|
||||
--color-dark-header: #101010;
|
||||
@@ -14,6 +15,38 @@
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--chart-grid: #222222;
|
||||
--chart-axis: #888888;
|
||||
--chart-cursor: rgba(255, 255, 255, 0.055);
|
||||
--chart-label: #ededed;
|
||||
--chart-tooltip-bg: #141414;
|
||||
--chart-tooltip-text: #ededed;
|
||||
--chart-tooltip-border: transparent;
|
||||
--chart-detail-bar: #9ECAE1;
|
||||
}
|
||||
|
||||
html[data-theme='offwhite'] {
|
||||
--color-brand-primary: #3f535a;
|
||||
--color-brand-contrast: #fbf8f1;
|
||||
--color-dark-bg: #f3f0e9;
|
||||
--color-dark-card: #fbf8f1;
|
||||
--color-dark-header: #f0ece4;
|
||||
--color-dark-sidebar: #ebe6dc;
|
||||
--color-dark-border: #d8d0c3;
|
||||
--color-dark-input: #f1ede5;
|
||||
--color-dark-text: #242321;
|
||||
--color-dark-muted: #6f6a61;
|
||||
--chart-grid: rgba(63, 58, 49, 0.14);
|
||||
--chart-axis: #756f66;
|
||||
--chart-cursor: rgba(63, 58, 49, 0.08);
|
||||
--chart-label: #242321;
|
||||
--chart-tooltip-bg: #fbf8f1;
|
||||
--chart-tooltip-text: #242321;
|
||||
--chart-tooltip-border: #d8d0c3;
|
||||
--chart-detail-bar: #3f535a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background:
|
||||
@@ -22,6 +55,13 @@
|
||||
#070707;
|
||||
@apply text-dark-text;
|
||||
}
|
||||
|
||||
html[data-theme='offwhite'] body {
|
||||
background:
|
||||
radial-gradient(circle at 16% -8%, rgba(255, 255, 255, 0.82), transparent 32rem),
|
||||
radial-gradient(circle at 88% 0%, rgba(116, 107, 91, 0.10), transparent 36rem),
|
||||
#f3f0e9;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -43,9 +83,23 @@
|
||||
#070707;
|
||||
}
|
||||
|
||||
html[data-theme='offwhite'] .app-shell {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.52), rgba(255, 255, 255, 0) 24rem),
|
||||
radial-gradient(circle at 28% -18%, rgba(255, 255, 255, 0.74), transparent 34rem),
|
||||
radial-gradient(circle at 88% 8%, rgba(116, 107, 91, 0.10), transparent 38rem),
|
||||
#f3f0e9;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.012), rgba(255, 255, 255, 0) 16rem),
|
||||
transparent;
|
||||
}
|
||||
|
||||
html[data-theme='offwhite'] .app-content {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0) 16rem),
|
||||
transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ const AdminUsers = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={openCreateModal}
|
||||
className="inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-zinc-950 transition-colors hover:bg-brand-primary/90"
|
||||
className="inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Novo usuário
|
||||
@@ -557,7 +557,7 @@ const AdminUsers = () => {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-zinc-950 transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="inline-flex h-10 cursor-pointer items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-colors hover:bg-brand-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
||||
{editingUser ? 'Salvar alterações' : 'Criar usuário'}
|
||||
|
||||
@@ -106,7 +106,7 @@ const Campaigns = () => {
|
||||
<button
|
||||
onClick={handleProcessNow}
|
||||
disabled={isProcessing || !preview?.readyProducts.length}
|
||||
className="inline-flex items-center gap-2 bg-brand-primary text-black px-4 py-2.5 rounded-xl hover:opacity-90 transition-opacity text-sm font-bold disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
className="inline-flex items-center gap-2 bg-brand-primary text-brand-contrast px-4 py-2.5 rounded-xl hover:opacity-90 transition-opacity text-sm font-bold disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
<Send size={16} />
|
||||
Processar agora
|
||||
|
||||
@@ -7,6 +7,11 @@ import type { ClientDetailsAnalytics, DateRange, OrderData } from '../types';
|
||||
import { fetchClientDetailsAnalytics } from '../dataService';
|
||||
import { formatDisplayName, removeTrailingSellerId } from '../displayFormatters';
|
||||
|
||||
const CHART_GRID_COLOR = 'var(--chart-grid)';
|
||||
const CHART_AXIS_COLOR = 'var(--chart-axis)';
|
||||
const CHART_CURSOR_COLOR = 'var(--chart-cursor)';
|
||||
const CHART_DETAIL_BAR_COLOR = 'var(--chart-detail-bar)';
|
||||
|
||||
type CustomTooltipProps = {
|
||||
active?: boolean;
|
||||
payload?: Array<{ value: number }>;
|
||||
@@ -215,10 +220,10 @@ const ClientDetails = () => {
|
||||
<div className="h-[320px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 80 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#222222" vertical={false} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="#888888"
|
||||
stroke={CHART_AXIS_COLOR}
|
||||
fontSize={10}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
@@ -227,9 +232,9 @@ const ClientDetails = () => {
|
||||
textAnchor="end"
|
||||
height={80}
|
||||
/>
|
||||
<YAxis stroke="#888888" fontSize={12} tickLine={false} axisLine={false} tickFormatter={(value) => formatCurrency(Number(value))} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#222222' }} />
|
||||
<Bar dataKey="value" fill="#9ECAE1" radius={[4, 4, 0, 0]} />
|
||||
<YAxis stroke={CHART_AXIS_COLOR} fontSize={12} tickLine={false} axisLine={false} tickFormatter={(value) => formatCurrency(Number(value))} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||
<Bar dataKey="value" fill={CHART_DETAIL_BAR_COLOR} radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,9 @@ type ClientTypeStyle = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
const clientTypeStyles: Record<string, ClientTypeStyle> = {
|
||||
type ThemeMode = 'dark' | 'offwhite';
|
||||
|
||||
const darkClientTypeStyles: Record<string, ClientTypeStyle> = {
|
||||
'Sem análise': { bg: 'rgba(140, 146, 152, 0.08)', border: 'rgba(140, 146, 152, 0.24)', text: '#b3b7bb' },
|
||||
'Campeão': { bg: 'rgba(24, 214, 181, 0.10)', border: 'rgba(24, 214, 181, 0.34)', text: '#18D6B5' },
|
||||
'Potencial Leal': { bg: 'rgba(163, 230, 53, 0.10)', border: 'rgba(163, 230, 53, 0.34)', text: '#A3E635' },
|
||||
@@ -26,6 +28,19 @@ const clientTypeStyles: Record<string, ClientTypeStyle> = {
|
||||
'Perdido': { bg: 'rgba(140, 146, 152, 0.08)', border: 'rgba(140, 146, 152, 0.24)', text: '#b3b7bb' }
|
||||
};
|
||||
|
||||
const offwhiteClientTypeStyles: Record<string, ClientTypeStyle> = {
|
||||
'Sem análise': { bg: 'rgba(117, 124, 130, 0.08)', border: 'rgba(117, 124, 130, 0.26)', text: '#757C82' },
|
||||
'Campeão': { bg: 'rgba(19, 139, 117, 0.10)', border: 'rgba(19, 139, 117, 0.34)', text: '#138B75' },
|
||||
'Potencial Leal': { bg: 'rgba(95, 143, 18, 0.11)', border: 'rgba(95, 143, 18, 0.34)', text: '#5F8F12' },
|
||||
'Novo Cliente': { bg: 'rgba(11, 126, 168, 0.10)', border: 'rgba(11, 126, 168, 0.34)', text: '#0B7EA8' },
|
||||
'Cliente Leal': { bg: 'rgba(53, 109, 204, 0.10)', border: 'rgba(53, 109, 204, 0.34)', text: '#356DCC' },
|
||||
'Precisa de Atenção': { bg: 'rgba(163, 108, 0, 0.11)', border: 'rgba(163, 108, 0, 0.34)', text: '#A36C00' },
|
||||
'Quase Dormindo': { bg: 'rgba(183, 91, 18, 0.11)', border: 'rgba(183, 91, 18, 0.34)', text: '#B75B12' },
|
||||
'Em Risco': { bg: 'rgba(199, 54, 86, 0.10)', border: 'rgba(199, 54, 86, 0.34)', text: '#C73656' },
|
||||
'Hibernando': { bg: 'rgba(140, 82, 214, 0.10)', border: 'rgba(140, 82, 214, 0.34)', text: '#8C52D6' },
|
||||
'Perdido': { bg: 'rgba(117, 124, 130, 0.08)', border: 'rgba(117, 124, 130, 0.26)', text: '#757C82' }
|
||||
};
|
||||
|
||||
const clientTypePillStyle = (style: ClientTypeStyle) => ({
|
||||
backgroundColor: style.bg,
|
||||
borderColor: style.border,
|
||||
@@ -119,10 +134,12 @@ const dateFilterPresets = [
|
||||
const filterSelectClassName = "w-full h-9 bg-dark-input border border-dark-border text-dark-text text-sm rounded-lg px-3 focus:outline-none focus:border-brand-primary transition-colors cursor-pointer";
|
||||
|
||||
const Clients = () => {
|
||||
const { dateRange, setDateRange } = useOutletContext<{
|
||||
const { dateRange, setDateRange, themeMode } = useOutletContext<{
|
||||
dateRange: DateRange,
|
||||
setDateRange: (range: DateRange) => void
|
||||
setDateRange: (range: DateRange) => void,
|
||||
themeMode?: ThemeMode
|
||||
}>();
|
||||
const clientTypeStyles = themeMode === 'offwhite' ? offwhiteClientTypeStyles : darkClientTypeStyles;
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortBy, setSortBy] = useState<ClientSortOption>('recent');
|
||||
const [clientTypeFilter, setClientTypeFilter] = useState('all');
|
||||
@@ -421,7 +438,7 @@ const Clients = () => {
|
||||
<Filter className="h-4 w-4" />
|
||||
Filtros
|
||||
{hasActiveFilters && (
|
||||
<span className="rounded-full bg-brand-primary px-1.5 py-0.5 text-[10px] font-bold text-black">
|
||||
<span className="rounded-full bg-brand-primary px-1.5 py-0.5 text-[10px] font-bold text-brand-contrast">
|
||||
{activeFilterCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -571,7 +588,7 @@ const Clients = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFilterMenuOpen(false)}
|
||||
className="cursor-pointer rounded-xl bg-brand-primary px-4 py-2 text-sm font-bold text-black transition-opacity hover:opacity-90"
|
||||
className="cursor-pointer rounded-xl bg-brand-primary px-4 py-2 text-sm font-bold text-brand-contrast transition-opacity hover:opacity-90"
|
||||
>
|
||||
Aplicar
|
||||
</button>
|
||||
|
||||
@@ -32,6 +32,10 @@ const SELLER_COLORS = [
|
||||
const BAR_FILL_OPACITY = 0.5;
|
||||
const BAR_STROKE_OPACITY = 0.85;
|
||||
const PIE_FILL_OPACITY = 0.68;
|
||||
const CHART_GRID_COLOR = 'var(--chart-grid)';
|
||||
const CHART_AXIS_COLOR = 'var(--chart-axis)';
|
||||
const CHART_CURSOR_COLOR = 'var(--chart-cursor)';
|
||||
const CHART_LABEL_COLOR = 'var(--chart-label)';
|
||||
|
||||
const getBarGlowStyle = (color: string) => ({
|
||||
filter: `drop-shadow(0 0 3px ${color}40)`
|
||||
@@ -67,14 +71,17 @@ type CustomTooltipProps = {
|
||||
|
||||
const CustomTooltip = ({ active, payload, label, isCurrency, valueLabel }: CustomTooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
const color = payload[0].payload?.fill || payload[0].color || '#9ECAE1';
|
||||
const color = payload[0].payload?.fill || payload[0].color || 'var(--chart-detail-bar)';
|
||||
const displayLabel = label || payload[0].name;
|
||||
const value = isCurrency ? formatCurrency(payload[0].value) : payload[0].value;
|
||||
const displayValueLabel = valueLabel || (isCurrency ? 'Receita:' : 'Vendas:');
|
||||
return (
|
||||
<div className="bg-[#141414] p-3 rounded-xl shadow-lg border-none">
|
||||
<div
|
||||
className="rounded-xl border p-3 shadow-lg"
|
||||
style={{ backgroundColor: 'var(--chart-tooltip-bg)', borderColor: 'var(--chart-tooltip-border)' }}
|
||||
>
|
||||
<p className="font-bold mb-1" style={{ color }}>{displayLabel}</p>
|
||||
<p className="text-[#ededed] m-0">{displayValueLabel} {value}</p>
|
||||
<p className="m-0" style={{ color: 'var(--chart-tooltip-text)' }}>{displayValueLabel} {value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -256,19 +263,19 @@ const Dashboard = () => {
|
||||
{revenueBySellerChartData.length ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={revenueBySellerChartData} layout="vertical" margin={{ top: 5, right: 88, left: 8, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#222222" horizontal={false} />
|
||||
<XAxis type="number" stroke="#888888" fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} horizontal={false} />
|
||||
<XAxis type="number" stroke={CHART_AXIS_COLOR} fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
||||
<YAxis
|
||||
dataKey="name"
|
||||
type="category"
|
||||
width={150}
|
||||
stroke="#888888"
|
||||
stroke={CHART_AXIS_COLOR}
|
||||
fontSize={11}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => truncateLabel(String(value))}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip isCurrency valueLabel="Receita:" />} cursor={{ fill: '#222222' }} />
|
||||
<Tooltip content={<CustomTooltip isCurrency valueLabel="Receita:" />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||
<Bar dataKey="value" radius={[0, 4, 4, 0]} isAnimationActive={false}>
|
||||
{revenueBySellerChartData.map((entry) => (
|
||||
<Cell
|
||||
@@ -281,7 +288,7 @@ const Dashboard = () => {
|
||||
style={getBarGlowStyle(entry.fill)}
|
||||
/>
|
||||
))}
|
||||
<LabelList dataKey="value" position="right" fill="#ededed" fontSize={11} fontWeight={700} formatter={(value) => formatCurrency(Number(value) || 0)} />
|
||||
<LabelList dataKey="value" position="right" fill={CHART_LABEL_COLOR} fontSize={11} fontWeight={700} formatter={(value) => formatCurrency(Number(value) || 0)} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -297,19 +304,19 @@ const Dashboard = () => {
|
||||
{ticketBySellerChartData.length ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={ticketBySellerChartData} layout="vertical" margin={{ top: 5, right: 88, left: 8, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#222222" horizontal={false} />
|
||||
<XAxis type="number" stroke="#888888" fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} horizontal={false} />
|
||||
<XAxis type="number" stroke={CHART_AXIS_COLOR} fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
||||
<YAxis
|
||||
dataKey="name"
|
||||
type="category"
|
||||
width={150}
|
||||
stroke="#888888"
|
||||
stroke={CHART_AXIS_COLOR}
|
||||
fontSize={11}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => truncateLabel(String(value))}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip isCurrency valueLabel="Ticket médio:" />} cursor={{ fill: '#222222' }} />
|
||||
<Tooltip content={<CustomTooltip isCurrency valueLabel="Ticket médio:" />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||
<Bar dataKey="value" radius={[0, 4, 4, 0]} isAnimationActive={false}>
|
||||
{ticketBySellerChartData.map((entry) => (
|
||||
<Cell
|
||||
@@ -322,7 +329,7 @@ const Dashboard = () => {
|
||||
style={getBarGlowStyle(entry.fill)}
|
||||
/>
|
||||
))}
|
||||
<LabelList dataKey="value" position="right" fill="#ededed" fontSize={11} fontWeight={700} formatter={(value) => formatCurrency(Number(value) || 0)} />
|
||||
<LabelList dataKey="value" position="right" fill={CHART_LABEL_COLOR} fontSize={11} fontWeight={700} formatter={(value) => formatCurrency(Number(value) || 0)} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -372,13 +379,13 @@ const Dashboard = () => {
|
||||
<div className="h-80 w-full flex items-center justify-center">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={salesByProduct} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#222222" vertical={false} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name" stroke="#888888" fontSize={10} tickLine={false} axisLine={false}
|
||||
dataKey="name" stroke={CHART_AXIS_COLOR} fontSize={10} tickLine={false} axisLine={false}
|
||||
tick={false}
|
||||
/>
|
||||
<YAxis stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#222222' }} />
|
||||
<YAxis stroke={CHART_AXIS_COLOR} fontSize={12} tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||
<Bar dataKey="value" radius={[4, 4, 0, 0]} onClick={(data) => { if(data?.payload?.id) navigate(`/products/${data.payload.id}`) }} style={{ cursor: 'pointer' }}>
|
||||
{salesByProduct.map((entry) => (
|
||||
<Cell
|
||||
@@ -423,7 +430,7 @@ const Dashboard = () => {
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip content={<CustomTooltip isCurrency={true} />} cursor={{ fill: '#222222' }} />
|
||||
<Tooltip content={<CustomTooltip isCurrency={true} />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,11 @@ import DateRangePicker from '../components/DateRangePicker';
|
||||
import type { DateRange, ProductDetailsAnalytics } from '../types';
|
||||
import { fetchProductDetailsAnalytics } from '../dataService';
|
||||
|
||||
const CHART_GRID_COLOR = 'var(--chart-grid)';
|
||||
const CHART_AXIS_COLOR = 'var(--chart-axis)';
|
||||
const CHART_CURSOR_COLOR = 'var(--chart-cursor)';
|
||||
const CHART_DETAIL_BAR_COLOR = 'var(--chart-detail-bar)';
|
||||
|
||||
type CustomTooltipProps = {
|
||||
active?: boolean;
|
||||
payload?: Array<{ value: number }>;
|
||||
@@ -15,9 +20,12 @@ type CustomTooltipProps = {
|
||||
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="bg-[#141414] p-3 rounded-xl shadow-lg border-none">
|
||||
<p className="font-bold mb-1" style={{ color: '#9ECAE1' }}>{label}</p>
|
||||
<p className="text-[#ededed] m-0">Vendas: {payload[0].value}</p>
|
||||
<div
|
||||
className="rounded-xl border p-3 shadow-lg"
|
||||
style={{ backgroundColor: 'var(--chart-tooltip-bg)', borderColor: 'var(--chart-tooltip-border)' }}
|
||||
>
|
||||
<p className="font-bold mb-1" style={{ color: CHART_DETAIL_BAR_COLOR }}>{label}</p>
|
||||
<p className="m-0" style={{ color: 'var(--chart-tooltip-text)' }}>Vendas: {payload[0].value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -135,17 +143,17 @@ const ProductDetails = () => {
|
||||
<div className="h-[400px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 80 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#222222" vertical={false} />
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} vertical={false} />
|
||||
<XAxis
|
||||
dataKey="date" stroke="#888888" fontSize={10} tickLine={false} axisLine={false}
|
||||
dataKey="date" stroke={CHART_AXIS_COLOR} fontSize={10} tickLine={false} axisLine={false}
|
||||
interval={0}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
height={80}
|
||||
/>
|
||||
<YAxis stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#222222' }} />
|
||||
<Bar dataKey="value" fill="#9ECAE1" radius={[4, 4, 0, 0]} />
|
||||
<YAxis stroke={CHART_AXIS_COLOR} fontSize={12} tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||
<Bar dataKey="value" fill={CHART_DETAIL_BAR_COLOR} radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,9 @@ type SegmentStyle = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
const segmentStyles: Record<string, SegmentStyle> = {
|
||||
type ThemeMode = 'dark' | 'offwhite';
|
||||
|
||||
const darkSegmentStyles: Record<string, SegmentStyle> = {
|
||||
champions: { accent: '#18D6B5', bg: 'rgba(24, 214, 181, 0.10)', border: 'rgba(24, 214, 181, 0.34)', text: '#18D6B5' },
|
||||
potential_loyalists: { accent: '#A3E635', bg: 'rgba(163, 230, 53, 0.10)', border: 'rgba(163, 230, 53, 0.34)', text: '#A3E635' },
|
||||
new_customers: { accent: '#25C2FF', bg: 'rgba(37, 194, 255, 0.10)', border: 'rgba(37, 194, 255, 0.34)', text: '#25C2FF' },
|
||||
@@ -27,6 +29,18 @@ const segmentStyles: Record<string, SegmentStyle> = {
|
||||
lost: { accent: '#8c9298', bg: 'rgba(140, 146, 152, 0.08)', border: 'rgba(140, 146, 152, 0.24)', text: '#b3b7bb' }
|
||||
};
|
||||
|
||||
const offwhiteSegmentStyles: Record<string, SegmentStyle> = {
|
||||
champions: { accent: '#138B75', bg: 'rgba(19, 139, 117, 0.10)', border: 'rgba(19, 139, 117, 0.34)', text: '#138B75' },
|
||||
potential_loyalists: { accent: '#5F8F12', bg: 'rgba(95, 143, 18, 0.11)', border: 'rgba(95, 143, 18, 0.34)', text: '#5F8F12' },
|
||||
new_customers: { accent: '#0B7EA8', bg: 'rgba(11, 126, 168, 0.10)', border: 'rgba(11, 126, 168, 0.34)', text: '#0B7EA8' },
|
||||
loyal_customers: { accent: '#356DCC', bg: 'rgba(53, 109, 204, 0.10)', border: 'rgba(53, 109, 204, 0.34)', text: '#356DCC' },
|
||||
need_attention: { accent: '#A36C00', bg: 'rgba(163, 108, 0, 0.11)', border: 'rgba(163, 108, 0, 0.34)', text: '#A36C00' },
|
||||
about_to_sleep: { accent: '#B75B12', bg: 'rgba(183, 91, 18, 0.11)', border: 'rgba(183, 91, 18, 0.34)', text: '#B75B12' },
|
||||
at_risk: { accent: '#C73656', bg: 'rgba(199, 54, 86, 0.10)', border: 'rgba(199, 54, 86, 0.34)', text: '#C73656' },
|
||||
hibernating: { accent: '#8C52D6', bg: 'rgba(140, 82, 214, 0.10)', border: 'rgba(140, 82, 214, 0.34)', text: '#8C52D6' },
|
||||
lost: { accent: '#757C82', bg: 'rgba(117, 124, 130, 0.08)', border: 'rgba(117, 124, 130, 0.26)', text: '#757C82' }
|
||||
};
|
||||
|
||||
const rfmSegmentDefinitions: Array<Pick<RfmSegment, 'key' | 'label'>> = [
|
||||
{ key: 'champions', label: 'Champions' },
|
||||
{ key: 'potential_loyalists', label: 'Potenciais Leais' },
|
||||
@@ -144,12 +158,14 @@ const ScoreBadge = ({ value }: { value: number }) => (
|
||||
);
|
||||
|
||||
const Rfm = () => {
|
||||
const { dateRange, setDateRange, refreshInterval, setRefreshInterval } = useOutletContext<{
|
||||
const { dateRange, setDateRange, refreshInterval, setRefreshInterval, themeMode } = useOutletContext<{
|
||||
dateRange: DateRange;
|
||||
setDateRange: (range: DateRange) => void;
|
||||
refreshInterval: number;
|
||||
setRefreshInterval: (interval: number) => void;
|
||||
themeMode?: ThemeMode;
|
||||
}>();
|
||||
const segmentStyles = themeMode === 'offwhite' ? offwhiteSegmentStyles : darkSegmentStyles;
|
||||
|
||||
const initialCachedAnalytics = getCachedRfmAnalytics(dateRange);
|
||||
const [analytics, setAnalytics] = useState<RfmAnalytics | null>(initialCachedAnalytics || null);
|
||||
|
||||
Reference in New Issue
Block a user