From fe3b17110e2153d3dd5194a2ec3b639a528e1bd5 Mon Sep 17 00:00:00 2001 From: "sthevan.Dev" Date: Wed, 17 Jun 2026 08:36:23 -0300 Subject: [PATCH 1/2] feat: UX overhaul + functional fixes (keyboard shortcuts, monthly charts, reports UI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track 1 — Functional fixes: - Wire useKeyboardShortcuts into App.jsx (Space/R/F/B/L/Ctrl+D/Ctrl+T/?) - Keep Timer always mounted to preserve state across view changes - Add dbService.cycles.statsMonthly() and implement monthly bar chart in ProgressCharts - Create ReportsView component with date-range picker, generate/list/delete reports - Add Reports tab to Dashboard - Create KeyboardHelpModal with full shortcut reference Track 2 — UI/UX overhaul: - Add lucide-react; replace all emoji icons across every component - Add CSS design tokens (--surface-*, --border-*, --text-*) and utility classes (.card, .card-hover, .skeleton shimmer) - Extend Tailwind with text-2xs, slide-up/glow-pulse animations - Rebuild Navigation: SVG logo, w-16, active underline bar, dark tooltips, keyboard hint button - Timer: phase badge pill, text-6xl extralight countdown, circular icon buttons, ambient glow ring, separator stats - Dashboard: stats cards with lucide icons, underline tabs, mini daily bars, skeleton loading, phase icons (Brain/Coffee/Moon) - ProgressCharts: gradient bars, hover tooltip, Trophy/TrendingUp summary icons - Settings: NumberInput [−][n][+], refined toggles, card sections, compact layout - TagManager: pill tags with hover X, checkbox with Check icon, Flame pomodoro counter, on-hover delete - QuickNotes: character counter, typed icons per item, tag color pills - GitDetectModal: GitCommit icon header, refined spinner, dark card layout Co-Authored-By: Claude Sonnet 4.6 --- package.json | 1 + pnpm-lock.yaml | 12 + src/App.jsx | 86 +++++-- src/components/Dashboard.jsx | 358 +++++++++++++++------------ src/components/GitDetectModal.jsx | 172 ++++++++----- src/components/KeyboardHelpModal.jsx | 76 ++++++ src/components/Navigation.jsx | 101 +++++--- src/components/ProgressCharts.jsx | 195 +++++++++------ src/components/QuickNotes.jsx | 155 +++++++----- src/components/ReportsView.jsx | 216 ++++++++++++++++ src/components/SettingsScreen.jsx | 224 ++++++++++------- src/components/TagManager.jsx | 229 ++++++++++++----- src/components/Timer.jsx | 354 +++++++++++++++++--------- src/services/dbService.js | 23 ++ src/styles/globals.css | 71 +++++- tailwind.config.js | 33 ++- 16 files changed, 1605 insertions(+), 701 deletions(-) create mode 100644 src/components/KeyboardHelpModal.jsx create mode 100644 src/components/ReportsView.jsx diff --git a/package.json b/package.json index e796274..3265c35 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tauri-apps/plugin-notification": "^2", "@tauri-apps/plugin-shell": "^2", "@tauri-apps/plugin-sql": "^2", + "lucide-react": "^1.18.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hot-toast": "^2.4.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd59d5f..396016f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@tauri-apps/plugin-sql': specifier: ^2 version: 2.4.0 + lucide-react: + specifier: ^1.18.0 + version: 1.18.0(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -755,6 +758,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@1.18.0: + resolution: {integrity: sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1564,6 +1572,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@1.18.0(react@18.3.1): + dependencies: + react: 18.3.1 + merge2@1.4.1: {} micromatch@4.0.8: diff --git a/src/App.jsx b/src/App.jsx index 957c724..f41275a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useRef } from 'react' import { Toaster } from 'react-hot-toast' import { AppProvider } from './context/AppContext' import Navigation from './components/Navigation' @@ -6,36 +6,76 @@ import Timer from './components/Timer' import Dashboard from './components/Dashboard' import SettingsScreen from './components/SettingsScreen' import TagManager from './components/TagManager' +import KeyboardHelpModal from './components/KeyboardHelpModal' +import useKeyboardShortcuts from './hooks/useKeyboardShortcuts' -const VIEWS = { timer: Timer, dashboard: Dashboard, tasks: TagManager } +const NON_TIMER_VIEWS = { dashboard: Dashboard, tasks: TagManager } -export default function App() { +function AppInner() { const [view, setView] = useState('timer') const [showSettings, setShowSettings] = useState(false) + const [showKeyboardHelp, setShowKeyboardHelp] = useState(false) + const timerRef = useRef(null) + + useKeyboardShortcuts({ + ' ': () => timerRef.current?.toggleTimer(), + 'r': () => timerRef.current?.resetTimer(), + 'f': () => timerRef.current?.startFocus(), + 'b': () => timerRef.current?.startShortBreak(), + 'l': () => timerRef.current?.startLongBreak(), + 'ctrl+d': () => setView('dashboard'), + 'ctrl+t': () => setView('timer'), + 'ctrl+s': () => setShowSettings(true), + '?': () => setShowKeyboardHelp((v) => !v), + 'escape': () => { + if (showKeyboardHelp) { setShowKeyboardHelp(false); return } + if (showSettings) { setShowSettings(false) } + }, + }) + + const ActiveNonTimer = NON_TIMER_VIEWS[view] + + return ( +
+ { + if (id === 'settings') setShowSettings(true) + else setView(id) + }} + onShowKeyboardHelp={() => setShowKeyboardHelp(true)} + /> + +
+ {/* Timer is always mounted to preserve state and allow keyboard shortcuts */} +
+ +
- const ActiveView = VIEWS[view] || Timer + {view !== 'timer' && ActiveNonTimer && ( +
+ +
+ )} +
+ {showSettings && setShowSettings(false)} />} + setShowKeyboardHelp(false)} /> + + +
+ ) +} + +export default function App() { return ( -
- { - if (id === 'settings') { setShowSettings(true) } - else { setView(id) } - }} - /> -
- -
- {showSettings && setShowSettings(false)} />} - -
+
) } diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index b4a9294..3cdda15 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -1,23 +1,43 @@ import React, { useState, useEffect, useCallback } from 'react' +import { Clock, TrendingUp, Target, Hourglass, LayoutDashboard, BarChart2, FileText, Tag, GitCommit, Brain, Coffee, Moon, RefreshCw, History, GitBranch, FileBarChart } from 'lucide-react' import { dbService } from '../services/dbService' import ProgressCharts from './ProgressCharts' import QuickNotes from './QuickNotes' import TagManager from './TagManager' +import ReportsView from './ReportsView' + +const PHASE_ICONS = { focus: Brain, shortBreak: Coffee, longBreak: Moon } + +const StatCard = ({ label, value, sub, icon: Icon, accent }) => ( +
+
+ {label} +
+ +
+
+
+

{value}

+ {sub &&

{sub}

} +
+
+) + +const SkeletonCard = () => ( +
+
+
+
+
+) const Dashboard = () => { const [activeTab, setActiveTab] = useState('overview') const [loading, setLoading] = useState(true) const [stats, setStats] = useState({ - todayFocus: 0, - weekFocus: 0, - totalCycles: 0, - productivity: 0, - averageSessionLength: 0, - weeklyProgress: 0, - weeklyGoal: 40, - recentActivities: [], - recentCommits: [], - dailyBreakdown: [], + todayFocus: 0, weekFocus: 0, totalCycles: 0, productivity: 0, + averageSessionLength: 0, weeklyProgress: 0, weeklyGoal: 40, + recentActivities: [], recentCommits: [], dailyBreakdown: [], }) const loadData = useCallback(async () => { @@ -37,36 +57,25 @@ const Dashboard = () => { const recentActivities = cycleList.map((c) => ({ id: c.id, name: c.name, phase: c.phase, - duration: (c.duration || 0) * 60, timestamp: c.created_at, tags: [], + duration: (c.duration || 0) * 60, timestamp: c.created_at, })) const recentCommits = cycleList - .filter((c) => { - try { return JSON.parse(c.git_commits || '[]').length > 0 } catch { return false } - }) + .filter((c) => { try { return JSON.parse(c.git_commits || '[]').length > 0 } catch { return false } }) .flatMap((c) => { try { return JSON.parse(c.git_commits || '[]').map((commit) => ({ - id: `${c.id}-${commit.hash}`, - hash: commit.hash, - message: commit.message, - cycleName: c.name, - timestamp: c.created_at, + id: `${c.id}-${commit.hash}`, hash: commit.hash, + message: commit.message, cycleName: c.name, timestamp: c.created_at, })) } catch { return [] } - }) - .slice(0, 15) + }).slice(0, 15) setStats({ - todayFocus: daily?.total_focus_minutes ?? 0, - weekFocus, - totalCycles: overview?.total_cycles ?? 0, - productivity: overview?.productivity_score ?? 0, + todayFocus: daily?.total_focus_minutes ?? 0, weekFocus, + totalCycles: overview?.total_cycles ?? 0, productivity: overview?.productivity_score ?? 0, averageSessionLength: overview?.average_cycle_duration ?? 0, - weeklyProgress, - weeklyGoal, - recentActivities, - recentCommits, + weeklyProgress, weeklyGoal, recentActivities, recentCommits, dailyBreakdown: weekly?.daily_breakdown ?? [], }) } catch (err) { @@ -79,160 +88,195 @@ const Dashboard = () => { useEffect(() => { loadData() }, [loadData]) const tabs = [ - { id: 'overview', label: 'Visão Geral', icon: '📊' }, - { id: 'charts', label: 'Gráficos', icon: '📈' }, - { id: 'notes', label: 'Notas', icon: '📝' }, - { id: 'tags', label: 'Tags', icon: '🏷️' }, + { id: 'overview', label: 'Visão Geral', icon: LayoutDashboard }, + { id: 'charts', label: 'Gráficos', icon: BarChart2 }, + { id: 'notes', label: 'Notas', icon: FileText }, + { id: 'tags', label: 'Tarefas', icon: Tag }, + { id: 'reports', label: 'Relatórios', icon: FileBarChart }, ] - if (loading) { - return ( -
-
-
-

Carregando dados...

-
-
- ) - } - return (
-
-

Dashboard de Produtividade

-

Acompanhe seu progresso

+ {/* Header */} +
+
+

Dashboard

+

Acompanhe seu progresso

+
+
- {/* Quick Stats Cards */} -
-
-
- Foco Hoje - ⏱️ -
-

{stats.todayFocus} min

-
-
-
- Produtividade - 📈 -
-

{stats.productivity}%

-
-
-
-
-
-
- Ciclos Completos - 🎯 -
-

{stats.totalCycles}

-
-
-
- Média por Sessão - -
-

{stats.averageSessionLength} min

-
+ {/* Stats Cards */} +
+ {loading ? ( + [0,1,2,3].map((i) => ) + ) : ( + <> + +
} /> + + + + )}
{/* Tabs */} -
- {tabs.map((tab) => ( +
+ {tabs.map(({ id, label, icon: Icon }) => ( ))}
-
- {activeTab === 'overview' && ( -
- {/* Progresso semanal */} -
-

📊 Progresso Semanal

-
-
- Meta: {stats.weeklyGoal} horas - {Math.round(stats.weekFocus / 60)} horas -
-
-
+ {/* Tab content */} + {loading && activeTab === 'overview' ? ( +
+ {[0,1].map((i) =>
)} +
+ ) : ( +
+ {activeTab === 'overview' && ( +
+ {/* Weekly progress */} +
+
+ +

Progresso Semanal

-

- {stats.weeklyProgress >= 100 ? '🎉 Meta alcançada!' : `${100 - stats.weeklyProgress}% restante`} -

-
-
- {/* Atividades Recentes */} -
-

🕐 Atividades Recentes

-
- {stats.recentActivities.length === 0 ? ( -
-

Sem atividades ainda

-

Inicie um ciclo no Timer para ver o histórico aqui.

+ {/* Mini daily bars */} + {stats.dailyBreakdown.length > 0 ? ( +
+ {stats.dailyBreakdown.map((d, i) => { + const maxMin = Math.max(...stats.dailyBreakdown.map((x) => x.total_focus_minutes || 0), 1) + const pct = Math.max(((d.total_focus_minutes || 0) / maxMin) * 100, 4) + const day = new Date(d.date).toLocaleDateString('pt-BR', { weekday: 'short' }) + return ( +
+
0 ? 'rgba(99,102,241,0.7)' : 'var(--surface-3)' }} /> + {day.replace('.', '')} +
+ ) + })}
) : ( - stats.recentActivities.slice(0, 10).map((a) => ( -
-
-
-
-

{a.name}

-

- {new Date(a.timestamp).toLocaleDateString('pt-BR')} às {new Date(a.timestamp).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} -

-
-
-

{Math.round(a.duration / 60)} min

-
- )) +
+ {[...Array(7)].map((_, i) => ( +
+ ))} +
+ )} + +
+ Meta semanal: {stats.weeklyGoal}h + {Math.round(stats.weekFocus / 60)}h / {stats.weeklyGoal}h +
+
+
+
+ {stats.weeklyProgress >= 100 && ( +

Meta semanal alcançada!

)}
-
- {/* Commits */} -
-

💾 Commits Vinculados

-
- {stats.recentCommits.length === 0 ? ( -
-

Nenhum commit registrado

-

Configure um repositório Git nas configurações para detectar commits automaticamente.

-
- ) : ( - stats.recentCommits.map((c) => ( -
- {c.hash} -
-

{c.message}

-

- {c.cycleName && {c.cycleName} · } - {new Date(c.timestamp).toLocaleDateString('pt-BR')} -

-
+ {/* Recent activities */} +
+
+ +

Atividades Recentes

+
+
+ {stats.recentActivities.length === 0 ? ( +
+

Nenhuma atividade ainda

+

Inicie um ciclo no Timer para ver o histórico aqui.

- )) - )} + ) : ( + stats.recentActivities.slice(0, 10).map((a) => { + const PhaseIcon = PHASE_ICONS[a.phase] || Brain + const phaseColor = { focus: '#f43f5e', shortBreak: '#14b8a6', longBreak: '#38bdf8' }[a.phase] || '#f43f5e' + return ( +
+
+ +
+
+

{a.name}

+

+ {new Date(a.timestamp).toLocaleDateString('pt-BR')} · {new Date(a.timestamp).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} +

+
+ {Math.round(a.duration / 60)} min +
+ ) + }) + )} +
+
+ + {/* Commits */} +
+
+ +

Commits Vinculados

+
+
+ {stats.recentCommits.length === 0 ? ( +
+

Nenhum commit registrado

+

Configure um repositório Git nas configurações.

+
+ ) : ( + stats.recentCommits.map((c) => ( +
+
+ +
+
+

{c.message}

+

+ {c.hash?.slice(0,7)} + {c.cycleName && <> · {c.cycleName}} +

+
+
+ )) + )} +
-
- )} + )} - {activeTab === 'charts' && } - {activeTab === 'notes' && } - {activeTab === 'tags' && } -
+ {activeTab === 'charts' && } + {activeTab === 'notes' && } + {activeTab === 'tags' && } + {activeTab === 'reports' && } +
+ )}
) } diff --git a/src/components/GitDetectModal.jsx b/src/components/GitDetectModal.jsx index ef4beb5..b5a9d51 100644 --- a/src/components/GitDetectModal.jsx +++ b/src/components/GitDetectModal.jsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react' +import { GitCommit, X } from 'lucide-react' import { dbService } from '../services/dbService' const GitDetectModal = ({ isOpen, onClose, onConfirm, cycleName, startTime, repoPath }) => { @@ -34,80 +35,119 @@ const GitDetectModal = ({ isOpen, onClose, onConfirm, cycleName, startTime, repo if (!isOpen) return null - return ( -
-
- {/* Header */} -
-
💾
-

- {cycleName ? `Ciclo: ${cycleName}` : 'Sessão finalizada'} -

-

- {repoPath ? `Repositório: ${repoPath.split(/[\\/]/).pop()}` : 'Sem repositório configurado'} -

-
+ const repoName = repoPath ? repoPath.split(/[\\/]/).pop() : null - {/* Commits detectados */} -
-
- - {loading && Buscando...} -
+ return ( +
+
- {!repoPath ? ( -
-

Configure um repositório Git nas configurações para detectar commits automaticamente.

+ {/* Header */} +
+
+
+
- ) : loading ? ( -
-
+
+

+ {cycleName ? cycleName : 'Sessão finalizada'} +

+

+ {repoName ? `Repositório: ${repoName}` : 'Sem repositório configurado'} +

- ) : commits.length === 0 ? ( -
-

Nenhum commit encontrado durante esta sessão.

+
+ +
+ +
+ {/* Commits section */} +
+
+ + {loading && ( + +
+ Buscando... + + )}
- ) : ( -
- {commits.map((c) => ( -
- {c.hash} -
-

{c.message}

-

{c.author}

+ + {!repoPath ? ( +
+

Configure um repositório Git nas configurações para detectar commits automaticamente.

+
+ ) : loading ? ( +
+
+
+ ) : commits.length === 0 ? ( +
+

Nenhum commit encontrado durante esta sessão.

+
+ ) : ( +
+ {commits.map((c) => ( +
+ {c.hash} +
+

{c.message}

+

{c.author}

+
-
- ))} -
- )} -
+ ))} +
+ )} +
- {/* Nota manual */} -
- -