diff --git a/components/StatusDashboard/CurrentNoteSection.tsx b/components/StatusDashboard/CurrentNoteSection.tsx new file mode 100644 index 0000000..9e7bf27 --- /dev/null +++ b/components/StatusDashboard/CurrentNoteSection.tsx @@ -0,0 +1,77 @@ +import { TFile } from "obsidian"; +import { NoteStatus } from "@/types/noteStatus"; +import { StatusBadge } from "@/components/atoms/StatusBadge"; +import { GroupLabel } from "@/components/atoms/GroupLabel"; + +interface CurrentNoteInfo { + file: TFile | null; + statuses: Record; + lastModified: number; +} + +interface CurrentNoteSectionProps { + currentNote: CurrentNoteInfo; +} + +export const CurrentNoteSection = ({ + currentNote, +}: CurrentNoteSectionProps) => { + return ( +
+
+

Current Note

+
+
+ {currentNote.file ? ( + <> +
+
+ {currentNote.file.basename} +
+
+ {currentNote.file.path} +
+
+ Last modified:{" "} + {new Date( + currentNote.lastModified, + ).toLocaleString()} +
+
+ +
+ {Object.entries(currentNote.statuses).map( + ([tag, statuses]) => ( +
+ +
+ {statuses.length > 0 ? ( + statuses.map((status) => ( + + )) + ) : ( + + No status assigned + + )} +
+
+ ), + )} +
+ + ) : ( +
+ No note currently active +
+ )} +
+
+ ); +}; diff --git a/components/StatusDashboard/QuickActionsPanel.tsx b/components/StatusDashboard/QuickActionsPanel.tsx new file mode 100644 index 0000000..3353834 --- /dev/null +++ b/components/StatusDashboard/QuickActionsPanel.tsx @@ -0,0 +1,59 @@ +import { BaseNoteStatusService } from "@/core/noteStatusService"; +import settingsService from "@/core/settingsService"; + +interface QuickActionsPanelProps { + onRefresh: () => void; +} + +export const QuickActionsPanel = ({ onRefresh }: QuickActionsPanelProps) => { + return ( +
+
+

Quick Actions

+
+
+ + + +
+
+ ); +}; diff --git a/components/StatusDashboard/StatusDashboard.tsx b/components/StatusDashboard/StatusDashboard.tsx index 67dd083..b72ffb4 100644 --- a/components/StatusDashboard/StatusDashboard.tsx +++ b/components/StatusDashboard/StatusDashboard.tsx @@ -1,137 +1,27 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; -import { TFile } from "obsidian"; -import { NoteStatus } from "@/types/noteStatus"; -import { - BaseNoteStatusService, - NoteStatusService, -} from "@/core/noteStatusService"; -import { StatusBadge } from "@/components/atoms/StatusBadge"; -import { GroupLabel } from "@/components/atoms/GroupLabel"; +import { useState, useEffect, useCallback } from "react"; +import { BaseNoteStatusService } from "@/core/noteStatusService"; import eventBus from "@/core/eventBus"; -import settingsService from "@/core/settingsService"; - -interface VaultStats { - totalNotes: number; - notesWithStatus: number; - statusDistribution: Record; - tagDistribution: Record; - recentChanges: Array<{ - file: TFile; - status: NoteStatus; - timestamp: number; - action: "added" | "removed"; - }>; -} - -interface CurrentNoteInfo { - file: TFile | null; - statuses: Record; - lastModified: number; -} +import { VaultStatsCard } from "./VaultStatsCard"; +import { CurrentNoteSection } from "./CurrentNoteSection"; +import { StatusDistributionChart } from "./StatusDistributionChart"; +import { QuickActionsPanel } from "./QuickActionsPanel"; +import { useVaultStats } from "./useVaultStats"; +import { useCurrentNote } from "./useCurrentNote"; export const StatusDashboard = () => { - const [vaultStats, setVaultStats] = useState({ - totalNotes: 0, - notesWithStatus: 0, - statusDistribution: {}, - tagDistribution: {}, - recentChanges: [], - }); - - const [currentNote, setCurrentNote] = useState({ - file: null, - statuses: {}, - lastModified: 0, - }); - const [isLoading, setIsLoading] = useState(true); - - const calculateVaultStats = useCallback((): VaultStats => { - const files = BaseNoteStatusService.app.vault.getMarkdownFiles(); - const availableStatuses = - BaseNoteStatusService.getAllAvailableStatuses(); - const statusMetadataKeys = [settingsService.settings.tagPrefix]; - - let notesWithStatus = 0; - const statusDistribution: Record = {}; - const tagDistribution: Record = {}; - - // Initialize counters - availableStatuses.forEach((status) => { - statusDistribution[status.name] = 0; - }); - - statusMetadataKeys.forEach((tag) => { - tagDistribution[tag] = 0; - }); - - files.forEach((file) => { - const cachedMetadata = - BaseNoteStatusService.app.metadataCache.getFileCache(file); - const frontmatter = cachedMetadata?.frontmatter; - - if (!frontmatter) return; - - let hasAnyStatus = false; - - statusMetadataKeys.forEach((key) => { - const value = frontmatter[key]; - if (value) { - hasAnyStatus = true; - tagDistribution[key]++; - - const statusNames = Array.isArray(value) ? value : [value]; - statusNames.forEach((statusName) => { - const statusStr = statusName.toString(); - if (statusDistribution.hasOwnProperty(statusStr)) { - statusDistribution[statusStr]++; - } - }); - } - }); - - if (hasAnyStatus) { - notesWithStatus++; - } - }); - - return { - totalNotes: files.length, - notesWithStatus, - statusDistribution, - tagDistribution, - recentChanges: [], // TODO: Implement recent changes tracking - }; - }, []); - - const updateCurrentNote = useCallback(() => { - const activeFile = BaseNoteStatusService.app.workspace.getActiveFile(); - - if (!activeFile) { - setCurrentNote({ file: null, statuses: {}, lastModified: 0 }); - return; - } - - const noteStatusService = new NoteStatusService(activeFile); - noteStatusService.populateStatuses(); - - setCurrentNote({ - file: activeFile, - statuses: noteStatusService.statuses, - lastModified: activeFile.stat.mtime, - }); - }, []); + const { vaultStats, updateVaultStats } = useVaultStats(); + const { currentNote, updateCurrentNote } = useCurrentNote(); const loadData = useCallback(() => { setIsLoading(true); try { - const stats = calculateVaultStats(); - setVaultStats(stats); + updateVaultStats(); updateCurrentNote(); } finally { setIsLoading(false); } - }, [calculateVaultStats, updateCurrentNote]); + }, [updateVaultStats, updateCurrentNote]); useEffect(() => { loadData(); @@ -173,26 +63,6 @@ export const StatusDashboard = () => { }; }, [loadData, updateCurrentNote]); - const statusChart = useMemo(() => { - const total = Object.values(vaultStats.statusDistribution).reduce( - (sum, count) => sum + count, - 0, - ); - if (total === 0) return []; - - return Object.entries(vaultStats.statusDistribution) - .filter(([_, count]) => count > 0) - .map(([statusName, count]) => ({ - name: statusName, - count, - percentage: Math.round((count / total) * 100), - })) - .sort((a, b) => b.count - a.count); - }, [vaultStats.statusDistribution]); - - const availableStatuses = BaseNoteStatusService.getAllAvailableStatuses(); - const statusMap = new Map(availableStatuses.map((s) => [s.name, s])); - if (isLoading) { return (
@@ -217,227 +87,10 @@ export const StatusDashboard = () => {
- {/* Current Note Section */} -
-
-

Current Note

-
-
- {currentNote.file ? ( - <> -
-
- {currentNote.file.basename} -
-
- {currentNote.file.path} -
-
- Last modified:{" "} - {new Date( - currentNote.lastModified, - ).toLocaleString()} -
-
- -
- {Object.entries(currentNote.statuses).map( - ([tag, statuses]) => ( -
- -
- {statuses.length > 0 ? ( - statuses.map( - (status) => ( - - ), - ) - ) : ( - - No status assigned - - )} -
-
- ), - )} -
- - ) : ( -
- No note currently active -
- )} -
-
- - {/* Vault Overview Section */} -
-
-

Vault Overview

-
-
-
-
-
- {vaultStats.totalNotes} -
-
- Total Notes -
-
-
-
- {vaultStats.notesWithStatus} -
-
- Notes with Status -
-
-
-
- {vaultStats.totalNotes > 0 - ? Math.round( - (vaultStats.notesWithStatus / - vaultStats.totalNotes) * - 100, - ) - : 0} - % -
-
Coverage
-
-
-
- {Object.values( - vaultStats.statusDistribution, - ).reduce((sum, count) => sum + count, 0)} -
-
- Total Statuses -
-
-
-
-
- - {/* Status Distribution Section */} -
-
-

Status Distribution

-
-
- {statusChart.length > 0 ? ( -
- {statusChart.map( - ({ name, count, percentage }) => { - const status = statusMap.get(name); - return ( -
-
- {status && ( - - )} - - {count} notes ( - {percentage}%) - -
-
-
-
-
- ); - }, - )} -
- ) : ( -
- No status data available -
- )} -
-
- - {/* Quick Actions Section */} -
-
-

Quick Actions

-
-
- - - -
-
+ + + +
); diff --git a/components/StatusDashboard/StatusDistributionChart.tsx b/components/StatusDashboard/StatusDistributionChart.tsx new file mode 100644 index 0000000..608248f --- /dev/null +++ b/components/StatusDashboard/StatusDistributionChart.tsx @@ -0,0 +1,76 @@ +import { useMemo } from "react"; +import { BaseNoteStatusService } from "@/core/noteStatusService"; +import { StatusBadge } from "@/components/atoms/StatusBadge"; +import { VaultStats } from "./useVaultStats"; + +interface StatusDistributionChartProps { + vaultStats: VaultStats; +} + +export const StatusDistributionChart = ({ + vaultStats, +}: StatusDistributionChartProps) => { + const statusChart = useMemo(() => { + const total = Object.values(vaultStats.statusDistribution).reduce( + (sum, count) => sum + count, + 0, + ); + if (total === 0) return []; + + return Object.entries(vaultStats.statusDistribution) + .filter(([_, count]) => count > 0) + .map(([statusName, count]) => ({ + name: statusName, + count, + percentage: Math.round((count / total) * 100), + })) + .sort((a, b) => b.count - a.count); + }, [vaultStats.statusDistribution]); + + const availableStatuses = BaseNoteStatusService.getAllAvailableStatuses(); + const statusMap = new Map(availableStatuses.map((s) => [s.name, s])); + + return ( +
+
+

Status Distribution

+
+
+ {statusChart.length > 0 ? ( +
+ {statusChart.map(({ name, count, percentage }) => { + const status = statusMap.get(name); + return ( +
+
+ {status && ( + + )} + + {count} notes ({percentage}%) + +
+
+
+
+
+ ); + })} +
+ ) : ( +
+ No status data available +
+ )} +
+
+ ); +}; diff --git a/components/StatusDashboard/VaultStatsCard.tsx b/components/StatusDashboard/VaultStatsCard.tsx new file mode 100644 index 0000000..a9ece36 --- /dev/null +++ b/components/StatusDashboard/VaultStatsCard.tsx @@ -0,0 +1,54 @@ +import { VaultStats } from "./useVaultStats"; + +interface VaultStatsCardProps { + vaultStats: VaultStats; +} + +export const VaultStatsCard = ({ vaultStats }: VaultStatsCardProps) => { + return ( +
+
+

Vault Overview

+
+
+
+
+
+ {vaultStats.totalNotes} +
+
Total Notes
+
+
+
+ {vaultStats.notesWithStatus} +
+
+ Notes with Status +
+
+
+
+ {vaultStats.totalNotes > 0 + ? Math.round( + (vaultStats.notesWithStatus / + vaultStats.totalNotes) * + 100, + ) + : 0} + % +
+
Coverage
+
+
+
+ {Object.values( + vaultStats.statusDistribution, + ).reduce((sum, count) => sum + count, 0)} +
+
Total Statuses
+
+
+
+
+ ); +}; diff --git a/components/StatusDashboard/useCurrentNote.tsx b/components/StatusDashboard/useCurrentNote.tsx new file mode 100644 index 0000000..805db41 --- /dev/null +++ b/components/StatusDashboard/useCurrentNote.tsx @@ -0,0 +1,44 @@ +import { useState, useCallback } from "react"; +import { TFile } from "obsidian"; +import { NoteStatus } from "@/types/noteStatus"; +import { + BaseNoteStatusService, + NoteStatusService, +} from "@/core/noteStatusService"; + +interface CurrentNoteInfo { + file: TFile | null; + statuses: Record; + lastModified: number; +} + +export const useCurrentNote = () => { + const [currentNote, setCurrentNote] = useState({ + file: null, + statuses: {}, + lastModified: 0, + }); + + const updateCurrentNote = useCallback(() => { + const activeFile = BaseNoteStatusService.app.workspace.getActiveFile(); + + if (!activeFile) { + setCurrentNote({ file: null, statuses: {}, lastModified: 0 }); + return; + } + + const noteStatusService = new NoteStatusService(activeFile); + noteStatusService.populateStatuses(); + + setCurrentNote({ + file: activeFile, + statuses: noteStatusService.statuses, + lastModified: activeFile.stat.mtime, + }); + }, []); + + return { + currentNote, + updateCurrentNote, + }; +}; diff --git a/components/StatusDashboard/useVaultStats.tsx b/components/StatusDashboard/useVaultStats.tsx new file mode 100644 index 0000000..45780fe --- /dev/null +++ b/components/StatusDashboard/useVaultStats.tsx @@ -0,0 +1,95 @@ +import { useState, useCallback } from "react"; +import { TFile } from "obsidian"; +import { NoteStatus } from "@/types/noteStatus"; +import { BaseNoteStatusService } from "@/core/noteStatusService"; +import settingsService from "@/core/settingsService"; + +export interface VaultStats { + totalNotes: number; + notesWithStatus: number; + statusDistribution: Record; + tagDistribution: Record; + recentChanges: Array<{ + file: TFile; + status: NoteStatus; + timestamp: number; + action: "added" | "removed"; + }>; +} + +export const useVaultStats = () => { + const [vaultStats, setVaultStats] = useState({ + totalNotes: 0, + notesWithStatus: 0, + statusDistribution: {}, + tagDistribution: {}, + recentChanges: [], + }); + + const calculateVaultStats = useCallback((): VaultStats => { + const files = BaseNoteStatusService.app.vault.getMarkdownFiles(); + const availableStatuses = + BaseNoteStatusService.getAllAvailableStatuses(); + const statusMetadataKeys = [settingsService.settings.tagPrefix]; + + let notesWithStatus = 0; + const statusDistribution: Record = {}; + const tagDistribution: Record = {}; + + availableStatuses.forEach((status) => { + statusDistribution[status.name] = 0; + }); + + statusMetadataKeys.forEach((tag) => { + tagDistribution[tag] = 0; + }); + + files.forEach((file) => { + const cachedMetadata = + BaseNoteStatusService.app.metadataCache.getFileCache(file); + const frontmatter = cachedMetadata?.frontmatter; + + if (!frontmatter) return; + + let hasAnyStatus = false; + + statusMetadataKeys.forEach((key) => { + const value = frontmatter[key]; + if (value) { + hasAnyStatus = true; + tagDistribution[key]++; + + const statusNames = Array.isArray(value) ? value : [value]; + statusNames.forEach((statusName) => { + const statusStr = statusName.toString(); + if (statusDistribution.hasOwnProperty(statusStr)) { + statusDistribution[statusStr]++; + } + }); + } + }); + + if (hasAnyStatus) { + notesWithStatus++; + } + }); + + return { + totalNotes: files.length, + notesWithStatus, + statusDistribution, + tagDistribution, + recentChanges: [], + }; + }, []); + + const updateVaultStats = useCallback(() => { + const stats = calculateVaultStats(); + setVaultStats(stats); + }, [calculateVaultStats]); + + return { + vaultStats, + updateVaultStats, + }; +}; diff --git a/styles.css b/styles.css index e4171c9..5ef03fa 100644 --- a/styles.css +++ b/styles.css @@ -532,33 +532,33 @@ hyphens: auto; } -/* styles/components/groupped-status-view.css */ -.groupped-status-view-container { +/* styles/components/grouped-status-view.css */ +.grouped-status-view-container { height: 100%; overflow: hidden; } -.groupped-status-view { +.grouped-status-view { height: 100%; display: flex; flex-direction: column; background-color: var(--background-primary); } -.groupped-status-header { +.grouped-status-header { padding: var(--size-4-2) var(--size-4-3); border-bottom: 1px solid var(--background-modifier-border); background-color: var(--background-secondary); flex-shrink: 0; } -.groupped-status-filters { +.grouped-status-filters { display: flex; flex-direction: column; gap: var(--size-4-2); } -.groupped-status-note-filter { +.grouped-status-note-filter { display: flex; flex-direction: column; } -.groupped-status-note-input { +.grouped-status-note-input { width: 100%; padding: var(--size-2-1) var(--size-2-3); border: 1px solid var(--background-modifier-border);