From 3d25bc517990c4efd2ff3b410409c9b601143b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 26 Jun 2026 11:11:36 -0300 Subject: [PATCH] Add off-white theme mode --- index.html | 2 +- src/components/Layout.tsx | 28 +++++++++++++++++-- src/index.css | 54 ++++++++++++++++++++++++++++++++++++ src/pages/AdminUsers.tsx | 4 +-- src/pages/Campaigns.tsx | 2 +- src/pages/ClientDetails.tsx | 15 ++++++---- src/pages/Clients.tsx | 27 ++++++++++++++---- src/pages/Dashboard.tsx | 43 ++++++++++++++++------------ src/pages/ProductDetails.tsx | 24 ++++++++++------ src/pages/Rfm.tsx | 20 +++++++++++-- 10 files changed, 174 insertions(+), 45 deletions(-) diff --git a/index.html b/index.html index d08d40c..b82bf46 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 34ad9b8..1331511 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -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(() => { + return localStorage.getItem('nexstar_theme') === 'offwhite' ? 'offwhite' : 'dark'; + }); const [dateRange, setDateRange] = useState(() => { 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 */}
{/* Header */} -
+

Painel de Análise

+
{/* Content Area */}
- +
diff --git a/src/index.css b/src/index.css index 322f7d2..3a06ca1 100644 --- a/src/index.css +++ b/src/index.css @@ -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; + } } diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index 73fd5d7..d77fb71 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -267,7 +267,7 @@ const AdminUsers = () => { diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 4235094..bc8d0a0 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -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 ( -
+

{displayLabel}

-

{displayValueLabel} {value}

+

{displayValueLabel} {value}

); } @@ -256,19 +263,19 @@ const Dashboard = () => { {revenueBySellerChartData.length ? ( - - `${Number(value) / 1000}k`} /> + + `${Number(value) / 1000}k`} /> truncateLabel(String(value))} /> - } cursor={{ fill: '#222222' }} /> + } cursor={{ fill: CHART_CURSOR_COLOR }} /> {revenueBySellerChartData.map((entry) => ( { style={getBarGlowStyle(entry.fill)} /> ))} - formatCurrency(Number(value) || 0)} /> + formatCurrency(Number(value) || 0)} /> @@ -297,19 +304,19 @@ const Dashboard = () => { {ticketBySellerChartData.length ? ( - - `${Number(value) / 1000}k`} /> + + `${Number(value) / 1000}k`} /> truncateLabel(String(value))} /> - } cursor={{ fill: '#222222' }} /> + } cursor={{ fill: CHART_CURSOR_COLOR }} /> {ticketBySellerChartData.map((entry) => ( { style={getBarGlowStyle(entry.fill)} /> ))} - formatCurrency(Number(value) || 0)} /> + formatCurrency(Number(value) || 0)} /> @@ -372,13 +379,13 @@ const Dashboard = () => {
- + - - } cursor={{ fill: '#222222' }} /> + + } cursor={{ fill: CHART_CURSOR_COLOR }} /> { if(data?.payload?.id) navigate(`/products/${data.payload.id}`) }} style={{ cursor: 'pointer' }}> {salesByProduct.map((entry) => ( { /> ))} - } cursor={{ fill: '#222222' }} /> + } cursor={{ fill: CHART_CURSOR_COLOR }} />
diff --git a/src/pages/ProductDetails.tsx b/src/pages/ProductDetails.tsx index 1302cf4..e9d91a4 100644 --- a/src/pages/ProductDetails.tsx +++ b/src/pages/ProductDetails.tsx @@ -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 ( -
-

{label}

-

Vendas: {payload[0].value}

+
+

{label}

+

Vendas: {payload[0].value}

); } @@ -135,17 +143,17 @@ const ProductDetails = () => {
- + - - } cursor={{ fill: '#222222' }} /> - + + } cursor={{ fill: CHART_CURSOR_COLOR }} /> +
diff --git a/src/pages/Rfm.tsx b/src/pages/Rfm.tsx index 9854a71..86793bb 100644 --- a/src/pages/Rfm.tsx +++ b/src/pages/Rfm.tsx @@ -15,7 +15,9 @@ type SegmentStyle = { text: string; }; -const segmentStyles: Record = { +type ThemeMode = 'dark' | 'offwhite'; + +const darkSegmentStyles: Record = { 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 = { lost: { accent: '#8c9298', bg: 'rgba(140, 146, 152, 0.08)', border: 'rgba(140, 146, 152, 0.24)', text: '#b3b7bb' } }; +const offwhiteSegmentStyles: Record = { + 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> = [ { 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(initialCachedAnalytics || null);