build ok,
This commit is contained in:
104
002_source/cms/src/components/dashboard/overview/app-chat.tsx
Normal file
104
002_source/cms/src/components/dashboard/overview/app-chat.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Badge from '@mui/material/Badge';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
|
||||
import { ChatCircleText as ChatCircleTextIcon } from '@phosphor-icons/react/dist/ssr/ChatCircleText';
|
||||
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
content: string;
|
||||
author: { name: string; avatar?: string; status?: 'online' | 'offline' | 'away' | 'busy' };
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface AppChatProps {
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export function AppChat({ messages }: AppChatProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<DotsThreeIcon weight="bold" />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ChatCircleTextIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="App chat"
|
||||
/>
|
||||
<List
|
||||
disablePadding
|
||||
sx={{
|
||||
p: 1,
|
||||
'& .MuiListItemButton-root': { borderRadius: 1 },
|
||||
'& .MuiBadge-dot': {
|
||||
border: '2px solid var(--mui-palette-background-paper)',
|
||||
borderRadius: '50%',
|
||||
bottom: '5px',
|
||||
height: '12px',
|
||||
right: '5px',
|
||||
width: '12px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{messages.map((message) => (
|
||||
<ListItem disablePadding key={message.id}>
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
{message.author.status === 'online' ? (
|
||||
<Badge anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} color="success" variant="dot">
|
||||
<Avatar src={message.author.avatar} />
|
||||
</Badge>
|
||||
) : (
|
||||
<Avatar src={message.author.avatar} />
|
||||
)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
disableTypography
|
||||
primary={
|
||||
<Typography noWrap variant="subtitle2">
|
||||
{message.author.name}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography color="text.secondary" noWrap variant="body2">
|
||||
{message.content}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<Typography color="text.secondary" sx={{ whiteSpace: 'nowrap' }} variant="caption">
|
||||
{dayjs(message.createdAt).fromNow()}
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<CardActions>
|
||||
<Button color="secondary" endIcon={<ArrowRightIcon />} size="small">
|
||||
Go to chat
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
119
002_source/cms/src/components/dashboard/overview/app-limits.tsx
Normal file
119
002_source/cms/src/components/dashboard/overview/app-limits.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Cpu as CpuIcon } from '@phosphor-icons/react/dist/ssr/Cpu';
|
||||
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
|
||||
import { Lightning as LightningIcon } from '@phosphor-icons/react/dist/ssr/Lightning';
|
||||
import { RadialBar, RadialBarChart } from 'recharts';
|
||||
|
||||
import { NoSsr } from '@/components/core/no-ssr';
|
||||
|
||||
export interface AppLimitsProps {
|
||||
usage: number;
|
||||
}
|
||||
|
||||
export function AppLimits({ usage }: AppLimitsProps): React.JSX.Element {
|
||||
const chartSize = 240;
|
||||
|
||||
const data = [
|
||||
{ name: 'Empty', value: 100 },
|
||||
{ name: 'Usage', value: usage },
|
||||
] satisfies { name: string; value: number }[];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<DotsThreeIcon weight="bold" />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CpuIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="App limits"
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<NoSsr fallback={<Box sx={{ height: `${chartSize}px` }} />}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
// hide the empty bar
|
||||
'& .recharts-layer path[name="Empty"]': { display: 'none' },
|
||||
'& .recharts-layer .recharts-radial-bar-background-sector': { fill: 'var(--mui-palette-neutral-100)' },
|
||||
}}
|
||||
>
|
||||
<RadialBarChart
|
||||
barSize={24}
|
||||
data={data}
|
||||
endAngle={-10}
|
||||
height={chartSize}
|
||||
innerRadius={166}
|
||||
startAngle={190}
|
||||
width={chartSize}
|
||||
>
|
||||
<RadialBar
|
||||
animationDuration={300}
|
||||
background
|
||||
cornerRadius={10}
|
||||
dataKey="value"
|
||||
endAngle={-320}
|
||||
fill="var(--mui-palette-primary-main)"
|
||||
startAngle={20}
|
||||
/>
|
||||
</RadialBarChart>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ textAlign: 'center', mt: '-40px' }}>
|
||||
<Typography variant="h5">
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(usage / 100)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</NoSsr>
|
||||
</Box>
|
||||
<Stack spacing={2} sx={{ mt: '-80px', textAlign: 'center' }}>
|
||||
<Typography variant="h6">You've almost reached your limit</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
You have used{' '}
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(usage / 100)} of your
|
||||
available spots. Upgrade plan to create more projects.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions sx={{ justifyContent: 'flex-end' }}>
|
||||
<Button color="secondary" startIcon={<LightningIcon />} variant="contained">
|
||||
Upgrade plan
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
134
002_source/cms/src/components/dashboard/overview/app-usage.tsx
Normal file
134
002_source/cms/src/components/dashboard/overview/app-usage.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import { NoSsr } from '@/components/core/no-ssr';
|
||||
|
||||
const bars = [
|
||||
{ name: 'This year', dataKey: 'v1', color: 'var(--mui-palette-primary-400)' },
|
||||
{ name: 'Last year', dataKey: 'v2', color: 'var(--mui-palette-primary-600)' },
|
||||
] satisfies { name: string; dataKey: string; color: string }[];
|
||||
|
||||
export interface AppUsageProps {
|
||||
data: { name: string; v1: number; v2: number }[];
|
||||
}
|
||||
|
||||
export function AppUsage({ data }: AppUsageProps): React.JSX.Element {
|
||||
const chartHeight = 300;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader title="App usage" />
|
||||
<CardContent>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={3}>
|
||||
<Stack spacing={3} sx={{ flex: '0 0 auto', justifyContent: 'space-between', width: '240px' }}>
|
||||
<Stack spacing={2}>
|
||||
<Typography color="success.main" variant="h2">
|
||||
+28%
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
increase in app usage with{' '}
|
||||
<Typography color="text.primary" component="span">
|
||||
6,521
|
||||
</Typography>{' '}
|
||||
new products purchased
|
||||
</Typography>
|
||||
</Stack>
|
||||
<div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
<Typography color="primary.main" component="span" variant="subtitle2">
|
||||
This year
|
||||
</Typography>{' '}
|
||||
is forecasted to increase in your traffic by the end of the current month
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack divider={<Divider />} spacing={2} sx={{ flex: '1 1 auto' }}>
|
||||
<NoSsr fallback={<Box sx={{ height: `${chartHeight}px` }} />}>
|
||||
<ResponsiveContainer height={chartHeight}>
|
||||
<BarChart barGap={-32} data={data} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
|
||||
<CartesianGrid strokeDasharray="2 4" vertical={false} />
|
||||
<XAxis axisLine={false} dataKey="name" tickLine={false} type="category" xAxisId={0} />
|
||||
<XAxis axisLine={false} dataKey="name" hide type="category" xAxisId={1} />
|
||||
<YAxis axisLine={false} domain={[0, 50]} hide tickCount={6} type="number" />
|
||||
{bars.map(
|
||||
(bar, index): React.JSX.Element => (
|
||||
<Bar
|
||||
animationDuration={300}
|
||||
barSize={32}
|
||||
dataKey={bar.dataKey}
|
||||
fill={bar.color}
|
||||
key={bar.name}
|
||||
name={bar.name}
|
||||
radius={[5, 5, 5, 5]}
|
||||
xAxisId={index}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Tooltip animationDuration={50} content={<TooltipContent />} cursor={false} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</NoSsr>
|
||||
<Legend />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Legend(): React.JSX.Element {
|
||||
return (
|
||||
<Stack direction="row" spacing={2}>
|
||||
{bars.map((bar) => (
|
||||
<Stack direction="row" key={bar.name} spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<Box sx={{ bgcolor: bar.color, borderRadius: '2px', height: '4px', width: '16px' }} />
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{bar.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface TooltipContentProps {
|
||||
active?: boolean;
|
||||
payload?: { fill: string; name: string; dataKey: string; value: number }[];
|
||||
label?: string;
|
||||
}
|
||||
|
||||
function TooltipContent({ active, payload }: TooltipContentProps): React.JSX.Element | null {
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={{ border: '1px solid var(--mui-palette-divider)', boxShadow: 'var(--mui-shadows-16)', p: 1 }}>
|
||||
<Stack spacing={2}>
|
||||
{payload?.map(
|
||||
(entry): React.JSX.Element => (
|
||||
<Stack direction="row" key={entry.name} spacing={3} sx={{ alignItems: 'center' }}>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flex: '1 1 auto' }}>
|
||||
<Box sx={{ bgcolor: entry.fill, borderRadius: '2px', height: '8px', width: '8px' }} />
|
||||
<Typography sx={{ whiteSpace: 'nowrap' }}>{entry.name}</Typography>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{new Intl.NumberFormat('en-US').format(entry.value)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
106
002_source/cms/src/components/dashboard/overview/events.tsx
Normal file
106
002_source/cms/src/components/dashboard/overview/events.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
|
||||
import { CalendarBlank as CalendarBlankIcon } from '@phosphor-icons/react/dist/ssr/CalendarBlank';
|
||||
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
export interface Event {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export function Events({ events }: EventsProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
action={
|
||||
<IconButton>
|
||||
<DotsThreeIcon weight="bold" />
|
||||
</IconButton>
|
||||
}
|
||||
avatar={
|
||||
<Avatar>
|
||||
<CalendarBlankIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
subheader="Based on the linked bank accounts"
|
||||
title="Upcoming events"
|
||||
/>
|
||||
<CardContent sx={{ py: '8px' }}>
|
||||
<List disablePadding>
|
||||
{events.map((event) => (
|
||||
<EventItem event={event} key={event.id} />
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions>
|
||||
<Button color="secondary" endIcon={<ArrowRightIcon />} size="small">
|
||||
See all events
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export interface EventItemProps {
|
||||
event: Event;
|
||||
}
|
||||
|
||||
function EventItem({ event }: EventItemProps): React.JSX.Element {
|
||||
return (
|
||||
<ListItem disableGutters key={event.id}>
|
||||
<ListItemAvatar>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-level1)',
|
||||
borderRadius: 1.5,
|
||||
flex: '0 0 auto',
|
||||
p: '4px 8px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">{dayjs(event.createdAt).format('MMM').toUpperCase()}</Typography>
|
||||
<Typography variant="h6">{dayjs(event.createdAt).format('D')}</Typography>
|
||||
</Box>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
disableTypography
|
||||
primary={
|
||||
<Typography noWrap variant="subtitle2">
|
||||
{event.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography color="text.secondary" noWrap variant="body2">
|
||||
{event.description}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<IconButton>
|
||||
<CalendarBlankIcon />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export interface EventsProps {
|
||||
events: Event[];
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import type { Icon } from '@phosphor-icons/react/dist/lib/types';
|
||||
|
||||
export interface HelperWidgetProps {
|
||||
action: React.ReactNode;
|
||||
description: string;
|
||||
icon: Icon;
|
||||
label: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function HelperWidget({ action, description, icon: Icon, label, title }: HelperWidgetProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
border: '1px solid var(--mui-palette-divider)',
|
||||
borderRadius: 1.5,
|
||||
boxShadow: 'var(--mui-shadows-8)',
|
||||
display: 'inline-flex',
|
||||
p: '6px 12px',
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="var(--icon-fontSize-md)" />
|
||||
<Typography variant="subtitle2">{label}</Typography>
|
||||
</Stack>
|
||||
</div>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{description}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions>{action}</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActions from '@mui/material/CardActions';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ArrowRight as ArrowRightIcon } from '@phosphor-icons/react/dist/ssr/ArrowRight';
|
||||
import { ContactlessPayment as ContactlessPaymentIcon } from '@phosphor-icons/react/dist/ssr/ContactlessPayment';
|
||||
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
|
||||
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
costs: string;
|
||||
billingCycle: string;
|
||||
status: 'paid' | 'canceled' | 'expiring';
|
||||
}
|
||||
|
||||
export interface SubscriptionsProps {
|
||||
subscriptions: Subscription[];
|
||||
}
|
||||
|
||||
export function Subscriptions({ subscriptions }: SubscriptionsProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ContactlessPaymentIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Our subscriptions"
|
||||
/>
|
||||
<CardContent sx={{ pb: '8px' }}>
|
||||
<List disablePadding>
|
||||
{subscriptions.map((subscription) => (
|
||||
<SubscriptionItem key={subscription.id} subscription={subscription} />
|
||||
))}
|
||||
</List>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<CardActions>
|
||||
<Button color="secondary" endIcon={<ArrowRightIcon />} size="small">
|
||||
See all subscriptions
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function SubscriptionItem({ subscription }: { subscription: Subscription }): React.JSX.Element {
|
||||
const { label, color } = (
|
||||
{
|
||||
paid: { label: 'Paid', color: 'success' },
|
||||
canceled: { label: 'Canceled', color: 'error' },
|
||||
expiring: { label: 'Expiring', color: 'warning' },
|
||||
} as const
|
||||
)[subscription.status];
|
||||
|
||||
return (
|
||||
<ListItem disableGutters>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
src={subscription.icon}
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-shadows-8)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
disableTypography
|
||||
primary={
|
||||
<Typography noWrap variant="subtitle2">
|
||||
{subscription.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography sx={{ whiteSpace: 'nowrap' }} variant="body2">
|
||||
{subscription.costs}{' '}
|
||||
<Typography color="text.secondary" component="span" variant="inherit">
|
||||
/{subscription.billingCycle}
|
||||
</Typography>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<Chip color={color} label={label} size="small" variant="soft" />
|
||||
<IconButton>
|
||||
<DotsThreeIcon weight="bold" />
|
||||
</IconButton>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
71
002_source/cms/src/components/dashboard/overview/summary.tsx
Normal file
71
002_source/cms/src/components/dashboard/overview/summary.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import type { Icon } from '@phosphor-icons/react/dist/lib/types';
|
||||
import { TrendDown as TrendDownIcon } from '@phosphor-icons/react/dist/ssr/TrendDown';
|
||||
import { TrendUp as TrendUpIcon } from '@phosphor-icons/react/dist/ssr/TrendUp';
|
||||
|
||||
export interface SummaryProps {
|
||||
amount: number;
|
||||
diff: number;
|
||||
icon: Icon;
|
||||
title: string;
|
||||
trend: 'up' | 'down';
|
||||
}
|
||||
|
||||
export function Summary({ amount, diff, icon: Icon, title, trend }: SummaryProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'center' }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
'--Avatar-size': '48px',
|
||||
bgcolor: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-shadows-8)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="var(--icon-fontSize-lg)" />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography color="text.secondary" variant="body1">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="h3">{new Intl.NumberFormat('en-US').format(amount)}</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
<Divider />
|
||||
<Box sx={{ p: '16px' }}>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
color: trend === 'up' ? 'var(--mui-palette-success-main)' : 'var(--mui-palette-error-main)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{trend === 'up' ? (
|
||||
<TrendUpIcon fontSize="var(--icon-fontSize-md)" />
|
||||
) : (
|
||||
<TrendDownIcon fontSize="var(--icon-fontSize-md)" />
|
||||
)}
|
||||
</Box>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
<Typography color={trend === 'up' ? 'success.main' : 'error.main'} component="span" variant="subtitle2">
|
||||
{new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 2 }).format(diff / 100)}
|
||||
</Typography>{' '}
|
||||
{trend === 'up' ? 'increase' : 'decrease'} vs last month
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user