rough dashboard components
This commit is contained in:
130
src/components/HistoryChart.tsx
Normal file
130
src/components/HistoryChart.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import * as React from "react";
|
||||
import { Box, Typography, ToggleButtonGroup, ToggleButton, Paper } from "@mui/material";
|
||||
|
||||
export interface ChartDataPoint {
|
||||
id: string;
|
||||
amount: number;
|
||||
count?: number;
|
||||
highlighted?: boolean;
|
||||
}
|
||||
|
||||
export interface HistoryChartProps {
|
||||
header: string;
|
||||
summary?: string;
|
||||
tabs: string[];
|
||||
data: Record<string, ChartDataPoint[]>;
|
||||
}
|
||||
|
||||
export default function HistoryChart({
|
||||
header,
|
||||
summary,
|
||||
tabs,
|
||||
data,
|
||||
}: HistoryChartProps) {
|
||||
const [activeTab, setActiveTab] = React.useState<string>(tabs[0] || "");
|
||||
|
||||
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
|
||||
if (newTab !== null) {
|
||||
setActiveTab(newTab);
|
||||
}
|
||||
};
|
||||
|
||||
const activeDataKey = activeTab.toLowerCase();
|
||||
const currentData = data[activeDataKey] || data[activeTab] || [];
|
||||
|
||||
const maxAmount = Math.max(...currentData.map((d) => d.amount), 1);
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: { xs: 2, sm: 4 }, borderRadius: 4, width: "100%", boxShadow: 'none', border: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="h6" fontWeight={700} gutterBottom>
|
||||
{header}
|
||||
</Typography>
|
||||
{summary && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{summary}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<ToggleButtonGroup
|
||||
value={activeTab}
|
||||
exclusive
|
||||
onChange={handleTabChange}
|
||||
fullWidth
|
||||
sx={{
|
||||
mb: 4,
|
||||
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)',
|
||||
borderRadius: 8,
|
||||
p: 0.5,
|
||||
"& .MuiToggleButton-root": {
|
||||
border: "none",
|
||||
borderRadius: 8,
|
||||
textTransform: "capitalize",
|
||||
fontWeight: 600,
|
||||
color: "text.secondary",
|
||||
"&.Mui-selected": {
|
||||
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'primary.dark' : 'primary.light',
|
||||
color: (theme) => theme.palette.mode === 'dark' ? 'primary.contrastText' : 'primary.main',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.05)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<ToggleButton key={tab} value={tab}>
|
||||
{tab}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{/* Chart Area */}
|
||||
{currentData.length > 0 ? (
|
||||
<Box sx={{ display: "flex", alignItems: "flex-end", height: 200, mt: 4, position: 'relative' }}>
|
||||
{currentData.map((point) => {
|
||||
const heightPerc = (point.amount / maxAmount) * 100;
|
||||
return (
|
||||
<Box
|
||||
key={point.id}
|
||||
sx={{
|
||||
flex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ mb: 1, opacity: 0.7, fontSize: '0.65rem', display: { xs: 'none', sm: 'block' } }}>
|
||||
{point.amount > 0 ? `Rs ${point.amount}` : ''}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "40%",
|
||||
minWidth: 12,
|
||||
maxWidth: 32,
|
||||
height: `${heightPerc}%`,
|
||||
minHeight: "4px",
|
||||
bgcolor: point.highlighted ? "error.main" : "grey.300",
|
||||
borderRadius: 4,
|
||||
transition: "height 0.5s cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
...(point.highlighted && {
|
||||
boxShadow: (theme) => `0 4px 12px ${theme.palette.error.main}40`,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, fontWeight: 500, fontSize: '0.7rem' }}>
|
||||
{point.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ height: 200, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<Typography color="text.secondary">No Data Available</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
110
src/components/LatestItemsList.tsx
Normal file
110
src/components/LatestItemsList.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
export interface LatestItem {
|
||||
id: string | number;
|
||||
icon: React.ReactNode;
|
||||
iconBgColor?: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
amount: string;
|
||||
timeAgo: string;
|
||||
}
|
||||
|
||||
export interface LatestItemsListProps {
|
||||
title?: string;
|
||||
items: LatestItem[];
|
||||
onViewAll?: () => void;
|
||||
}
|
||||
|
||||
export default function LatestItemsList({
|
||||
title = "Recent Transactions",
|
||||
items,
|
||||
onViewAll,
|
||||
}: LatestItemsListProps) {
|
||||
return (
|
||||
<Box sx={{ width: "100%", bgcolor: "background.paper", borderRadius: 4, p: 2 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2, px: 2 }}>
|
||||
<Typography variant="h6" fontWeight="bold">
|
||||
{title}
|
||||
</Typography>
|
||||
{onViewAll && (
|
||||
<Button
|
||||
variant="text"
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ textTransform: "none", color: "text.secondary", fontWeight: "medium" }}
|
||||
onClick={onViewAll}
|
||||
>
|
||||
view all
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* List */}
|
||||
<List disablePadding>
|
||||
{items.map((item, index) => (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
sx={{
|
||||
px: { xs: 1, sm: 2 },
|
||||
py: 2,
|
||||
mb: index !== items.length - 1 ? 1 : 0,
|
||||
borderRadius: 3,
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
transition: "background-color 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{
|
||||
bgcolor: item.iconBgColor || "grey.200",
|
||||
color: "inherit",
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 3,
|
||||
mr: 2,
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="subtitle1" fontWeight={600} color="text.primary">
|
||||
{item.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{item.subtitle}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
|
||||
<Box sx={{ textAlign: "right" }}>
|
||||
<Typography variant="subtitle1" fontWeight={700} color="text.primary">
|
||||
{item.amount}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
|
||||
{item.timeAgo}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
80
src/components/ProgressCard.tsx
Normal file
80
src/components/ProgressCard.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as React from "react";
|
||||
import { Box, Typography, Paper, LinearProgress, linearProgressClasses } from "@mui/material";
|
||||
|
||||
export interface ProgressCardProps {
|
||||
header: string;
|
||||
summary?: string;
|
||||
progressAmount: number;
|
||||
totalAmount: number;
|
||||
colorTheme?: "primary" | "secondary" | "error" | "info" | "success" | "warning";
|
||||
}
|
||||
|
||||
export default function ProgressCard({
|
||||
header,
|
||||
summary,
|
||||
progressAmount,
|
||||
totalAmount,
|
||||
colorTheme = "info",
|
||||
}: ProgressCardProps) {
|
||||
const percentage = Math.min(100, Math.max(0, (progressAmount / totalAmount) * 100)) || 0;
|
||||
|
||||
const displaySummary = summary ?? `Rs ${progressAmount} / Rs ${totalAmount}`;
|
||||
|
||||
const parts = displaySummary.split('/');
|
||||
const prefixAmount = parts[0]?.trim() || '';
|
||||
const suffixString = parts.length > 1 ? `/ ${parts.slice(1).join('/').trim()}` : '';
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={4}
|
||||
sx={{
|
||||
width: "100%",
|
||||
p: { xs: 3, md: 4 },
|
||||
borderRadius: 4,
|
||||
background: (theme) =>
|
||||
colorTheme === "info"
|
||||
? "linear-gradient(135deg, #0284c7 0%, #06b6d4 100%)"
|
||||
: `linear-gradient(135deg, ${theme.palette[colorTheme].main} 0%, ${theme.palette[colorTheme].light} 100%)`,
|
||||
color: "#fff",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
boxShadow: (theme) => `0 12px 24px -10px ${theme.palette.mode === 'dark' ? '#000' : theme.palette[colorTheme].main}`,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight={600} sx={{ opacity: 0.9, mb: 1 }}>
|
||||
{header}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h3" fontWeight={800} sx={{ mb: 3 }}>
|
||||
{prefixAmount}{" "}
|
||||
{suffixString && (
|
||||
<Typography component="span" variant="subtitle1" sx={{ opacity: 0.7, fontWeight: 500 }}>
|
||||
{suffixString}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: "85%" }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={percentage}
|
||||
sx={{
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
[`&.${linearProgressClasses.colorPrimary}`]: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
||||
},
|
||||
[`& .${linearProgressClasses.bar}`]: {
|
||||
borderRadius: 5,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user