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>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="dark">
|
<html lang="en" class="dark" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
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 type { DateRange, OrderData } from '../types';
|
||||||
import { isSuperAdmin, logout } from '../dataService';
|
import { isSuperAdmin, logout } from '../dataService';
|
||||||
import { rangeForLastDays } from '../dateRanges';
|
import { rangeForLastDays } from '../dateRanges';
|
||||||
|
|
||||||
const emptyOrdersData: OrderData[] = [];
|
const emptyOrdersData: OrderData[] = [];
|
||||||
|
type ThemeMode = 'dark' | 'offwhite';
|
||||||
|
|
||||||
const Layout = () => {
|
const Layout = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => {
|
||||||
return localStorage.getItem('graph_sidebar_collapsed') === 'true';
|
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 [dateRange, setDateRange] = useState<DateRange>(() => {
|
||||||
const saved = localStorage.getItem('nexstar_date_range');
|
const saved = localStorage.getItem('nexstar_date_range');
|
||||||
@@ -29,6 +33,12 @@ const Layout = () => {
|
|||||||
return saved ? Number(saved) : 0;
|
return saved ? Number(saved) : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.documentElement.dataset.theme = themeMode;
|
||||||
|
localStorage.setItem('nexstar_theme', themeMode);
|
||||||
|
}, [themeMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('nexstar_refresh_interval', refreshInterval.toString());
|
localStorage.setItem('nexstar_refresh_interval', refreshInterval.toString());
|
||||||
}, [refreshInterval]);
|
}, [refreshInterval]);
|
||||||
@@ -45,6 +55,9 @@ const Layout = () => {
|
|||||||
setIsSidebarCollapsed(newState);
|
setIsSidebarCollapsed(newState);
|
||||||
localStorage.setItem('graph_sidebar_collapsed', String(newState));
|
localStorage.setItem('graph_sidebar_collapsed', String(newState));
|
||||||
};
|
};
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setThemeMode(current => current === 'dark' ? 'offwhite' : 'dark');
|
||||||
|
};
|
||||||
|
|
||||||
const appNavigation = [
|
const appNavigation = [
|
||||||
{ name: 'Dashboard', href: '/graph', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/graph', icon: LayoutDashboard },
|
||||||
@@ -133,13 +146,22 @@ const Layout = () => {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 flex flex-col h-screen overflow-hidden">
|
<main className="flex-1 flex flex-col h-screen overflow-hidden">
|
||||||
{/* Header */}
|
{/* 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>
|
<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>
|
</header>
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
<div className="app-content flex-1 overflow-y-auto p-8 relative">
|
<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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-brand-primary: #b8c7d1;
|
--color-brand-primary: #b8c7d1;
|
||||||
|
--color-brand-contrast: #070707;
|
||||||
--color-dark-bg: #070707;
|
--color-dark-bg: #070707;
|
||||||
--color-dark-card: #141414;
|
--color-dark-card: #141414;
|
||||||
--color-dark-header: #101010;
|
--color-dark-header: #101010;
|
||||||
@@ -14,6 +15,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@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 {
|
body {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
background:
|
background:
|
||||||
@@ -23,6 +56,13 @@
|
|||||||
@apply text-dark-text;
|
@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 {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
@@ -43,9 +83,23 @@
|
|||||||
#070707;
|
#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 {
|
.app-content {
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.012), rgba(255, 255, 255, 0) 16rem),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.012), rgba(255, 255, 255, 0) 16rem),
|
||||||
transparent;
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openCreateModal}
|
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" />
|
<Plus className="h-4 w-4" />
|
||||||
Novo usuário
|
Novo usuário
|
||||||
@@ -557,7 +557,7 @@ const AdminUsers = () => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting}
|
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" />}
|
{isSubmitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
||||||
{editingUser ? 'Salvar alterações' : 'Criar usuário'}
|
{editingUser ? 'Salvar alterações' : 'Criar usuário'}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ const Campaigns = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleProcessNow}
|
onClick={handleProcessNow}
|
||||||
disabled={isProcessing || !preview?.readyProducts.length}
|
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} />
|
<Send size={16} />
|
||||||
Processar agora
|
Processar agora
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import type { ClientDetailsAnalytics, DateRange, OrderData } from '../types';
|
|||||||
import { fetchClientDetailsAnalytics } from '../dataService';
|
import { fetchClientDetailsAnalytics } from '../dataService';
|
||||||
import { formatDisplayName, removeTrailingSellerId } from '../displayFormatters';
|
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 = {
|
type CustomTooltipProps = {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
payload?: Array<{ value: number }>;
|
payload?: Array<{ value: number }>;
|
||||||
@@ -215,10 +220,10 @@ const ClientDetails = () => {
|
|||||||
<div className="h-[320px] w-full">
|
<div className="h-[320px] w-full">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 80 }}>
|
<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
|
<XAxis
|
||||||
dataKey="date"
|
dataKey="date"
|
||||||
stroke="#888888"
|
stroke={CHART_AXIS_COLOR}
|
||||||
fontSize={10}
|
fontSize={10}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
@@ -227,9 +232,9 @@ const ClientDetails = () => {
|
|||||||
textAnchor="end"
|
textAnchor="end"
|
||||||
height={80}
|
height={80}
|
||||||
/>
|
/>
|
||||||
<YAxis stroke="#888888" fontSize={12} tickLine={false} axisLine={false} tickFormatter={(value) => formatCurrency(Number(value))} />
|
<YAxis stroke={CHART_AXIS_COLOR} fontSize={12} tickLine={false} axisLine={false} tickFormatter={(value) => formatCurrency(Number(value))} />
|
||||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#222222' }} />
|
<Tooltip content={<CustomTooltip />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||||
<Bar dataKey="value" fill="#9ECAE1" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="value" fill={CHART_DETAIL_BAR_COLOR} radius={[4, 4, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ type ClientTypeStyle = {
|
|||||||
text: string;
|
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' },
|
'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' },
|
'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' },
|
'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' }
|
'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) => ({
|
const clientTypePillStyle = (style: ClientTypeStyle) => ({
|
||||||
backgroundColor: style.bg,
|
backgroundColor: style.bg,
|
||||||
borderColor: style.border,
|
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 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 Clients = () => {
|
||||||
const { dateRange, setDateRange } = useOutletContext<{
|
const { dateRange, setDateRange, themeMode } = useOutletContext<{
|
||||||
dateRange: DateRange,
|
dateRange: DateRange,
|
||||||
setDateRange: (range: DateRange) => void
|
setDateRange: (range: DateRange) => void,
|
||||||
|
themeMode?: ThemeMode
|
||||||
}>();
|
}>();
|
||||||
|
const clientTypeStyles = themeMode === 'offwhite' ? offwhiteClientTypeStyles : darkClientTypeStyles;
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortBy, setSortBy] = useState<ClientSortOption>('recent');
|
const [sortBy, setSortBy] = useState<ClientSortOption>('recent');
|
||||||
const [clientTypeFilter, setClientTypeFilter] = useState('all');
|
const [clientTypeFilter, setClientTypeFilter] = useState('all');
|
||||||
@@ -421,7 +438,7 @@ const Clients = () => {
|
|||||||
<Filter className="h-4 w-4" />
|
<Filter className="h-4 w-4" />
|
||||||
Filtros
|
Filtros
|
||||||
{hasActiveFilters && (
|
{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}
|
{activeFilterCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -571,7 +588,7 @@ const Clients = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsFilterMenuOpen(false)}
|
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
|
Aplicar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ const SELLER_COLORS = [
|
|||||||
const BAR_FILL_OPACITY = 0.5;
|
const BAR_FILL_OPACITY = 0.5;
|
||||||
const BAR_STROKE_OPACITY = 0.85;
|
const BAR_STROKE_OPACITY = 0.85;
|
||||||
const PIE_FILL_OPACITY = 0.68;
|
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) => ({
|
const getBarGlowStyle = (color: string) => ({
|
||||||
filter: `drop-shadow(0 0 3px ${color}40)`
|
filter: `drop-shadow(0 0 3px ${color}40)`
|
||||||
@@ -67,14 +71,17 @@ type CustomTooltipProps = {
|
|||||||
|
|
||||||
const CustomTooltip = ({ active, payload, label, isCurrency, valueLabel }: CustomTooltipProps) => {
|
const CustomTooltip = ({ active, payload, label, isCurrency, valueLabel }: CustomTooltipProps) => {
|
||||||
if (active && payload && payload.length) {
|
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 displayLabel = label || payload[0].name;
|
||||||
const value = isCurrency ? formatCurrency(payload[0].value) : payload[0].value;
|
const value = isCurrency ? formatCurrency(payload[0].value) : payload[0].value;
|
||||||
const displayValueLabel = valueLabel || (isCurrency ? 'Receita:' : 'Vendas:');
|
const displayValueLabel = valueLabel || (isCurrency ? 'Receita:' : 'Vendas:');
|
||||||
return (
|
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="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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -256,19 +263,19 @@ const Dashboard = () => {
|
|||||||
{revenueBySellerChartData.length ? (
|
{revenueBySellerChartData.length ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={revenueBySellerChartData} layout="vertical" margin={{ top: 5, right: 88, left: 8, bottom: 5 }}>
|
<BarChart data={revenueBySellerChartData} layout="vertical" margin={{ top: 5, right: 88, left: 8, bottom: 5 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#222222" horizontal={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} horizontal={false} />
|
||||||
<XAxis type="number" stroke="#888888" fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
<XAxis type="number" stroke={CHART_AXIS_COLOR} fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
||||||
<YAxis
|
<YAxis
|
||||||
dataKey="name"
|
dataKey="name"
|
||||||
type="category"
|
type="category"
|
||||||
width={150}
|
width={150}
|
||||||
stroke="#888888"
|
stroke={CHART_AXIS_COLOR}
|
||||||
fontSize={11}
|
fontSize={11}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => truncateLabel(String(value))}
|
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}>
|
<Bar dataKey="value" radius={[0, 4, 4, 0]} isAnimationActive={false}>
|
||||||
{revenueBySellerChartData.map((entry) => (
|
{revenueBySellerChartData.map((entry) => (
|
||||||
<Cell
|
<Cell
|
||||||
@@ -281,7 +288,7 @@ const Dashboard = () => {
|
|||||||
style={getBarGlowStyle(entry.fill)}
|
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>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
@@ -297,19 +304,19 @@ const Dashboard = () => {
|
|||||||
{ticketBySellerChartData.length ? (
|
{ticketBySellerChartData.length ? (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={ticketBySellerChartData} layout="vertical" margin={{ top: 5, right: 88, left: 8, bottom: 5 }}>
|
<BarChart data={ticketBySellerChartData} layout="vertical" margin={{ top: 5, right: 88, left: 8, bottom: 5 }}>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#222222" horizontal={false} />
|
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} horizontal={false} />
|
||||||
<XAxis type="number" stroke="#888888" fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
<XAxis type="number" stroke={CHART_AXIS_COLOR} fontSize={11} tickLine={false} axisLine={false} tickFormatter={(value) => `${Number(value) / 1000}k`} />
|
||||||
<YAxis
|
<YAxis
|
||||||
dataKey="name"
|
dataKey="name"
|
||||||
type="category"
|
type="category"
|
||||||
width={150}
|
width={150}
|
||||||
stroke="#888888"
|
stroke={CHART_AXIS_COLOR}
|
||||||
fontSize={11}
|
fontSize={11}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(value) => truncateLabel(String(value))}
|
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}>
|
<Bar dataKey="value" radius={[0, 4, 4, 0]} isAnimationActive={false}>
|
||||||
{ticketBySellerChartData.map((entry) => (
|
{ticketBySellerChartData.map((entry) => (
|
||||||
<Cell
|
<Cell
|
||||||
@@ -322,7 +329,7 @@ const Dashboard = () => {
|
|||||||
style={getBarGlowStyle(entry.fill)}
|
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>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
@@ -372,13 +379,13 @@ const Dashboard = () => {
|
|||||||
<div className="h-80 w-full flex items-center justify-center">
|
<div className="h-80 w-full flex items-center justify-center">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={salesByProduct} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
<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
|
<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}
|
tick={false}
|
||||||
/>
|
/>
|
||||||
<YAxis stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
|
<YAxis stroke={CHART_AXIS_COLOR} fontSize={12} tickLine={false} axisLine={false} />
|
||||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#222222' }} />
|
<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' }}>
|
<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) => (
|
{salesByProduct.map((entry) => (
|
||||||
<Cell
|
<Cell
|
||||||
@@ -423,7 +430,7 @@ const Dashboard = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Pie>
|
</Pie>
|
||||||
<Tooltip content={<CustomTooltip isCurrency={true} />} cursor={{ fill: '#222222' }} />
|
<Tooltip content={<CustomTooltip isCurrency={true} />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import DateRangePicker from '../components/DateRangePicker';
|
|||||||
import type { DateRange, ProductDetailsAnalytics } from '../types';
|
import type { DateRange, ProductDetailsAnalytics } from '../types';
|
||||||
import { fetchProductDetailsAnalytics } from '../dataService';
|
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 = {
|
type CustomTooltipProps = {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
payload?: Array<{ value: number }>;
|
payload?: Array<{ value: number }>;
|
||||||
@@ -15,9 +20,12 @@ type CustomTooltipProps = {
|
|||||||
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
|
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#141414] p-3 rounded-xl shadow-lg border-none">
|
<div
|
||||||
<p className="font-bold mb-1" style={{ color: '#9ECAE1' }}>{label}</p>
|
className="rounded-xl border p-3 shadow-lg"
|
||||||
<p className="text-[#ededed] m-0">Vendas: {payload[0].value}</p>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -135,17 +143,17 @@ const ProductDetails = () => {
|
|||||||
<div className="h-[400px] w-full">
|
<div className="h-[400px] w-full">
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<BarChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 80 }}>
|
<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
|
<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}
|
interval={0}
|
||||||
angle={-45}
|
angle={-45}
|
||||||
textAnchor="end"
|
textAnchor="end"
|
||||||
height={80}
|
height={80}
|
||||||
/>
|
/>
|
||||||
<YAxis stroke="#888888" fontSize={12} tickLine={false} axisLine={false} />
|
<YAxis stroke={CHART_AXIS_COLOR} fontSize={12} tickLine={false} axisLine={false} />
|
||||||
<Tooltip content={<CustomTooltip />} cursor={{ fill: '#222222' }} />
|
<Tooltip content={<CustomTooltip />} cursor={{ fill: CHART_CURSOR_COLOR }} />
|
||||||
<Bar dataKey="value" fill="#9ECAE1" radius={[4, 4, 0, 0]} />
|
<Bar dataKey="value" fill={CHART_DETAIL_BAR_COLOR} radius={[4, 4, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ type SegmentStyle = {
|
|||||||
text: string;
|
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' },
|
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' },
|
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' },
|
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' }
|
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'>> = [
|
const rfmSegmentDefinitions: Array<Pick<RfmSegment, 'key' | 'label'>> = [
|
||||||
{ key: 'champions', label: 'Champions' },
|
{ key: 'champions', label: 'Champions' },
|
||||||
{ key: 'potential_loyalists', label: 'Potenciais Leais' },
|
{ key: 'potential_loyalists', label: 'Potenciais Leais' },
|
||||||
@@ -144,12 +158,14 @@ const ScoreBadge = ({ value }: { value: number }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const Rfm = () => {
|
const Rfm = () => {
|
||||||
const { dateRange, setDateRange, refreshInterval, setRefreshInterval } = useOutletContext<{
|
const { dateRange, setDateRange, refreshInterval, setRefreshInterval, themeMode } = useOutletContext<{
|
||||||
dateRange: DateRange;
|
dateRange: DateRange;
|
||||||
setDateRange: (range: DateRange) => void;
|
setDateRange: (range: DateRange) => void;
|
||||||
refreshInterval: number;
|
refreshInterval: number;
|
||||||
setRefreshInterval: (interval: number) => void;
|
setRefreshInterval: (interval: number) => void;
|
||||||
|
themeMode?: ThemeMode;
|
||||||
}>();
|
}>();
|
||||||
|
const segmentStyles = themeMode === 'offwhite' ? offwhiteSegmentStyles : darkSegmentStyles;
|
||||||
|
|
||||||
const initialCachedAnalytics = getCachedRfmAnalytics(dateRange);
|
const initialCachedAnalytics = getCachedRfmAnalytics(dateRange);
|
||||||
const [analytics, setAnalytics] = useState<RfmAnalytics | null>(initialCachedAnalytics || null);
|
const [analytics, setAnalytics] = useState<RfmAnalytics | null>(initialCachedAnalytics || null);
|
||||||
|
|||||||
Reference in New Issue
Block a user