Skip to content

Instantly share code, notes, and snippets.

@shricodev
Last active January 18, 2026 15:44
Show Gist options
  • Select an option

  • Save shricodev/934c3841101c073b50a5dad18746d78d to your computer and use it in GitHub Desktop.

Select an option

Save shricodev/934c3841101c073b50a5dad18746d78d to your computer and use it in GitHub Desktop.
Test 2: Tool Usage Analytics + Insights Dashboard - claude-opus-4.5
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..227cd31
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": ["Bash(cat:*)", "Bash(tree:*)"],
+ "deny": [],
+ "ask": []
+ }
+}
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 98b0d5a..90b15d6 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -135,6 +135,7 @@
"buyMeACoffee": "Buy me a coffee",
"hireMe": "Hire me",
"home": "Home",
+ "insights": "Insights",
"tools": "Tools"
},
"number": {
diff --git a/src/@types/analytics.ts b/src/@types/analytics.ts
new file mode 100644
index 0000000..d7ae4f2
--- /dev/null
+++ b/src/@types/analytics.ts
@@ -0,0 +1,41 @@
+import { ToolCategory, UserType } from '@tools/defineTool';
+
+export type ToolEventType = 'open' | 'execute';
+
+export interface ToolUsageEvent {
+ toolPath: string;
+ toolName: string;
+ category: ToolCategory;
+ eventType: ToolEventType;
+ timestamp: number;
+ userType?: UserType;
+}
+
+export interface ToolUsageStats {
+ toolPath: string;
+ toolName: string;
+ category: ToolCategory;
+ totalCount: number;
+ openCount: number;
+ executeCount: number;
+ lastUsed: number;
+ userTypes: UserType[];
+}
+
+export interface AnalyticsData {
+ events: ToolUsageEvent[];
+ version: number;
+}
+
+export type TimeRangeFilter = 'last7days' | 'allTime';
+
+export interface AnalyticsFilters {
+ timeRange: TimeRangeFilter;
+ userTypes: UserType[];
+}
+
+export interface CategoryUsageStats {
+ category: ToolCategory;
+ totalCount: number;
+ toolCount: number;
+}
diff --git a/src/components/ActionPalette/ActionPalette.tsx b/src/components/ActionPalette/ActionPalette.tsx
index eacdc72..07f3850 100644
--- a/src/components/ActionPalette/ActionPalette.tsx
+++ b/src/components/ActionPalette/ActionPalette.tsx
@@ -226,6 +226,20 @@ const ActionPalette: React.FC<ActionPaletteProps> = ({
}
});
+ actions.push({
+ id: 'go-insights',
+ type: 'action',
+ label: 'View Insights',
+ description: 'See your tool usage analytics',
+ icon: 'mdi:chart-box',
+ category: 'Navigation',
+ keywords: ['insights', 'analytics', 'stats', 'usage', 'dashboard'],
+ action: () => {
+ navigate('/insights');
+ onClose();
+ }
+ });
+
// Bookmarks action
if (bookmarkedPaths.length > 0) {
actions.push({
@@ -354,7 +368,11 @@ const ActionPalette: React.FC<ActionPaletteProps> = ({
} else if (action.type === 'action' && action.action) {
action.action();
// Only close for navigation actions
- if (action.id.startsWith('go-') || action.id === 'view-bookmarks') {
+ if (
+ action.id.startsWith('go-') ||
+ action.id === 'view-bookmarks' ||
+ action.id === 'go-insights'
+ ) {
onClose();
}
}
diff --git a/src/components/App.tsx b/src/components/App.tsx
index 889442d..326d1d5 100644
--- a/src/components/App.tsx
+++ b/src/components/App.tsx
@@ -13,6 +13,7 @@ import ScrollToTopButton from './ScrollToTopButton';
import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n';
import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider';
+import { AnalyticsProvider } from 'providers/AnalyticsProvider';
import ActionPalette from './ActionPalette';
export type Mode = 'dark' | 'light' | 'system';
@@ -80,18 +81,20 @@ function App() {
>
<CustomSnackBarProvider>
<UserTypeFilterProvider>
- <BrowserRouter>
- <Navbar mode={mode} onChangeMode={handleChangeMode} />
- <Suspense fallback={<Loading />}>
- <AppRoutes />
- </Suspense>
- <ActionPalette
- open={actionPaletteOpen}
- onClose={() => setActionPaletteOpen(false)}
- mode={mode}
- onChangeMode={handleChangeMode}
- />
- </BrowserRouter>
+ <AnalyticsProvider>
+ <BrowserRouter>
+ <Navbar mode={mode} onChangeMode={handleChangeMode} />
+ <Suspense fallback={<Loading />}>
+ <AppRoutes />
+ </Suspense>
+ <ActionPalette
+ open={actionPaletteOpen}
+ onClose={() => setActionPaletteOpen(false)}
+ mode={mode}
+ onChangeMode={handleChangeMode}
+ />
+ </BrowserRouter>
+ </AnalyticsProvider>
</UserTypeFilterProvider>
</CustomSnackBarProvider>
</SnackbarProvider>
diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx
index dbacb3a..46636dd 100644
--- a/src/components/Navbar/index.tsx
+++ b/src/components/Navbar/index.tsx
@@ -61,8 +61,7 @@ const Navbar: React.FC<NavbarProps> = ({
};
const navItems: { label: string; path: string }[] = [
- // { label: 'Features', path: '/features' }
- // { label: 'About Us', path: '/about-us' }
+ { label: t('navbar.insights'), path: '/insights' }
];
const languageSelector = (
diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx
index 40ad761..d6779d7 100644
--- a/src/components/ToolLayout.tsx
+++ b/src/components/ToolLayout.tsx
@@ -1,5 +1,5 @@
import { Box } from '@mui/material';
-import React, { ReactNode } from 'react';
+import React, { ReactNode, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet';
import ToolHeader from './ToolHeader';
import Separator from './Separator';
@@ -11,8 +11,9 @@ import {
} from '../utils/string';
import { IconifyIcon } from '@iconify/react';
import { useTranslation } from 'react-i18next';
-import { ToolCategory } from '@tools/defineTool';
+import { ToolCategory, UserType } from '@tools/defineTool';
import { FullI18nKey } from '../i18n';
+import { useAnalytics } from '../providers/AnalyticsProvider';
export default function ToolLayout({
children,
@@ -29,12 +30,15 @@ export default function ToolLayout({
name: FullI18nKey;
description: FullI18nKey;
shortDescription: FullI18nKey;
+ userTypes?: UserType[];
};
}) {
const { t } = useTranslation([
'translation',
getI18nNamespaceFromToolCategory(type)
]);
+ const { trackToolOpen } = useAnalytics();
+ const hasTrackedOpen = useRef(false);
// Use i18n keys if available, otherwise fall back to provided strings
//@ts-ignore
@@ -42,6 +46,15 @@ export default function ToolLayout({
//@ts-ignore
const toolDescription: string = t(i18n.description);
+ // Track tool open on mount
+ useEffect(() => {
+ if (!hasTrackedOpen.current && toolTitle) {
+ const userType = i18n?.userTypes?.[0];
+ trackToolOpen(fullPath, toolTitle, type, userType);
+ hasTrackedOpen.current = true;
+ }
+ }, [fullPath, toolTitle, type, i18n?.userTypes, trackToolOpen]);
+
const otherCategoryTools =
getToolsByCategory([], t)
.find((category) => category.type === type)
diff --git a/src/config/routesConfig.tsx b/src/config/routesConfig.tsx
index 78f47e6..e9cdb7e 100644
--- a/src/config/routesConfig.tsx
+++ b/src/config/routesConfig.tsx
@@ -3,6 +3,7 @@ import { lazy } from 'react';
const Home = lazy(() => import('../pages/home'));
const ToolsByCategory = lazy(() => import('../pages/tools-by-category'));
+const Insights = lazy(() => import('../pages/insights'));
const routes: RouteObject[] = [
{
@@ -13,6 +14,10 @@ const routes: RouteObject[] = [
path: '/categories/:categoryName',
element: <ToolsByCategory />
},
+ {
+ path: '/insights',
+ element: <Insights />
+ },
{
path: '*',
element: <Navigate to="404" />
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 1929925..ac7e1ee 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -2,3 +2,4 @@ export { default as useDebounce } from './useDebounce';
export { default as useTimeout } from './useTimeout';
export { default as usePrevious } from './usePrevious';
export { default as useUpdateEffect } from './useUpdateEffect';
+export { useToolExecution } from './useToolExecution';
diff --git a/src/hooks/useToolExecution.ts b/src/hooks/useToolExecution.ts
new file mode 100644
index 0000000..2b321f2
--- /dev/null
+++ b/src/hooks/useToolExecution.ts
@@ -0,0 +1,47 @@
+import { useCallback, useRef } from 'react';
+import { useAnalytics } from '../providers/AnalyticsProvider';
+import { ToolCategory, UserType } from '@tools/defineTool';
+
+interface UseToolExecutionProps {
+ toolPath: string;
+ toolName: string;
+ category: ToolCategory;
+ userType?: UserType;
+}
+
+/**
+ * Hook for tracking tool execution events.
+ * Use this in individual tool components to track when the tool is executed.
+ *
+ * @example
+ * const { trackExecution } = useToolExecution({
+ * toolPath: 'string/reverse',
+ * toolName: 'Reverse String',
+ * category: 'string'
+ * });
+ *
+ * const handleCompute = () => {
+ * // ... your compute logic
+ * trackExecution();
+ * };
+ */
+export function useToolExecution({
+ toolPath,
+ toolName,
+ category,
+ userType
+}: UseToolExecutionProps) {
+ const { trackToolExecute } = useAnalytics();
+ const lastExecutionTime = useRef<number>(0);
+
+ // Debounce execution tracking to prevent spam (min 1 second between tracks)
+ const trackExecution = useCallback(() => {
+ const now = Date.now();
+ if (now - lastExecutionTime.current > 1000) {
+ trackToolExecute(toolPath, toolName, category, userType);
+ lastExecutionTime.current = now;
+ }
+ }, [toolPath, toolName, category, userType, trackToolExecute]);
+
+ return { trackExecution };
+}
diff --git a/src/pages/insights/components/CategoryUsage.tsx b/src/pages/insights/components/CategoryUsage.tsx
new file mode 100644
index 0000000..4510be6
--- /dev/null
+++ b/src/pages/insights/components/CategoryUsage.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import { Box, Typography, Grid, Paper, LinearProgress } from '@mui/material';
+import { Icon } from '@iconify/react';
+import { useNavigate } from 'react-router-dom';
+import { CategoryUsageStats } from '../../../@types/analytics';
+import { ToolCategory } from '@tools/defineTool';
+import InsightCard from './InsightCard';
+
+interface CategoryUsageProps {
+ categories: CategoryUsageStats[];
+}
+
+const categoryIcons: Record<ToolCategory, string> = {
+ string: 'solar:text-bold-duotone',
+ 'image-generic': 'material-symbols-light:image-outline-rounded',
+ png: 'ph:file-png-thin',
+ number: 'lsicon:number-filled',
+ gif: 'material-symbols-light:gif-rounded',
+ list: 'solar:list-bold-duotone',
+ json: 'lets-icons:json-light',
+ time: 'mdi:clock-time-five',
+ csv: 'material-symbols-light:csv-outline',
+ video: 'lets-icons:video-light',
+ pdf: 'tabler:pdf',
+ audio: 'ic:twotone-audiotrack',
+ xml: 'mdi-light:xml',
+ converters: 'streamline-plump:convert-pdf-1'
+};
+
+export default function CategoryUsage({ categories }: CategoryUsageProps) {
+ const navigate = useNavigate();
+ const maxCount = categories.length > 0 ? categories[0].totalCount : 0;
+
+ return (
+ <InsightCard
+ title="Usage by Category"
+ icon="mdi:folder-multiple"
+ isEmpty={categories.length === 0}
+ emptyMessage="No category usage data"
+ >
+ <Grid container spacing={2}>
+ {categories.map((category) => {
+ const percentage =
+ maxCount > 0 ? (category.totalCount / maxCount) * 100 : 0;
+ const icon = categoryIcons[category.category] || 'mdi:folder';
+
+ return (
+ <Grid item xs={12} sm={6} key={category.category}>
+ <Paper
+ elevation={0}
+ sx={{
+ p: 2,
+ borderRadius: 1,
+ border: 1,
+ borderColor: 'divider',
+ cursor: 'pointer',
+ transition: 'all 0.2s',
+ '&:hover': {
+ borderColor: 'primary.main',
+ backgroundColor: 'action.hover'
+ }
+ }}
+ onClick={() => navigate(`/categories/${category.category}`)}
+ >
+ <Box
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1,
+ mb: 1
+ }}
+ >
+ <Icon icon={icon} fontSize={20} />
+ <Typography
+ variant="body2"
+ fontWeight={600}
+ sx={{ textTransform: 'capitalize' }}
+ >
+ {category.category.replace('-', ' ')}
+ </Typography>
+ </Box>
+ <Box
+ sx={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ mb: 1
+ }}
+ >
+ <Typography variant="caption" color="text.secondary">
+ {category.toolCount} tool
+ {category.toolCount !== 1 ? 's' : ''} used
+ </Typography>
+ <Typography variant="body2" fontWeight={500} color="primary">
+ {category.totalCount}
+ </Typography>
+ </Box>
+ <LinearProgress
+ variant="determinate"
+ value={percentage}
+ sx={{
+ height: 4,
+ borderRadius: 1,
+ backgroundColor: 'action.hover'
+ }}
+ />
+ </Paper>
+ </Grid>
+ );
+ })}
+ </Grid>
+ </InsightCard>
+ );
+}
diff --git a/src/pages/insights/components/InsightCard.tsx b/src/pages/insights/components/InsightCard.tsx
new file mode 100644
index 0000000..25195a5
--- /dev/null
+++ b/src/pages/insights/components/InsightCard.tsx
@@ -0,0 +1,64 @@
+import React, { ReactNode } from 'react';
+import { Box, Typography, Paper } from '@mui/material';
+import { Icon } from '@iconify/react';
+
+interface InsightCardProps {
+ title: string;
+ icon: string;
+ children: ReactNode;
+ emptyMessage?: string;
+ isEmpty?: boolean;
+}
+
+export default function InsightCard({
+ title,
+ icon,
+ children,
+ emptyMessage = 'No data available',
+ isEmpty = false
+}: InsightCardProps) {
+ return (
+ <Paper
+ elevation={0}
+ sx={{
+ p: 3,
+ borderRadius: 2,
+ border: 1,
+ borderColor: 'divider',
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column'
+ }}
+ >
+ <Box
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1,
+ mb: 2
+ }}
+ >
+ <Icon icon={icon} fontSize={24} />
+ <Typography variant="h6" fontWeight={600}>
+ {title}
+ </Typography>
+ </Box>
+ {isEmpty ? (
+ <Box
+ sx={{
+ flex: 1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: 'text.secondary',
+ minHeight: 120
+ }}
+ >
+ <Typography variant="body2">{emptyMessage}</Typography>
+ </Box>
+ ) : (
+ <Box sx={{ flex: 1 }}>{children}</Box>
+ )}
+ </Paper>
+ );
+}
diff --git a/src/pages/insights/components/InsightsFilters.tsx b/src/pages/insights/components/InsightsFilters.tsx
new file mode 100644
index 0000000..8315678
--- /dev/null
+++ b/src/pages/insights/components/InsightsFilters.tsx
@@ -0,0 +1,109 @@
+import React from 'react';
+import {
+ Box,
+ ToggleButton,
+ ToggleButtonGroup,
+ Typography,
+ Stack,
+ Paper
+} from '@mui/material';
+import { Icon } from '@iconify/react';
+import { AnalyticsFilters, TimeRangeFilter } from '../../../@types/analytics';
+import { UserType } from '@tools/defineTool';
+
+interface InsightsFiltersProps {
+ filters: AnalyticsFilters;
+ onFiltersChange: (filters: AnalyticsFilters) => void;
+}
+
+export default function InsightsFilters({
+ filters,
+ onFiltersChange
+}: InsightsFiltersProps) {
+ const handleTimeRangeChange = (
+ _: React.MouseEvent<HTMLElement>,
+ newValue: TimeRangeFilter | null
+ ) => {
+ if (newValue) {
+ onFiltersChange({ ...filters, timeRange: newValue });
+ }
+ };
+
+ const handleUserTypesChange = (
+ _: React.MouseEvent<HTMLElement>,
+ newValue: UserType[]
+ ) => {
+ onFiltersChange({ ...filters, userTypes: newValue });
+ };
+
+ return (
+ <Paper
+ elevation={0}
+ sx={{
+ p: 2,
+ borderRadius: 2,
+ border: 1,
+ borderColor: 'divider',
+ mb: 3
+ }}
+ >
+ <Stack
+ direction={{ xs: 'column', sm: 'row' }}
+ spacing={3}
+ alignItems={{ xs: 'stretch', sm: 'center' }}
+ justifyContent="space-between"
+ >
+ <Stack direction="row" spacing={3} alignItems="center" flexWrap="wrap">
+ <Box>
+ <Typography
+ variant="caption"
+ color="text.secondary"
+ sx={{ mb: 0.5, display: 'block' }}
+ >
+ Time Range
+ </Typography>
+ <ToggleButtonGroup
+ value={filters.timeRange}
+ exclusive
+ onChange={handleTimeRangeChange}
+ size="small"
+ >
+ <ToggleButton value="last7days">
+ <Icon icon="mdi:calendar-week" style={{ marginRight: 4 }} />
+ Last 7 Days
+ </ToggleButton>
+ <ToggleButton value="allTime">
+ <Icon icon="mdi:calendar-all" style={{ marginRight: 4 }} />
+ All Time
+ </ToggleButton>
+ </ToggleButtonGroup>
+ </Box>
+
+ <Box>
+ <Typography
+ variant="caption"
+ color="text.secondary"
+ sx={{ mb: 0.5, display: 'block' }}
+ >
+ User Type
+ </Typography>
+ <ToggleButtonGroup
+ value={filters.userTypes}
+ onChange={handleUserTypesChange}
+ size="small"
+ >
+ <ToggleButton value="generalUsers">
+ <Icon icon="mdi:account" style={{ marginRight: 4 }} />
+ General
+ </ToggleButton>
+ <ToggleButton value="developers">
+ <Icon icon="mdi:code-braces" style={{ marginRight: 4 }} />
+ Developer
+ </ToggleButton>
+ </ToggleButtonGroup>
+ </Box>
+ </Stack>
+ </Stack>
+ </Paper>
+ );
+}
diff --git a/src/pages/insights/components/MostUsedTools.tsx b/src/pages/insights/components/MostUsedTools.tsx
new file mode 100644
index 0000000..cedd992
--- /dev/null
+++ b/src/pages/insights/components/MostUsedTools.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import {
+ Box,
+ Typography,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ LinearProgress,
+ Chip,
+ Stack
+} from '@mui/material';
+import { Icon } from '@iconify/react';
+import { useNavigate } from 'react-router-dom';
+import { ToolUsageStats } from '../../../@types/analytics';
+import InsightCard from './InsightCard';
+
+interface MostUsedToolsProps {
+ tools: ToolUsageStats[];
+}
+
+export default function MostUsedTools({ tools }: MostUsedToolsProps) {
+ const navigate = useNavigate();
+ const maxCount = tools.length > 0 ? tools[0].totalCount : 0;
+
+ return (
+ <InsightCard
+ title="Most Used Tools"
+ icon="mdi:chart-bar"
+ isEmpty={tools.length === 0}
+ emptyMessage="No tool usage recorded yet"
+ >
+ <List sx={{ py: 0 }}>
+ {tools.map((tool, index) => {
+ const percentage =
+ maxCount > 0 ? (tool.totalCount / maxCount) * 100 : 0;
+ return (
+ <ListItem
+ key={tool.toolPath}
+ sx={{
+ px: 0,
+ cursor: 'pointer',
+ borderRadius: 1,
+ '&:hover': {
+ backgroundColor: 'action.hover'
+ }
+ }}
+ onClick={() => navigate('/' + tool.toolPath)}
+ >
+ <ListItemIcon sx={{ minWidth: 32 }}>
+ <Typography
+ variant="body2"
+ color="text.secondary"
+ fontWeight={500}
+ >
+ {index + 1}
+ </Typography>
+ </ListItemIcon>
+ <ListItemText
+ primary={
+ <Stack spacing={1}>
+ <Box
+ sx={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
+ <Typography variant="body2" fontWeight={500}>
+ {tool.toolName}
+ </Typography>
+ <Chip
+ label={tool.totalCount}
+ size="small"
+ color="primary"
+ variant="outlined"
+ sx={{ height: 20, fontSize: '0.7rem' }}
+ />
+ </Box>
+ <LinearProgress
+ variant="determinate"
+ value={percentage}
+ sx={{
+ height: 6,
+ borderRadius: 1,
+ backgroundColor: 'action.hover'
+ }}
+ />
+ </Stack>
+ }
+ secondary={
+ <Typography
+ variant="caption"
+ color="text.secondary"
+ sx={{ mt: 0.5, display: 'block' }}
+ >
+ {tool.category}
+ </Typography>
+ }
+ />
+ </ListItem>
+ );
+ })}
+ </List>
+ </InsightCard>
+ );
+}
diff --git a/src/pages/insights/components/RecentlyUsedTools.tsx b/src/pages/insights/components/RecentlyUsedTools.tsx
new file mode 100644
index 0000000..71d2332
--- /dev/null
+++ b/src/pages/insights/components/RecentlyUsedTools.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import {
+ Box,
+ Typography,
+ List,
+ ListItem,
+ ListItemText,
+ Chip
+} from '@mui/material';
+import { Icon } from '@iconify/react';
+import { useNavigate } from 'react-router-dom';
+import { ToolUsageStats } from '../../../@types/analytics';
+import InsightCard from './InsightCard';
+
+interface RecentlyUsedToolsProps {
+ tools: ToolUsageStats[];
+}
+
+function formatTimeAgo(timestamp: number): string {
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
+
+ if (seconds < 60) return 'Just now';
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
+ if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
+ return new Date(timestamp).toLocaleDateString();
+}
+
+export default function RecentlyUsedTools({ tools }: RecentlyUsedToolsProps) {
+ const navigate = useNavigate();
+
+ return (
+ <InsightCard
+ title="Recently Used"
+ icon="mdi:history"
+ isEmpty={tools.length === 0}
+ emptyMessage="No recent tool usage"
+ >
+ <List sx={{ py: 0 }}>
+ {tools.map((tool) => (
+ <ListItem
+ key={tool.toolPath}
+ sx={{
+ px: 1,
+ py: 1,
+ cursor: 'pointer',
+ borderRadius: 1,
+ '&:hover': {
+ backgroundColor: 'action.hover'
+ }
+ }}
+ onClick={() => navigate('/' + tool.toolPath)}
+ >
+ <ListItemText
+ primary={
+ <Box
+ sx={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
+ <Typography variant="body2" fontWeight={500}>
+ {tool.toolName}
+ </Typography>
+ <Typography variant="caption" color="text.secondary">
+ {formatTimeAgo(tool.lastUsed)}
+ </Typography>
+ </Box>
+ }
+ secondary={
+ <Box
+ sx={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ mt: 0.5
+ }}
+ >
+ <Typography variant="caption" color="text.secondary">
+ {tool.category}
+ </Typography>
+ <Chip
+ label={`${tool.totalCount} uses`}
+ size="small"
+ variant="outlined"
+ sx={{ height: 18, fontSize: '0.65rem' }}
+ />
+ </Box>
+ }
+ />
+ </ListItem>
+ ))}
+ </List>
+ </InsightCard>
+ );
+}
diff --git a/src/pages/insights/components/StatsOverview.tsx b/src/pages/insights/components/StatsOverview.tsx
new file mode 100644
index 0000000..b2c8590
--- /dev/null
+++ b/src/pages/insights/components/StatsOverview.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import { Box, Typography, Paper, Grid, Stack } from '@mui/material';
+import { Icon } from '@iconify/react';
+
+interface StatCardProps {
+ label: string;
+ value: number | string;
+ icon: string;
+ color?: string;
+}
+
+function StatCard({
+ label,
+ value,
+ icon,
+ color = 'primary.main'
+}: StatCardProps) {
+ return (
+ <Paper
+ elevation={0}
+ sx={{
+ p: 2.5,
+ borderRadius: 2,
+ border: 1,
+ borderColor: 'divider',
+ height: '100%'
+ }}
+ >
+ <Stack direction="row" spacing={2} alignItems="center">
+ <Box
+ sx={{
+ p: 1.5,
+ borderRadius: 2,
+ backgroundColor: `${color}15`,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center'
+ }}
+ >
+ <Icon icon={icon} fontSize={28} color={color} />
+ </Box>
+ <Box>
+ <Typography variant="h4" fontWeight={700}>
+ {value}
+ </Typography>
+ <Typography variant="body2" color="text.secondary">
+ {label}
+ </Typography>
+ </Box>
+ </Stack>
+ </Paper>
+ );
+}
+
+interface StatsOverviewProps {
+ totalEvents: number;
+ uniqueTools: number;
+ categoryCount: number;
+}
+
+export default function StatsOverview({
+ totalEvents,
+ uniqueTools,
+ categoryCount
+}: StatsOverviewProps) {
+ return (
+ <Grid container spacing={2} sx={{ mb: 3 }}>
+ <Grid item xs={12} sm={4}>
+ <StatCard
+ label="Total Usage Events"
+ value={totalEvents}
+ icon="mdi:chart-line"
+ color="#1976d2"
+ />
+ </Grid>
+ <Grid item xs={12} sm={4}>
+ <StatCard
+ label="Tools Used"
+ value={uniqueTools}
+ icon="mdi:tools"
+ color="#2e7d32"
+ />
+ </Grid>
+ <Grid item xs={12} sm={4}>
+ <StatCard
+ label="Categories"
+ value={categoryCount}
+ icon="mdi:folder-multiple"
+ color="#ed6c02"
+ />
+ </Grid>
+ </Grid>
+ );
+}
diff --git a/src/pages/insights/index.tsx b/src/pages/insights/index.tsx
new file mode 100644
index 0000000..7f8da15
--- /dev/null
+++ b/src/pages/insights/index.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { Box, Typography, Grid, Button, Stack } from '@mui/material';
+import { Helmet } from 'react-helmet';
+import { Icon } from '@iconify/react';
+import { useAnalytics } from '../../providers/AnalyticsProvider';
+import InsightsFilters from './components/InsightsFilters';
+import StatsOverview from './components/StatsOverview';
+import MostUsedTools from './components/MostUsedTools';
+import RecentlyUsedTools from './components/RecentlyUsedTools';
+import CategoryUsage from './components/CategoryUsage';
+
+export default function Insights() {
+ const {
+ filters,
+ setFilters,
+ mostUsedTools,
+ recentlyUsedTools,
+ usageByCategory,
+ totalEventCount,
+ uniqueToolCount,
+ clearAnalytics
+ } = useAnalytics();
+
+ const hasData = totalEventCount > 0;
+
+ return (
+ <Box
+ sx={{
+ width: '100%',
+ minHeight: '100vh',
+ backgroundColor: 'background.default',
+ py: 4
+ }}
+ >
+ <Helmet>
+ <title>Insights - OmniTools</title>
+ </Helmet>
+ <Box
+ sx={{
+ maxWidth: 1200,
+ mx: 'auto',
+ px: { xs: 2, md: 4 }
+ }}
+ >
+ <Stack
+ direction={{ xs: 'column', sm: 'row' }}
+ justifyContent="space-between"
+ alignItems={{ xs: 'flex-start', sm: 'center' }}
+ sx={{ mb: 3 }}
+ >
+ <Box>
+ <Typography variant="h4" fontWeight={700} gutterBottom>
+ Insights
+ </Typography>
+ <Typography variant="body1" color="text.secondary">
+ Track and analyze your tool usage patterns
+ </Typography>
+ </Box>
+ {hasData && (
+ <Button
+ variant="outlined"
+ color="error"
+ size="small"
+ startIcon={<Icon icon="mdi:delete-outline" />}
+ onClick={clearAnalytics}
+ sx={{ mt: { xs: 2, sm: 0 } }}
+ >
+ Clear Data
+ </Button>
+ )}
+ </Stack>
+
+ <InsightsFilters filters={filters} onFiltersChange={setFilters} />
+
+ <StatsOverview
+ totalEvents={totalEventCount}
+ uniqueTools={uniqueToolCount}
+ categoryCount={usageByCategory.length}
+ />
+
+ {!hasData ? (
+ <Box
+ sx={{
+ textAlign: 'center',
+ py: 8,
+ px: 2,
+ border: 1,
+ borderColor: 'divider',
+ borderRadius: 2,
+ backgroundColor: 'background.paper'
+ }}
+ >
+ <Icon
+ icon="mdi:chart-box-outline"
+ fontSize={64}
+ style={{ color: '#9e9e9e', marginBottom: 16 }}
+ />
+ <Typography variant="h6" gutterBottom>
+ No Usage Data Yet
+ </Typography>
+ <Typography variant="body2" color="text.secondary">
+ Start using tools to see your analytics here.
+ <br />
+ Usage data is tracked when you open and execute tools.
+ </Typography>
+ </Box>
+ ) : (
+ <Grid container spacing={3}>
+ <Grid item xs={12} md={6}>
+ <MostUsedTools tools={mostUsedTools} />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <RecentlyUsedTools tools={recentlyUsedTools} />
+ </Grid>
+ <Grid item xs={12}>
+ <CategoryUsage categories={usageByCategory} />
+ </Grid>
+ </Grid>
+ )}
+ </Box>
+ </Box>
+ );
+}
diff --git a/src/providers/AnalyticsProvider.tsx b/src/providers/AnalyticsProvider.tsx
new file mode 100644
index 0000000..8b376eb
--- /dev/null
+++ b/src/providers/AnalyticsProvider.tsx
@@ -0,0 +1,183 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ useMemo,
+ ReactNode
+} from 'react';
+import { ToolCategory, UserType } from '@tools/defineTool';
+import {
+ ToolUsageEvent,
+ ToolUsageStats,
+ ToolEventType,
+ AnalyticsFilters,
+ CategoryUsageStats
+} from '../@types/analytics';
+import {
+ getAnalyticsData,
+ recordToolEvent,
+ filterEvents,
+ getMostUsedTools,
+ getRecentlyUsedTools,
+ getUsageByCategory,
+ getTotalEventCount,
+ getUniqueToolCount,
+ clearAnalyticsData
+} from '../utils/analytics';
+
+interface AnalyticsContextType {
+ // Actions
+ trackToolOpen: (
+ toolPath: string,
+ toolName: string,
+ category: ToolCategory,
+ userType?: UserType
+ ) => void;
+ trackToolExecute: (
+ toolPath: string,
+ toolName: string,
+ category: ToolCategory,
+ userType?: UserType
+ ) => void;
+ clearAnalytics: () => void;
+ refreshAnalytics: () => void;
+
+ // Filters
+ filters: AnalyticsFilters;
+ setFilters: (filters: AnalyticsFilters) => void;
+
+ // Computed data
+ filteredEvents: ToolUsageEvent[];
+ mostUsedTools: ToolUsageStats[];
+ recentlyUsedTools: ToolUsageStats[];
+ usageByCategory: CategoryUsageStats[];
+ totalEventCount: number;
+ uniqueToolCount: number;
+}
+
+const AnalyticsContext = createContext<AnalyticsContextType | null>(null);
+
+interface AnalyticsProviderProps {
+ children: ReactNode;
+}
+
+export function AnalyticsProvider({ children }: AnalyticsProviderProps) {
+ const [events, setEvents] = useState<ToolUsageEvent[]>(() => {
+ return getAnalyticsData().events;
+ });
+
+ const [filters, setFilters] = useState<AnalyticsFilters>({
+ timeRange: 'allTime',
+ userTypes: []
+ });
+
+ const refreshAnalytics = useCallback(() => {
+ setEvents(getAnalyticsData().events);
+ }, []);
+
+ const trackToolOpen = useCallback(
+ (
+ toolPath: string,
+ toolName: string,
+ category: ToolCategory,
+ userType?: UserType
+ ) => {
+ recordToolEvent(toolPath, toolName, category, 'open', userType);
+ refreshAnalytics();
+ },
+ [refreshAnalytics]
+ );
+
+ const trackToolExecute = useCallback(
+ (
+ toolPath: string,
+ toolName: string,
+ category: ToolCategory,
+ userType?: UserType
+ ) => {
+ recordToolEvent(toolPath, toolName, category, 'execute', userType);
+ refreshAnalytics();
+ },
+ [refreshAnalytics]
+ );
+
+ const clearAnalytics = useCallback(() => {
+ clearAnalyticsData();
+ setEvents([]);
+ }, []);
+
+ // Memoized computed data
+ const filteredEvents = useMemo(() => {
+ return filterEvents(events, filters);
+ }, [events, filters]);
+
+ const mostUsedTools = useMemo(() => {
+ return getMostUsedTools(filteredEvents, 10);
+ }, [filteredEvents]);
+
+ const recentlyUsedTools = useMemo(() => {
+ return getRecentlyUsedTools(filteredEvents, 10);
+ }, [filteredEvents]);
+
+ const usageByCategory = useMemo(() => {
+ return getUsageByCategory(filteredEvents);
+ }, [filteredEvents]);
+
+ const totalEventCount = useMemo(() => {
+ return getTotalEventCount(filteredEvents);
+ }, [filteredEvents]);
+
+ const uniqueToolCount = useMemo(() => {
+ return getUniqueToolCount(filteredEvents);
+ }, [filteredEvents]);
+
+ const contextValue = useMemo(
+ () => ({
+ trackToolOpen,
+ trackToolExecute,
+ clearAnalytics,
+ refreshAnalytics,
+ filters,
+ setFilters,
+ filteredEvents,
+ mostUsedTools,
+ recentlyUsedTools,
+ usageByCategory,
+ totalEventCount,
+ uniqueToolCount
+ }),
+ [
+ trackToolOpen,
+ trackToolExecute,
+ clearAnalytics,
+ refreshAnalytics,
+ filters,
+ filteredEvents,
+ mostUsedTools,
+ recentlyUsedTools,
+ usageByCategory,
+ totalEventCount,
+ uniqueToolCount
+ ]
+ );
+
+ return (
+ <AnalyticsContext.Provider value={contextValue}>
+ {children}
+ </AnalyticsContext.Provider>
+ );
+}
+
+export function useAnalytics(): AnalyticsContextType {
+ const context = useContext(AnalyticsContext);
+
+ if (!context) {
+ throw new Error(
+ 'useAnalytics must be used within an AnalyticsProvider. ' +
+ 'Make sure your component is wrapped with <AnalyticsProvider>.'
+ );
+ }
+
+ return context;
+}
diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts
new file mode 100644
index 0000000..d4de54c
--- /dev/null
+++ b/src/utils/analytics.ts
@@ -0,0 +1,234 @@
+import {
+ AnalyticsData,
+ ToolUsageEvent,
+ ToolUsageStats,
+ ToolEventType,
+ TimeRangeFilter,
+ CategoryUsageStats,
+ AnalyticsFilters
+} from '../@types/analytics';
+import { ToolCategory, UserType } from '@tools/defineTool';
+
+const ANALYTICS_STORAGE_KEY = 'toolAnalytics';
+const ANALYTICS_VERSION = 1;
+
+// 7 days in milliseconds
+const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
+
+/**
+ * Get analytics data from localStorage
+ */
+export function getAnalyticsData(): AnalyticsData {
+ try {
+ const stored = localStorage.getItem(ANALYTICS_STORAGE_KEY);
+ if (stored) {
+ const data = JSON.parse(stored) as AnalyticsData;
+ // Handle version migrations if needed
+ if (data.version === ANALYTICS_VERSION) {
+ return data;
+ }
+ }
+ } catch (error) {
+ console.error('Error loading analytics data:', error);
+ }
+ return { events: [], version: ANALYTICS_VERSION };
+}
+
+/**
+ * Save analytics data to localStorage
+ */
+export function saveAnalyticsData(data: AnalyticsData): void {
+ try {
+ localStorage.setItem(ANALYTICS_STORAGE_KEY, JSON.stringify(data));
+ } catch (error) {
+ console.error('Error saving analytics data:', error);
+ }
+}
+
+/**
+ * Record a tool usage event
+ */
+export function recordToolEvent(
+ toolPath: string,
+ toolName: string,
+ category: ToolCategory,
+ eventType: ToolEventType,
+ userType?: UserType
+): ToolUsageEvent {
+ const event: ToolUsageEvent = {
+ toolPath,
+ toolName,
+ category,
+ eventType,
+ timestamp: Date.now(),
+ userType
+ };
+
+ const data = getAnalyticsData();
+ data.events.push(event);
+
+ // Keep only last 1000 events to prevent localStorage from getting too large
+ if (data.events.length > 1000) {
+ data.events = data.events.slice(-1000);
+ }
+
+ saveAnalyticsData(data);
+ return event;
+}
+
+/**
+ * Filter events by time range
+ */
+export function filterEventsByTimeRange(
+ events: ToolUsageEvent[],
+ timeRange: TimeRangeFilter
+): ToolUsageEvent[] {
+ if (timeRange === 'allTime') {
+ return events;
+ }
+
+ const cutoffTime = Date.now() - SEVEN_DAYS_MS;
+ return events.filter((event) => event.timestamp >= cutoffTime);
+}
+
+/**
+ * Filter events by user types
+ */
+export function filterEventsByUserTypes(
+ events: ToolUsageEvent[],
+ userTypes: UserType[]
+): ToolUsageEvent[] {
+ if (userTypes.length === 0) {
+ return events;
+ }
+ return events.filter(
+ (event) => event.userType && userTypes.includes(event.userType)
+ );
+}
+
+/**
+ * Apply all filters to events
+ */
+export function filterEvents(
+ events: ToolUsageEvent[],
+ filters: AnalyticsFilters
+): ToolUsageEvent[] {
+ let filtered = filterEventsByTimeRange(events, filters.timeRange);
+ filtered = filterEventsByUserTypes(filtered, filters.userTypes);
+ return filtered;
+}
+
+/**
+ * Calculate usage statistics for each tool
+ */
+export function calculateToolStats(events: ToolUsageEvent[]): ToolUsageStats[] {
+ const statsMap = new Map<string, ToolUsageStats>();
+
+ events.forEach((event) => {
+ const existing = statsMap.get(event.toolPath);
+ if (existing) {
+ existing.totalCount++;
+ if (event.eventType === 'open') {
+ existing.openCount++;
+ } else {
+ existing.executeCount++;
+ }
+ if (event.timestamp > existing.lastUsed) {
+ existing.lastUsed = event.timestamp;
+ }
+ if (event.userType && !existing.userTypes.includes(event.userType)) {
+ existing.userTypes.push(event.userType);
+ }
+ } else {
+ statsMap.set(event.toolPath, {
+ toolPath: event.toolPath,
+ toolName: event.toolName,
+ category: event.category,
+ totalCount: 1,
+ openCount: event.eventType === 'open' ? 1 : 0,
+ executeCount: event.eventType === 'execute' ? 1 : 0,
+ lastUsed: event.timestamp,
+ userTypes: event.userType ? [event.userType] : []
+ });
+ }
+ });
+
+ return Array.from(statsMap.values());
+}
+
+/**
+ * Get most used tools sorted by total count
+ */
+export function getMostUsedTools(
+ events: ToolUsageEvent[],
+ limit: number = 10
+): ToolUsageStats[] {
+ const stats = calculateToolStats(events);
+ return stats.sort((a, b) => b.totalCount - a.totalCount).slice(0, limit);
+}
+
+/**
+ * Get recently used tools sorted by last used timestamp
+ */
+export function getRecentlyUsedTools(
+ events: ToolUsageEvent[],
+ limit: number = 10
+): ToolUsageStats[] {
+ const stats = calculateToolStats(events);
+ return stats.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, limit);
+}
+
+/**
+ * Get usage statistics grouped by category
+ */
+export function getUsageByCategory(
+ events: ToolUsageEvent[]
+): CategoryUsageStats[] {
+ const categoryMap = new Map<
+ ToolCategory,
+ { count: number; tools: Set<string> }
+ >();
+
+ events.forEach((event) => {
+ const existing = categoryMap.get(event.category);
+ if (existing) {
+ existing.count++;
+ existing.tools.add(event.toolPath);
+ } else {
+ categoryMap.set(event.category, {
+ count: 1,
+ tools: new Set([event.toolPath])
+ });
+ }
+ });
+
+ return Array.from(categoryMap.entries())
+ .map(([category, data]) => ({
+ category,
+ totalCount: data.count,
+ toolCount: data.tools.size
+ }))
+ .sort((a, b) => b.totalCount - a.totalCount);
+}
+
+/**
+ * Get total event count
+ */
+export function getTotalEventCount(events: ToolUsageEvent[]): number {
+ return events.length;
+}
+
+/**
+ * Get unique tool count
+ */
+export function getUniqueToolCount(events: ToolUsageEvent[]): number {
+ const uniqueTools = new Set(events.map((e) => e.toolPath));
+ return uniqueTools.size;
+}
+
+/**
+ * Clear all analytics data
+ */
+export function clearAnalyticsData(): void {
+ localStorage.removeItem(ANALYTICS_STORAGE_KEY);
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment